From b2aac29211ed9d94fc7ddcd756947ebc953321c1 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 21 Apr 2021 15:57:55 -0700 Subject: [PATCH 0001/1690] Rename package folders --- packages/remix-dev/__tests__/build-test.ts | 218 ++++++++ packages/remix-dev/__tests__/cli-test.ts | 56 ++ .../remix-dev/__tests__/defineRoutes-test.ts | 83 +++ .../remix-dev/__tests__/fixtures/basic.mdx | 8 + packages/remix-dev/__tests__/mdx-test.ts | 152 +++++ .../remix-dev/__tests__/readConfig-test.ts | 173 ++++++ packages/remix-dev/build.ts | 28 + packages/remix-dev/cache.ts | 13 + packages/remix-dev/cli.ts | 75 +++ packages/remix-dev/cli/commands.ts | 174 ++++++ packages/remix-dev/compiler.ts | 355 ++++++++++++ packages/remix-dev/compiler/browserIgnore.ts | 41 ++ packages/remix-dev/compiler/createUrl.ts | 8 + packages/remix-dev/compiler/crypto.ts | 39 ++ packages/remix-dev/compiler/importHints.ts | 5 + .../compiler/rollup/assetsManifest.ts | 213 +++++++ .../remix-dev/compiler/rollup/clientServer.ts | 38 ++ packages/remix-dev/compiler/rollup/css.ts | 108 ++++ packages/remix-dev/compiler/rollup/empty.ts | 24 + packages/remix-dev/compiler/rollup/img.ts | 270 +++++++++ packages/remix-dev/compiler/rollup/mdx.ts | 126 +++++ .../remix-dev/compiler/rollup/remixConfig.ts | 52 ++ .../remix-dev/compiler/rollup/remixInputs.ts | 22 + .../remix-dev/compiler/rollup/routeModules.ts | 111 ++++ .../compiler/rollup/serverManifest.ts | 121 ++++ packages/remix-dev/compiler/rollup/url.ts | 52 ++ .../compiler/rollup/watchDirectory.ts | 49 ++ packages/remix-dev/compiler2.ts | 521 ++++++++++++++++++ packages/remix-dev/compiler2/assets.ts | 148 +++++ packages/remix-dev/compiler2/dependencies.ts | 21 + packages/remix-dev/compiler2/loaders.ts | 37 ++ packages/remix-dev/compiler2/routes.ts | 59 ++ packages/remix-dev/compiler2/shims/react.ts | 2 + packages/remix-dev/compiler2/utils/crypto.ts | 19 + packages/remix-dev/compiler2/utils/fs.ts | 25 + packages/remix-dev/compiler2/utils/url.ts | 5 + packages/remix-dev/config.ts | 253 +++++++++ packages/remix-dev/config/routes.ts | 167 ++++++ packages/remix-dev/config/routesConvention.ts | 109 ++++ packages/remix-dev/config/serverModes.ts | 16 + packages/remix-dev/invariant.ts | 18 + packages/remix-dev/modules.ts | 29 + packages/remix-dev/package.json | 49 ++ packages/remix-dev/server.ts | 88 +++ packages/remix-dev/tsconfig.json | 18 + packages/remix-dev/warnings.ts | 12 + .../remix-express/__tests__/server-test.ts | 79 +++ packages/remix-express/globals.ts | 2 + packages/remix-express/index.ts | 4 + packages/remix-express/package.json | 17 + packages/remix-express/server.ts | 113 ++++ packages/remix-express/tsconfig.json | 17 + packages/remix-node/__tests__/cookies-test.ts | 95 ++++ .../remix-node/__tests__/responses-test.ts | 75 +++ .../remix-node/__tests__/sessions-test.ts | 205 +++++++ packages/remix-node/__tests__/utils.ts | 5 + packages/remix-node/assetImportTypes.ts | 153 +++++ packages/remix-node/build.ts | 24 + packages/remix-node/cookies.ts | 160 ++++++ packages/remix-node/data.ts | 75 +++ packages/remix-node/entry.ts | 130 +++++ packages/remix-node/fetch.ts | 27 + packages/remix-node/globals.ts | 24 + packages/remix-node/headers.ts | 89 +++ packages/remix-node/index.ts | 73 +++ packages/remix-node/invariant.ts | 16 + packages/remix-node/links.ts | 161 ++++++ packages/remix-node/match.ts | 39 ++ packages/remix-node/mode.ts | 16 + packages/remix-node/package.json | 27 + packages/remix-node/responses.ts | 38 ++ packages/remix-node/routes.ts | 113 ++++ packages/remix-node/server.ts | 301 ++++++++++ packages/remix-node/sessions.ts | 250 +++++++++ packages/remix-node/sessions/cookieStorage.ts | 47 ++ packages/remix-node/sessions/fileStorage.ts | 98 ++++ packages/remix-node/sessions/memoryStorage.ts | 57 ++ packages/remix-node/tsconfig.json | 20 + packages/remix-node/warnings.ts | 8 + packages/remix-serve/app.ts | 35 ++ packages/remix-serve/index.ts | 18 + packages/remix-serve/package.json | 22 + packages/remix-serve/tsconfig.json | 17 + 83 files changed, 6860 insertions(+) create mode 100644 packages/remix-dev/__tests__/build-test.ts create mode 100644 packages/remix-dev/__tests__/cli-test.ts create mode 100644 packages/remix-dev/__tests__/defineRoutes-test.ts create mode 100644 packages/remix-dev/__tests__/fixtures/basic.mdx create mode 100644 packages/remix-dev/__tests__/mdx-test.ts create mode 100644 packages/remix-dev/__tests__/readConfig-test.ts create mode 100644 packages/remix-dev/build.ts create mode 100644 packages/remix-dev/cache.ts create mode 100644 packages/remix-dev/cli.ts create mode 100644 packages/remix-dev/cli/commands.ts create mode 100644 packages/remix-dev/compiler.ts create mode 100644 packages/remix-dev/compiler/browserIgnore.ts create mode 100644 packages/remix-dev/compiler/createUrl.ts create mode 100644 packages/remix-dev/compiler/crypto.ts create mode 100644 packages/remix-dev/compiler/importHints.ts create mode 100644 packages/remix-dev/compiler/rollup/assetsManifest.ts create mode 100644 packages/remix-dev/compiler/rollup/clientServer.ts create mode 100644 packages/remix-dev/compiler/rollup/css.ts create mode 100644 packages/remix-dev/compiler/rollup/empty.ts create mode 100644 packages/remix-dev/compiler/rollup/img.ts create mode 100644 packages/remix-dev/compiler/rollup/mdx.ts create mode 100644 packages/remix-dev/compiler/rollup/remixConfig.ts create mode 100644 packages/remix-dev/compiler/rollup/remixInputs.ts create mode 100644 packages/remix-dev/compiler/rollup/routeModules.ts create mode 100644 packages/remix-dev/compiler/rollup/serverManifest.ts create mode 100644 packages/remix-dev/compiler/rollup/url.ts create mode 100644 packages/remix-dev/compiler/rollup/watchDirectory.ts create mode 100644 packages/remix-dev/compiler2.ts create mode 100644 packages/remix-dev/compiler2/assets.ts create mode 100644 packages/remix-dev/compiler2/dependencies.ts create mode 100644 packages/remix-dev/compiler2/loaders.ts create mode 100644 packages/remix-dev/compiler2/routes.ts create mode 100644 packages/remix-dev/compiler2/shims/react.ts create mode 100644 packages/remix-dev/compiler2/utils/crypto.ts create mode 100644 packages/remix-dev/compiler2/utils/fs.ts create mode 100644 packages/remix-dev/compiler2/utils/url.ts create mode 100644 packages/remix-dev/config.ts create mode 100644 packages/remix-dev/config/routes.ts create mode 100644 packages/remix-dev/config/routesConvention.ts create mode 100644 packages/remix-dev/config/serverModes.ts create mode 100644 packages/remix-dev/invariant.ts create mode 100644 packages/remix-dev/modules.ts create mode 100644 packages/remix-dev/package.json create mode 100644 packages/remix-dev/server.ts create mode 100644 packages/remix-dev/tsconfig.json create mode 100644 packages/remix-dev/warnings.ts create mode 100644 packages/remix-express/__tests__/server-test.ts create mode 100644 packages/remix-express/globals.ts create mode 100644 packages/remix-express/index.ts create mode 100644 packages/remix-express/package.json create mode 100644 packages/remix-express/server.ts create mode 100644 packages/remix-express/tsconfig.json create mode 100644 packages/remix-node/__tests__/cookies-test.ts create mode 100644 packages/remix-node/__tests__/responses-test.ts create mode 100644 packages/remix-node/__tests__/sessions-test.ts create mode 100644 packages/remix-node/__tests__/utils.ts create mode 100644 packages/remix-node/assetImportTypes.ts create mode 100644 packages/remix-node/build.ts create mode 100644 packages/remix-node/cookies.ts create mode 100644 packages/remix-node/data.ts create mode 100644 packages/remix-node/entry.ts create mode 100644 packages/remix-node/fetch.ts create mode 100644 packages/remix-node/globals.ts create mode 100644 packages/remix-node/headers.ts create mode 100644 packages/remix-node/index.ts create mode 100644 packages/remix-node/invariant.ts create mode 100644 packages/remix-node/links.ts create mode 100644 packages/remix-node/match.ts create mode 100644 packages/remix-node/mode.ts create mode 100644 packages/remix-node/package.json create mode 100644 packages/remix-node/responses.ts create mode 100644 packages/remix-node/routes.ts create mode 100644 packages/remix-node/server.ts create mode 100644 packages/remix-node/sessions.ts create mode 100644 packages/remix-node/sessions/cookieStorage.ts create mode 100644 packages/remix-node/sessions/fileStorage.ts create mode 100644 packages/remix-node/sessions/memoryStorage.ts create mode 100644 packages/remix-node/tsconfig.json create mode 100644 packages/remix-node/warnings.ts create mode 100644 packages/remix-serve/app.ts create mode 100644 packages/remix-serve/index.ts create mode 100644 packages/remix-serve/package.json create mode 100644 packages/remix-serve/tsconfig.json diff --git a/packages/remix-dev/__tests__/build-test.ts b/packages/remix-dev/__tests__/build-test.ts new file mode 100644 index 0000000000..42ba09eb8d --- /dev/null +++ b/packages/remix-dev/__tests__/build-test.ts @@ -0,0 +1,218 @@ +import path from "path"; +import type { RollupOutput } from "rollup"; + +import { BuildMode, BuildTarget } from "../build"; +import type { BuildOptions } from "../compiler"; +import { build, generate } from "../compiler"; +import type { RemixConfig } from "../config"; +import { readConfig } from "../config"; + +const remixRoot = path.resolve(__dirname, "../../../fixtures/gists-app"); + +async function generateBuild(config: RemixConfig, options: BuildOptions) { + return await generate(await build(config, options)); +} + +function getFilenames(output: RollupOutput) { + return output.output.map(item => item.fileName).sort(); +} + +describe.skip("building", () => { + // describe("building", () => { + let config: RemixConfig; + beforeAll(async () => { + config = await readConfig(remixRoot); + }); + + beforeEach(() => { + jest.setTimeout(20000); + }); + + describe("the development server build", () => { + it("generates the correct bundles", async () => { + let output = await generateBuild(config, { + mode: BuildMode.Development, + target: BuildTarget.Server + }); + + expect(getFilenames(output)).toMatchInlineSnapshot(` + Array [ + "_shared/Shared-072c977d.js", + "_shared/_rollupPluginBabelHelpers-8a275fd9.js", + "entry.server.js", + "index.js", + "pages/one.js", + "pages/two.js", + "root.js", + "routes/404.js", + "routes/gists.js", + "routes/gists.mine.js", + "routes/gists/$username.js", + "routes/gists/index.js", + "routes/index.js", + "routes/links.js", + "routes/loader-errors.js", + "routes/loader-errors/nested.js", + "routes/methods.js", + "routes/page/four.js", + "routes/page/three.js", + "routes/prefs.js", + "routes/render-errors.js", + "routes/render-errors/nested.js", + ] + `); + }); + }); + + describe("the production server build", () => { + it("generates the correct bundles", async () => { + let output = await generateBuild(config, { + mode: BuildMode.Production, + target: BuildTarget.Server + }); + + expect(getFilenames(output)).toMatchInlineSnapshot(` + Array [ + "_shared/Shared-072c977d.js", + "_shared/_rollupPluginBabelHelpers-8a275fd9.js", + "entry.server.js", + "index.js", + "pages/one.js", + "pages/two.js", + "root.js", + "routes/404.js", + "routes/gists.js", + "routes/gists.mine.js", + "routes/gists/$username.js", + "routes/gists/index.js", + "routes/index.js", + "routes/links.js", + "routes/loader-errors.js", + "routes/loader-errors/nested.js", + "routes/methods.js", + "routes/page/four.js", + "routes/page/three.js", + "routes/prefs.js", + "routes/render-errors.js", + "routes/render-errors/nested.js", + ] + `); + }); + }); + + describe("the development browser build", () => { + it("generates the correct bundles", async () => { + let output = await generateBuild(config, { + mode: BuildMode.Development, + target: BuildTarget.Browser + }); + + expect(getFilenames(output)).toMatchInlineSnapshot(` + Array [ + "_shared/Shared-7d084ccf.js", + "_shared/__babel/runtime-88c72f87.js", + "_shared/__mdx-js/react-4b004046.js", + "_shared/__remix-run/react-cf018015.js", + "_shared/_rollupPluginBabelHelpers-bfa6c712.js", + "_shared/history-7c196d23.js", + "_shared/object-assign-510802f4.js", + "_shared/prop-types-1122a697.js", + "_shared/react-a3c235ca.js", + "_shared/react-dom-ec89bb6e.js", + "_shared/react-is-6b44b080.js", + "_shared/react-router-dom-ef82d700.js", + "_shared/react-router-e7697632.js", + "_shared/scheduler-8fd1645e.js", + "components/guitar-1080x720-a9c95518.jpg", + "components/guitar-2048x1365-f42efd6b.jpg", + "components/guitar-500x333-3a1a0bd1.jpg", + "components/guitar-500x500-c6f1ab94.jpg", + "components/guitar-600x600-b329e428.jpg", + "components/guitar-720x480-729becce.jpg", + "entry.client.js", + "manifest-8c53378e.js", + "pages/one.js", + "pages/two.js", + "root.js", + "routes/404.js", + "routes/gists.js", + "routes/gists.mine.js", + "routes/gists/$username.js", + "routes/gists/index.js", + "routes/index.js", + "routes/links.js", + "routes/loader-errors.js", + "routes/loader-errors/nested.js", + "routes/methods.js", + "routes/page/four.js", + "routes/page/three.js", + "routes/prefs.js", + "routes/render-errors.js", + "routes/render-errors/nested.js", + "styles/app-72f634dc.css", + "styles/gists-d7ad5f49.css", + "styles/methods-d182a270.css", + "styles/redText-2b391c21.css", + ] + `); + }); + }); + + describe("the production browser build", () => { + it("generates the correct bundles", async () => { + let output = await generateBuild(config, { + mode: BuildMode.Production, + target: BuildTarget.Browser + }); + + expect(getFilenames(output)).toMatchInlineSnapshot(` + Array [ + "_shared/Shared-bae6070c.js", + "_shared/__babel/runtime-88c72f87.js", + "_shared/__mdx-js/react-a9edf40b.js", + "_shared/__remix-run/react-991ebd19.js", + "_shared/_rollupPluginBabelHelpers-bfa6c712.js", + "_shared/history-e6417d88.js", + "_shared/object-assign-510802f4.js", + "_shared/prop-types-939a16b3.js", + "_shared/react-dom-9dcf9947.js", + "_shared/react-e3656f88.js", + "_shared/react-is-5765fb91.js", + "_shared/react-router-dom-baf54395.js", + "_shared/react-router-fc62a14c.js", + "_shared/scheduler-f1282356.js", + "components/guitar-1080x720-a9c95518.jpg", + "components/guitar-2048x1365-f42efd6b.jpg", + "components/guitar-500x333-3a1a0bd1.jpg", + "components/guitar-500x500-c6f1ab94.jpg", + "components/guitar-600x600-b329e428.jpg", + "components/guitar-720x480-729becce.jpg", + "entry.client-b7de4be6.js", + "manifest-943fff78.js", + "pages/one-829d2fc6.js", + "pages/two-31b88726.js", + "root-de6ed2a5.js", + "routes/404-a4edec5f.js", + "routes/gists-236207fe.js", + "routes/gists.mine-ac017552.js", + "routes/gists/$username-c4819bb8.js", + "routes/gists/index-0f39313f.js", + "routes/index-eb238abf.js", + "routes/links-50cd630a.js", + "routes/loader-errors-e4502176.js", + "routes/loader-errors/nested-741a07ef.js", + "routes/methods-8241c6fa.js", + "routes/page/four-efa66f69.js", + "routes/page/three-dfbf7520.js", + "routes/prefs-12bae83f.js", + "routes/render-errors-cb72f859.js", + "routes/render-errors/nested-ef1c619f.js", + "styles/app-72f634dc.css", + "styles/gists-d7ad5f49.css", + "styles/methods-d182a270.css", + "styles/redText-2b391c21.css", + ] + `); + }); + }); +}); diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts new file mode 100644 index 0000000000..29d1be2a94 --- /dev/null +++ b/packages/remix-dev/__tests__/cli-test.ts @@ -0,0 +1,56 @@ +import childProcess from "child_process"; +import fs from "fs"; +import path from "path"; +import util from "util"; +import semver from "semver"; + +const execFile = util.promisify(childProcess.execFile); + +const remix = path.resolve( + __dirname, + "../../../build/node_modules/@remix-run/dev/cli" +); + +describe("remix cli", () => { + beforeAll(() => { + if (!fs.existsSync(remix)) { + throw new Error(`Cannot run Remix CLI tests w/out building Remix`); + } + }); + + describe("the --help flag", () => { + it("prints help info", async () => { + let { stdout } = await execFile("node", [remix, "--help"]); + expect(stdout).toMatchInlineSnapshot(` + " + Usage + $ remix build [remixRoot] + $ remix run [remixRoot] + + Options + --help Print this help message and exit + --version, -v Print the CLI version and exit + + Examples + $ remix build my-website + $ remix run my-website + + " + `); + }); + }); + + describe("the --version flag", () => { + it("prints the current version", async () => { + let { stdout } = await execFile("node", [remix, "--version"]); + expect(!!semver.valid(stdout.trim())).toBe(true); + }); + }); + + describe("the -v flag", () => { + it("prints the current version", async () => { + let { stdout } = await execFile("node", [remix, "-v"]); + expect(!!semver.valid(stdout.trim())).toBe(true); + }); + }); +}); diff --git a/packages/remix-dev/__tests__/defineRoutes-test.ts b/packages/remix-dev/__tests__/defineRoutes-test.ts new file mode 100644 index 0000000000..4f176afd47 --- /dev/null +++ b/packages/remix-dev/__tests__/defineRoutes-test.ts @@ -0,0 +1,83 @@ +import { defineRoutes } from "../config/routes"; + +describe("defineRoutes", () => { + it("returns an array of routes", () => { + let routes = defineRoutes(route => { + route("/", "routes/home.js"); + route("inbox", "routes/inbox.js", () => { + route("/", "routes/inbox/index.js"); + route(":messageId", "routes/inbox/$messageId.js"); + route("archive", "routes/inbox/archive.js"); + }); + }); + + expect(routes).toMatchInlineSnapshot(` + Object { + "routes/home": Object { + "caseSensitive": false, + "file": "routes/home.js", + "id": "routes/home", + "parentId": undefined, + "path": "/", + }, + "routes/inbox": Object { + "caseSensitive": false, + "file": "routes/inbox.js", + "id": "routes/inbox", + "parentId": undefined, + "path": "inbox", + }, + "routes/inbox/$messageId": Object { + "caseSensitive": false, + "file": "routes/inbox/$messageId.js", + "id": "routes/inbox/$messageId", + "parentId": "routes/inbox", + "path": ":messageId", + }, + "routes/inbox/archive": Object { + "caseSensitive": false, + "file": "routes/inbox/archive.js", + "id": "routes/inbox/archive", + "parentId": "routes/inbox", + "path": "archive", + }, + "routes/inbox/index": Object { + "caseSensitive": false, + "file": "routes/inbox/index.js", + "id": "routes/inbox/index", + "parentId": "routes/inbox", + "path": "/", + }, + } + `); + }); + + it("works with async data", async () => { + // Read everything *before* calling defineRoutes. + let fakeDirectory = await Promise.resolve(["one.md", "two.md"]); + let routes = defineRoutes(route => { + for (let file of fakeDirectory) { + route(file.replace(/\.md$/, ""), file); + } + }); + + expect(routes).toMatchInlineSnapshot(` + Object { + "one": Object { + "caseSensitive": false, + "file": "one.md", + "id": "one", + "parentId": undefined, + "path": "one", + }, + "two": Object { + "caseSensitive": false, + "file": "two.md", + "id": "two", + "parentId": undefined, + "path": "two", + }, + } + `); + }); +}); diff --git a/packages/remix-dev/__tests__/fixtures/basic.mdx b/packages/remix-dev/__tests__/fixtures/basic.mdx new file mode 100644 index 0000000000..caa99a15f5 --- /dev/null +++ b/packages/remix-dev/__tests__/fixtures/basic.mdx @@ -0,0 +1,8 @@ +--- +meta: + title: Some title +headers: + cache-control: max-age=60 +--- + +I am mdx diff --git a/packages/remix-dev/__tests__/mdx-test.ts b/packages/remix-dev/__tests__/mdx-test.ts new file mode 100644 index 0000000000..b46eb995ad --- /dev/null +++ b/packages/remix-dev/__tests__/mdx-test.ts @@ -0,0 +1,152 @@ +import path from "path"; +import type { RollupBuild } from "rollup"; +import { rollup } from "rollup"; +import babel from "@rollup/plugin-babel"; +import nodeResolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import React from "react"; +import ReactDOMServer from "react-dom/server"; + +import type { MdxConfig, MdxFunctionOption } from "../compiler/rollup/mdx"; +import mdxPlugin from "../compiler/rollup/mdx"; + +import { getRemixConfig } from "../compiler/rollup/remixConfig"; +let mockedGetRemixConfig = (getRemixConfig as unknown) as jest.MockedFunction< + () => any +>; + +jest.mock("../compiler/rollup/remixConfig"); + +describe("mdx rollup plugin", () => { + beforeEach(() => { + mockedGetRemixConfig.mockImplementation(async () => { + return {}; + }); + }); + + afterEach(() => { + mockedGetRemixConfig.mockReset(); + }); + + it("renders", async () => { + let mod = await bundleMdxFile("basic.mdx"); + expect(renderMdxModule(mod)).toMatchInlineSnapshot(`"

I am mdx

"`); + }); + + it("exports meta and headers", async () => { + let mod = await bundleMdxFile("basic.mdx"); + expect(mod.headers()).toMatchInlineSnapshot(` + Object { + "cache-control": "max-age=60", + } + `); + expect(mod.meta()).toMatchInlineSnapshot(` + Object { + "title": "Some title", + } + `); + }); + + it("supports a function as config", async () => { + expect.assertions(3); + let options: MdxFunctionOption = (attributes, filename) => { + expect(attributes).toMatchInlineSnapshot(` + Object { + "headers": Object { + "cache-control": "max-age=60", + }, + "meta": Object { + "title": "Some title", + }, + } + `); + expect(filename.endsWith("basic.mdx")).toBeTruthy(); + return { + rehypePlugins: [fakeRehypePlugin] + }; + }; + let mod = await bundleMdxFile("basic.mdx", options); + expect(renderMdxModule(mod)).toMatchInlineSnapshot( + `"

I am mdx

"` + ); + }); +}); + +//////////////////////////////////////////////////////////////////////////////// +function fakeRehypePlugin(): any { + return (root: any) => { + root.children.push({ + type: "element", + tagName: "footer", + children: [ + { + type: "text", + value: "injected!" + } + ] + }); + return root; + }; +} +interface MdxTestModule { + default: () => React.ReactElement; + meta: () => { [key: string]: string }; + headers: () => { [key: string]: string }; +} + +function renderMdxModule(mod: MdxTestModule) { + return ReactDOMServer.renderToString(React.createElement(mod.default)); +} + +async function bundleMdxFile( + filename: string, + mdxConfig: MdxConfig = {} +): Promise { + let filepath = path.resolve(__dirname, "fixtures", filename); + let bundle = await rollup({ + input: filepath, + plugins: [mdxPlugin({ mdxConfig }), ...getPlugins()], + external: ["@mdx-js/react"] + }); + let code = await getBundleOutput(bundle); + let module = { exports: {} }; + let params = [ + "module", + "exports", + "require", + `${code}; return module.exports;` + ]; + let evalFunc = new Function(...params); // eslint-disable-line + return evalFunc(module, module.exports, require); +} + +async function getBundleOutput(bundle: RollupBuild) { + let { output } = await bundle.generate({ format: "cjs", exports: "named" }); + return output[0].code; +} + +function getPlugins() { + return [ + babel({ + babelHelpers: "bundled", + configFile: false, + exclude: /node_modules/, + extensions: [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"], + presets: [ + ["@babel/preset-react", { runtime: "automatic" }], + ["@babel/preset-env", { targets: { node: true } }], + [ + "@babel/preset-typescript", + { + allExtensions: true, + isTSX: true + } + ] + ] + }), + nodeResolve({ + extensions: [".js", ".json", ".ts", ".tsx"] + }), + commonjs() + ]; +} diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts new file mode 100644 index 0000000000..915050c89a --- /dev/null +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -0,0 +1,173 @@ +import path from "path"; + +import type { RemixConfig } from "../config"; +import { readConfig } from "../config"; + +const remixRoot = path.resolve(__dirname, "../../../fixtures/gists-app"); + +describe("readConfig", () => { + let config: RemixConfig; + beforeEach(async () => { + config = await readConfig(remixRoot); + }); + + it("generates a config", async () => { + expect(config).toMatchInlineSnapshot( + { + rootDirectory: expect.any(String), + appDirectory: expect.any(String), + cacheDirectory: expect.any(String), + serverBuildDirectory: expect.any(String), + assetsBuildDirectory: expect.any(String) + }, + ` + Object { + "appDirectory": Any, + "assetsBuildDirectory": Any, + "cacheDirectory": Any, + "devServerPort": 8002, + "entryClientFile": "entry.client.js", + "entryServerFile": "entry.server.js", + "mdx": undefined, + "publicPath": "/build/", + "rootDirectory": Any, + "routes": Object { + "pages/one": Object { + "caseSensitive": false, + "file": "pages/one.mdx", + "id": "pages/one", + "parentId": "root", + "path": "/page/one", + }, + "pages/two": Object { + "caseSensitive": false, + "file": "pages/two.mdx", + "id": "pages/two", + "parentId": "root", + "path": "/page/two", + }, + "root": Object { + "file": "root.js", + "id": "root", + "path": "/", + }, + "routes/404": Object { + "caseSensitive": false, + "file": "routes/404.js", + "id": "routes/404", + "parentId": "root", + "path": "*", + }, + "routes/empty": Object { + "caseSensitive": false, + "file": "routes/empty.js", + "id": "routes/empty", + "parentId": "root", + "path": "empty", + }, + "routes/gists": Object { + "caseSensitive": false, + "file": "routes/gists.js", + "id": "routes/gists", + "parentId": "root", + "path": "gists", + }, + "routes/gists.mine": Object { + "caseSensitive": false, + "file": "routes/gists.mine.js", + "id": "routes/gists.mine", + "parentId": "root", + "path": "gists/mine", + }, + "routes/gists/$username": Object { + "caseSensitive": false, + "file": "routes/gists/$username.js", + "id": "routes/gists/$username", + "parentId": "routes/gists", + "path": ":username", + }, + "routes/gists/index": Object { + "caseSensitive": false, + "file": "routes/gists/index.js", + "id": "routes/gists/index", + "parentId": "routes/gists", + "path": "/", + }, + "routes/index": Object { + "caseSensitive": false, + "file": "routes/index.js", + "id": "routes/index", + "parentId": "root", + "path": "/", + }, + "routes/links": Object { + "caseSensitive": false, + "file": "routes/links.tsx", + "id": "routes/links", + "parentId": "root", + "path": "links", + }, + "routes/loader-errors": Object { + "caseSensitive": false, + "file": "routes/loader-errors.js", + "id": "routes/loader-errors", + "parentId": "root", + "path": "loader-errors", + }, + "routes/loader-errors/nested": Object { + "caseSensitive": false, + "file": "routes/loader-errors/nested.js", + "id": "routes/loader-errors/nested", + "parentId": "routes/loader-errors", + "path": "nested", + }, + "routes/methods": Object { + "caseSensitive": false, + "file": "routes/methods.tsx", + "id": "routes/methods", + "parentId": "root", + "path": "methods", + }, + "routes/page/four": Object { + "caseSensitive": false, + "file": "routes/page/four.mdx", + "id": "routes/page/four", + "parentId": "root", + "path": "page/four", + }, + "routes/page/three": Object { + "caseSensitive": false, + "file": "routes/page/three.md", + "id": "routes/page/three", + "parentId": "root", + "path": "page/three", + }, + "routes/prefs": Object { + "caseSensitive": false, + "file": "routes/prefs.tsx", + "id": "routes/prefs", + "parentId": "root", + "path": "prefs", + }, + "routes/render-errors": Object { + "caseSensitive": false, + "file": "routes/render-errors.js", + "id": "routes/render-errors", + "parentId": "root", + "path": "render-errors", + }, + "routes/render-errors/nested": Object { + "caseSensitive": false, + "file": "routes/render-errors/nested.js", + "id": "routes/render-errors/nested", + "parentId": "routes/render-errors", + "path": "nested", + }, + }, + "serverBuildDirectory": Any, + "serverMode": "production", + } + ` + ); + }); +}); diff --git a/packages/remix-dev/build.ts b/packages/remix-dev/build.ts new file mode 100644 index 0000000000..18052ddbf2 --- /dev/null +++ b/packages/remix-dev/build.ts @@ -0,0 +1,28 @@ +export enum BuildMode { + Development = "development", + Production = "production" +} + +export function isBuildMode(mode: any): mode is BuildMode { + return mode === BuildMode.Development || mode === BuildMode.Production; +} + +export enum BuildTarget { + Browser = "browser", // TODO: remove + Server = "server", // TODO: remove + CloudflareWorkers = "cloudflare-workers", + Node14 = "node14" +} + +export function isBuildTarget(target: any): target is BuildTarget { + return ( + target === BuildTarget.Browser || + target === BuildTarget.Server || + target === BuildTarget.Node14 + ); +} + +export interface BuildOptions { + mode: BuildMode; + target: BuildTarget; +} diff --git a/packages/remix-dev/cache.ts b/packages/remix-dev/cache.ts new file mode 100644 index 0000000000..0ce3e3df61 --- /dev/null +++ b/packages/remix-dev/cache.ts @@ -0,0 +1,13 @@ +import { put, get } from "cacache"; + +export { put, get }; + +export function putJson(cachePath: string, key: string, data: any) { + return put(cachePath, key, JSON.stringify(data)); +} + +export function getJson(cachePath: string, key: string) { + return get(cachePath, key).then(obj => + JSON.parse(obj.data.toString("utf-8")) + ); +} diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts new file mode 100644 index 0000000000..605d129215 --- /dev/null +++ b/packages/remix-dev/cli.ts @@ -0,0 +1,75 @@ +import type { AnyFlags } from "meow"; +import meow from "meow"; + +import * as commands from "./cli/commands"; + +const helpText = ` +Usage + $ remix build [remixRoot] + $ remix run [remixRoot] + +Options + --help Print this help message and exit + --version, -v Print the CLI version and exit + +Examples + $ remix build my-website + $ remix run my-website +`; + +const flags: AnyFlags = { + version: { + type: "boolean", + alias: "v" + } +}; + +const cli = meow(helpText, { + autoHelp: true, + autoVersion: false, + description: false, + flags +}); + +if (cli.flags.version) { + cli.showVersion(); +} + +// In 0.17 we only have +// - remix build (build2) +// - remix dev (dev) +// - remix run (run3) +switch (cli.input[0]) { + // rollup + case "build": // gone in 0.17 + commands.build(cli.input[1], process.env.NODE_ENV); + break; + case "run": // gone in 0.17 + commands.run(cli.input[1]); + break; + + // esbuild + case "build2": // becomes `remix build` in 0.17 + commands.build2(cli.input[1], process.env.NODE_ENV); + break; + + // these three are all the same + case "watch2": // gone in 0.17 + commands.watch2(cli.input[1], process.env.NODE_ENV); + break; + case "run2": // gone in 0.17 + commands.run2(cli.input[1]); + break; + case "dev": // stays in 0.17 + commands.watch2(cli.input[1], process.env.NODE_ENV); + break; + + // built-in app/dev server + case "run3": // becomes `remix run` in 0.17 + commands.run3(cli.input[1]); + break; + + default: + // `remix my-project` is shorthand for `remix run3 my-project` + commands.run3(cli.input[0]); +} diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts new file mode 100644 index 0000000000..4d66fcc99e --- /dev/null +++ b/packages/remix-dev/cli/commands.ts @@ -0,0 +1,174 @@ +//////////////////////////////////////////////////////////////////////////////// +// In 0.17 +// +// - fn run3() -> fn run() +// - fn watch2() -> fn dev() +// - fn build2() -> fn build() +import * as path from "path"; +import signalExit from "signal-exit"; +import prettyMs from "pretty-ms"; +import { BuildMode, isBuildMode, BuildTarget } from "../build"; +import * as compiler from "../compiler"; +import * as compiler2 from "../compiler2"; +import { readConfig } from "../config"; +import type { RemixConfig } from "../config"; +import { startDevServer } from "../server"; +import WebSocket from "ws"; + +/** + * Runs the build for a Remix app with the old rollup compiler + */ +export async function build(remixRoot: string, mode?: string) { + let buildMode = isBuildMode(mode) ? mode : BuildMode.Production; + + console.log(`Building Remix app for ${buildMode}...`); + + let config = await readConfig(remixRoot); + + await Promise.all([ + compiler.write( + await compiler.build(config, { + mode: buildMode, + target: BuildTarget.Server + }), + config.serverBuildDirectory + ), + compiler.write( + await compiler.build(config, { + mode: buildMode, + target: BuildTarget.Browser + }), + config.assetsBuildDirectory + ) + ]); + + console.log("done!"); +} + +/** + * Runs the old rollup dev watcher. + */ +export async function run(remixRoot: string) { + let config = await readConfig(remixRoot); + + startDevServer(config, { + onListen() { + console.log( + `Remix dev server running on port ${config.devServerPort}...` + ); + } + }); +} + +/////////////////////////////////////////////////////////////////////////////// + +/** + * Runs the new esbuild compiler + */ +export async function build2( + remixRoot: string, + modeArg?: string +): Promise { + let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Production; + + console.log(`Building Remix app in ${mode} mode...`); + + let start = Date.now(); + let config = await readConfig(remixRoot); + await compiler2.build(config, { mode: mode }); + + console.log(`Built in ${prettyMs(Date.now() - start)}`); +} + +/** + * Watches with the new esbuild compiler + */ +export async function watch2( + remixRootOrConfig: string | RemixConfig, + modeArg?: string, + onRebuildStart?: () => void +): Promise { + let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Development; + console.log(`Watching Remix app in ${mode} mode...`); + + let start = Date.now(); + let config = + typeof remixRootOrConfig === "object" + ? remixRootOrConfig + : await readConfig(remixRootOrConfig); + + let wss = new WebSocket.Server({ port: 3001 }); + function broadcast(event: { type: string; [key: string]: any }) { + wss.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(event)); + } + }); + } + + function log(_message: string) { + let message = `💿 ${_message}`; + console.log(message); + broadcast({ type: "LOG", message }); + } + + signalExit( + await compiler2.watch(config, { + mode, + // TODO: esbuild compiler just blows up on syntax errors in the app + // onError(errorMessage) { + // console.error(errorMessage); + // }, + onRebuildStart() { + start = Date.now(); + onRebuildStart && onRebuildStart(); + log("Rebuilding..."); + }, + onRebuildFinish() { + log(`Rebuilt in ${prettyMs(Date.now() - start)}`); + broadcast({ type: "RELOAD" }); + }, + onFileCreated(file) { + log(`File created: ${path.relative(process.cwd(), file)}`); + }, + onFileChanged(file) { + log(`File changed: ${path.relative(process.cwd(), file)}`); + }, + onFileDeleted(file) { + log(`File deleted: ${path.relative(process.cwd(), file)}`); + } + }) + ); + + console.log(`💿 Built in ${prettyMs(Date.now() - start)}`); +} + +export function run2(remixRoot: string): Promise { + return watch2(remixRoot); +} + +/** + * Runs the built-in remix app server and dev asset server + */ +export async function run3(remixRoot: string) { + if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; + let config = await readConfig(remixRoot); + let getAppServer = require("@remix-run/serve/app"); + let port = process.env.PORT || 3000; + + getAppServer(config.serverBuildDirectory).listen(port, () => { + console.log(`Remix App Server started at http://localhost:${port}`); + }); + + watch2(config, BuildMode.Development, () => { + purgeAppRequireCache(config.serverBuildDirectory); + }); +} + +function purgeAppRequireCache(buildPath: string) { + for (let key in require.cache) { + if (key.startsWith(buildPath)) { + delete require.cache[key]; + } + } +} diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts new file mode 100644 index 0000000000..4fc52bb3b6 --- /dev/null +++ b/packages/remix-dev/compiler.ts @@ -0,0 +1,355 @@ +import fs from "fs"; +import path from "path"; +import type { + ExternalOption, + InputOption, + InputOptions, + OutputOptions, + Plugin, + RollupBuild, + RollupError, + RollupOutput, + TreeshakingOptions +} from "rollup"; +import * as rollup from "rollup"; +import babel from "@rollup/plugin-babel"; +import commonjs from "@rollup/plugin-commonjs"; +import json from "@rollup/plugin-json"; +import nodeResolve from "@rollup/plugin-node-resolve"; +import replace from "@rollup/plugin-replace"; +import { terser } from "rollup-plugin-terser"; + +import { BuildMode, BuildTarget } from "./build"; +import type { RemixConfig } from "./config"; +import { isImportHint } from "./compiler/importHints"; +import { ignorePackages } from "./compiler/browserIgnore"; + +import assetsManifest from "./compiler/rollup/assetsManifest"; +import clientServer from "./compiler/rollup/clientServer"; +import css from "./compiler/rollup/css"; +import img from "./compiler/rollup/img"; +import mdx from "./compiler/rollup/mdx"; +import remixConfig from "./compiler/rollup/remixConfig"; +import remixInputs from "./compiler/rollup/remixInputs"; +import routeModules from "./compiler/rollup/routeModules"; +import serverManifest from "./compiler/rollup/serverManifest"; +import url from "./compiler/rollup/url"; +import watchDirectory from "./compiler/rollup/watchDirectory"; + +export interface RemixBuild extends RollupBuild { + options: Required; +} + +export function createBuild( + rollupBuild: RollupBuild, + options: Required +): RemixBuild { + let build = (rollupBuild as unknown) as RemixBuild; + build.options = options; + return build; +} + +export interface BuildOptions { + mode?: BuildMode; + target?: BuildTarget; +} + +/** + * Runs the build. + */ +export async function build( + config: RemixConfig, + { + mode = BuildMode.Production, + target = BuildTarget.Server + }: BuildOptions = {} +): Promise { + let buildOptions = { mode, target }; + let plugins = [ + remixConfig({ rootDir: config.rootDirectory }), + ...getBuildPlugins(buildOptions) + ]; + + let rollupBuild = await rollup.rollup({ + external: getExternalOption(target), + treeshake: getTreeshakeOption(target), + onwarn: getOnWarnOption(target), + plugins + }); + + return createBuild(rollupBuild, buildOptions); +} + +export interface WatchOptions extends BuildOptions { + onBuildStart?: () => void; + onBuildEnd?: (build: RemixBuild) => void; + onError?: (error: RollupError) => void; +} + +/** + * Runs the build in watch mode. + */ +export function watch( + config: RemixConfig, + { + mode = BuildMode.Development, + target = BuildTarget.Browser, + onBuildStart, + onBuildEnd, + onError + }: WatchOptions = {} +): () => void { + let buildOptions = { mode, target }; + let plugins = [ + remixConfig({ rootDir: config.rootDirectory }), + // Watch for newly created route files. + watchDirectory({ dir: config.appDirectory }), + ...getBuildPlugins(buildOptions) + ]; + + let watcher = rollup.watch({ + external: getExternalOption(target), + treeshake: getTreeshakeOption(target), + onwarn: getOnWarnOption(target), + plugins, + watch: { + buildDelay: 100, + // Skip the write here and do it in a callback instead. This gives us + // a more consistent interface between `build` and `watch`. Both of them + // give you access to the raw build and let you do the generate/write + // step separately. + skipWrite: true + } + }); + + watcher.on("event", async event => { + if (event.code === "ERROR") { + if (onError) { + onError(event.error); + } else { + console.error(event.error); + } + } else if (event.code === "BUNDLE_START") { + if (onBuildStart) onBuildStart(); + } else if (event.code === "BUNDLE_END") { + if (onBuildEnd) { + let rollupBuild = event.result; + onBuildEnd(createBuild(rollupBuild, buildOptions)); + } + } + }); + + return () => { + watcher.close(); + }; +} + +/** + * Creates an in-memory build. This is useful in both the asset server and the + * main server in dev mode to avoid writing the builds to disk. + */ +export function generate(build: RemixBuild): Promise { + return build.generate(getOutputOptions(build)); +} + +/** + * Writes the build to disk. + */ +export function write(build: RemixBuild, dir: string): Promise { + return build.write({ ...getOutputOptions(build), dir }); +} + +//////////////////////////////////////////////////////////////////////////////// + +function isLocalModuleId(id: string): boolean { + return ( + // This is a relative id that hasn't been resolved yet, e.g. "./App" + id.startsWith(".") || + // This is an absolute filesystem path that has already been resolved, e.g. + // "/path/to/node_modules/react/index.js" + path.isAbsolute(id) + ); +} + +function getExternalOption(target: string): ExternalOption | undefined { + return target === BuildTarget.Server + ? (id: string) => + // We need to bundle @remix-run/react since it is ESM and we + // are building CommonJS output. + id !== "@remix-run/react" && + // Exclude non-local module identifiers from the server bundles. + // This includes identifiers like "react" which will be resolved + // dynamically at runtime using require(). + !isLocalModuleId(id) && + !isImportHint(id) + : // Exclude packages we know we don't want in the browser bundles. + // These *should* be stripped from the browser bundles anyway when + // tree-shaking kicks in, so making them external just saves Rollup + // some time having to load and parse them and their dependencies. + ignorePackages; +} + +function getInputOption(config: RemixConfig, target: BuildTarget): InputOption { + let input: InputOption = {}; + + if (target === BuildTarget.Browser) { + input["entry.client"] = path.resolve( + config.appDirectory, + config.entryClientFile + ); + } else if (target === BuildTarget.Server) { + input["entry.server"] = path.resolve( + config.appDirectory, + config.entryServerFile + ); + } + + for (let key of Object.keys(config.routes)) { + let route = config.routes[key]; + input[route.id] = path.resolve(config.appDirectory, route.file); + } + + return input; +} + +function getTreeshakeOption( + target: BuildTarget +): TreeshakingOptions | undefined { + return target === BuildTarget.Browser + ? // When building for the browser, we need to be very aggressive with code + // removal so we can be sure all imports of server-only code are removed. + { + moduleSideEffects(id) { + // Allow node_modules to have side effects. Everything else (all app + // modules) should be pure. This allows weird dependencies like + // "firebase/auth" to have side effects. + return /\bnode_modules\b/.test(id); + } + } + : undefined; +} + +function getOnWarnOption( + target: BuildTarget +): InputOptions["onwarn"] | undefined { + return target === BuildTarget.Browser + ? (warning, warn) => { + if (warning.code === "EMPTY_BUNDLE") { + // Ignore "Generated an empty chunk: blah" warnings when building for + // the browser. There may be quite a few of them because we are + // aggressively removing server-only packages from the build. + // TODO: Can we get Rollup to avoid generating these chunks entirely? + return; + } + + warn(warning); + } + : undefined; +} + +function getBuildPlugins({ mode, target }: Required): Plugin[] { + let plugins: Plugin[] = [ + remixInputs({ + getInput(config) { + return getInputOption(config, target); + } + }) + ]; + + plugins.push( + clientServer({ target }), + mdx(), + routeModules({ target }), + json(), + img({ target }), + css({ target, mode }), + url({ target }), + babel({ + babelHelpers: "bundled", + configFile: false, + exclude: /node_modules/, + extensions: [".md", ".mdx", ".js", ".jsx", ".ts", ".tsx"], + presets: [ + ["@babel/preset-react", { runtime: "automatic" }], + // TODO: Different targets for browsers vs. node. + ["@babel/preset-env", { bugfixes: true, targets: { node: "12" } }], + [ + "@babel/preset-typescript", + { + allExtensions: true, + isTSX: true + } + ] + ] + }), + nodeResolve({ + browser: target === BuildTarget.Browser, + extensions: [".js", ".json", ".jsx", ".ts", ".tsx"], + preferBuiltins: target !== BuildTarget.Browser + }), + commonjs() + ); + + if (target !== BuildTarget.Server) { + plugins.push( + replace({ + preventAssignment: true, + values: { + "process.env.NODE_ENV": JSON.stringify(mode) + } + }) + ); + } + + if (target !== BuildTarget.Server && mode === BuildMode.Production) { + plugins.push(terser({ ecma: 2017 })); + } + + if (target === BuildTarget.Browser) { + plugins.push(assetsManifest()); + } else if (target === BuildTarget.Server) { + plugins.push(serverManifest()); + } + + return plugins; +} + +function getOutputOptions(build: RemixBuild): OutputOptions { + let { mode, target } = build.options; + + return { + format: target === BuildTarget.Server ? "cjs" : "esm", + exports: target === BuildTarget.Server ? "named" : undefined, + assetFileNames: + mode === BuildMode.Production && target === BuildTarget.Browser + ? "[name]-[hash][extname]" + : "[name][extname]", + chunkFileNames: "_shared/[name]-[hash].js", + entryFileNames: + mode === BuildMode.Production && target === BuildTarget.Browser + ? "[name]-[hash].js" + : "[name].js", + manualChunks(id) { + return getNpmPackageName(id); + } + }; +} + +function getNpmPackageName(id: string): string | undefined { + let pieces = id.split(path.sep); + let index = pieces.lastIndexOf("node_modules"); + + if (index !== -1 && pieces.length > index + 1) { + let packageName = pieces[index + 1]; + + if (packageName.startsWith("@") && pieces.length > index + 2) { + packageName = + // S3 hates @folder, so we switch it to __ + packageName.replace("@", "__") + "/" + pieces[index + 2]; + } + + return packageName; + } + + return undefined; +} diff --git a/packages/remix-dev/compiler/browserIgnore.ts b/packages/remix-dev/compiler/browserIgnore.ts new file mode 100644 index 0000000000..d6d06422a9 --- /dev/null +++ b/packages/remix-dev/compiler/browserIgnore.ts @@ -0,0 +1,41 @@ +// This file is an optimization so that rollup won't try to bundle any of these +// modules, which greatly speeds up the browser tree-shaking +import builtins from "builtin-modules"; + +const remixServerPackages = [ + "@remix-run/architect", + "@remix-run/express", + "@remix-run/node", + "@remix-run/vercel" +]; + +const thirdPartyPackages = [ + "@databases/mysql", + "@databases/pg", + "@databases/sqlite", + "@prisma/client", + "apollo-server", + "better-sqlite3", + "bookshelf", + "dynamodb", + "firebase-admin", + "mariadb", + "mongoose", + "mysql", + "mysql2", + "pg", + "pg-hstore", + "pg-native", + "pg-pool", + "postgres", + "sequelize", + "sqlite", + "sqlite3", + "tedious" +]; + +export let ignorePackages = [ + ...builtins, + ...remixServerPackages, + ...thirdPartyPackages +]; diff --git a/packages/remix-dev/compiler/createUrl.ts b/packages/remix-dev/compiler/createUrl.ts new file mode 100644 index 0000000000..e6a52be318 --- /dev/null +++ b/packages/remix-dev/compiler/createUrl.ts @@ -0,0 +1,8 @@ +import * as path from "path"; + +export default function createUrl( + publicPath: string, + fileName: string +): string { + return publicPath + fileName.split(path.win32.sep).join("/"); +} diff --git a/packages/remix-dev/compiler/crypto.ts b/packages/remix-dev/compiler/crypto.ts new file mode 100644 index 0000000000..1e1cbd0478 --- /dev/null +++ b/packages/remix-dev/compiler/crypto.ts @@ -0,0 +1,39 @@ +import type { BinaryLike } from "crypto"; +import { createHash } from "crypto"; +import { createReadStream } from "fs"; +import type { OutputBundle } from "rollup"; + +export function getHash(source: BinaryLike): string { + return createHash("sha1").update(source).digest("hex"); +} + +export async function getFileHash(file: string): Promise { + return new Promise((accept, reject) => { + let hash = createHash("sha1"); + let stream = createReadStream(file); + + stream + .on("error", reject) + .on("data", data => { + hash.update(data); + }) + .on("end", () => { + accept(hash.digest("hex")); + }); + }); +} + +export function getBundleHash(bundle: OutputBundle): string { + let hash = createHash("sha1"); + + for (let key of Object.keys(bundle).sort()) { + let output = bundle[key]; + hash.update(output.type === "asset" ? output.source : output.code); + } + + return hash.digest("hex"); +} + +export function addHash(fileName: string, hash: string): string { + return fileName.replace(/(\.\w+)?$/, `-${hash}$1`); +} diff --git a/packages/remix-dev/compiler/importHints.ts b/packages/remix-dev/compiler/importHints.ts new file mode 100644 index 0000000000..28ae5cff76 --- /dev/null +++ b/packages/remix-dev/compiler/importHints.ts @@ -0,0 +1,5 @@ +const importHints = ["css:", "img:", "url:"]; + +export function isImportHint(id: string): boolean { + return importHints.some(hint => id.startsWith(hint)); +} diff --git a/packages/remix-dev/compiler/rollup/assetsManifest.ts b/packages/remix-dev/compiler/rollup/assetsManifest.ts new file mode 100644 index 0000000000..45b9f8389e --- /dev/null +++ b/packages/remix-dev/compiler/rollup/assetsManifest.ts @@ -0,0 +1,213 @@ +import path from "path"; +import { promises as fsp } from "fs"; +import type { OutputBundle, Plugin, RenderedModule } from "rollup"; + +import invariant from "../../invariant"; +import { getBundleHash } from "../crypto"; +import { routeModuleProxy, emptyRouteModule } from "./routeModules"; +import type { RemixConfig } from "./remixConfig"; +import { getRemixConfig } from "./remixConfig"; + +/** + * Generates 2 files: + * + * - An "assets manifest" file in the assets build directory that contains the + * URLs for all bundles needed in the browser + * - An `assets.json` file in the server build directory that contains the URL + * for the assets manifest file + */ +export default function assetsManifestPlugin({ + fileName = "manifest-[hash].js", + globalVar = "__remixManifest" +}: { + fileName?: string; + globalVar?: string; +} = {}): Plugin { + let config: RemixConfig; + + return { + name: "assetsManifest", + + async buildStart({ plugins }) { + config = await getRemixConfig(plugins); + }, + + async generateBundle(_options, bundle) { + let manifest = getAssetsManifest( + bundle, + config.routes, + config.publicPath + ); + + fileName = fileName.replace("[hash]", manifest.version); + + manifest.url = config.publicPath + fileName; + + // Emit the manifest for direct consumption by the browser. + let source = getGlobalScript(manifest, globalVar); + this.emitFile({ type: "asset", fileName, source }); + + // Write the manifest to the server build directory so it knows the asset + // URLs when server rendering and the URL of the manifest. + let assetsFile = path.join(config.serverBuildDirectory, "assets.json"); + await fsp.mkdir(path.dirname(assetsFile), { recursive: true }); + await fsp.writeFile(assetsFile, JSON.stringify(manifest, null, 2)); + } + }; +} + +interface AssetsManifest { + version: string; + url?: string; + entry: { + module: string; + imports: string[]; + }; + routes: { + [routeId: string]: { + id: string; + parentId?: string; + path: string; + caseSensitive?: boolean; + module: string; + imports?: string[]; + hasAction?: boolean; + hasLoader?: boolean; + }; + }; +} + +function getAssetsManifest( + bundle: OutputBundle, + routeManifest: RemixConfig["routes"], + publicPath: string +): AssetsManifest { + let version = getBundleHash(bundle).slice(0, 8); + + let routeIds = Object.keys(routeManifest); + let entry: AssetsManifest["entry"] | undefined; + let routes: AssetsManifest["routes"] = Object.create(null); + + for (let key in bundle) { + let chunk = bundle[key]; + if (chunk.type !== "chunk") continue; + + if (chunk.name === "entry.client") { + entry = { + module: publicPath + chunk.fileName, + imports: chunk.imports.map(path => publicPath + path) + }; + } else if ( + routeIds.includes(chunk.name) && + chunk.facadeModuleId?.endsWith(routeModuleProxy) + ) { + let route = routeManifest[chunk.name]; + + // When we build route modules, we put a shim in front that ends with a + // ?route-module-proxy string. Removing this suffix gets us back to the + // original source module id. + let sourceModuleId = chunk.facadeModuleId.replace(routeModuleProxy, ""); + + // Usually the source module will be contained in this chunk, but if + // someone imports a route module from within another route module, Rollup + // will place the source module in a shared chunk. So we have to go find + // the chunk with the source module in it. If the source module was empty, + // it will have the ?empty-route-module suffix on it. + let sourceModule = + chunk.modules[sourceModuleId] || + chunk.modules[sourceModuleId + emptyRouteModule] || + findRenderedModule(bundle, sourceModuleId) || + findRenderedModule(bundle, sourceModuleId + emptyRouteModule); + + invariant(sourceModule, `Cannot find source module for ${route.id}`); + + routes[route.id] = { + path: route.path, + caseSensitive: route.caseSensitive, + id: route.id, + parentId: route.parentId, + module: publicPath + chunk.fileName, + imports: chunk.imports.map(path => publicPath + path), + hasAction: sourceModule.removedExports.includes("action") + ? true + : // Using `undefined` here prevents this from showing up in the + // manifest JSON when there is no action. + undefined, + hasLoader: sourceModule.removedExports.includes("loader") + ? true + : // Using `undefined` here prevents this from showing up in the + // manifest JSON when there is no loader. + undefined + }; + } + } + + invariant(entry, `Missing entry.client chunk`); + + // Slim down the overall size of the manifest by pruning imports from child + // routes that their parents will have loaded already by the time they render. + optimizeRoutes(routes, entry.imports); + + return { version, entry, routes }; +} + +function findRenderedModule( + bundle: OutputBundle, + name: string +): RenderedModule | undefined { + for (let key in bundle) { + let chunk = bundle[key]; + if (chunk.type === "chunk" && name in chunk.modules) { + return chunk.modules[name]; + } + } +} + +type ImportsCache = { [routeId: string]: string[] }; + +function optimizeRoutes( + routes: AssetsManifest["routes"], + entryImports: string[] +): void { + // This cache is an optimization that allows us to avoid pruning the same + // route's imports more than once. + let importsCache: ImportsCache = Object.create(null); + + for (let key in routes) { + optimizeRouteImports(key, routes, entryImports, importsCache); + } +} + +function optimizeRouteImports( + routeId: string, + routes: AssetsManifest["routes"], + parentImports: string[], + importsCache: ImportsCache +): string[] { + if (importsCache[routeId]) return importsCache[routeId]; + + let route = routes[routeId]; + + if (route.parentId) { + parentImports = parentImports.concat( + optimizeRouteImports(route.parentId, routes, parentImports, importsCache) + ); + } + + let routeImports = (route.imports || []).filter( + url => !parentImports.includes(url) + ); + + // Setting `route.imports = undefined` prevents `imports: []` from showing up + // in the manifest JSON when there are no imports. + route.imports = routeImports.length > 0 ? routeImports : undefined; + + // Cache so the next lookup for this route is faster. + importsCache[routeId] = routeImports; + + return routeImports; +} + +function getGlobalScript(manifest: AssetsManifest, globalVar: string): string { + return `window.${globalVar} = ${JSON.stringify(manifest)}`; +} diff --git a/packages/remix-dev/compiler/rollup/clientServer.ts b/packages/remix-dev/compiler/rollup/clientServer.ts new file mode 100644 index 0000000000..de1396fdc7 --- /dev/null +++ b/packages/remix-dev/compiler/rollup/clientServer.ts @@ -0,0 +1,38 @@ +import type { Plugin } from "rollup"; + +import { BuildTarget } from "../../build"; +import empty from "./empty"; + +/** + * All file extensions we support for JavaScript modules. + */ +const moduleExts = [".md", ".mdx", ".js", ".jsx", ".ts", ".tsx"]; + +function isClientOnlyModuleId(id: string): boolean { + return moduleExts.some(ext => id.endsWith(`.client${ext}`)); +} + +function isServerOnlyModuleId(id: string): boolean { + return moduleExts.some(ext => id.endsWith(`.server${ext}`)); +} + +/** + * Rollup plugin that excludes `*.client.js` files from the server build and + * `*.server.js` files from the browser build. + */ +export default function clientServerPlugin({ + target +}: { + target: string; +}): Plugin { + return empty({ + isEmptyModuleId(id) { + if (/\bnode_modules\b/.test(id)) return false; + + return ( + (isClientOnlyModuleId(id) && target === BuildTarget.Server) || + (isServerOnlyModuleId(id) && target === BuildTarget.Browser) + ); + } + }); +} diff --git a/packages/remix-dev/compiler/rollup/css.ts b/packages/remix-dev/compiler/rollup/css.ts new file mode 100644 index 0000000000..a6d8dc7a62 --- /dev/null +++ b/packages/remix-dev/compiler/rollup/css.ts @@ -0,0 +1,108 @@ +import path from "path"; +import { promises as fsp } from "fs"; +import cacache from "cacache"; +import postcss from "postcss"; +import type Processor from "postcss/lib/processor"; +import type { Plugin } from "rollup"; +import prettyBytes from "pretty-bytes"; +import prettyMs from "pretty-ms"; + +import { BuildTarget } from "../../build"; +import createUrl from "../createUrl"; +import { getHash, addHash } from "../crypto"; +import type { RemixConfig } from "./remixConfig"; +import { getRemixConfig } from "./remixConfig"; + +export default function cssPlugin({ + target, + mode +}: { + target: string; + mode: string; +}): Plugin { + let config: RemixConfig; + let processor: Processor; + + return { + name: "css", + + async buildStart({ plugins }) { + config = await getRemixConfig(plugins); + + if (!processor) { + let postCssConfig = await getPostCssConfig(config.rootDirectory, mode); + processor = postcss(postCssConfig.plugins); + } + }, + + async resolveId(id, importer) { + if (!id.startsWith("css:")) return null; + + let resolved = await this.resolve(id.slice(4), importer, { + skipSelf: true + }); + + return resolved && `\0css:${resolved.id}`; + }, + + async load(id) { + if (!id.startsWith("\0css:")) return; + + let file = id.slice(5); + let originalSource = await fsp.readFile(file); + let hash = getHash(originalSource).slice(0, 8); + let fileName = addHash( + path.relative(config.appDirectory, file), + hash + ).replace(/(\.\w+)?$/, ".css"); + + this.addWatchFile(file); + + if (target === BuildTarget.Browser) { + let source: string | Uint8Array; + try { + let cached = await cacache.get(config.cacheDirectory, hash); + source = cached.data; + } catch (error) { + if (error.code !== "ENOENT") throw error; + source = await generateCssSource(file, originalSource, processor); + await cacache.put(config.cacheDirectory, hash, source); + } + + this.emitFile({ type: "asset", fileName, source }); + } + + return `export default ${JSON.stringify( + createUrl(config.publicPath, fileName) + )}`; + } + }; +} + +async function generateCssSource( + file: string, + content: Buffer, + processor: Processor +): Promise { + let start = Date.now(); + let result = await processor.process(content, { from: file }); + + console.log( + 'Built CSS for "%s", %s, %s', + path.basename(file), + prettyBytes(Buffer.byteLength(result.css)), + prettyMs(Date.now() - start) + ); + + return result.css; +} + +async function getPostCssConfig(appDirectory: string, mode: string) { + let requirePath = path.resolve(appDirectory, "postcss.config.js"); + try { + await fsp.access(requirePath); + return require(requirePath); + } catch (e) { + return { plugins: mode ? [] : [] }; + } +} diff --git a/packages/remix-dev/compiler/rollup/empty.ts b/packages/remix-dev/compiler/rollup/empty.ts new file mode 100644 index 0000000000..33ca61ad79 --- /dev/null +++ b/packages/remix-dev/compiler/rollup/empty.ts @@ -0,0 +1,24 @@ +import type { Plugin } from "rollup"; + +/** + * Rollup plugin that uses an empty shim for any module id that is considered + * "empty" according to the given `isEmptyModuleId` test function. + */ +export default function emptyPlugin({ + isEmptyModuleId +}: { + isEmptyModuleId: (id: string) => boolean; +}): Plugin { + return { + name: "empty", + + load(id) { + if (!isEmptyModuleId(id)) return null; + + return { + code: "export default {}", + syntheticNamedExports: true + }; + } + }; +} diff --git a/packages/remix-dev/compiler/rollup/img.ts b/packages/remix-dev/compiler/rollup/img.ts new file mode 100644 index 0000000000..8764d6070c --- /dev/null +++ b/packages/remix-dev/compiler/rollup/img.ts @@ -0,0 +1,270 @@ +import * as path from "path"; +import cacache from "cacache"; +import type { Plugin } from "rollup"; +import sharp from "sharp"; +import prettyBytes from "pretty-bytes"; +import prettyMs from "pretty-ms"; + +import invariant from "../../invariant"; +import { BuildTarget } from "../../build"; +import { addHash, getFileHash, getHash } from "../crypto"; +import type { RemixConfig } from "./remixConfig"; +import { getRemixConfig } from "./remixConfig"; + +// Don't use the sharp cache, we use Rollup's built-in cache so we don't process +// images between restarts of the dev server. Also, through some experimenting, +// the sharp cache seems to be based on filenames, not the content of the file, +// so replacing an image with a new one by the same name didn't work. +sharp.cache(false); + +const transparent1x1gif = + "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; + +const imageFormats = ["avif", "jpeg", "png", "webp"]; + +export default function imgPlugin({ target }: { target: string }): Plugin { + let config: RemixConfig; + + return { + name: "img", + + async buildStart({ plugins }) { + config = await getRemixConfig(plugins); + }, + + async resolveId(id, importer) { + if (!id.startsWith("img:")) return null; + + let { baseId, search } = parseId(id.slice(4)); + + let resolved = await this.resolve(baseId, importer, { skipSelf: true }); + + return resolved && `\0img:${resolved.id}${search}`; + }, + + async load(id) { + if (!id.startsWith("\0img:")) return; + + let { baseId: file, search } = parseId(id.slice(5)); + let params = new URLSearchParams(search); + let hash = (await getFileHash(file)).slice(0, 8); + + this.addWatchFile(file); + + let assets = await getImageAssets( + config.appDirectory, + file, + hash, + params + ); + + if (target === BuildTarget.Browser) { + for (let asset of assets) { + let { fileName, hash } = asset; + + let source: string | Uint8Array; + try { + let cached = await cacache.get(config.cacheDirectory, hash); + source = cached.data; + } catch (error) { + if (error.code !== "ENOENT") throw error; + source = await generateImageAssetSource(file, asset); + await cacache.put(config.cacheDirectory, hash, source); + } + + this.emitFile({ type: "asset", fileName, source }); + } + } + + let placeholder = + params.get("placeholder") != null + ? await generateImagePlaceholder(file, hash, config.cacheDirectory) + : transparent1x1gif; + + let images = assets.map(asset => ({ + src: config.publicPath + asset.fileName, + width: asset.width, + height: asset.height, + format: asset.transform.format + })); + + return ` + export let images = ${JSON.stringify(images, null, 2)}; + let primaryImage = images[images.length - 1]; + let srcset = images.map(image => image.src + " " + image.width + "w").join(","); + let placeholder = ${JSON.stringify(placeholder)}; + let mod = { ...primaryImage, srcset, placeholder }; + export default mod; + `; + } + }; +} + +function parseId(id: string): { baseId: string; search: string } { + let searchIndex = id.indexOf("?"); + return searchIndex === -1 + ? { baseId: id, search: "" } + : { + baseId: id.slice(0, searchIndex), + search: id.slice(searchIndex) + }; +} + +interface ImageTransform { + width?: number; + height?: number; + quality?: number; + format: string; +} + +function getImageTransforms( + params: URLSearchParams, + defaultFormat: string +): ImageTransform[] { + let width = params.get("width"); + let height = params.get("height"); + let quality = params.get("quality"); + let format = params.get("format") || defaultFormat; + + if (format === "jpg") { + format = "jpeg"; + } else if (!imageFormats.includes(format)) { + throw new Error(`Invalid image format: ${format}`); + } + + let transform = { + width: width ? parseInt(width, 10) : undefined, + height: height ? parseInt(height, 10) : undefined, + quality: quality ? parseInt(quality, 10) : undefined, + format + }; + + let srcset = params.get("srcset"); + + return srcset + ? srcset.split(",").map(width => ({ + ...transform, + width: parseInt(width, 10) + })) + : [transform]; +} + +interface ImageAsset { + fileName: string; + hash: string; + width: number; + height: number; + transform: ImageTransform; +} + +async function getImageAssets( + dir: string, + file: string, + sourceHash: string, + params: URLSearchParams +): Promise { + let defaultFormat = path.extname(file).slice(1); + let transforms = getImageTransforms(params, defaultFormat); + + return Promise.all( + transforms.map(async transform => { + let width: number; + let height: number; + + if (transform.width && transform.height) { + width = transform.width; + height = transform.height; + } else { + let meta = await sharp(file).metadata(); + + invariant( + typeof meta.width === "number" && typeof meta.height === "number", + `Cannot get image metadata: ${file}` + ); + + if (transform.width) { + width = transform.width; + height = Math.round(transform.width / (meta.width / meta.height)); + } else if (transform.height) { + width = Math.round(transform.height / (meta.height / meta.width)); + height = transform.height; + } else { + width = meta.width; + height = meta.height; + } + } + + let hash = getHash( + sourceHash + + transform.width + + transform.height + + transform.quality + + transform.format + ).slice(0, 8); + let fileName = addHash( + addHash(path.relative(dir, file), `${width}x${height}`), + hash + ); + + return { fileName, hash, width, height, transform }; + }) + ); +} + +async function generateImageAssetSource( + file: string, + asset: ImageAsset +): Promise { + let start = Date.now(); + let image = sharp(file); + + if (asset.width || asset.height) { + image.resize({ width: asset.width, height: asset.height }); + } + + // image.jpeg(), image.png(), etc. + // @ts-ignore + image[asset.transform.format]({ quality: asset.transform.quality }); + + let buffer = await image.toBuffer(); + + console.log( + 'Built image "%s", %s, %s', + asset.fileName, + prettyBytes(buffer.byteLength), + prettyMs(Date.now() - start) + ); + + return buffer; +} + +async function generateImagePlaceholder( + file: string, + hash: string, + cacheDir: string +): Promise { + let cacheKey = `placeholder-${hash}`; + + let buffer: Buffer; + try { + let cached = await cacache.get(cacheDir, cacheKey); + buffer = cached.data; + } catch (error) { + if (error.code !== "ENOENT") throw error; + + let start = Date.now(); + let image = sharp(file).resize({ width: 50 }).jpeg({ quality: 25 }); + buffer = await image.toBuffer(); + + console.log( + 'Built placeholder image for "%s", %s, %s', + path.basename(file), + prettyBytes(buffer.byteLength), + prettyMs(Date.now() - start) + ); + + await cacache.put(cacheDir, cacheKey, buffer); + } + + return `data:image/jpeg;base64,${buffer.toString("base64")}`; +} diff --git a/packages/remix-dev/compiler/rollup/mdx.ts b/packages/remix-dev/compiler/rollup/mdx.ts new file mode 100644 index 0000000000..6a383b28ed --- /dev/null +++ b/packages/remix-dev/compiler/rollup/mdx.ts @@ -0,0 +1,126 @@ +import { promises as fsp } from "fs"; +import * as path from "path"; +import cacache from "cacache"; +import type { Plugin } from "rollup"; +import parseFrontMatter from "front-matter"; +import mdx from "@mdx-js/mdx"; +import prettyMs from "pretty-ms"; + +import { getRemixConfig } from "./remixConfig"; +import { getHash } from "../crypto"; + +const imports = ` +import { mdx } from "@mdx-js/react"; +`; + +let regex = /\.mdx?$/; + +interface RemixFrontMatter { + meta?: { [name: string]: string }; + headers?: { [header: string]: string }; +} + +// They don't have types, so we could go figure it all out and add it as an +// interface here +export type MdxOptions = any; +export type MdxFunctionOption = ( + attributes: { [key: string]: any }, + filename: string +) => MdxOptions; + +export type MdxConfig = MdxFunctionOption | MdxOptions; + +/** + * Loads .mdx files as JavaScript modules with support for Remix's `headers` + * and `meta` route module functions as static object declarations in the + * frontmatter. + */ +export default function mdxPlugin({ + mdxConfig: mdxConfigArg, + cache: cacheArg +}: { + mdxConfig?: MdxConfig; + cache?: string; +} = {}): Plugin { + let mdxConfig: MdxConfig; + let cache: string; + + return { + name: "mdx", + + async buildStart({ plugins }) { + let config = await getRemixConfig(plugins); + mdxConfig = mdxConfigArg || config.mdx; + cache = cacheArg || config.cacheDirectory; + }, + + async load(id) { + if (id.startsWith("\0") || !regex.test(id)) return null; + + let file = id; + let source = await fsp.readFile(file, "utf-8"); + let hash = getHash(source).slice(0, 8); + + let code: string; + if (cache) { + try { + let cached = await cacache.get(cache, hash); + code = cached.data.toString("utf-8"); + } catch (error) { + if (error.code !== "ENOENT") throw error; + code = await generateRouteModule(file, source, mdxConfig); + await cacache.put(cache, hash, code); + } + } else { + code = await generateRouteModule(file, source, mdxConfig); + } + + return code; + } + }; +} + +async function generateRouteModule( + file: string, + source: string, + mdxConfig: MdxConfig +): Promise { + let start = Date.now(); + + let { + body, + attributes + }: { + body: string; + attributes: RemixFrontMatter; + } = parseFrontMatter(source); + + let code = imports; + + if (attributes && attributes.meta) { + code += `export function meta() { return ${JSON.stringify( + attributes.meta + )}}\n`; + } + + if (attributes && attributes.headers) { + code += `export function headers() { return ${JSON.stringify( + attributes.headers + )}}\n`; + } + + let mdxOptions = + typeof mdxConfig === "function" ? mdxConfig(attributes, file) : mdxConfig; + + code += await mdx(body, mdxOptions); + + if (process.env.NODE_ENV !== "test") { + console.log( + 'Built MDX for "%s", %s', + path.basename(file), + prettyMs(Date.now() - start) + ); + } + + return code; +} diff --git a/packages/remix-dev/compiler/rollup/remixConfig.ts b/packages/remix-dev/compiler/rollup/remixConfig.ts new file mode 100644 index 0000000000..9f24d226a2 --- /dev/null +++ b/packages/remix-dev/compiler/rollup/remixConfig.ts @@ -0,0 +1,52 @@ +import path from "path"; +import type { Plugin } from "rollup"; + +import type { RemixConfig } from "../../config"; +import { readConfig } from "../../config"; +import invariant from "../../invariant"; +import { purgeModuleCache } from "../../modules"; + +export type { RemixConfig }; + +export default function remixConfigPlugin({ + rootDir +}: { + rootDir: string; +}): Plugin { + let configPromise: Promise | null = null; + + return { + name: "remixConfig", + + options(options) { + configPromise = null; + return options; + }, + + buildStart() { + this.addWatchFile(path.join(rootDir, "remix.config.js")); + }, + + api: { + getConfig(): Promise { + if (!configPromise) { + // Purge the cache in case remix.config.js loads any other files. + purgeModuleCache(rootDir); + configPromise = readConfig(rootDir); + } + + return configPromise; + } + } + }; +} + +export function findConfigPlugin(plugins?: Plugin[]): Plugin | undefined { + return plugins && plugins.find(plugin => plugin.name === "remixConfig"); +} + +export function getRemixConfig(plugins?: Plugin[]): Promise { + let plugin = findConfigPlugin(plugins); + invariant(plugin, `Missing remixConfig plugin`); + return plugin.api.getConfig(); +} diff --git a/packages/remix-dev/compiler/rollup/remixInputs.ts b/packages/remix-dev/compiler/rollup/remixInputs.ts new file mode 100644 index 0000000000..e5d6e87496 --- /dev/null +++ b/packages/remix-dev/compiler/rollup/remixInputs.ts @@ -0,0 +1,22 @@ +import type { InputOption, Plugin } from "rollup"; + +import type { RemixConfig } from "./remixConfig"; +import { getRemixConfig } from "./remixConfig"; + +/** + * Enables setting the compiler's input dynamically via a hook function. + */ +export default function remixInputsPlugin({ + getInput +}: { + getInput: (config: RemixConfig) => InputOption; +}): Plugin { + return { + name: "remixInputs", + + async options(options) { + let config = await getRemixConfig(options.plugins || []); + return { ...options, input: getInput(config) }; + } + }; +} diff --git a/packages/remix-dev/compiler/rollup/routeModules.ts b/packages/remix-dev/compiler/rollup/routeModules.ts new file mode 100644 index 0000000000..d8d7bbe5cf --- /dev/null +++ b/packages/remix-dev/compiler/rollup/routeModules.ts @@ -0,0 +1,111 @@ +import * as fs from "fs"; +import type { Plugin } from "rollup"; + +import { BuildTarget } from "../../build"; +import { getRemixConfig } from "./remixConfig"; + +export const routeModuleProxy = "?route-module-proxy"; +export const emptyRouteModule = "?empty-route-module"; + +/** + * A resolver/loader for route modules that does a few things: + * + * - when building for the browser, it excludes server-only code from the build + * - when new route files are created in development (watch) mode, it creates + * an empty shim for the module so Rollup doesn't complain and the build + * doesn't break + */ +export default function routeModulesPlugin({ + target +}: { + target: string; +}): Plugin { + return { + name: "routeModules", + + async options(options) { + let input = options.input; + + if (input && typeof input === "object" && !Array.isArray(input)) { + let config = await getRemixConfig(options.plugins); + let routeIds = Object.keys(config.routes); + + for (let alias in input) { + if (routeIds.includes(alias)) { + input[alias] = input[alias] + routeModuleProxy; + } + } + } + + return options; + }, + + async resolveId(id, importer) { + if (id.endsWith(routeModuleProxy) || id.endsWith(emptyRouteModule)) { + return id; + } + + if ( + importer && + importer.endsWith(routeModuleProxy) && + importer.slice(0, -routeModuleProxy.length) === id + ) { + let resolved = await this.resolve(id, importer, { skipSelf: true }); + + if (resolved) { + if (isEmptyFile(resolved.id)) { + resolved.id = resolved.id + emptyRouteModule; + } + + // Using syntheticNamedExports here prevents Rollup from complaining + // when the route source module may not have some of the properties + // we explicitly list in the proxy module. + resolved.syntheticNamedExports = true; + + return resolved; + } + } + + return null; + }, + + load(id) { + if (id.endsWith(emptyRouteModule)) { + let source = id.slice(0, -emptyRouteModule.length); + + this.addWatchFile(source); + + // In a new file, default to an empty component. This prevents errors in + // dev (watch) mode when creating new routes. + return `export default function () { throw new Error('Route "${source}" is empty, put a default export in there!') }`; + } + + if (id.endsWith(routeModuleProxy)) { + let source = id.slice(0, -routeModuleProxy.length); + + if (target === BuildTarget.Browser) { + // Create a proxy module that re-exports only the things we want to be + // available in the browser. All the rest will be tree-shaken out so + // we don't end up with server-only code (and its dependencies) in the + // browser bundles. + return `export { ErrorBoundary, default, handle, links, meta } from ${JSON.stringify( + source + )};`; + } + + // Create a proxy module that transparently re-exports everything from + // the original module. + return ( + `export { default } from ${JSON.stringify(source)};\n` + + `export * from ${JSON.stringify(source)};` + ); + } + + return null; + } + }; +} + +function isEmptyFile(file: string): boolean { + return fs.existsSync(file) && fs.statSync(file).size === 0; +} diff --git a/packages/remix-dev/compiler/rollup/serverManifest.ts b/packages/remix-dev/compiler/rollup/serverManifest.ts new file mode 100644 index 0000000000..2b631d39ad --- /dev/null +++ b/packages/remix-dev/compiler/rollup/serverManifest.ts @@ -0,0 +1,121 @@ +import path from "path"; +import type { OutputBundle, Plugin } from "rollup"; + +import invariant from "../../invariant"; +import { getBundleHash } from "../crypto"; +import type { RemixConfig } from "./remixConfig"; +import { getRemixConfig } from "./remixConfig"; + +/** + * Generates a server module that loads all build artifacts. + */ +export default function serverManifestPlugin({ + fileName = "index.js" +}: { + fileName?: string; +} = {}): Plugin { + let config: RemixConfig; + + return { + name: "serverManifest", + + async buildStart({ plugins }) { + config = await getRemixConfig(plugins); + }, + + async generateBundle(_options, bundle) { + let manifest = getServerManifest(bundle, config.routes); + let source = getCommonjsModule(manifest); + this.emitFile({ type: "asset", fileName, source }); + } + }; +} + +interface ServerManifest { + version: string; + assets: { + moduleId: string; + }; + entry: { + moduleId: string; + }; + routes: { + [routeId: string]: { + id: string; + parentId?: string; + path: string; + caseSensitive?: boolean; + moduleId: string; + }; + }; +} + +function getServerManifest( + bundle: OutputBundle, + routeManifest: RemixConfig["routes"] +): ServerManifest { + let version = getBundleHash(bundle).slice(0, 8); + + let relModuleIdPrefix = "." + path.sep; + let assets = { + moduleId: relModuleIdPrefix + "assets.json" + }; + + let routeIds = Object.keys(routeManifest); + let entry: ServerManifest["entry"] | undefined; + let routes: ServerManifest["routes"] = Object.create(null); + + for (let key in bundle) { + let chunk = bundle[key]; + + if (chunk.type === "chunk") { + if (chunk.name === "entry.server") { + entry = { + moduleId: relModuleIdPrefix + chunk.fileName + }; + } else if (routeIds.includes(chunk.name)) { + let route = routeManifest[chunk.name]; + + routes[chunk.name] = { + id: route.id, + parentId: route.parentId, + path: route.path, + caseSensitive: route.caseSensitive, + moduleId: relModuleIdPrefix + chunk.fileName + }; + } + } + } + + invariant(entry, `Missing entry.server chunk`); + + return { version, assets, entry, routes }; +} + +function getCommonjsModule(manifest: ServerManifest): string { + return ( + `module.exports = { + "version": ${JSON.stringify(manifest.version)}, + "assets": require(${JSON.stringify(manifest.assets.moduleId)}), + "entry": { + "module": require(${JSON.stringify(manifest.entry.moduleId)}) + }, + "routes": { + ` + + Object.keys(manifest.routes) + .map(key => { + let route = manifest.routes[key]; + return `${JSON.stringify(route.id)}: { + "id": ${JSON.stringify(route.id)}, + "parentId": ${JSON.stringify(route.parentId)}, + "path": ${JSON.stringify(route.path)}, + "caseSensitive": ${JSON.stringify(route.caseSensitive)}, + "module": require(${JSON.stringify(route.moduleId)}) + }`; + }) + .join(",\n ") + + ` + } +}` + ); +} diff --git a/packages/remix-dev/compiler/rollup/url.ts b/packages/remix-dev/compiler/rollup/url.ts new file mode 100644 index 0000000000..cd6db197d0 --- /dev/null +++ b/packages/remix-dev/compiler/rollup/url.ts @@ -0,0 +1,52 @@ +import path from "path"; +import { promises as fsp } from "fs"; +import type { Plugin } from "rollup"; + +import { BuildTarget } from "../../build"; +import createUrl from "../createUrl"; +import { getHash, addHash } from "../crypto"; +import type { RemixConfig } from "./remixConfig"; +import { getRemixConfig } from "./remixConfig"; + +export default function urlPlugin({ target }: { target: string }): Plugin { + let config: RemixConfig; + + return { + name: "url", + + async buildStart({ plugins }) { + config = await getRemixConfig(plugins); + }, + + async resolveId(id, importer) { + if (!id.startsWith("url:")) return null; + + let resolved = await this.resolve(id.slice(4), importer, { + skipSelf: true + }); + + return resolved && `\0url:${resolved.id}`; + }, + + async load(id) { + if (!id.startsWith("\0url:")) return; + + let file = id.slice(5); + let source = await fsp.readFile(file); + let fileName = addHash( + path.relative(config.appDirectory, file), + getHash(source).slice(0, 8) + ); + + this.addWatchFile(file); + + if (target === BuildTarget.Browser) { + this.emitFile({ type: "asset", fileName, source }); + } + + return `export default ${JSON.stringify( + createUrl(config.publicPath, fileName) + )}`; + } + }; +} diff --git a/packages/remix-dev/compiler/rollup/watchDirectory.ts b/packages/remix-dev/compiler/rollup/watchDirectory.ts new file mode 100644 index 0000000000..f691912ebe --- /dev/null +++ b/packages/remix-dev/compiler/rollup/watchDirectory.ts @@ -0,0 +1,49 @@ +import { promises as fsp } from "fs"; +import type { Plugin } from "rollup"; +import chokidar from "chokidar"; +import tmp from "tmp"; + +/** + * Triggers a rebuild whenever anything in the given `dir` changes, including + * adding new files. + */ +export default function watchDirectoryPlugin({ dir }: { dir: string }): Plugin { + let tmpfile = tmp.fileSync(); + let startedWatcher = false; + + function startWatcher() { + return new Promise((accept, reject) => { + chokidar + .watch(dir, { + ignoreInitial: true, + ignored: /node_modules/, + followSymlinks: false + }) + .on("add", triggerRebuild) + .on("ready", accept) + .on("error", reject); + }); + } + + async function triggerRebuild() { + let now = new Date(); + await fsp.utimes(tmpfile.name, now, now); + } + + return { + name: "watchDirectory", + + async buildStart() { + // We have to use our own watcher because `this.addWatchFile` does not + // listen for the `add` event, and we want to know when new files show up + // in the `app` directory. + // See https://github.com/rollup/rollup/issues/3704 + if (!startedWatcher) { + await startWatcher(); + startedWatcher = true; + } + + this.addWatchFile(tmpfile.name); + } + }; +} diff --git a/packages/remix-dev/compiler2.ts b/packages/remix-dev/compiler2.ts new file mode 100644 index 0000000000..ac21039078 --- /dev/null +++ b/packages/remix-dev/compiler2.ts @@ -0,0 +1,521 @@ +import { promises as fsp } from "fs"; +import * as path from "path"; +import { builtinModules as nodeBuiltins } from "module"; +import * as esbuild from "esbuild"; +import debounce from "lodash.debounce"; +import chokidar from "chokidar"; + +import { BuildMode, BuildTarget } from "./build"; +import type { RemixConfig } from "./config"; +import { readConfig } from "./config"; +import invariant from "./invariant"; +import { warnOnce } from "./warnings"; +import { createAssetsManifest } from "./compiler2/assets"; +import { getAppDependencies } from "./compiler2/dependencies"; +import { loaders, getLoaderForFile } from "./compiler2/loaders"; +import { getRouteModuleExportsCached } from "./compiler2/routes"; +import { writeFileSafe } from "./compiler2/utils/fs"; + +// When we build Remix, this shim file is copied directly into the output +// directory in the same place relative to this file. It is eventually injected +// as a source file when building the app. +const reactShim = path.resolve(__dirname, "compiler2/shims/react.ts"); + +interface BuildConfig { + mode: BuildMode; + target: BuildTarget; +} + +function defaultWarningHandler(message: string, key: string) { + warnOnce(false, message, key); +} + +function defaultErrorHandler(message: string) { + console.error(message); +} + +interface BuildOptions extends Partial { + onWarning?(message: string, key: string): void; + onError?(message: string): void; +} + +export async function build( + config: RemixConfig, + { + mode = BuildMode.Production, + target = BuildTarget.Node14, + onWarning = defaultWarningHandler, + onError = defaultErrorHandler + }: BuildOptions = {} +): Promise { + await buildEverything(config, { mode, target, onWarning, onError }); +} + +interface WatchOptions extends BuildOptions { + onRebuildStart?(): void; + onRebuildFinish?(): void; + onFileCreated?(file: string): void; + onFileChanged?(file: string): void; + onFileDeleted?(file: string): void; +} + +export async function watch( + config: RemixConfig, + { + mode = BuildMode.Development, + target = BuildTarget.Node14, + onWarning = defaultWarningHandler, + onError = defaultErrorHandler, + onRebuildStart, + onRebuildFinish, + onFileCreated, + onFileChanged, + onFileDeleted + }: WatchOptions = {} +): Promise<() => void> { + let options = { mode, target, onWarning, onError, incremental: true }; + let [browserBuild, serverBuild] = await buildEverything(config, options); + + async function disposeBuilders() { + await Promise.all([ + browserBuild.rebuild?.dispose(), + serverBuild.rebuild?.dispose() + ]); + } + + let restartBuilders = debounce(async (newConfig?: RemixConfig) => { + await disposeBuilders(); + config = newConfig || (await readConfig(config.rootDirectory)); + if (onRebuildStart) onRebuildStart(); + let builders = await buildEverything(config, options); + if (onRebuildFinish) onRebuildFinish(); + browserBuild = builders[0]; + serverBuild = builders[1]; + }, 500); + + let rebuildEverything = debounce(async () => { + if (onRebuildStart) onRebuildStart(); + await Promise.all([ + browserBuild.rebuild!().then(build => + generateManifests(config, build.metafile!) + ), + serverBuild.rebuild!() + ]); + if (onRebuildFinish) onRebuildFinish(); + }, 100); + + let watcher = chokidar + .watch(config.appDirectory, { + persistent: true, + ignoreInitial: true, + awaitWriteFinish: { + stabilityThreshold: 100, + pollInterval: 100 + } + }) + .on("error", error => console.error(error)) + .on("change", async file => { + if (onFileChanged) onFileChanged(file); + await rebuildEverything(); + }) + .on("add", async file => { + if (onFileCreated) onFileCreated(file); + let newConfig = await readConfig(config.rootDirectory); + if (isEntryPoint(newConfig, file)) { + await restartBuilders(newConfig); + } else { + await rebuildEverything(); + } + }) + .on("unlink", async file => { + if (onFileDeleted) onFileDeleted(file); + if (isEntryPoint(config, file)) { + await restartBuilders(); + } else { + await rebuildEverything(); + } + }); + + return async () => { + await watcher.close(); + await disposeBuilders(); + }; +} + +function isEntryPoint(config: RemixConfig, file: string) { + let appFile = path.relative(config.appDirectory, file); + + if ( + appFile === config.entryClientFile || + appFile === config.entryServerFile + ) { + return true; + } + for (let key in config.routes) { + if (appFile === config.routes[key].file) return true; + } + + return false; +} + +/////////////////////////////////////////////////////////////////////////////// + +async function buildEverything( + config: RemixConfig, + options: Required & { incremental?: boolean } +): Promise { + // TODO: + // When building for node, we build both the browser and server builds in + // parallel and emit the asset manifest as a separate file in the output + // directory. + // When building for Cloudflare Workers, we need to run the browser and server + // builds serially so we can inline the asset manifest into the server build + // in a single JavaScript file. + + let browserBuildPromise = createBrowserBuild(config, options); + let serverBuildPromise = createServerBuild(config, options); + + return Promise.all([ + browserBuildPromise.then(async build => { + await generateManifests(config, build.metafile!); + return build; + }), + serverBuildPromise + ]); +} + +async function createBrowserBuild( + config: RemixConfig, + options: BuildOptions & { incremental?: boolean } +): Promise { + // For the browser build, exclude node built-ins that don't have a + // browser-safe alternative installed in node_modules. Nothing should + // *actually* be external in the browser build (we want to bundle all deps) so + // this is really just making sure we don't accidentally have any dependencies + // on node built-ins in browser bundles. + let dependencies = Object.keys(await getAppDependencies(config)); + let externals = nodeBuiltins.filter(mod => !dependencies.includes(mod)); + + let entryPoints: esbuild.BuildOptions["entryPoints"] = { + "entry.client": path.resolve(config.appDirectory, config.entryClientFile) + }; + for (let id of Object.keys(config.routes)) { + // All route entry points are virtual modules that will be loaded by the + // browserEntryPointsPlugin. This allows us to tree-shake server-only code + // that we don't want to run in the browser (i.e. action & loader). + entryPoints[id] = + path.resolve(config.appDirectory, config.routes[id].file) + "?browser"; + } + + return esbuild.build({ + entryPoints, + outdir: config.assetsBuildDirectory, + format: "esm", + external: externals, + inject: [reactShim], + loader: loaders, + bundle: true, + splitting: true, + metafile: true, + incremental: options.incremental, + minify: options.mode === BuildMode.Production, + entryNames: "[dir]/[name]-[hash]", + chunkNames: "_shared/[name]-[hash]", + assetNames: "_assets/[name]-[hash]", + publicPath: config.publicPath, + define: { + "process.env.NODE_ENV": JSON.stringify(options.mode) + }, + plugins: [ + browserRouteModulesPlugin(config, /\?browser$/), + emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/) + ] + }); +} + +async function createServerBuild( + config: RemixConfig, + options: Required & { incremental?: boolean } +): Promise { + let dependencies = Object.keys(await getAppDependencies(config)); + + return esbuild.build({ + stdin: { + contents: getServerEntryPointModule(config, options), + resolveDir: config.serverBuildDirectory + }, + outfile: path.resolve(config.serverBuildDirectory, "index.js"), + format: "cjs", + platform: "node", + target: options.target, + inject: [reactShim], + loader: loaders, + bundle: true, + incremental: options.incremental, + // The server build needs to know how to generate asset URLs for imports + // of CSS and other files. + assetNames: "_assets/[name]-[hash]", + publicPath: config.publicPath, + plugins: [ + serverRouteModulesPlugin(config), + emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), + manualExternalsPlugin((id, importer) => { + // assets.json is external because this build runs in parallel with the + // browser build and it's not there yet. + if (id === "./assets.json" && importer === "") return true; + + // We need to bundle @remix-run/react because it is ESM and we can't + // require it from the CommonJS output. + if (id === "@remix-run/react") return false; + + // Mark all bare imports as external. They will be require()'d at + // runtime from node_modules. + if (isBareModuleId(id)) { + let packageName = getNpmPackageName(id); + if ( + !/\bnode_modules\b/.test(importer) && + !nodeBuiltins.includes(packageName) && + !dependencies.includes(packageName) + ) { + options.onWarning( + `The path "${id}" is imported in ` + + `${path.relative(process.cwd(), importer)} but ` + + `${packageName} is not listed in your package.json dependencies. ` + + `Did you forget to install it?`, + packageName + ); + } + return true; + } + + return false; + }) + ] + }); +} + +function isBareModuleId(id: string): boolean { + return !id.startsWith(".") && !path.isAbsolute(id); +} + +function getNpmPackageName(id: string): string { + let split = id.split("/"); + let packageName = split[0]; + if (packageName.startsWith("@")) packageName += `/${split[1]}`; + return packageName; +} + +async function generateManifests( + config: RemixConfig, + metafile: esbuild.Metafile +): Promise { + let assetsManifest = await createAssetsManifest(config, metafile); + + let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; + assetsManifest.url = config.publicPath + filename; + + return Promise.all([ + writeFileSafe( + path.join(config.assetsBuildDirectory, filename), + `window.__remixManifest=${JSON.stringify(assetsManifest)}` + ), + writeFileSafe( + path.join(config.serverBuildDirectory, "assets.json"), + JSON.stringify(assetsManifest, null, 2) + ) + ]); +} + +function getServerEntryPointModule( + config: RemixConfig, + options: BuildOptions +): string { + switch (options.target) { + case BuildTarget.Node14: + return ` +import * as entryServer from ${JSON.stringify( + path.resolve(config.appDirectory, config.entryServerFile) + )}; +${Object.keys(config.routes) + .map((key, index) => { + let route = config.routes[key]; + return `import * as route${index} from ${JSON.stringify( + path.resolve(config.appDirectory, route.file) + )};`; + }) + .join("\n")} +export { default as assets } from "./assets.json"; +export const entry = { module: entryServer }; +export const routes = { + ${Object.keys(config.routes) + .map((key, index) => { + let route = config.routes[key]; + return `${JSON.stringify(key)}: { + id: ${JSON.stringify(route.id)}, + parentId: ${JSON.stringify(route.parentId)}, + path: ${JSON.stringify(route.path)}, + caseSensitive: ${JSON.stringify(route.caseSensitive)}, + module: route${index} + }`; + }) + .join(",\n ")} +};`; + default: + throw new Error( + `Cannot generate server entry point module for target: ${options.target}` + ); + } +} + +type Route = RemixConfig["routes"][string]; + +const browserSafeRouteExports: { [name: string]: boolean } = { + ErrorBoundary: true, + default: true, + handle: true, + links: true, + meta: true +}; + +/** + * This plugin loads route modules for the browser build, using module shims + * that re-export only the route module exports that are safe for the browser. + */ +function browserRouteModulesPlugin( + config: RemixConfig, + suffixMatcher: RegExp +): esbuild.Plugin { + return { + name: "browser-route-modules", + async setup(build) { + let routesByFile: Map = Object.keys(config.routes).reduce( + (map, key) => { + let route = config.routes[key]; + map.set(path.resolve(config.appDirectory, route.file), route); + return map; + }, + new Map() + ); + + build.onResolve({ filter: suffixMatcher }, args => { + return { path: args.path, namespace: "browser-route-module" }; + }); + + build.onLoad( + { filter: suffixMatcher, namespace: "browser-route-module" }, + async args => { + let file = args.path.replace(suffixMatcher, ""); + let route = routesByFile.get(file); + invariant(route, `Cannot get route by path: ${args.path}`); + + let exports = ( + await getRouteModuleExportsCached(config, route.id) + ).filter(ex => !!browserSafeRouteExports[ex]); + let spec = exports.length > 0 ? `{ ${exports.join(", ")} }` : "*"; + let contents = `export ${spec} from ${JSON.stringify(file)};`; + + return { + contents, + resolveDir: path.dirname(file), + loader: "js" + }; + } + ); + } + }; +} + +/** + * This plugin substitutes an empty module for any modules in the `app` + * directory that match the given `filter`. + */ +function emptyModulesPlugin( + config: RemixConfig, + filter: RegExp +): esbuild.Plugin { + return { + name: "empty-modules", + setup(build) { + build.onResolve({ filter }, args => { + let resolved = path.resolve(args.resolveDir, args.path); + if ( + // Limit this behavior to modules found in only the `app` directory. + // This allows node_modules to use the `.server.js` and `.client.js` + // naming conventions with different semantics. + resolved.startsWith(config.appDirectory) + ) { + return { path: args.path, namespace: "empty-module" }; + } + }); + + build.onLoad({ filter: /.*/, namespace: "empty-module" }, () => { + return { + // Use an empty CommonJS module here instead of ESM to avoid "No + // matching export" errors in esbuild for stuff that is imported + // from this file. + contents: "module.exports = {};", + loader: "js" + }; + }); + } + }; +} + +/** + * This plugin loads route modules for the server build. + */ +function serverRouteModulesPlugin(config: RemixConfig): esbuild.Plugin { + return { + name: "server-route-modules", + setup(build) { + let routeFiles = new Set( + Object.keys(config.routes).map(key => + path.resolve(config.appDirectory, config.routes[key].file) + ) + ); + + build.onResolve({ filter: /.*/ }, args => { + if (routeFiles.has(args.path)) { + return { path: args.path, namespace: "route-module" }; + } + }); + + build.onLoad({ filter: /.*/, namespace: "route-module" }, async args => { + let file = args.path; + let contents = await fsp.readFile(file, "utf-8"); + + // Default to `export {}` if the file is empty so esbuild interprets + // this file as ESM instead of CommonJS with `default: {}`. This helps + // in development when creating new files. + // See https://github.com/evanw/esbuild/issues/1043 + if (!/\S/.test(contents)) { + return { contents: "export {}", loader: "js" }; + } + + return { + contents, + resolveDir: path.dirname(file), + loader: getLoaderForFile(file) + }; + }); + } + }; +} + +/** + * This plugin marks paths external using a callback function. + */ +function manualExternalsPlugin( + isExternal: (id: string, importer: string) => boolean +): esbuild.Plugin { + return { + name: "manual-externals", + setup(build) { + build.onResolve({ filter: /.*/ }, args => { + if (isExternal(args.path, args.importer)) { + return { path: args.path, external: true }; + } + }); + } + }; +} diff --git a/packages/remix-dev/compiler2/assets.ts b/packages/remix-dev/compiler2/assets.ts new file mode 100644 index 0000000000..9b6156db4a --- /dev/null +++ b/packages/remix-dev/compiler2/assets.ts @@ -0,0 +1,148 @@ +import * as path from "path"; +import * as esbuild from "esbuild"; + +import type { RemixConfig } from "../config"; +import invariant from "../invariant"; +import { getRouteModuleExportsCached } from "./routes"; +import { getHash } from "./utils/crypto"; +import { createUrl } from "./utils/url"; + +type Route = RemixConfig["routes"][string]; + +interface AssetsManifest { + version: string; + url?: string; + entry: { + module: string; + imports: string[]; + }; + routes: { + [routeId: string]: { + id: string; + parentId?: string; + path: string; + caseSensitive?: boolean; + module: string; + imports?: string[]; + hasAction?: boolean; + hasLoader?: boolean; + }; + }; +} + +export async function createAssetsManifest( + config: RemixConfig, + metafile: esbuild.Metafile +): Promise { + function resolveUrl(outputPath: string): string { + return createUrl( + config.publicPath, + path.relative(config.assetsBuildDirectory, path.resolve(outputPath)) + ); + } + + function resolveImports( + imports: esbuild.Metafile["outputs"][string]["imports"] + ): string[] { + return imports + .filter(im => im.kind === "import-statement") + .map(im => resolveUrl(im.path)); + } + + let entryClientFile = path.resolve( + config.appDirectory, + config.entryClientFile + ); + let routesByFile: Map = Object.keys(config.routes).reduce( + (map, key) => { + let route = config.routes[key]; + map.set(path.resolve(config.appDirectory, route.file), route); + return map; + }, + new Map() + ); + + let entry: AssetsManifest["entry"] | undefined; + let routes: AssetsManifest["routes"] = {}; + + for (let key of Object.keys(metafile.outputs).sort()) { + let output = metafile.outputs[key]; + if (!output.entryPoint) continue; + + let entryPointFile = path.resolve( + output.entryPoint.replace(/(^browser-route-module:|\?browser$)/g, "") + ); + if (entryPointFile === entryClientFile) { + entry = { + module: resolveUrl(key), + imports: resolveImports(output.imports) + }; + } else { + let route = routesByFile.get(entryPointFile); + invariant(route, `Cannot get route for entry point ${output.entryPoint}`); + let sourceExports = await getRouteModuleExportsCached(config, route.id); + routes[route.id] = { + id: route.id, + parentId: route.parentId, + path: route.path, + caseSensitive: route.caseSensitive, + module: resolveUrl(key), + imports: resolveImports(output.imports), + hasAction: sourceExports.includes("action"), + hasLoader: sourceExports.includes("loader") + }; + } + } + + invariant(entry, `Missing output for entry point`); + + optimizeRoutes(routes, entry.imports); + let version = getHash(JSON.stringify({ entry, routes })).slice(0, 8); + + return { version, entry, routes }; +} + +type ImportsCache = { [routeId: string]: string[] }; + +function optimizeRoutes( + routes: AssetsManifest["routes"], + entryImports: string[] +): void { + // This cache is an optimization that allows us to avoid pruning the same + // route's imports more than once. + let importsCache: ImportsCache = Object.create(null); + + for (let key in routes) { + optimizeRouteImports(key, routes, entryImports, importsCache); + } +} + +function optimizeRouteImports( + routeId: string, + routes: AssetsManifest["routes"], + parentImports: string[], + importsCache: ImportsCache +): string[] { + if (importsCache[routeId]) return importsCache[routeId]; + + let route = routes[routeId]; + + if (route.parentId) { + parentImports = parentImports.concat( + optimizeRouteImports(route.parentId, routes, parentImports, importsCache) + ); + } + + let routeImports = (route.imports || []).filter( + url => !parentImports.includes(url) + ); + + // Setting `route.imports = undefined` prevents `imports: []` from showing up + // in the manifest JSON when there are no imports. + route.imports = routeImports.length > 0 ? routeImports : undefined; + + // Cache so the next lookup for this route is faster. + importsCache[routeId] = routeImports; + + return routeImports; +} diff --git a/packages/remix-dev/compiler2/dependencies.ts b/packages/remix-dev/compiler2/dependencies.ts new file mode 100644 index 0000000000..633f8dfcd9 --- /dev/null +++ b/packages/remix-dev/compiler2/dependencies.ts @@ -0,0 +1,21 @@ +import * as path from "path"; +// @ts-expect-error +import readPackageJson from "read-package-json-fast"; + +import type { RemixConfig } from "../config"; + +type PackageDependencies = { [packageName: string]: string }; + +export async function getPackageDependencies( + packageJsonFile: string +): Promise { + return (await readPackageJson(packageJsonFile)).dependencies; +} + +export function getAppDependencies( + config: RemixConfig +): Promise { + return getPackageDependencies( + path.resolve(config.rootDirectory, "package.json") + ); +} diff --git a/packages/remix-dev/compiler2/loaders.ts b/packages/remix-dev/compiler2/loaders.ts new file mode 100644 index 0000000000..d6cb9cccf7 --- /dev/null +++ b/packages/remix-dev/compiler2/loaders.ts @@ -0,0 +1,37 @@ +import * as path from "path"; +import * as esbuild from "esbuild"; + +export const loaders: { [ext: string]: esbuild.Loader } = { + ".aac": "file", + ".css": "file", + ".eot": "file", + ".flac": "file", + ".gif": "file", + ".jpeg": "file", + ".jpg": "file", + ".js": "jsx", + ".jsx": "jsx", + ".json": "json", + ".md": "text", + ".mdx": "text", + ".mp3": "file", + ".mp4": "file", + ".ogg": "file", + ".otf": "file", + ".png": "file", + ".svg": "file", + ".ts": "ts", + ".tsx": "tsx", + ".ttf": "file", + ".wav": "file", + ".webm": "file", + ".webp": "file", + ".woff": "file", + ".woff2": "file" +}; + +export function getLoaderForFile(file: string): esbuild.Loader { + let ext = path.extname(file); + if (ext in loaders) return loaders[ext]; + throw new Error(`Cannot get loader for file ${file}`); +} diff --git a/packages/remix-dev/compiler2/routes.ts b/packages/remix-dev/compiler2/routes.ts new file mode 100644 index 0000000000..624848a993 --- /dev/null +++ b/packages/remix-dev/compiler2/routes.ts @@ -0,0 +1,59 @@ +import * as path from "path"; +import * as esbuild from "esbuild"; + +import * as cache from "../cache"; +import type { RemixConfig } from "../config"; +import { getFileHash } from "./utils/crypto"; + +type CachedRouteExports = { hash: string; exports: string[] }; + +export async function getRouteModuleExportsCached( + config: RemixConfig, + routeId: string +): Promise { + let file = path.resolve(config.appDirectory, config.routes[routeId].file); + let hash = await getFileHash(file); + let key = routeId + ".exports"; + + let cached: CachedRouteExports | null = null; + try { + cached = await cache.getJson(config.cacheDirectory, key); + } catch (error) { + // Ignore cache read errors. + } + + if (!cached || cached.hash !== hash) { + let exports = await getRouteModuleExports(config, routeId); + cached = { hash, exports }; + try { + await cache.putJson(config.cacheDirectory, key, cached); + } catch (error) { + // Ignore cache put errors. + } + } + + return cached.exports; +} + +export async function getRouteModuleExports( + config: RemixConfig, + routeId: string +): Promise { + let result = await esbuild.build({ + entryPoints: [ + path.resolve(config.appDirectory, config.routes[routeId].file) + ], + platform: "neutral", + format: "esm", + metafile: true, + write: false + }); + let metafile = result.metafile!; + + for (let key in metafile.outputs) { + let output = metafile.outputs[key]; + if (output.entryPoint) return output.exports; + } + + throw new Error(`Unable to get exports for route ${routeId}`); +} diff --git a/packages/remix-dev/compiler2/shims/react.ts b/packages/remix-dev/compiler2/shims/react.ts new file mode 100644 index 0000000000..eb0f102ecb --- /dev/null +++ b/packages/remix-dev/compiler2/shims/react.ts @@ -0,0 +1,2 @@ +import * as React from "react"; +export { React }; diff --git a/packages/remix-dev/compiler2/utils/crypto.ts b/packages/remix-dev/compiler2/utils/crypto.ts new file mode 100644 index 0000000000..cc8eae86c3 --- /dev/null +++ b/packages/remix-dev/compiler2/utils/crypto.ts @@ -0,0 +1,19 @@ +import * as fs from "fs"; +import type { BinaryLike } from "crypto"; +import { createHash } from "crypto"; + +export function getHash(source: BinaryLike): string { + return createHash("sha256").update(source).digest("hex"); +} + +export async function getFileHash(file: string): Promise { + return new Promise((accept, reject) => { + let hash = createHash("sha256"); + fs.createReadStream(file) + .on("error", error => reject(error)) + .on("data", data => hash.update(data)) + .on("close", () => { + accept(hash.digest("hex")); + }); + }); +} diff --git a/packages/remix-dev/compiler2/utils/fs.ts b/packages/remix-dev/compiler2/utils/fs.ts new file mode 100644 index 0000000000..bf38d480a6 --- /dev/null +++ b/packages/remix-dev/compiler2/utils/fs.ts @@ -0,0 +1,25 @@ +import { promises as fsp } from "fs"; +import * as path from "path"; + +export async function writeFileSafe( + file: string, + contents: string +): Promise { + await fsp.mkdir(path.dirname(file), { recursive: true }); + await fsp.writeFile(file, contents); + return file; +} + +export async function writeFilesSafe( + files: { file: string; contents: string }[] +): Promise { + return Promise.all( + files.map(({ file, contents }) => writeFileSafe(file, contents)) + ); +} + +export async function createTemporaryDirectory( + baseDir: string +): Promise { + return fsp.mkdtemp(path.join(baseDir, "remix-")); +} diff --git a/packages/remix-dev/compiler2/utils/url.ts b/packages/remix-dev/compiler2/utils/url.ts new file mode 100644 index 0000000000..7c30e4d248 --- /dev/null +++ b/packages/remix-dev/compiler2/utils/url.ts @@ -0,0 +1,5 @@ +import * as path from "path"; + +export function createUrl(publicPath: string, file: string): string { + return publicPath + file.split(path.win32.sep).join("/"); +} diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts new file mode 100644 index 0000000000..29d86dc858 --- /dev/null +++ b/packages/remix-dev/config.ts @@ -0,0 +1,253 @@ +import * as fs from "fs"; +import * as path from "path"; +import type { MdxOptions } from "@mdx-js/mdx"; + +import { loadModule } from "./modules"; +import type { RouteManifest, DefineRoutesFunction } from "./config/routes"; +import { defineRoutes } from "./config/routes"; +import { defineConventionalRoutes } from "./config/routesConvention"; +import { ServerMode, isValidServerMode } from "./config/serverModes"; + +/** + * The user-provided config in `remix.config.js`. + */ +export interface AppConfig { + /** + * The path to the `app` directory, relative to `remix.config.js`. Defaults + * to "app". + */ + appDirectory?: string; + + /** + * The path to a directory Remix can use for caching things in development, + * relative to `remix.config.js`. Defaults to ".cache". + */ + cacheDirectory?: string; + + /** + * A function for defining custom routes, in addition to those already defined + * using the filesystem convention in `app/routes`. + */ + routes?: ( + defineRoutes: DefineRoutesFunction + ) => Promise>; + + /** + * The path to the server build, relative to `remix.config.js`. Defaults to + * "build". + */ + serverBuildDirectory?: string; + + /** + * The path to the browser build, relative to `remix.config.js`. Defaults to + * "public/build". + */ + assetsBuildDirectory?: string; + + /** + * The path to the browser build, relative to remix.config.js. Defaults to + * "public/build". + * + * @deprecated Use `assetsBuildDirectory` instead + */ + browserBuildDirectory?: string; + + /** + * The URL prefix of the browser build with a trailing slash. Defaults to + * "/build/". + */ + publicPath?: string; + + /** + * The port number to use for the dev server. Defaults to 8002. + */ + devServerPort?: number; + + /** + * Options to use when compiling MDX. + */ + mdx?: MdxOptions; +} + +/** + * Fully resolved configuration object we use throughout Remix. + */ +export interface RemixConfig { + /** + * The absolute path to the root of the Remix project. + */ + rootDirectory: string; + + /** + * The absolute path to the application source directory. + */ + appDirectory: string; + + /** + * The absolute path to the cache directory. + */ + cacheDirectory: string; + + /** + * The path to the entry.client file, relative to `config.appDirectory`. + */ + entryClientFile: string; + + /** + * The path to the entry.server file, relative to `config.appDirectory`. + */ + entryServerFile: string; + + /** + * An object of all available routes, keyed by route id. + */ + routes: RouteManifest; + + /** + * The absolute path to the server build directory. + */ + serverBuildDirectory: string; + + /** + * The absolute path to the assets build directory. + */ + assetsBuildDirectory: string; + + /** + * The URL prefix of the public build with a trailing slash. + */ + publicPath: string; + + /** + * The mode to use to run the server. + */ + serverMode: ServerMode; + + /** + * The port number to use for the dev (asset) server. + */ + devServerPort: number; + + /** + * Options to use when compiling MDX. + */ + mdx?: MdxOptions; +} + +/** + * Returns a fully resolved config object from the remix.config.js in the given + * root directory. + */ +export async function readConfig( + remixRoot?: string, + serverMode = ServerMode.Production +): Promise { + if (!remixRoot) { + remixRoot = process.env.REMIX_ROOT || process.cwd(); + } + + if (!isValidServerMode(serverMode)) { + throw new Error(`Invalid server mode "${serverMode}"`); + } + + let rootDirectory = path.resolve(remixRoot); + let configFile = path.resolve(rootDirectory, "remix.config.js"); + + let appConfig: AppConfig; + try { + appConfig = loadModule(configFile); + } catch (error) { + console.error(`Error loading Remix config in ${configFile}`); + console.error(error); + process.exit(); + } + + let appDirectory = path.resolve( + rootDirectory, + appConfig.appDirectory || "app" + ); + + let cacheDirectory = path.resolve( + rootDirectory, + appConfig.cacheDirectory || ".cache" + ); + + let entryClientFile = findEntry(appDirectory, "entry.client"); + if (!entryClientFile) { + throw new Error(`Missing "entry.client" file in ${appDirectory}`); + } + + let entryServerFile = findEntry(appDirectory, "entry.server"); + if (!entryServerFile) { + throw new Error(`Missing "entry.server" file in ${appDirectory}`); + } + + let serverBuildDirectory = path.resolve( + rootDirectory, + appConfig.serverBuildDirectory || "build" + ); + + let assetsBuildDirectory = path.resolve( + rootDirectory, + appConfig.assetsBuildDirectory || + appConfig.browserBuildDirectory || + path.join("public", "build") + ); + + let devServerPort = appConfig.devServerPort || 8002; + + let publicPath = addTrailingSlash(appConfig.publicPath || "/build/"); + + let rootRouteFile = findEntry(appDirectory, "root"); + if (!rootRouteFile) { + throw new Error(`Missing "root" route file in ${appDirectory}`); + } + + let routes: RouteManifest = { + root: { path: "/", id: "root", file: rootRouteFile } + }; + if (fs.existsSync(path.resolve(appDirectory, "routes"))) { + let conventionalRoutes = defineConventionalRoutes(appDirectory); + for (let key of Object.keys(conventionalRoutes)) { + let route = conventionalRoutes[key]; + routes[route.id] = { ...route, parentId: route.parentId || "root" }; + } + } + if (appConfig.routes) { + let manualRoutes = await appConfig.routes(defineRoutes); + for (let key of Object.keys(manualRoutes)) { + let route = manualRoutes[key]; + routes[route.id] = { ...route, parentId: route.parentId || "root" }; + } + } + + return { + appDirectory, + cacheDirectory, + entryClientFile, + entryServerFile, + devServerPort, + mdx: appConfig.mdx, + assetsBuildDirectory, + publicPath, + rootDirectory, + routes, + serverBuildDirectory, + serverMode + }; +} + +function addTrailingSlash(path: string): string { + return path.endsWith("/") ? path : path + "/"; +} + +const entryExts = [".js", ".jsx", ".ts", ".tsx"]; + +function findEntry(dir: string, basename: string): string | undefined { + for (let ext of entryExts) { + let file = path.resolve(dir, basename + ext); + if (fs.existsSync(file)) return path.relative(dir, file); + } + + return undefined; +} diff --git a/packages/remix-dev/config/routes.ts b/packages/remix-dev/config/routes.ts new file mode 100644 index 0000000000..f9cf5d7ea0 --- /dev/null +++ b/packages/remix-dev/config/routes.ts @@ -0,0 +1,167 @@ +import * as path from "path"; + +/** + * A route that was created using `defineRoutes` or created conventionally from + * looking at the files on the filesystem. + */ +export interface ConfigRoute { + /** + * The path this route uses to match on the URL pathname. + */ + path: string; + + /** + * Should be `true` if the `path` is case-sensitive. Defaults to `false`. + */ + caseSensitive?: boolean; + + /** + * The unique id for this route, named like its `file` but without the + * extension. So `app/routes/gists/$username.jsx` will have an `id` of + * `routes/gists/$username`. + */ + id: string; + + /** + * The unique `id` for this route's parent route, if there is one. + */ + parentId?: string; + + /** + * The path to the entry point for this route, relative to + * `config.appDirectory`. + */ + file: string; +} + +export interface RouteManifest { + [routeId: string]: ConfigRoute; +} + +export interface DefineRouteOptions { + /** + * Should be `true` if the route `path` is case-sensitive. Defaults to + * `false`. + */ + caseSensitive?: boolean; +} + +interface DefineRouteChildren { + (): void; +} + +/** + * A function for defining a route that is passed as the argument to the + * `defineRoutes` callback. + * + * Calls to this function are designed to be nested, using the `children` + * callback argument. + * + * defineRoutes(route => { + * route('/', 'pages/layout', () => { + * route('react-router', 'pages/react-router'); + * route('reach-ui', 'pages/reach-ui'); + * }); + * }); + */ +export interface DefineRouteFunction { + ( + /** + * The path this route uses to match the URL pathname. + */ + path: string, + + /** + * The path to the file that exports the React component rendered by this + * route as its default export, relative to the `app` directory. + */ + file: string, + + /** + * Options for defining routes, or a function for defining child routes. + */ + optionsOrChildren?: DefineRouteOptions | DefineRouteChildren, + + /** + * A function for defining child routes. + */ + children?: DefineRouteChildren + ): void; +} + +export type DefineRoutesFunction = typeof defineRoutes; + +/** + * A function for defining routes programmatically, instead of using the + * filesystem convention. + */ +export function defineRoutes( + callback: (defineRoute: DefineRouteFunction) => void +): RouteManifest { + let routes: RouteManifest = Object.create(null); + let parentRoutes: ConfigRoute[] = []; + let alreadyReturned = false; + + let defineRoute: DefineRouteFunction = ( + path, + file, + optionsOrChildren, + children + ) => { + if (alreadyReturned) { + throw new Error( + "You tried to define routes asynchronously but started defining " + + "routes before the async work was done. Please await all async " + + "data before calling `defineRoutes()`" + ); + } + + let options: DefineRouteOptions; + if (typeof optionsOrChildren === "function") { + // route(path, file, children) + options = {}; + children = optionsOrChildren; + } else { + // route(path, file, options, children) + // route(path, file, options) + options = optionsOrChildren || {}; + } + + let route: ConfigRoute = { + path: path || "/", + caseSensitive: !!options.caseSensitive, + id: createRouteId(file), + parentId: + parentRoutes.length > 0 + ? parentRoutes[parentRoutes.length - 1].id + : undefined, + file + }; + + routes[route.id] = route; + + if (children) { + parentRoutes.push(route); + children(); + parentRoutes.pop(); + } + }; + + callback(defineRoute); + + alreadyReturned = true; + + return routes; +} + +export function createRouteId(file: string) { + return normalizeSlashes(stripFileExtension(file)); +} + +function normalizeSlashes(file: string) { + return file.split(path.win32.sep).join("/"); +} + +function stripFileExtension(file: string) { + return file.replace(/\.[a-z0-9]+$/i, ""); +} diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts new file mode 100644 index 0000000000..3ac7c392ed --- /dev/null +++ b/packages/remix-dev/config/routesConvention.ts @@ -0,0 +1,109 @@ +import * as fs from "fs"; +import * as path from "path"; + +import type { RouteManifest, DefineRouteFunction } from "./routes"; +import { defineRoutes, createRouteId } from "./routes"; + +/** + * All file extensions we support for route modules. + */ +export const routeModuleExts = [".js", ".jsx", ".md", ".mdx", ".ts", ".tsx"]; + +export function isRouteModuleFile(filename: string): boolean { + return routeModuleExts.includes(path.extname(filename)); +} + +/** + * Defines routes using the filesystem convention in `app/routes`. The rules are: + * + * - Route paths are derived from the file path. A `.` in the filename indicates + * a `/` in the URL (a "nested" URL, but no route nesting). A `$` in the + * filename indicates a dynamic URL segment. + * - Subdirectories are used for nested routes. + * + * For example, a file named `app/routes/gists/$username.tsx` creates a route + * with a path of `gists/:username`. + */ +export function defineConventionalRoutes(appDir: string): RouteManifest { + let files: { + [routeId: string]: string; + } = {}; + + function defineNestedRoutes( + defineRoute: DefineRouteFunction, + parentId?: string + ): void { + let routeIds = Object.keys(files); + let childRouteIds = routeIds.filter( + id => findParentRouteId(routeIds, id) === parentId + ); + + for (let routeId of childRouteIds) { + let routePath = + routeId === "routes/404" + ? "*" + : createRoutePath(routeId.slice((parentId || "routes").length + 1)); + + defineRoute(routePath, files[routeId], () => { + defineNestedRoutes(defineRoute, routeId); + }); + } + } + + // First, find all route modules in app/routes + visitFiles(path.join(appDir, "routes"), file => { + let routeId = createRouteId(path.join("routes", file)); + + if (isRouteModuleFile(file)) { + files[routeId] = path.join("routes", file); + } else { + throw new Error( + `Invalid route module file: ${path.join(appDir, "routes", file)}` + ); + } + }); + + return defineRoutes(defineNestedRoutes); +} + +function createRoutePath(routeId: string): string { + let path = routeId.replace(/\$/g, ":").replace(/\./g, "/"); + return /\b\/?index$/.test(path) ? path.replace(/\/?index$/, "") : path; +} + +function findParentRouteId( + routeIds: string[], + childRouteId: string +): string | undefined { + return ( + routeIds + .slice(0) + .sort(byLongestFirst) + // FIXME: this will probably break with two routes like foo/ and foo-bar/, + // we use `startsWith` with we also need to factor in the segment `/` + // boundaries. There are bugs in React Router NavLink with this too. + // Probably need to ditch all uses of `startsWith` in route matching. + .find(id => childRouteId.startsWith(`${id}/`)) + ); +} + +function byLongestFirst(a: string, b: string): number { + return b.length - a.length; +} + +function visitFiles( + dir: string, + visitor: (file: string) => void, + baseDir = dir +): void { + for (let filename of fs.readdirSync(dir)) { + let file = path.resolve(dir, filename); + let stat = fs.lstatSync(file); + + if (stat.isDirectory()) { + visitFiles(file, visitor, baseDir); + } else if (stat.isFile()) { + visitor(path.relative(baseDir, file)); + } + } +} diff --git a/packages/remix-dev/config/serverModes.ts b/packages/remix-dev/config/serverModes.ts new file mode 100644 index 0000000000..387cfa5cf2 --- /dev/null +++ b/packages/remix-dev/config/serverModes.ts @@ -0,0 +1,16 @@ +/** + * The mode to use when running the server. + */ +export enum ServerMode { + Development = "development", + Production = "production", + Test = "test" +} + +export function isValidServerMode(mode: string): mode is ServerMode { + return ( + mode === ServerMode.Development || + mode === ServerMode.Production || + mode === ServerMode.Test + ); +} diff --git a/packages/remix-dev/invariant.ts b/packages/remix-dev/invariant.ts new file mode 100644 index 0000000000..d75cc80110 --- /dev/null +++ b/packages/remix-dev/invariant.ts @@ -0,0 +1,18 @@ +export default function invariant( + value: boolean, + message?: string +): asserts value; + +export default function invariant( + value: T | null | undefined, + message?: string +): asserts value is T; + +export default function invariant(value: any, message?: string) { + if (value === false || value === null || typeof value === "undefined") { + console.error( + "The following error is a bug in Remix, please file an issue! https://remix.run/dashboard/support" + ); + throw new Error(message); + } +} diff --git a/packages/remix-dev/modules.ts b/packages/remix-dev/modules.ts new file mode 100644 index 0000000000..cf1b6c3371 --- /dev/null +++ b/packages/remix-dev/modules.ts @@ -0,0 +1,29 @@ +/** + * Loads a CommonJS module from the filesystem using node's `require` function. + */ +export function loadModule(file: string): any { + return require(file); +} + +/** + * Purges all entries that begin with the given prefix from node's internal + * `require` cache. + * + * This is useful when running the Remix build in watch mode because we + * currently load remix.config.js using CommonJS require, which means that it + * can require() other files it might need. So we just purge them all to make + * sure we pick up the latest changes. + */ +export function purgeModuleCache( + prefix: string, + includeNodeModules = false +): void { + for (let key of Object.keys(require.cache)) { + if ( + key.startsWith(prefix) && + (includeNodeModules || !/\bnode_modules\b/.test(key)) + ) { + delete require.cache[key]; + } + } +} diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json new file mode 100644 index 0000000000..58899855a5 --- /dev/null +++ b/packages/remix-dev/package.json @@ -0,0 +1,49 @@ +{ + "name": "@remix-run/dev", + "description": "Dev tools and CLI for Remix", + "version": "0.16.1", + "repository": "https://github.com/remix-run/remix", + "bin": { + "remix": "cli.js" + }, + "dependencies": { + "@babel/core": "^7.13.10", + "@babel/preset-env": "^7.13.10", + "@babel/preset-react": "^7.12.13", + "@babel/preset-typescript": "^7.13.0", + "@mdx-js/mdx": "^1.6.22", + "@mdx-js/react": "^1.6.22", + "@rollup/plugin-babel": "^5.3.0", + "@rollup/plugin-commonjs": "^17.1.0", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^11.2.0", + "@rollup/plugin-replace": "^2.4.1", + "cacache": "^15.0.5", + "chokidar": "^3.5.1", + "esbuild": "0.11.4", + "express": "^4.17.1", + "front-matter": "^4.0.2", + "lodash.debounce": "^4.0.8", + "meow": "^7.1.1", + "morgan": "^1.10.0", + "postcss": "^8.2.6", + "pretty-bytes": "^5.5.0", + "pretty-ms": "^7.0.1", + "read-package-json-fast": "^2.0.2", + "rollup": "^2.39.0", + "rollup-plugin-terser": "^7.0.2", + "sharp": "^0.27.1", + "signal-exit": "^3.0.3", + "ws": "^7.4.5" + }, + "devDependencies": { + "@types/cacache": "^15.0.0", + "@types/express": "^4.17.11", + "@types/lodash.debounce": "^4.0.6", + "@types/morgan": "^1.9.2", + "@types/sharp": "^0.27.1", + "@types/signal-exit": "^3.0.0", + "@types/ws": "^7.4.1", + "semver": "^7.3.4" + } +} diff --git a/packages/remix-dev/server.ts b/packages/remix-dev/server.ts new file mode 100644 index 0000000000..8955e89eaa --- /dev/null +++ b/packages/remix-dev/server.ts @@ -0,0 +1,88 @@ +import http from "http"; +import path from "path"; +import type { Request, Response } from "express"; +import express from "express"; +import morgan from "morgan"; +import signalExit from "signal-exit"; + +import { BuildMode, BuildTarget } from "./build"; +import * as compiler from "./compiler"; +import type { RemixConfig } from "./config"; + +export function startDevServer( + config: RemixConfig, + { + onListen + }: { + onListen?: () => void; + } = {} +) { + let requestHandler = createRequestHandler(config); + let server = http.createServer(requestHandler); + + server.listen(config.devServerPort, onListen); + + signalExit(() => { + server.close(); + }); +} + +function createRequestHandler(config: RemixConfig) { + let serverBuildStart = 0; + let assetsBuildStart = 0; + + signalExit( + compiler.watch(config, { + mode: BuildMode.Development, + target: BuildTarget.Server, + onBuildStart() { + console.log("Building Remix..."); + serverBuildStart = Date.now(); + }, + async onBuildEnd(build) { + await compiler.write(build, config.serverBuildDirectory); + + let dir = path.relative(process.cwd(), config.serverBuildDirectory); + let time = Date.now() - serverBuildStart; + console.log(`Wrote server build to ./${dir} in ${time}ms`); + }, + onError(error) { + console.error(error); + } + }) + ); + + signalExit( + compiler.watch(config, { + mode: BuildMode.Development, + target: BuildTarget.Browser, + onBuildStart() { + assetsBuildStart = Date.now(); + }, + async onBuildEnd(build) { + await compiler.write(build, config.assetsBuildDirectory); + + let dir = path.relative(process.cwd(), config.assetsBuildDirectory); + let time = Date.now() - assetsBuildStart; + console.log(`Wrote assets build to ./${dir} in ${time}ms`); + }, + onError(error) { + console.error(error); + } + }) + ); + + function handleRequest(_req: Request, res: Response) { + res.status(200).send(); + } + + let app = express(); + + app.disable("x-powered-by"); + + app.use(morgan("dev")); + + app.get("*", handleRequest); + + return app; +} diff --git a/packages/remix-dev/tsconfig.json b/packages/remix-dev/tsconfig.json new file mode 100644 index 0000000000..96b18ef815 --- /dev/null +++ b/packages/remix-dev/tsconfig.json @@ -0,0 +1,18 @@ +{ + "include": ["../../types/mdx-js__mdx.d.ts", "**/*"], + "exclude": ["__tests__/**/*"], + "compilerOptions": { + "lib": ["ES2019"], + "target": "ES2019", + + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "strict": true, + + "declaration": true, + "emitDeclarationOnly": true, + + "outDir": "../../build/node_modules/@remix-run/dev", + "rootDir": "." + } +} diff --git a/packages/remix-dev/warnings.ts b/packages/remix-dev/warnings.ts new file mode 100644 index 0000000000..f38d979842 --- /dev/null +++ b/packages/remix-dev/warnings.ts @@ -0,0 +1,12 @@ +const alreadyWarned: { [message: string]: boolean } = {}; + +export function warnOnce( + condition: boolean, + message: string, + key = message +): void { + if (!condition && !alreadyWarned[key]) { + alreadyWarned[key] = true; + console.warn(message); + } +} diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts new file mode 100644 index 0000000000..f55dd44ac3 --- /dev/null +++ b/packages/remix-express/__tests__/server-test.ts @@ -0,0 +1,79 @@ +import express from "express"; +import supertest from "supertest"; + +import { createRequestHandler } from "../server"; + +import { Response } from "@remix-run/node"; + +import { createRequestHandler as createRemixRequestHandler } from "@remix-run/node/server"; + +// We don't want to test that the remix server works here (that's what the +// puppetteer tests do), we just want to test the express adapter +jest.mock("@remix-run/node/server"); +let mockedCreateRequestHandler = createRemixRequestHandler as jest.MockedFunction< + typeof createRemixRequestHandler +>; + +function createApp() { + let app = express(); + + app.all( + "*", + createRequestHandler({ + // We don't have a real app to test, but it doesn't matter. We + // won't ever call through to the real createRequestHandler + build: undefined + }) + ); + + return app; +} + +describe("express createRequestHandler", () => { + describe("basic requests", () => { + afterEach(() => { + mockedCreateRequestHandler.mockReset(); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("handles requests", async () => { + mockedCreateRequestHandler.mockImplementation(() => async req => { + return new Response(`URL: ${new URL(req.url).pathname}`); + }); + + let request = supertest(createApp()); + let res = await request.get("/foo/bar"); + + expect(res.status).toBe(200); + expect(res.text).toBe("URL: /foo/bar"); + expect(res.headers["x-powered-by"]).toBe("Express"); + }); + + it("handles status codes", async () => { + mockedCreateRequestHandler.mockImplementation(() => async () => { + return new Response("", { status: 204 }); + }); + + let request = supertest(createApp()); + let res = await request.get("/"); + + expect(res.status).toBe(204); + }); + + it("sets headers", async () => { + mockedCreateRequestHandler.mockImplementation(() => async () => { + return new Response("", { + headers: { "X-Time-Of-Year": "most wonderful" } + }); + }); + + let request = supertest(createApp()); + let res = await request.get("/"); + + expect(res.headers["x-time-of-year"]).toBe("most wonderful"); + }); + }); +}); diff --git a/packages/remix-express/globals.ts b/packages/remix-express/globals.ts new file mode 100644 index 0000000000..917305ac93 --- /dev/null +++ b/packages/remix-express/globals.ts @@ -0,0 +1,2 @@ +import { installGlobals } from "@remix-run/node"; +installGlobals(); diff --git a/packages/remix-express/index.ts b/packages/remix-express/index.ts new file mode 100644 index 0000000000..fb640bf479 --- /dev/null +++ b/packages/remix-express/index.ts @@ -0,0 +1,4 @@ +import "./globals"; + +export type { GetLoadContextFunction, RequestHandler } from "./server"; +export { createRequestHandler } from "./server"; diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json new file mode 100644 index 0000000000..e426aa4dd5 --- /dev/null +++ b/packages/remix-express/package.json @@ -0,0 +1,17 @@ +{ + "name": "@remix-run/express", + "description": "Express server request handler for Remix", + "version": "0.16.1", + "repository": "https://github.com/remix-run/remix", + "dependencies": { + "@remix-run/node": "0.16.1" + }, + "peerDependencies": { + "express": "^4.17.1" + }, + "devDependencies": { + "@types/express": "^4.17.9", + "@types/supertest": "^2.0.10", + "supertest": "^6.0.1" + } +} diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts new file mode 100644 index 0000000000..3d7bd7b06b --- /dev/null +++ b/packages/remix-express/server.ts @@ -0,0 +1,113 @@ +import { PassThrough } from "stream"; +import { URL } from "url"; +import type * as express from "express"; +import type { + AppLoadContext, + RequestInit, + Response, + ServerBuild +} from "@remix-run/node"; +import { + Headers, + Request, + createRequestHandler as createRemixRequestHandler +} from "@remix-run/node"; + +/** + * A function that returns the value to use as `context` in route `loader` and + * `action` functions. + * + * You can think of this as an escape hatch that allows you to pass + * environment/platform-specific values through to your loader/action, such as + * values that are generated by Express middleware like `req.session`. + */ +export interface GetLoadContextFunction { + (req: express.Request, res: express.Response): AppLoadContext; +} + +export type RequestHandler = ReturnType; + +/** + * Returns a request handler for Express that serves the response using Remix. + */ +export function createRequestHandler({ + build, + getLoadContext, + mode = process.env.NODE_ENV +}: { + build: ServerBuild; + getLoadContext?: GetLoadContextFunction; + mode?: string; +}) { + let handleRequest = createRemixRequestHandler(build, mode); + + return async ( + req: express.Request, + res: express.Response, + next: express.NextFunction + ) => { + try { + let request = createRemixRequest(req); + let loadContext = + typeof getLoadContext === "function" + ? getLoadContext(req, res) + : undefined; + + let response = await handleRequest(request, loadContext); + + sendRemixResponse(res, response); + } catch (error) { + // Express doesn't support async functions, so we have to pass along the + // error manually using next(). + next(error); + } + }; +} + +function createRemixHeaders( + requestHeaders: express.Request["headers"] +): Headers { + return new Headers( + Object.keys(requestHeaders).reduce((memo, key) => { + let value = requestHeaders[key]; + + if (typeof value === "string") { + memo[key] = value; + } else if (Array.isArray(value)) { + memo[key] = value.join(","); + } + + return memo; + }, {} as { [headerName: string]: string }) + ); +} + +function createRemixRequest(req: express.Request): Request { + let origin = `${req.protocol}://${req.hostname}`; + let url = new URL(req.url, origin); + + let init: RequestInit = { + method: req.method, + headers: createRemixHeaders(req.headers) + }; + + if (req.method !== "GET" && req.method !== "HEAD") { + init.body = req.pipe(new PassThrough({ highWaterMark: 16384 })); + } + + return new Request(url.toString(), init); +} + +function sendRemixResponse(res: express.Response, response: Response): void { + res.status(response.status); + + for (let [key, value] of response.headers.entries()) { + res.set(key, value); + } + + if (Buffer.isBuffer(response.body)) { + res.end(response.body); + } else { + response.body.pipe(res); + } +} diff --git a/packages/remix-express/tsconfig.json b/packages/remix-express/tsconfig.json new file mode 100644 index 0000000000..55305c7bbd --- /dev/null +++ b/packages/remix-express/tsconfig.json @@ -0,0 +1,17 @@ +{ + "exclude": ["__tests__/**/*"], + "compilerOptions": { + "lib": ["ES2019"], + "target": "ES2019", + + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "strict": true, + + "declaration": true, + "emitDeclarationOnly": true, + + "outDir": "../../build/node_modules/@remix-run/express", + "rootDir": "." + } +} diff --git a/packages/remix-node/__tests__/cookies-test.ts b/packages/remix-node/__tests__/cookies-test.ts new file mode 100644 index 0000000000..f53a947487 --- /dev/null +++ b/packages/remix-node/__tests__/cookies-test.ts @@ -0,0 +1,95 @@ +import { createCookie, isCookie } from "../cookies"; + +function getCookieFromSetCookie(setCookie: string): string { + return setCookie.split(/;\s*/)[0]; +} + +describe("isCookie", () => { + it("returns `true` for Cookie objects", () => { + expect(isCookie(createCookie("my-cookie"))).toBe(true); + }); + + it("returns `false` for non-Cookie objects", () => { + expect(isCookie({})).toBe(false); + expect(isCookie([])).toBe(false); + expect(isCookie("")).toBe(false); + expect(isCookie(true)).toBe(false); + }); +}); + +describe("cookies", () => { + it("parses/serializes empty string values", () => { + let cookie = createCookie("my-cookie"); + let setCookie = cookie.serialize(""); + let value = cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toMatchInlineSnapshot(`""`); + }); + + it("parses/serializes unsigned string values", () => { + let cookie = createCookie("my-cookie"); + let setCookie = cookie.serialize("hello world"); + let value = cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toEqual("hello world"); + }); + + it("parses/serializes unsigned boolean values", () => { + let cookie = createCookie("my-cookie"); + let setCookie = cookie.serialize(true); + let value = cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toBe(true); + }); + + it("parses/serializes signed string values", () => { + let cookie = createCookie("my-cookie", { + secrets: ["secret1"] + }); + let setCookie = cookie.serialize("hello michael"); + let value = cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toMatchInlineSnapshot(`"hello michael"`); + }); + + it("parses/serializes signed object values", () => { + let cookie = createCookie("my-cookie", { + secrets: ["secret1"] + }); + let setCookie = cookie.serialize({ hello: "mjackson" }); + let value = cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toMatchInlineSnapshot(` + Object { + "hello": "mjackson", + } + `); + }); + + it("supports secret rotation", () => { + let cookie = createCookie("my-cookie", { + secrets: ["secret1"] + }); + let setCookie = cookie.serialize({ hello: "mjackson" }); + let value = cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toMatchInlineSnapshot(` + Object { + "hello": "mjackson", + } + `); + + // A new secret enters the rotation... + cookie = createCookie("my-cookie", { + secrets: ["secret2", "secret1"] + }); + + // cookie should still be able to parse old cookies. + let oldValue = cookie.parse(getCookieFromSetCookie(setCookie)); + expect(oldValue).toMatchObject(value); + + // New Set-Cookie should be different, it uses a differet secret. + let setCookie2 = cookie.serialize(value); + expect(setCookie).not.toEqual(setCookie2); + }); +}); diff --git a/packages/remix-node/__tests__/responses-test.ts b/packages/remix-node/__tests__/responses-test.ts new file mode 100644 index 0000000000..c431bd75ac --- /dev/null +++ b/packages/remix-node/__tests__/responses-test.ts @@ -0,0 +1,75 @@ +import { json, redirect } from "../index"; + +describe("json", () => { + it("sets the Content-Type header", () => { + let response = json({}); + expect(response.headers.get("Content-Type")).toEqual( + "application/json; charset=utf-8" + ); + }); + + it("preserves existing headers, including Content-Type", () => { + let response = json( + {}, + { + headers: { + "Content-Type": "application/json; charset=iso-8859-1", + "X-Remix": "is awesome" + } + } + ); + + expect(response.headers.get("Content-Type")).toEqual( + "application/json; charset=iso-8859-1" + ); + expect(response.headers.get("X-Remix")).toEqual("is awesome"); + }); + + it("encodes the response body", async () => { + let response = json({ hello: "remix" }); + expect(await response.json()).toEqual({ hello: "remix" }); + }); + + it("accepts status as a second parameter", () => { + let response = json({}, 201); + expect(response.status).toEqual(201); + }); +}); + +describe("redirect", () => { + it("sets the status to 302 by default", () => { + let response = redirect("/login"); + expect(response.status).toEqual(302); + }); + + it("sets the status to 302 when only headers are given", () => { + let response = redirect("/login", { + headers: { + "X-Remix": "is awesome" + } + }); + expect(response.status).toEqual(302); + }); + + it("sets the Location header", () => { + let response = redirect("/login"); + expect(response.headers.get("Location")).toEqual("/login"); + }); + + it("preserves existing headers, but not Location", () => { + let response = redirect("/login", { + headers: { + Location: "/", + "X-Remix": "is awesome" + } + }); + + expect(response.headers.get("Location")).toEqual("/login"); + expect(response.headers.get("X-Remix")).toEqual("is awesome"); + }); + + it("accepts status as a second parameter", () => { + let response = redirect("/profile", 301); + expect(response.status).toEqual(301); + }); +}); diff --git a/packages/remix-node/__tests__/sessions-test.ts b/packages/remix-node/__tests__/sessions-test.ts new file mode 100644 index 0000000000..3ba2393a73 --- /dev/null +++ b/packages/remix-node/__tests__/sessions-test.ts @@ -0,0 +1,205 @@ +import path from "path"; +import { promises as fsp } from "fs"; +import os from "os"; + +import { createSession, isSession } from "../sessions"; +import { createCookieSessionStorage } from "../sessions/cookieStorage"; +import { createFileSessionStorage } from "../sessions/fileStorage"; +import { createMemorySessionStorage } from "../sessions/memoryStorage"; + +function getCookieFromSetCookie(setCookie: string): string { + return setCookie.split(/;\s*/)[0]; +} + +describe("Session", () => { + it("has an empty id by default", () => { + expect(createSession().id).toEqual(""); + }); + + it("correctly stores and retrieves values", () => { + let session = createSession(); + + session.set("user", "mjackson"); + session.flash("error", "boom"); + + expect(session.has("user")).toBe(true); + expect(session.get("user")).toBe("mjackson"); + // Normal values should remain in the session after get() + expect(session.has("user")).toBe(true); + expect(session.get("user")).toBe("mjackson"); + + expect(session.has("error")).toBe(true); + expect(session.get("error")).toBe("boom"); + // Flash values disappear after the first get() + expect(session.has("error")).toBe(false); + expect(session.get("error")).toBeUndefined(); + + session.unset("user"); + + expect(session.has("user")).toBe(false); + expect(session.get("user")).toBeUndefined(); + }); +}); + +describe("isSession", () => { + it("returns `true` for Session objects", () => { + expect(isSession(createSession())).toBe(true); + }); + + it("returns `false` for non-Session objects", () => { + expect(isSession({})).toBe(false); + expect(isSession([])).toBe(false); + expect(isSession("")).toBe(false); + expect(isSession(true)).toBe(false); + }); +}); + +describe("In-memory session storage", () => { + it("persists session data across requests", async () => { + let { getSession, commitSession } = createMemorySessionStorage({ + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + let setCookie = await commitSession(session); + session = await getSession(getCookieFromSetCookie(setCookie)); + + expect(session.get("user")).toEqual("mjackson"); + }); +}); + +describe("Cookie session storage", () => { + it("persists session data across requests", async () => { + let { getSession, commitSession } = createCookieSessionStorage({ + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + let setCookie = await commitSession(session); + session = await getSession(getCookieFromSetCookie(setCookie)); + + expect(session.get("user")).toEqual("mjackson"); + }); + + it("returns an empty session for cookies that are not signed properly", async () => { + let { getSession, commitSession } = createCookieSessionStorage({ + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + + expect(session.get("user")).toEqual("mjackson"); + + let setCookie = await commitSession(session); + session = await getSession( + // Tamper with the session cookie... + getCookieFromSetCookie(setCookie).slice(0, -1) + ); + + expect(session.get("user")).toBeUndefined(); + }); + + describe("when a new secret shows up in the rotation", () => { + it("unsigns old session cookies using the old secret and encodes new cookies using the new secret", async () => { + let { getSession, commitSession } = createCookieSessionStorage({ + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + let setCookie = await commitSession(session); + session = await getSession(getCookieFromSetCookie(setCookie)); + + expect(session.get("user")).toEqual("mjackson"); + + // A new secret enters the rotation... + let storage = createCookieSessionStorage({ + cookie: { secrets: ["secret2", "secret1"] } + }); + getSession = storage.getSession; + commitSession = storage.commitSession; + + // Old cookies should still work with the old secret. + session = await storage.getSession(getCookieFromSetCookie(setCookie)); + expect(session.get("user")).toEqual("mjackson"); + + // New cookies should be signed using the new secret. + let setCookie2 = await storage.commitSession(session); + expect(setCookie2).not.toEqual(setCookie); + }); + }); +}); + +describe("File session storage", () => { + let dir = path.join(os.tmpdir(), "file-session-storage"); + + beforeAll(async () => { + await fsp.mkdir(dir, { recursive: true }); + }); + + afterAll(async () => { + await fsp.rm(dir, { recursive: true, force: true }); + }); + + it("persists session data across requests", async () => { + let { getSession, commitSession } = createFileSessionStorage({ + dir, + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + let setCookie = await commitSession(session); + session = await getSession(getCookieFromSetCookie(setCookie)); + + expect(session.get("user")).toEqual("mjackson"); + }); + + it("returns an empty session for cookies that are not signed properly", async () => { + let { getSession, commitSession } = createFileSessionStorage({ + dir, + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + + expect(session.get("user")).toBe("mjackson"); + + let setCookie = await commitSession(session); + session = await getSession( + // Tamper with the cookie... + getCookieFromSetCookie(setCookie).slice(0, -1) + ); + + expect(session.get("user")).toBeUndefined(); + }); + + describe("when a new secret shows up in the rotation", () => { + it("unsigns old session cookies using the old secret and encodes new cookies using the new secret", async () => { + let { getSession, commitSession } = createFileSessionStorage({ + dir, + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + let setCookie = await commitSession(session); + session = await getSession(getCookieFromSetCookie(setCookie)); + + expect(session.get("user")).toEqual("mjackson"); + + // A new secret enters the rotation... + let storage = createFileSessionStorage({ + dir, + cookie: { secrets: ["secret2", "secret1"] } + }); + getSession = storage.getSession; + commitSession = storage.commitSession; + + // Old cookies should still work with the old secret. + session = await getSession(getCookieFromSetCookie(setCookie)); + expect(session.get("user")).toEqual("mjackson"); + + // New cookies should be signed using the new secret. + let setCookie2 = await commitSession(session); + expect(setCookie2).not.toEqual(setCookie); + }); + }); +}); diff --git a/packages/remix-node/__tests__/utils.ts b/packages/remix-node/__tests__/utils.ts new file mode 100644 index 0000000000..06ad0653dd --- /dev/null +++ b/packages/remix-node/__tests__/utils.ts @@ -0,0 +1,5 @@ +import prettier from "prettier"; + +export function prettyHtml(source: string): string { + return prettier.format(source, { parser: "html" }); +} diff --git a/packages/remix-node/assetImportTypes.ts b/packages/remix-node/assetImportTypes.ts new file mode 100644 index 0000000000..4edf992785 --- /dev/null +++ b/packages/remix-node/assetImportTypes.ts @@ -0,0 +1,153 @@ +declare module "*.aac" { + const asset: string; + export default asset; +} +declare module "*.css" { + const asset: string; + export default asset; +} +declare module "*.eot" { + const asset: string; + export default asset; +} +declare module "*.flac" { + const asset: string; + export default asset; +} +declare module "*.gif" { + const asset: string; + export default asset; +} +declare module "*.jpeg" { + const asset: string; + export default asset; +} +declare module "*.jpg" { + const asset: string; + export default asset; +} +declare module "*.json" { + const asset: string; + export default asset; +} +declare module "*.md" { + const asset: string; + export default asset; +} +declare module "*.mdx" { + const asset: string; + export default asset; +} +declare module "*.mp3" { + const asset: string; + export default asset; +} +declare module "*.mp4" { + const asset: string; + export default asset; +} +declare module "*.ogg" { + const asset: string; + export default asset; +} +declare module "*.otf" { + const asset: string; + export default asset; +} +declare module "*.png" { + const asset: string; + export default asset; +} +declare module "*.svg" { + const asset: string; + export default asset; +} +declare module "*.ttf" { + const asset: string; + export default asset; +} +declare module "*.wav" { + const asset: string; + export default asset; +} +declare module "*.webm" { + const asset: string; + export default asset; +} +declare module "*.webp" { + const asset: string; + export default asset; +} +declare module "*.woff" { + const asset: string; + export default asset; +} +declare module "*.woff2" { + const asset: string; + export default asset; +} + +// 🔪🔪🔪🔪 On the esbuild CHOPPING BLOCK! 🔪🔪🔪🔪 + +declare module "css:*" { + const asset: string; + export default asset; +} + +declare module "img:*" { + const asset: ImageAsset; + export default asset; +} + +declare module "url:*" { + const asset: string; + export default asset; +} + +/** + * Image urls and metadata for images imported into applications. + */ +interface ImageAsset { + /** + * The url of the image. When using srcset, it's the last size defined. + */ + src: string; + + /** + * The width of the image. When using srcset, it's the last size defined. + */ + width: number; + + /** + * The height of the image. When using srcset, it's the last size defined. + */ + height: number; + + /** + * The string to be passed do `` for responsive images. Sizes + * defined by the asset import `srcset=...sizes` query string param, like + * `./file.jpg?srcset=720,1080`. + */ + srcset: string; + + /** + * Base64 string that can be inlined for immediate render and scaled up. Typically set as the background + * of an image: + * + * ```jsx + * + * ``` + */ + placeholder: string; + + /** + * The image format. + */ + format: "jpeg" | "png" | "webp" | "avif"; +} diff --git a/packages/remix-node/build.ts b/packages/remix-node/build.ts new file mode 100644 index 0000000000..715dc52d76 --- /dev/null +++ b/packages/remix-node/build.ts @@ -0,0 +1,24 @@ +import type { EntryContext, AssetsManifest } from "./entry"; +import type { Headers, Request, Response } from "./fetch"; +import type { ServerRouteManifest } from "./routes"; + +export interface ServerBuild { + entry: { + module: ServerEntryModule; + }; + routes: ServerRouteManifest; + assets: AssetsManifest; +} + +/** + * A module that serves as the entry point for a Remix app during server + * rendering. + */ +export interface ServerEntryModule { + default( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + context: EntryContext + ): Promise; +} diff --git a/packages/remix-node/cookies.ts b/packages/remix-node/cookies.ts new file mode 100644 index 0000000000..f8ac7a56c9 --- /dev/null +++ b/packages/remix-node/cookies.ts @@ -0,0 +1,160 @@ +import type { CookieParseOptions, CookieSerializeOptions } from "cookie"; +import { parse, serialize } from "cookie"; +import { sign, unsign } from "cookie-signature"; + +export type { CookieParseOptions, CookieSerializeOptions }; + +export interface CookieSignatureOptions { + /** + * An array of secrets that may be used to sign/unsign the value of a cookie. + * + * The array makes it easy to rotate secrets. New secrets should be added to + * the beginning of the array. `cookie.serialize()` will always use the first + * value in the array, but `cookie.parse()` may use any of them so that + * cookies that were signed with older secrets still work. + */ + secrets?: string[]; +} + +export type CookieOptions = CookieParseOptions & + CookieSerializeOptions & + CookieSignatureOptions; + +/** + * A HTTP cookie. + * + * A Cookie is a logical container for metadata about a HTTP cookie; its name + * and options. But it doesn't contain a value. Instead, it has `parse()` and + * `serialize()` methods that allow a single instance to be reused for + * parsing/encoding multiple different values. + */ +export interface Cookie { + /** + * The name of the cookie, used in the `Cookie` and `Set-Cookie` headers. + */ + readonly name: string; + + /** + * True if this cookie uses one or more secrets for verification. + * + * See https://remix.run/dashboard/docs/cookies#signing-cookies + */ + readonly isSigned: boolean; + + /** + * The Date this cookie expires. + * + * Note: This is calculated at access time using `maxAge` when no `expires` + * option is provided to `createCookie()`. + */ + readonly expires?: Date; + + /** + * Parses a raw `Cookie` header and returns the value of this cookie or + * `null` if it's not present. + */ + parse(cookieHeader: string | null, options?: CookieParseOptions): any; + + /** + * Serializes the given value to a string and returns the `Set-Cookie` + * header. + */ + serialize(value: any, options?: CookieSerializeOptions): string; +} + +/** + * Creates and returns a new Cookie. + */ +export function createCookie( + name: string, + { secrets = [], ...options }: CookieOptions = {} +): Cookie { + return { + get name() { + return name; + }, + get isSigned() { + return secrets.length > 0; + }, + get expires() { + // Max-Age takes precedence over Expires + return typeof options.maxAge !== "undefined" + ? new Date(Date.now() + options.maxAge * 1000) + : options.expires; + }, + parse(cookieHeader, parseOptions) { + if (!cookieHeader) return null; + let cookies = parse(cookieHeader, { ...options, ...parseOptions }); + return name in cookies + ? cookies[name] === "" + ? "" + : decodeCookieValue(cookies[name], secrets) + : null; + }, + serialize(value, serializeOptions) { + return serialize( + name, + value === "" ? "" : encodeCookieValue(value, secrets), + { + ...options, + ...serializeOptions + } + ); + } + }; +} + +export function isCookie(object: any): object is Cookie { + return ( + object != null && + typeof object.name === "string" && + typeof object.isSigned === "boolean" && + typeof object.parse === "function" && + typeof object.serialize === "function" + ); +} + +function encodeCookieValue(value: any, secrets: string[]): string { + let encoded = encodeData(value); + + if (secrets.length > 0) { + encoded = sign(encoded, secrets[0]); + } + + return encoded; +} + +function decodeCookieValue(value: string, secrets: string[]): any { + if (secrets.length > 0) { + for (let secret of secrets) { + let unsignedValue = unsign(value, secret); + if (unsignedValue !== false) { + return decodeData(unsignedValue); + } + } + + return null; + } + + return decodeData(value); +} + +function encodeData(value: any): string { + return btoa(JSON.stringify(value)); +} + +function decodeData(value: string): any { + try { + return JSON.parse(atob(value)); + } catch (error) { + return {}; + } +} + +function btoa(b: string): string { + return Buffer.from(b, "binary").toString("base64"); +} + +function atob(a: string): string { + return Buffer.from(a, "base64").toString("binary"); +} diff --git a/packages/remix-node/data.ts b/packages/remix-node/data.ts new file mode 100644 index 0000000000..8a5d11e329 --- /dev/null +++ b/packages/remix-node/data.ts @@ -0,0 +1,75 @@ +import type { Params } from "react-router"; + +import type { ServerBuild } from "./build"; +import type { Request } from "./fetch"; +import { Response } from "./fetch"; +import { json } from "./responses"; +import type { AppLoadContext } from "./routes"; + +export async function loadRouteData( + build: ServerBuild, + routeId: string, + request: Request, + context: AppLoadContext, + params: Params +): Promise { + let routeModule = build.routes[routeId].module; + + if (!routeModule.loader) { + return Promise.resolve(json(null)); + } + + let result = await routeModule.loader({ request, context, params }); + + if (result === undefined) { + throw new Error( + `You defined a loader for route "${routeId}" but didn't return ` + + `anything from your \`loader\` function. We can't do everything for you! 😅` + ); + } + + return isResponse(result) ? result : json(result); +} + +export async function callRouteAction( + build: ServerBuild, + routeId: string, + request: Request, + context: AppLoadContext, + params: Params +): Promise { + let routeModule = build.routes[routeId].module; + + if (!routeModule.action) { + throw new Error( + `You made a ${request.method} request to ${request.url} but did not provide ` + + `an \`action\` for route "${routeId}", so there is no way to handle the ` + + `request.` + ); + } + + let result = await routeModule.action({ request, context, params }); + + if (!isResponse(result) || result.headers.get("Location") == null) { + throw new Error( + `You made a ${request.method} request to ${request.url} but did not return ` + + `a redirect. Please \`return redirect(newUrl)\` from your \`action\` ` + + `function to avoid reposts when users click the back button.` + ); + } + + return new Response("", { + status: 303, + headers: result.headers + }); +} + +function isResponse(value: any): value is Response { + return ( + value != null && + typeof value.status === "number" && + typeof value.statusText === "string" && + typeof value.headers === "object" && + typeof value.body !== "undefined" + ); +} diff --git a/packages/remix-node/entry.ts b/packages/remix-node/entry.ts new file mode 100644 index 0000000000..7ae463abd5 --- /dev/null +++ b/packages/remix-node/entry.ts @@ -0,0 +1,130 @@ +import jsesc from "jsesc"; + +import type { Response } from "./fetch"; +import type { RouteMatch, ServerRouteMatch } from "./match"; +import type { + AppData, + RouteManifest, + RouteData, + RouteModules, + Route, + ServerRouteManifest +} from "./routes"; + +export interface EntryContext { + manifest: AssetsManifest; + matches: EntryRouteMatch[]; + componentDidCatchEmulator: ComponentDidCatchEmulator; + routeData: RouteData; + routeModules: RouteModules; + serverHandoffString?: string; +} + +export interface AssetsManifest { + version: string; + url: string; + entry: { + module: string; + imports: string[]; + }; + routes: EntryRouteManifest; +} + +export interface EntryRoute extends Route { + module: string; + imports?: string[]; + hasAction?: boolean; + hasLoader?: boolean; +} + +export type EntryRouteManifest = RouteManifest; +export type EntryRouteMatch = RouteMatch; + +/** + * Because `componentDidCatch` is stateful it doesn't participate in server + * rendering, so we emulate it with this value. Each mutates the + * value so we know which route was the last to attempt to render. We then use + * it to render a second time along with the caught error and emulate + * `componentDidCatch` on the server render 🎉 + * + * This is optional because it only exists in the server render, we don't hand + * this off to the browser because `componentDidCatch` already works there. + */ +export interface ComponentDidCatchEmulator { + trackBoundaries: boolean; + // `null` means the app layout threw before any routes rendered + renderBoundaryRouteId: string | null; + loaderBoundaryRouteId: string | null; + error?: SerializedError; +} + +export interface SerializedError { + message: string; + stack?: string; +} + +export function serializeError(error: Error): SerializedError { + return { + message: error.message, + stack: + error.stack && + error.stack.replace( + /\((.+?)\)/g, + (_match: string, file: string) => `(file://${file})` + ) + }; +} + +export function createMatches( + matches: ServerRouteMatch[], + routes: EntryRouteManifest +): EntryRouteMatch[] { + return matches.map(match => ({ + params: match.params, + pathname: match.pathname, + route: routes[match.route.id] + })); +} + +export async function createRouteData( + matches: ServerRouteMatch[], + loadResults: Response[] +): Promise { + let data = await Promise.all(loadResults.map(extractData)); + + return matches.reduce((memo, match, index) => { + memo[match.route.id] = data[index]; + return memo; + }, {} as RouteData); +} + +function extractData(response: Response): Promise { + let contentType = response.headers.get("Content-Type"); + + if (contentType && /\bapplication\/json\b/.test(contentType)) { + return response.json(); + } + + // What other data types do we need to handle here? What other kinds of + // responses are people going to be returning from their loaders? + // - application/x-www-form-urlencoded ? + // - multipart/form-data ? + // - binary (audio/video) ? + + return response.text(); +} + +export function createRouteModules( + routeManifest: ServerRouteManifest +): RouteModules { + return Object.keys(routeManifest).reduce((memo, routeId) => { + memo[routeId] = routeManifest[routeId].module; + return memo; + }, {} as RouteModules); +} + +export function createServerHandoffString(serverHandoff: any): string { + // Use jsesc to escape data returned from the loaders. This string is + // inserted directly into the HTML in the `` element. + return jsesc(serverHandoff, { isScriptContext: true }); +} diff --git a/packages/remix-node/fetch.ts b/packages/remix-node/fetch.ts new file mode 100644 index 0000000000..0a2be2b5a5 --- /dev/null +++ b/packages/remix-node/fetch.ts @@ -0,0 +1,27 @@ +import type { RequestInfo, RequestInit, Response } from "node-fetch"; +import nodeFetch from "node-fetch"; + +export type { + HeadersInit, + RequestInfo, + RequestInit, + ResponseInit +} from "node-fetch"; +export { Headers, Request, Response } from "node-fetch"; + +/** + * A `fetch` function for node that matches the web Fetch API. Based on + * `node-fetch`. + * + * @see https://github.com/node-fetch/node-fetch + * @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API + */ +export function fetch( + input: RequestInfo, + init?: RequestInit +): Promise { + // Default to { compress: false } so responses can be proxied through more + // easily in loaders. Otherwise the response stream encoding will not match + // the Content-Encoding response header. + return nodeFetch(input, { compress: false, ...init }); +} diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts new file mode 100644 index 0000000000..ea6186748b --- /dev/null +++ b/packages/remix-node/globals.ts @@ -0,0 +1,24 @@ +import { + Headers as NodeHeaders, + Request as NodeRequest, + Response as NodeResponse, + fetch as nodeFetch +} from "./fetch"; + +declare global { + namespace NodeJS { + interface Global { + Headers: typeof NodeHeaders; + Request: typeof NodeRequest; + Response: typeof NodeResponse; + fetch: typeof nodeFetch; + } + } +} + +export function installGlobals() { + (global as NodeJS.Global).Headers = NodeHeaders; + (global as NodeJS.Global).Request = NodeRequest; + (global as NodeJS.Global).Response = NodeResponse; + (global as NodeJS.Global).fetch = nodeFetch; +} diff --git a/packages/remix-node/headers.ts b/packages/remix-node/headers.ts new file mode 100644 index 0000000000..650e2cbb8e --- /dev/null +++ b/packages/remix-node/headers.ts @@ -0,0 +1,89 @@ +import type { ServerBuild } from "./build"; +import type { Response } from "./fetch"; +import { Headers } from "./fetch"; +import type { ServerRouteMatch } from "./match"; + +export function getDocumentHeaders( + build: ServerBuild, + matches: ServerRouteMatch[], + routeLoaderResponses: Response[] +): Headers { + return matches.reduce((parentHeaders, match, index) => { + let routeModule = build.routes[match.route.id].module; + let loaderResponse = routeLoaderResponses[index]; + let loaderHeaders = loaderResponse.headers; + + let headers = new Headers( + routeModule.headers + ? routeModule.headers({ loaderHeaders, parentHeaders }) + : undefined + ); + + // Automatically preserve Set-Cookie headers that were set either by the + // loader or by a parent route. + prependCookies(loaderHeaders, headers); + prependCookies(parentHeaders, headers); + + return headers; + }, new Headers()); +} + +function prependCookies(parentHeaders: Headers, childHeaders: Headers): void { + if (parentHeaders.has("Set-Cookie")) { + childHeaders.set( + "Set-Cookie", + concatSetCookieHeaders( + parentHeaders.get("Set-Cookie")!, + childHeaders.get("Set-Cookie") + ) + ); + } +} + +/** + * Merges two `Set-Cookie` headers, eliminating duplicates and preserving the + * original ordering. + */ +function concatSetCookieHeaders( + parentHeader: string, + childHeader: string | null +): string { + if (!childHeader || childHeader === parentHeader) { + return parentHeader; + } + + let finalCookies: RawSetCookies = new Map(); + let parentCookies = parseSetCookieHeader(parentHeader); + let childCookies = parseSetCookieHeader(childHeader); + + for (let [name, value] of parentCookies) { + finalCookies.set(name, childCookies.get(name) || value); + } + + for (let [name, value] of childCookies) { + if (!finalCookies.has(name)) { + finalCookies.set(name, value); + } + } + + return serializeSetCookieHeader(finalCookies); +} + +type RawSetCookies = Map; + +function parseSetCookieHeader(header: string): RawSetCookies { + return header.split(/\s*,\s*/g).reduce((map, pair) => { + let [name, value] = pair.split("="); + return map.set(name, value); + }, new Map()); +} + +function serializeSetCookieHeader(cookies: RawSetCookies): string { + let pairs: string[] = []; + + for (let [name, value] of cookies) { + pairs.push(name + "=" + value); + } + + return pairs.join(", "); +} diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts new file mode 100644 index 0000000000..963192a7f6 --- /dev/null +++ b/packages/remix-node/index.ts @@ -0,0 +1,73 @@ +import "./assetImportTypes"; + +export type { ServerBuild, ServerEntryModule } from "./build"; + +export type { + CookieParseOptions, + CookieSerializeOptions, + CookieSignatureOptions, + CookieOptions, + Cookie +} from "./cookies"; +export { createCookie, isCookie } from "./cookies"; + +export type { + EntryContext, + AssetsManifest, + EntryRoute, + EntryRouteManifest, + EntryRouteMatch, + ComponentDidCatchEmulator, + SerializedError +} from "./entry"; + +export type { + HeadersInit, + RequestInfo, + RequestInit, + ResponseInit +} from "./fetch"; +export { Headers, Request, Response, fetch } from "./fetch"; + +export { installGlobals } from "./globals"; + +export type { + LinkDescriptor, + HTMLLinkDescriptor, + BlockLinkDescriptor, + PageLinkDescriptor +} from "./links"; + +export type { + AppLoadContext, + AppData, + RouteComponent, + ErrorBoundaryComponent, + HeadersFunction, + MetaFunction, + LinksFunction, + LoaderFunction, + ActionFunction, + RouteModule, + RouteManifest, + RouteData, + RouteModules +} from "./routes"; + +export { json, redirect } from "./responses"; + +export type { RequestHandler } from "./server"; +export { createRequestHandler } from "./server"; + +export type { + SessionData, + Session, + SessionStorage, + SessionIdStorageStrategy +} from "./sessions"; +export { createSession, isSession, createSessionStorage } from "./sessions"; +export { createCookieSessionStorage } from "./sessions/cookieStorage"; +export { createFileSessionStorage } from "./sessions/fileStorage"; +export { createMemorySessionStorage } from "./sessions/memoryStorage"; + +export { warnOnce } from "./warnings"; diff --git a/packages/remix-node/invariant.ts b/packages/remix-node/invariant.ts new file mode 100644 index 0000000000..6f91537bf4 --- /dev/null +++ b/packages/remix-node/invariant.ts @@ -0,0 +1,16 @@ +export default function invariant( + value: boolean, + message?: string +): asserts value; +export default function invariant( + value: T | null | undefined, + message?: string +): asserts value is T; +export default function invariant(value: any, message?: string) { + if (value === false || value === null || typeof value === "undefined") { + console.error( + "The following error is a bug in Remix, please file an issue! https://remix.run/dashbaord/support" + ); + throw new Error(message); + } +} diff --git a/packages/remix-node/links.ts b/packages/remix-node/links.ts new file mode 100644 index 0000000000..4d83f918ba --- /dev/null +++ b/packages/remix-node/links.ts @@ -0,0 +1,161 @@ +/** + * Remix Link descriptor, an object representation of the HTML `` element. + * + * WHATWG Specification: https://html.spec.whatwg.org/multipage/semantics.html#the-link-element + */ +export interface HTMLLinkDescriptor { + /** + * Address of the hyperlink + */ + href: string; + + /** + * How the element handles crossorigin requests + */ + crossOrigin?: "anonymous" | "use-credentials"; + + /** + * Relationship between the document containing the hyperlink and the destination resource + */ + rel: + | "alternate" + | "dns-prefetch" + | "icon" + | "manifest" + | "modulepreload" + | "next" + | "pingback" + | "preconnect" + | "prefetch" + | "preload" + | "prerender" + | "search" + | "stylesheet" + | string; + + /** + * Applicable media: "screen", "print", "(max-width: 764px)" + */ + media?: string; + + /** + * Integrity metadata used in Subresource Integrity checks + */ + integrity?: string; + + /** + * Language of the linked resource + */ + hrefLang?: string; + + /** + * Hint for the type of the referenced resource + */ + type?: string; + + /** + * Referrer policy for fetches initiated by the element + */ + referrerPolicy?: + | "" + | "no-referrer" + | "no-referrer-when-downgrade" + | "same-origin" + | "origin" + | "strict-origin" + | "origin-when-cross-origin" + | "strict-origin-when-cross-origin" + | "unsafe-url"; + + /** + * Sizes of the icons (for rel="icon") + */ + sizes?: string; + + /** + * Images to use in different situations, e.g., high-resolution displays, small monitors, etc. (for rel="preload") + */ + imagesrcset?: string; + + /** + * Image sizes for different page layouts (for rel="preload") + */ + imagesizes?: string; + + /** + * Potential destination for a preload request (for rel="preload" and rel="modulepreload") + */ + as?: + | "audio" + | "audioworklet" + | "document" + | "embed" + | "fetch" + | "font" + | "frame" + | "iframe" + | "image" + | "manifest" + | "object" + | "paintworklet" + | "report" + | "script" + | "serviceworker" + | "sharedworker" + | "style" + | "track" + | "video" + | "worker" + | "xslt" + | string; + + /** + * Color to use when customizing a site's icon (for rel="mask-icon") + */ + color?: string; + + /** + * Whether the link is disabled + */ + disabled?: boolean; + + /** + * The title attribute has special semantics on this element: Title of the link; CSS style sheet set name. + */ + title?: string; +} + +export interface PageLinkDescriptor + extends Omit< + HTMLLinkDescriptor, + | "href" + | "rel" + | "type" + | "sizes" + | "imagesrcset" + | "imagesizes" + | "as" + | "color" + | "title" + > { + /** + * The absolute path of the page to prefetch. + */ + page: string; + + /** + * If `true` when using `transition: "client"`, instructs Remix to prefetch + * the data for the destination page. + */ + data?: boolean; +} + +export interface BlockLinkDescriptor { + blocker: true; + link: HTMLLinkDescriptor; +} + +export type LinkDescriptor = + | HTMLLinkDescriptor + | BlockLinkDescriptor + | PageLinkDescriptor; diff --git a/packages/remix-node/match.ts b/packages/remix-node/match.ts new file mode 100644 index 0000000000..dceba36749 --- /dev/null +++ b/packages/remix-node/match.ts @@ -0,0 +1,39 @@ +import type { RouteObject, Params } from "react-router"; +import { matchRoutes as match } from "react-router"; + +import type { ServerRoute, ServerRouteManifest } from "./routes"; + +export interface RouteMatch { + params: Params; + pathname: string; + route: Route; +} + +export type ServerRouteMatch = RouteMatch; + +export function createRoutes( + routeManifest: ServerRouteManifest, + parentId?: string +): ServerRoute[] { + return Object.keys(routeManifest) + .filter(key => routeManifest[key].parentId === parentId) + .map(id => ({ + ...routeManifest[id], + children: createRoutes(routeManifest, id) + })); +} + +export function matchRoutes( + routes: ServerRoute[], + pathname: string +): ServerRouteMatch[] | null { + let matches = match((routes as unknown) as RouteObject[], pathname); + + if (!matches) return null; + + return matches.map(match => ({ + params: match.params, + pathname: match.pathname, + route: (match.route as unknown) as ServerRoute + })); +} diff --git a/packages/remix-node/mode.ts b/packages/remix-node/mode.ts new file mode 100644 index 0000000000..817352d294 --- /dev/null +++ b/packages/remix-node/mode.ts @@ -0,0 +1,16 @@ +/** + * The mode to use when running the server. + */ +export enum ServerMode { + Development = "development", + Production = "production", + Test = "test" +} + +export function isServerMode(value: any): value is ServerMode { + return ( + value === ServerMode.Development || + value === ServerMode.Production || + value === ServerMode.Test + ); +} diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json new file mode 100644 index 0000000000..83de4762b9 --- /dev/null +++ b/packages/remix-node/package.json @@ -0,0 +1,27 @@ +{ + "name": "@remix-run/node", + "description": "Node.js bindings for Remix", + "version": "0.16.1", + "repository": "https://github.com/remix-run/remix", + "dependencies": { + "@types/cookie": "^0.4.0", + "@types/node-fetch": "^2.5.7", + "cookie": "^0.4.1", + "cookie-signature": "^1.1.0", + "history": "^5.0.0", + "jsesc": "^3.0.1", + "node-fetch": "^2.6.1", + "react-router-dom": "^6.0.0-beta.0", + "tmp": "^0.2.1" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + }, + "devDependencies": { + "@types/cookie-signature": "^1.0.3", + "@types/jsesc": "^2.5.1", + "@types/tmp": "^0.2.0" + }, + "sideEffects": false +} diff --git a/packages/remix-node/responses.ts b/packages/remix-node/responses.ts new file mode 100644 index 0000000000..a3931aec69 --- /dev/null +++ b/packages/remix-node/responses.ts @@ -0,0 +1,38 @@ +import type { ResponseInit } from "./fetch"; +import { Headers, Response } from "./fetch"; + +/** + * A JSON response. Converts `data` to JSON and sets the `Content-Type` header. + */ +export function json(data: any, init: number | ResponseInit = {}): Response { + if (typeof init === "number") { + init = { status: init }; + } + + let headers = new Headers(init.headers); + if (!headers.has("Content-Type")) { + headers.set("Content-Type", "application/json; charset=utf-8"); + } + + return new Response(JSON.stringify(data), { ...init, headers }); +} + +/** + * A redirect response. Sets the status code and the `Location` header. + * Defaults to "302 Found". + */ +export function redirect( + url: string, + init: number | ResponseInit = 302 +): Response { + if (typeof init === "number") { + init = { status: init }; + } else if (typeof init.status === "undefined") { + init.status = 302; + } + + let headers = new Headers(init.headers); + headers.set("Location", url); + + return new Response("", { ...init, headers }); +} diff --git a/packages/remix-node/routes.ts b/packages/remix-node/routes.ts new file mode 100644 index 0000000000..59705273dc --- /dev/null +++ b/packages/remix-node/routes.ts @@ -0,0 +1,113 @@ +import type { Location } from "history"; +import type { ComponentType } from "react"; +import type { Params } from "react-router"; + +import type { HeadersInit, Headers, Request, Response } from "./fetch"; +import type { LinkDescriptor } from "./links"; + +/** + * An object of data returned from the server's `getLoadContext` function. This + * will be passed to the data loaders. + */ +export type AppLoadContext = any; + +/** + * Some data that was returned from a route data loader. + */ +export type AppData = any; + +/** + * A React component that is rendered for a route. + */ +export type RouteComponent = ComponentType; + +/** + * A React component that is rendered when there is an error on a route. + */ +export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; + +/** + * A function that returns HTTP headers to be used for a route. These headers + * will be merged with (and take precedence over) headers from parent routes. + */ +export interface HeadersFunction { + (args: { loaderHeaders: Headers; parentHeaders: Headers }): + | Headers + | HeadersInit; +} + +/** + * A function that returns an object of name + content pairs to use for + * `` tags for a route. These tags will be merged with (and take + * precedence over) tags from parent routes. + */ +export interface MetaFunction { + (args: { + data: AppData; + parentsData: RouteData; + params: Params; + location: Location; + }): { [name: string]: string }; +} + +/** + * A function that defines `` tags to be inserted into the `` of + * the document on route transitions. + */ +export interface LinksFunction { + (args: { data: AppData }): LinkDescriptor[]; +} + +/** + * A function that loads data for a route. + */ +export interface LoaderFunction { + (args: { request: Request; context: AppLoadContext; params: Params }): + | Promise + | AppData; +} + +/** + * A function that handles data mutations for a route. + */ +export interface ActionFunction { + (args: { request: Request; context: AppLoadContext; params: Params }): + | Promise + | Response; +} + +/** + * A module that contains info about a route including headers, meta tags, and + * the route component for rendering HTML markup. + */ +export interface RouteModule { + default: RouteComponent; + ErrorBoundary?: ErrorBoundaryComponent; + headers?: HeadersFunction; + meta?: MetaFunction; + loader?: LoaderFunction; + action?: ActionFunction; + links?: LinksFunction; + handle?: any; +} + +export interface RouteManifest { + [routeId: string]: Route; +} + +export type RouteData = RouteManifest; +export type RouteModules = RouteManifest; + +export interface Route { + path: string; + caseSensitive?: boolean; + id: string; + parentId?: string; +} + +export interface ServerRoute extends Route { + module: RouteModule; + children: ServerRoute[]; +} + +export type ServerRouteManifest = RouteManifest>; diff --git a/packages/remix-node/server.ts b/packages/remix-node/server.ts new file mode 100644 index 0000000000..51941a7802 --- /dev/null +++ b/packages/remix-node/server.ts @@ -0,0 +1,301 @@ +import { loadRouteData, callRouteAction } from "./data"; +import type { ServerBuild } from "./build"; +import type { ComponentDidCatchEmulator, EntryContext } from "./entry"; +import * as entry from "./entry"; +import type { Request } from "./fetch"; +import { Response } from "./fetch"; +import { getDocumentHeaders } from "./headers"; +import type { ServerRouteMatch } from "./match"; +import { createRoutes, matchRoutes } from "./match"; +import { ServerMode, isServerMode } from "./mode"; +import type { AppLoadContext, ServerRoute } from "./routes"; +import { json } from "./responses"; + +/** + * The main request handler for a Remix server. This handler runs in the context + * of a cloud provider's server (e.g. Express on Firebase) or locally via their + * dev tools. + */ +export interface RequestHandler { + (request: Request, loadContext?: AppLoadContext): Promise; +} + +/** + * Creates a function that serves HTTP requests. + */ +export function createRequestHandler( + build: ServerBuild, + mode?: string +): RequestHandler { + let routes = createRoutes(build.routes); + let serverMode = isServerMode(mode) ? mode : ServerMode.Production; + + return (request, loadContext = {}) => + isDataRequest(request) + ? handleDataRequest(request, loadContext, build, routes) + : handleDocumentRequest(request, loadContext, build, routes, serverMode); +} + +async function handleDataRequest( + request: Request, + loadContext: AppLoadContext, + build: ServerBuild, + routes: ServerRoute[] +): Promise { + let url = new URL(request.url); + + let matches = matchRoutes(routes, url.pathname); + if (!matches) { + return jsonError(`No route matches URL "${url.pathname}"`, 404); + } + + let routeMatch: ServerRouteMatch; + if (isActionRequest(request)) { + routeMatch = matches[matches.length - 1]; + } else { + let routeId = url.searchParams.get("_data"); + if (!routeId) { + return jsonError(`Missing route id in ?_data`, 403); + } + + let match = matches.find(match => match.route.id === routeId); + if (!match) { + return jsonError( + `Route "${routeId}" does not match URL "${url.pathname}"`, + 403 + ); + } + + routeMatch = match; + } + + let response: Response; + try { + response = isActionRequest(request) + ? await callRouteAction( + build, + routeMatch.route.id, + request, + loadContext, + routeMatch.params + ) + : await loadRouteData( + build, + routeMatch.route.id, + request, + loadContext, + routeMatch.params + ); + } catch (error) { + return json(entry.serializeError(error), { + status: 500, + headers: { + "X-Remix-Error": "unfortunately, yes" + } + }); + } + + if (isRedirectResponse(response)) { + // We don't have any way to prevent a fetch request from following + // redirects. So we use the `X-Remix-Redirect` header to indicate the + // next URL, and then "follow" the redirect manually on the client. + let locationHeader = response.headers.get("Location"); + response.headers.delete("Location"); + + return new Response("", { + status: 204, + headers: { + ...Object.fromEntries(response.headers), + "X-Remix-Redirect": locationHeader! + } + }); + } + + return response; +} + +async function handleDocumentRequest( + request: Request, + loadContext: AppLoadContext, + build: ServerBuild, + routes: ServerRoute[], + serverMode: ServerMode +): Promise { + let url = new URL(request.url); + + let matches = matchRoutes(routes, url.pathname); + if (!matches) { + // TODO: Provide a default 404 page + throw new Error( + `There is no route that matches ${url.pathname}. Please add ` + + `a routes/404.js file` + ); + } + + if (isActionRequest(request)) { + let leafMatch = matches[matches.length - 1]; + let response = await callRouteAction( + build, + leafMatch.route.id, + request, + loadContext, + leafMatch.params + ); + + // TODO: How do we handle errors here? + + return response; + } + + let componentDidCatchEmulator: ComponentDidCatchEmulator = { + trackBoundaries: true, + renderBoundaryRouteId: null, + loaderBoundaryRouteId: null, + error: undefined + }; + + // Run all data loaders in parallel. Await them in series below. + // Note: This code is a little weird due to the way unhandled promise + // rejections are handled in node. We use a .catch() handler on each + // promise to avoid the warning, then handle errors manually afterwards. + let routeLoaderPromises: Promise[] = matches.map(match => + loadRouteData( + build, + match.route.id, + request.clone(), + loadContext, + match.params + ).catch(error => error) + ); + + let routeLoaderResults = await Promise.all(routeLoaderPromises); + for (let [index, response] of routeLoaderResults.entries()) { + if (componentDidCatchEmulator.error) { + continue; + } + + let route = matches[index].route; + let routeModule = build.routes[route.id].module; + + if (routeModule.ErrorBoundary) { + componentDidCatchEmulator.loaderBoundaryRouteId = route.id; + } + + if (response instanceof Error) { + if (serverMode !== ServerMode.Test) { + console.error( + `There was an error running the data loader for route ${route.id}` + ); + } + + componentDidCatchEmulator.error = entry.serializeError(response); + routeLoaderResults[index] = json(null, { status: 500 }); + } else if (isRedirectResponse(response)) { + return response; + } + } + + // We already filtered out all Errors, so these are all Responses. + let routeLoaderResponses: Response[] = routeLoaderResults as Response[]; + + // Handle responses with a non-200 status code. The first loader with a + // non-200 status code determines the status code for the whole response. + let notOkResponse = routeLoaderResponses.find( + response => response.status !== 200 + ); + + let statusCode = notOkResponse + ? notOkResponse.status + : matches[matches.length - 1].route.id === "routes/404" + ? 404 + : 200; + + let serverEntryModule = build.entry.module; + let headers = getDocumentHeaders(build, matches, routeLoaderResponses); + let entryMatches = entry.createMatches(matches, build.assets.routes); + let routeData = await entry.createRouteData(matches, routeLoaderResponses); + let routeModules = entry.createRouteModules(build.routes); + let serverHandoff = { + matches: entryMatches, + componentDidCatchEmulator, + routeData + }; + let serverEntryContext: EntryContext = { + ...serverHandoff, + manifest: build.assets, + routeModules, + serverHandoffString: entry.createServerHandoffString(serverHandoff) + }; + + let response: Response | Promise; + try { + response = serverEntryModule.default( + request, + statusCode, + headers, + serverEntryContext + ); + } catch (error) { + if (serverMode !== ServerMode.Test) { + console.error(error); + } + + statusCode = 500; + + // Go again, this time with the componentDidCatch emulation. Remember, the + // routes `componentDidCatch.routeId` because we can't know that here. (Well + // ... maybe we could, we could search the error.stack lines for the first + // file matching the id of a route from the route manifest, but that would + // require us to have source maps installed so the filenames don't get + // changed when we bundle, and just feels a little too shakey for me right + // now. I'm okay with tracking our position in the route tree while + // rendering, that's pretty much how hooks work 😂) + componentDidCatchEmulator.trackBoundaries = false; + componentDidCatchEmulator.error = entry.serializeError(error); + serverEntryContext.serverHandoffString = entry.createServerHandoffString( + serverHandoff + ); + + try { + response = serverEntryModule.default( + request, + statusCode, + headers, + serverEntryContext + ); + } catch (error) { + if (serverMode !== ServerMode.Test) { + console.error(error); + } + + // Good grief folks, get your act together 😂! + // TODO: Something is wrong in serverEntryModule, use the default root error handler + response = new Response(`Unexpected Server Error\n\n${error.message}`, { + status: 500, + headers: { + "Content-Type": "text/plain" + } + }); + } + } + + return response; +} + +function jsonError(error: string, status = 403): Response { + return json({ error }, { status }); +} + +function isActionRequest(request: Request): boolean { + return request.method.toLowerCase() !== "get"; +} + +function isDataRequest(request: Request): boolean { + return new URL(request.url).searchParams.has("_data"); +} + +const redirectStatusCodes = new Set([301, 302, 303, 307, 308]); + +function isRedirectResponse(response: Response): boolean { + return redirectStatusCodes.has(response.status); +} diff --git a/packages/remix-node/sessions.ts b/packages/remix-node/sessions.ts new file mode 100644 index 0000000000..ea26f6c690 --- /dev/null +++ b/packages/remix-node/sessions.ts @@ -0,0 +1,250 @@ +import type { CookieParseOptions, CookieSerializeOptions } from "cookie"; + +import type { Cookie, CookieOptions } from "./cookies"; +import { createCookie, isCookie } from "./cookies"; +import { warnOnce } from "./warnings"; + +/** + * An object of name/value pairs to be used in the session. + */ +export interface SessionData { + [name: string]: any; +} + +/** + * Session persists data across HTTP requests. + */ +export interface Session { + /** + * A unique identifier for this session. + * + * Note: This will be the empty string for newly created sessions and + * sessions that are not backed by a database (i.e. cookie-based sessions). + */ + readonly id: string; + + /** + * The raw data contained in this session. + * + * This is useful mostly for SessionStorage internally to access the raw + * session data to persist. + */ + readonly data: SessionData; + + /** + * Returns `true` if the session has a value for the given `name`, `false` + * otherwise. + */ + has(name: string): boolean; + + /** + * Returns the value for the given `name` in this session. + */ + get(name: string): any; + + /** + * Sets a value in the session for the given `name`. + */ + set(name: string, value: any): void; + + /** + * Sets a value in the session that is only valid until the next `get()`. + * This can be useful for temporary values, like error messages. + */ + flash(name: string, value: any): void; + + /** + * Removes a value from the session. + */ + unset(name: string): void; +} + +function flash(name: string): string { + return `__flash_${name}__`; +} + +/** + * Creates a new Session object. + * + * Note: This function is typically not invoked directly by application code. + * Instead, use a `SessionStorage` object's `getSession` method. + */ +export function createSession(initialData: SessionData = {}, id = ""): Session { + let map = new Map(Object.entries(initialData)); + + return { + get id() { + return id; + }, + get data() { + return Object.fromEntries(map); + }, + has(name) { + return map.has(name) || map.has(flash(name)); + }, + get(name) { + if (map.has(name)) return map.get(name); + + let flashName = flash(name); + if (map.has(flashName)) { + let value = map.get(flashName); + map.delete(flashName); + return value; + } + + return undefined; + }, + set(name, value) { + map.set(name, value); + }, + flash(name, value) { + map.set(flash(name), value); + }, + unset(name) { + map.delete(name); + } + }; +} + +export function isSession(object: any): object is Session { + return ( + object != null && + typeof object.id === "string" && + typeof object.data !== "undefined" && + typeof object.has === "function" && + typeof object.get === "function" && + typeof object.set === "function" && + typeof object.flash === "function" && + typeof object.unset === "function" + ); +} + +/** + * SessionStorage stores session data between HTTP requests and knows how to + * parse and create cookies. + * + * A SessionStorage creates Session objects using a `Cookie` header as input. + * Then, later it generates the `Set-Cookie` header to be used in the response. + */ +export interface SessionStorage { + /** + * Parses a Cookie header from a HTTP request and returns the associated + * Session. If there is no session associated with the cookie, this will + * return a new Session with no data. + */ + getSession( + cookieHeader?: string | null, + options?: CookieParseOptions + ): Promise; + + /** + * Stores all data in the Session and returns the Set-Cookie header to be + * used in the HTTP response. + */ + commitSession( + session: Session, + options?: CookieSerializeOptions + ): Promise; + + /** + * Deletes all data associated with the Session and returns the Set-Cookie + * header to be used in the HTTP response. + */ + destroySession( + session: Session, + options?: CookieSerializeOptions + ): Promise; +} + +/** + * SessionIdStorageStrategy is designed to allow anyone to easily build their + * own SessionStorage using `createSessionStorage(strategy)`. + * + * This strategy describes a common scenario where the session id is stored in + * a cookie but the actual session data is stored elsewhere, usually in a + * database or on disk. A set of create, read, update, and delete operations + * are provided for managing the session data. + */ +export interface SessionIdStorageStrategy { + /** + * The Cookie used to store the session id, or options used to automatically + * create one. + */ + cookie?: Cookie | (CookieOptions & { name?: string }); + + /** + * Creates a new record with the given data and returns the session id. + */ + createData: (data: SessionData, expires?: Date) => Promise; + + /** + * Returns data for a given session id, or `null` if there isn't any. + */ + readData: (id: string) => Promise; + + /** + * Updates data for the given session id. + */ + updateData: (id: string, data: SessionData, expires?: Date) => Promise; + + /** + * Deletes data for a given session id from the data store. + */ + deleteData: (id: string) => Promise; +} + +/** + * Creates a SessionStorage object using a SessionIdStorageStrategy. + * + * Note: This is a low-level API that should only be used if none of the + * existing session storage options meet your requirements. + */ +export function createSessionStorage({ + cookie: cookieArg, + createData, + readData, + updateData, + deleteData +}: SessionIdStorageStrategy): SessionStorage { + let cookie = isCookie(cookieArg) + ? cookieArg + : createCookie((cookieArg && cookieArg.name) || "__session", cookieArg); + + warnOnceAboutSigningSessionCookie(cookie); + + return { + async getSession(cookieHeader, options) { + let id = cookieHeader && cookie.parse(cookieHeader, options); + let data = id && (await readData(id)); + return createSession(data || {}, id || ""); + }, + async commitSession(session, options) { + let { id, data } = session; + + if (id) { + await updateData(id, data, cookie.expires); + } else { + id = await createData(data, cookie.expires); + } + + return cookie.serialize(id, options); + }, + async destroySession(session, options) { + await deleteData(session.id); + return cookie.serialize("", { + ...options, + expires: new Date(0) + }); + } + }; +} + +export function warnOnceAboutSigningSessionCookie(cookie: Cookie) { + warnOnce( + cookie.isSigned, + `The "${cookie.name}" cookie is not signed, but session cookies should be ` + + `signed to prevent tampering on the client before they are sent back to the ` + + `server. See https://remix.run/dashboard/docs/cookies#signing-cookies ` + + `for more information.` + ); +} diff --git a/packages/remix-node/sessions/cookieStorage.ts b/packages/remix-node/sessions/cookieStorage.ts new file mode 100644 index 0000000000..cdaa221804 --- /dev/null +++ b/packages/remix-node/sessions/cookieStorage.ts @@ -0,0 +1,47 @@ +import { createCookie, isCookie } from "../cookies"; +import type { SessionStorage, SessionIdStorageStrategy } from "../sessions"; +import { warnOnceAboutSigningSessionCookie, createSession } from "../sessions"; + +interface CookieSessionStorageOptions { + /** + * The Cookie used to store the session data on the client, or options used + * to automatically create one. + */ + cookie?: SessionIdStorageStrategy["cookie"]; +} + +/** + * Creates and returns a SessionStorage object that stores all session data + * directly in the session cookie itself. + * + * This has the advantage that no database or other backend services are + * needed, and can help to simplify some load-balanced scenarios. However, it + * also has the limitation that serialized session data may not exceed the + * browser's maximum cookie size. Trade-offs! + */ +export function createCookieSessionStorage({ + cookie: cookieArg +}: CookieSessionStorageOptions = {}): SessionStorage { + let cookie = isCookie(cookieArg) + ? cookieArg + : createCookie((cookieArg && cookieArg.name) || "__session", cookieArg); + + warnOnceAboutSigningSessionCookie(cookie); + + return { + async getSession(cookieHeader, options) { + return createSession( + (cookieHeader && cookie.parse(cookieHeader, options)) || {} + ); + }, + async commitSession(session, options) { + return cookie.serialize(session.data, options); + }, + async destroySession(_session, options) { + return cookie.serialize("", { + ...options, + expires: new Date(0) + }); + } + }; +} diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts new file mode 100644 index 0000000000..46153723b6 --- /dev/null +++ b/packages/remix-node/sessions/fileStorage.ts @@ -0,0 +1,98 @@ +import { randomBytes } from "crypto"; +import { promises as fsp } from "fs"; +import * as path from "path"; + +import type { SessionStorage, SessionIdStorageStrategy } from "../sessions"; +import { createSessionStorage } from "../sessions"; + +interface FileSessionStorageOptions { + /** + * The Cookie used to store the session id on the client, or options used + * to automatically create one. + */ + cookie?: SessionIdStorageStrategy["cookie"]; + + /** + * The directory to use to store session files. + */ + dir: string; +} + +/** + * Creates a SessionStorage that stores session data on a filesystem. + * + * The advantage of using this instead of cookie session storage is that + * files may contain much more data than cookies. + */ +export function createFileSessionStorage({ + cookie, + dir +}: FileSessionStorageOptions): SessionStorage { + return createSessionStorage({ + cookie, + async createData(data, expires) { + let content = JSON.stringify({ data, expires }); + + while (true) { + // This storage manages an id space of 2^64 ids, which is far greater + // than the maximum number of files allowed on an NTFS or ext4 volume + // (2^32). However, the larger id space should help to avoid collisions + // with existing ids when creating new sessions, which speeds things up. + let id = randomBytes(8).toString("hex"); + + try { + let file = getFile(dir, id); + await fsp.mkdir(path.dirname(file), { recursive: true }); + await fsp.writeFile(file, content, { encoding: "utf-8", flag: "wx" }); + return id; + } catch (error) { + if (error.code !== "EEXIST") throw error; + } + } + }, + async readData(id) { + try { + let file = getFile(dir, id); + let content = JSON.parse(await fsp.readFile(file, "utf-8")); + let data = content.data; + let expires = + typeof content.expires === "string" + ? new Date(content.expires) + : null; + + if (!expires || expires > new Date()) { + return data; + } + + // Remove expired session data. + if (expires) await fsp.unlink(file); + + return null; + } catch (error) { + if (error.code !== "ENOENT") throw error; + return null; + } + }, + async updateData(id, data, expires) { + let content = JSON.stringify({ data, expires }); + let file = getFile(dir, id); + await fsp.mkdir(path.dirname(file), { recursive: true }); + await fsp.writeFile(file, content, "utf-8"); + }, + async deleteData(id) { + try { + await fsp.unlink(getFile(dir, id)); + } catch (error) { + if (error.code !== "ENOENT") throw error; + } + } + }); +} + +function getFile(dir: string, id: string): string { + // Divide the session id up into a directory (first 2 bytes) and filename + // (remaining 6 bytes) to reduce the chance of having very large directories, + // which should speed up file access. This is a maximum of 2^16 directories, + // each with 2^48 files. + return path.join(dir, id.slice(0, 4), id.slice(4)); +} diff --git a/packages/remix-node/sessions/memoryStorage.ts b/packages/remix-node/sessions/memoryStorage.ts new file mode 100644 index 0000000000..2233e1d7a3 --- /dev/null +++ b/packages/remix-node/sessions/memoryStorage.ts @@ -0,0 +1,57 @@ +import type { + SessionData, + SessionStorage, + SessionIdStorageStrategy +} from "../sessions"; +import { createSessionStorage } from "../sessions"; + +interface MemorySessionStorageOptions { + /** + * The Cookie used to store the session id on the client, or options used + * to automatically create one. + */ + cookie?: SessionIdStorageStrategy["cookie"]; +} + +/** + * Creates and returns a simple in-memory SessionStorage object, mostly useful + * for testing and as a reference implementation. + * + * Note: This storage does not scale beyond a single process, so it is not + * suitable for most production scenarios. + */ +export function createMemorySessionStorage({ + cookie +}: MemorySessionStorageOptions = {}): SessionStorage { + let uniqueId = 0; + let map = new Map(); + + return createSessionStorage({ + cookie, + async createData(data, expires) { + let id = (++uniqueId).toString(); + map.set(id, { data, expires }); + return id; + }, + async readData(id) { + if (map.has(id)) { + let { data, expires } = map.get(id)!; + + if (!expires || expires > new Date()) { + return data; + } + + // Remove expired session data. + if (expires) map.delete(id); + } + + return null; + }, + async updateData(id, data, expires) { + map.set(id, { data, expires }); + }, + async deleteData(id) { + map.delete(id); + } + }); +} diff --git a/packages/remix-node/tsconfig.json b/packages/remix-node/tsconfig.json new file mode 100644 index 0000000000..a7a4ed1724 --- /dev/null +++ b/packages/remix-node/tsconfig.json @@ -0,0 +1,20 @@ +{ + "exclude": ["__tests__/**/*"], + "compilerOptions": { + "lib": ["ES2019"], + "target": "ES2019", + + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "strict": true, + + "declaration": true, + "emitDeclarationOnly": true, + + "outDir": "../../build/node_modules/@remix-run/node", + "rootDir": ".", + + // Avoid naming conflicts between lib.dom.d.ts and globals.ts + "skipLibCheck": true + } +} diff --git a/packages/remix-node/warnings.ts b/packages/remix-node/warnings.ts new file mode 100644 index 0000000000..45acd96010 --- /dev/null +++ b/packages/remix-node/warnings.ts @@ -0,0 +1,8 @@ +const alreadyWarned: { [message: string]: boolean } = {}; + +export function warnOnce(condition: boolean, message: string): void { + if (!condition && !alreadyWarned[message]) { + alreadyWarned[message] = true; + console.warn(message); + } +} diff --git a/packages/remix-serve/app.ts b/packages/remix-serve/app.ts new file mode 100644 index 0000000000..7837a75450 --- /dev/null +++ b/packages/remix-serve/app.ts @@ -0,0 +1,35 @@ +import express from "express"; +import compression from "compression"; +import morgan from "morgan"; +import { createRequestHandler } from "@remix-run/express"; + +export default function getApp(buildPath: string) { + let app = express(); + + app.use(compression()); + + app.use( + express.static("public", { + immutable: true, + maxAge: "1y" + }) + ); + + app.use(morgan("tiny")); + + app.all( + "*", + process.env.NODE_ENV === "production" + ? createRequestHandler({ + build: require(buildPath) + }) + : (req, res, next) => { + // require cache is purged in @remix-run/dev where the file watcher is + return createRequestHandler({ + build: require(buildPath) + })(req, res, next); + } + ); + + return app; +} diff --git a/packages/remix-serve/index.ts b/packages/remix-serve/index.ts new file mode 100644 index 0000000000..35a9fac8a8 --- /dev/null +++ b/packages/remix-serve/index.ts @@ -0,0 +1,18 @@ +import path from "path"; +import getServer from "./app"; + +let port = process.env.PORT || 3000; + +let buildPath = process.argv[2]; +if (!buildPath) { + console.log( + `Please pass in the directory of your Remix server build directory: + + remix-serve ./build` + ); +} else { + let resolovedBuildPath = path.resolve(process.cwd(), buildPath); + getServer(resolovedBuildPath).listen(port, () => { + console.log(`Remix App Server started on port ${port}`); + }); +} diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json new file mode 100644 index 0000000000..7de302f9ec --- /dev/null +++ b/packages/remix-serve/package.json @@ -0,0 +1,22 @@ +{ + "name": "@remix-run/serve", + "description": "Production application server for Remix", + "version": "0.16.1", + "repository": "https://github.com/remix-run/remix", + "bin": { + "remix-serve": "index.js" + }, + "dependencies": { + "@remix-run/express": "0.16.1", + "express": "^4.17.1", + "morgan": "^1.10.0", + "compression": "^1.7.4" + }, + "devDependencies": { + "@types/express": "^4.17.9", + "@types/supertest": "^2.0.10", + "@types/morgan": "^1.9.2", + "@types/compression": "^1.7.0", + "supertest": "^6.0.1" + } +} diff --git a/packages/remix-serve/tsconfig.json b/packages/remix-serve/tsconfig.json new file mode 100644 index 0000000000..0d5c397549 --- /dev/null +++ b/packages/remix-serve/tsconfig.json @@ -0,0 +1,17 @@ +{ + "exclude": ["__tests__/**/*"], + "compilerOptions": { + "lib": ["ES2019"], + "target": "ES2019", + + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "strict": true, + + "declaration": true, + "emitDeclarationOnly": true, + + "outDir": "../../build/node_modules/@remix-run/serve", + "rootDir": "." + } +} From 34b16668b076e7b0a2361ee7cab62fa25fc201ee Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 22 Apr 2021 12:42:03 -0600 Subject: [PATCH 0002/1690] Version 0.16.2 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 58899855a5..190d04099e 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.16.1", + "version": "0.16.2", "repository": "https://github.com/remix-run/remix", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index e426aa4dd5..712f2541f7 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.16.1", + "version": "0.16.2", "repository": "https://github.com/remix-run/remix", "dependencies": { - "@remix-run/node": "0.16.1" + "@remix-run/node": "0.16.2" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 83de4762b9..076a8d888b 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.16.1", + "version": "0.16.2", "repository": "https://github.com/remix-run/remix", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 7de302f9ec..d31732246b 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.16.1", + "version": "0.16.2", "repository": "https://github.com/remix-run/remix", "bin": { "remix-serve": "index.js" }, "dependencies": { - "@remix-run/express": "0.16.1", + "@remix-run/express": "0.16.2", "express": "^4.17.1", "morgan": "^1.10.0", "compression": "^1.7.4" From 51b09b3022346b67b401557a3c887cdcd7096411 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 22 Apr 2021 14:54:30 -0600 Subject: [PATCH 0003/1690] Version 0.16.3 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 190d04099e..2f264b942c 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.16.2", + "version": "0.16.3", "repository": "https://github.com/remix-run/remix", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 712f2541f7..1bf9be354f 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.16.2", + "version": "0.16.3", "repository": "https://github.com/remix-run/remix", "dependencies": { - "@remix-run/node": "0.16.2" + "@remix-run/node": "0.16.3" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 076a8d888b..8be4912030 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.16.2", + "version": "0.16.3", "repository": "https://github.com/remix-run/remix", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index d31732246b..9aa15b9b59 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.16.2", + "version": "0.16.3", "repository": "https://github.com/remix-run/remix", "bin": { "remix-serve": "index.js" }, "dependencies": { - "@remix-run/express": "0.16.2", + "@remix-run/express": "0.16.3", "express": "^4.17.1", "morgan": "^1.10.0", "compression": "^1.7.4" From e1438dc0183a62bafde691ec8b2faa2843629cd8 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Apr 2021 17:55:45 -0700 Subject: [PATCH 0004/1690] Remove unused "tmp" package --- packages/remix-node/package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 8be4912030..eb89ba4a8b 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -11,8 +11,7 @@ "history": "^5.0.0", "jsesc": "^3.0.1", "node-fetch": "^2.6.1", - "react-router-dom": "^6.0.0-beta.0", - "tmp": "^0.2.1" + "react-router-dom": "^6.0.0-beta.0" }, "peerDependencies": { "react": ">=16.8", @@ -20,8 +19,7 @@ }, "devDependencies": { "@types/cookie-signature": "^1.0.3", - "@types/jsesc": "^2.5.1", - "@types/tmp": "^0.2.0" + "@types/jsesc": "^2.5.1" }, "sideEffects": false } From 51a69765cec564c7bc8e2975c97619edae4359ee Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 22 Apr 2021 18:16:53 -0700 Subject: [PATCH 0005/1690] Add "tmp" dep to remix-dev --- packages/remix-dev/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 2f264b942c..83ff60a99e 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -34,6 +34,7 @@ "rollup-plugin-terser": "^7.0.2", "sharp": "^0.27.1", "signal-exit": "^3.0.3", + "tmp": "^0.2.1", "ws": "^7.4.5" }, "devDependencies": { @@ -43,6 +44,7 @@ "@types/morgan": "^1.9.2", "@types/sharp": "^0.27.1", "@types/signal-exit": "^3.0.0", + "@types/tmp": "^0.2.0", "@types/ws": "^7.4.1", "semver": "^7.3.4" } From dadd2863609f437732ad74009b54e7a57d93cc64 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 23 Apr 2021 10:59:59 -0700 Subject: [PATCH 0006/1690] Structure remix-serve like remix-dev Also: - Don't set NODE_ENV variable in `remix run3` - Log all HTTP requests in `remix-serve` - Remove unneeded deps from remix-serve --- packages/remix-dev/cli/commands.ts | 13 ++++++----- packages/remix-serve/app.ts | 35 ------------------------------ packages/remix-serve/cli.ts | 18 +++++++++++++++ packages/remix-serve/index.ts | 33 +++++++++++++++++----------- packages/remix-serve/package.json | 12 +++++----- 5 files changed, 51 insertions(+), 60 deletions(-) delete mode 100644 packages/remix-serve/app.ts create mode 100644 packages/remix-serve/cli.ts diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 4d66fcc99e..de138e70ac 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -7,13 +7,14 @@ import * as path from "path"; import signalExit from "signal-exit"; import prettyMs from "pretty-ms"; +import WebSocket from "ws"; + import { BuildMode, isBuildMode, BuildTarget } from "../build"; import * as compiler from "../compiler"; import * as compiler2 from "../compiler2"; -import { readConfig } from "../config"; import type { RemixConfig } from "../config"; +import { readConfig } from "../config"; import { startDevServer } from "../server"; -import WebSocket from "ws"; /** * Runs the build for a Remix app with the old rollup compiler @@ -151,12 +152,14 @@ export function run2(remixRoot: string): Promise { * Runs the built-in remix app server and dev asset server */ export async function run3(remixRoot: string) { - if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; + // TODO: Warn about the need to install @remix-run/serve if it isn't there? + let { createApp } = require("@remix-run/serve"); + let config = await readConfig(remixRoot); - let getAppServer = require("@remix-run/serve/app"); + let mode = process.env.NODE_ENV || "development"; let port = process.env.PORT || 3000; - getAppServer(config.serverBuildDirectory).listen(port, () => { + createApp(config.serverBuildDirectory, mode).listen(port, () => { console.log(`Remix App Server started at http://localhost:${port}`); }); diff --git a/packages/remix-serve/app.ts b/packages/remix-serve/app.ts deleted file mode 100644 index 7837a75450..0000000000 --- a/packages/remix-serve/app.ts +++ /dev/null @@ -1,35 +0,0 @@ -import express from "express"; -import compression from "compression"; -import morgan from "morgan"; -import { createRequestHandler } from "@remix-run/express"; - -export default function getApp(buildPath: string) { - let app = express(); - - app.use(compression()); - - app.use( - express.static("public", { - immutable: true, - maxAge: "1y" - }) - ); - - app.use(morgan("tiny")); - - app.all( - "*", - process.env.NODE_ENV === "production" - ? createRequestHandler({ - build: require(buildPath) - }) - : (req, res, next) => { - // require cache is purged in @remix-run/dev where the file watcher is - return createRequestHandler({ - build: require(buildPath) - })(req, res, next); - } - ); - - return app; -} diff --git a/packages/remix-serve/cli.ts b/packages/remix-serve/cli.ts new file mode 100644 index 0000000000..563af9a282 --- /dev/null +++ b/packages/remix-serve/cli.ts @@ -0,0 +1,18 @@ +import path from "path"; + +import { createApp } from "./index"; + +let port = process.env.PORT || 3000; +let buildPathArg = process.argv[2]; + +if (!buildPathArg) { + console.error(` + Usage: remix-serve `); + process.exit(1); +} + +let buildPath = path.resolve(process.cwd(), buildPathArg); + +createApp(buildPath).listen(port, () => { + console.log(`Remix App Server started at http://localhost:${port}`); +}); diff --git a/packages/remix-serve/index.ts b/packages/remix-serve/index.ts index 35a9fac8a8..c25c1348f0 100644 --- a/packages/remix-serve/index.ts +++ b/packages/remix-serve/index.ts @@ -1,18 +1,25 @@ -import path from "path"; -import getServer from "./app"; +import express from "express"; +import compression from "compression"; +import morgan from "morgan"; +import { createRequestHandler } from "@remix-run/express"; -let port = process.env.PORT || 3000; +export function createApp(buildPath: string, mode = "production") { + let app = express(); -let buildPath = process.argv[2]; -if (!buildPath) { - console.log( - `Please pass in the directory of your Remix server build directory: + app.use(compression()); + app.use(morgan("tiny")); + app.use(express.static("public", { immutable: true, maxAge: "1y" })); - remix-serve ./build` + app.all( + "*", + mode === "production" + ? createRequestHandler({ build: require(buildPath), mode }) + : (req, res, next) => { + // require cache is purged in @remix-run/dev where the file watcher is + let build = require(buildPath); + return createRequestHandler({ build, mode })(req, res, next); + } ); -} else { - let resolovedBuildPath = path.resolve(process.cwd(), buildPath); - getServer(resolovedBuildPath).listen(port, () => { - console.log(`Remix App Server started on port ${port}`); - }); + + return app; } diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 9aa15b9b59..bc0b0961dd 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -4,19 +4,17 @@ "version": "0.16.3", "repository": "https://github.com/remix-run/remix", "bin": { - "remix-serve": "index.js" + "remix-serve": "cli.js" }, "dependencies": { "@remix-run/express": "0.16.3", + "compression": "^1.7.4", "express": "^4.17.1", - "morgan": "^1.10.0", - "compression": "^1.7.4" + "morgan": "^1.10.0" }, "devDependencies": { - "@types/express": "^4.17.9", - "@types/supertest": "^2.0.10", - "@types/morgan": "^1.9.2", "@types/compression": "^1.7.0", - "supertest": "^6.0.1" + "@types/express": "^4.17.9", + "@types/morgan": "^1.9.2" } } From 61cd42b3196b7d7521d2a849db18687766eea1a1 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Sun, 25 Apr 2021 08:12:52 -0600 Subject: [PATCH 0007/1690] Version 0.16.4 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 2f264b942c..60d8741f99 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.16.3", + "version": "0.16.4", "repository": "https://github.com/remix-run/remix", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 1bf9be354f..0972f86266 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.16.3", + "version": "0.16.4", "repository": "https://github.com/remix-run/remix", "dependencies": { - "@remix-run/node": "0.16.3" + "@remix-run/node": "0.16.4" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 8be4912030..9cac2de98e 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.16.3", + "version": "0.16.4", "repository": "https://github.com/remix-run/remix", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 9aa15b9b59..2e2874c697 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.16.3", + "version": "0.16.4", "repository": "https://github.com/remix-run/remix", "bin": { "remix-serve": "index.js" }, "dependencies": { - "@remix-run/express": "0.16.3", + "@remix-run/express": "0.16.4", "express": "^4.17.1", "morgan": "^1.10.0", "compression": "^1.7.4" From 27d41e313ce4a9064f9a3da1a5453b83862d00d2 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Sun, 25 Apr 2021 08:23:22 -0600 Subject: [PATCH 0008/1690] Version 0.16.5 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 60d8741f99..197696f434 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.16.4", + "version": "0.16.5", "repository": "https://github.com/remix-run/remix", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 0972f86266..11bc1f712d 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.16.4", + "version": "0.16.5", "repository": "https://github.com/remix-run/remix", "dependencies": { - "@remix-run/node": "0.16.4" + "@remix-run/node": "0.16.5" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 9cac2de98e..a05f43d945 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.16.4", + "version": "0.16.5", "repository": "https://github.com/remix-run/remix", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 2e2874c697..d1e1c1c54f 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.16.4", + "version": "0.16.5", "repository": "https://github.com/remix-run/remix", "bin": { "remix-serve": "index.js" }, "dependencies": { - "@remix-run/express": "0.16.4", + "@remix-run/express": "0.16.5", "express": "^4.17.1", "morgan": "^1.10.0", "compression": "^1.7.4" From eaffeaa2f29c698af611acf846cfec4fe9df52af Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 27 Apr 2021 07:33:58 -0700 Subject: [PATCH 0009/1690] Remove remix-node types from remix-react --- packages/remix-node/build.ts | 3 + packages/remix-node/data.ts | 28 +++++- packages/remix-node/entry.ts | 129 +++++---------------------- packages/remix-node/errors.ts | 24 +++++ packages/remix-node/headers.ts | 5 +- packages/remix-node/index.ts | 27 ++---- packages/remix-node/links.ts | 2 +- packages/remix-node/match.ts | 39 -------- packages/remix-node/routeData.ts | 21 +++++ packages/remix-node/routeMatching.ts | 24 +++++ packages/remix-node/routeModules.ts | 93 +++++++++++++++++++ packages/remix-node/routes.ts | 121 +++++-------------------- packages/remix-node/server.ts | 46 +++++----- packages/remix-node/serverHandoff.ts | 7 ++ 14 files changed, 281 insertions(+), 288 deletions(-) create mode 100644 packages/remix-node/errors.ts delete mode 100644 packages/remix-node/match.ts create mode 100644 packages/remix-node/routeData.ts create mode 100644 packages/remix-node/routeMatching.ts create mode 100644 packages/remix-node/routeModules.ts create mode 100644 packages/remix-node/serverHandoff.ts diff --git a/packages/remix-node/build.ts b/packages/remix-node/build.ts index 715dc52d76..4d55a75d47 100644 --- a/packages/remix-node/build.ts +++ b/packages/remix-node/build.ts @@ -2,6 +2,9 @@ import type { EntryContext, AssetsManifest } from "./entry"; import type { Headers, Request, Response } from "./fetch"; import type { ServerRouteManifest } from "./routes"; +/** + * The output of the compiler for the server build. + */ export interface ServerBuild { entry: { module: ServerEntryModule; diff --git a/packages/remix-node/data.ts b/packages/remix-node/data.ts index 8a5d11e329..9e4c197d30 100644 --- a/packages/remix-node/data.ts +++ b/packages/remix-node/data.ts @@ -4,7 +4,17 @@ import type { ServerBuild } from "./build"; import type { Request } from "./fetch"; import { Response } from "./fetch"; import { json } from "./responses"; -import type { AppLoadContext } from "./routes"; + +/** + * An object of arbitrary for route loaders and actions provided by the + * server's `getLoadContext()` function. + */ +export type AppLoadContext = any; + +/** + * Data for a route that was returned from a `loader()`. + */ +export type AppData = any; export async function loadRouteData( build: ServerBuild, @@ -73,3 +83,19 @@ function isResponse(value: any): value is Response { typeof value.body !== "undefined" ); } + +export function extractData(response: Response): Promise { + let contentType = response.headers.get("Content-Type"); + + if (contentType && /\bapplication\/json\b/.test(contentType)) { + return response.json(); + } + + // What other data types do we need to handle here? What other kinds of + // responses are people going to be returning from their loaders? + // - application/x-www-form-urlencoded ? + // - multipart/form-data ? + // - binary (audio/video) ? + + return response.text(); +} diff --git a/packages/remix-node/entry.ts b/packages/remix-node/entry.ts index 7ae463abd5..64b0e72e16 100644 --- a/packages/remix-node/entry.ts +++ b/packages/remix-node/entry.ts @@ -1,84 +1,37 @@ -import jsesc from "jsesc"; - -import type { Response } from "./fetch"; -import type { RouteMatch, ServerRouteMatch } from "./match"; +import type { ComponentDidCatchEmulator } from "./errors"; import type { - AppData, RouteManifest, - RouteData, - RouteModules, - Route, - ServerRouteManifest + ServerRouteManifest, + EntryRoute, + ServerRoute } from "./routes"; +import type { RouteData } from "./routeData"; +import type { RouteMatch } from "./routeMatching"; +import type { RouteModules, EntryRouteModule } from "./routeModules"; export interface EntryContext { - manifest: AssetsManifest; - matches: EntryRouteMatch[]; componentDidCatchEmulator: ComponentDidCatchEmulator; + manifest: AssetsManifest; + matches: RouteMatch[]; routeData: RouteData; - routeModules: RouteModules; + routeModules: RouteModules; serverHandoffString?: string; } export interface AssetsManifest { - version: string; - url: string; entry: { - module: string; imports: string[]; + module: string; }; - routes: EntryRouteManifest; -} - -export interface EntryRoute extends Route { - module: string; - imports?: string[]; - hasAction?: boolean; - hasLoader?: boolean; -} - -export type EntryRouteManifest = RouteManifest; -export type EntryRouteMatch = RouteMatch; - -/** - * Because `componentDidCatch` is stateful it doesn't participate in server - * rendering, so we emulate it with this value. Each mutates the - * value so we know which route was the last to attempt to render. We then use - * it to render a second time along with the caught error and emulate - * `componentDidCatch` on the server render 🎉 - * - * This is optional because it only exists in the server render, we don't hand - * this off to the browser because `componentDidCatch` already works there. - */ -export interface ComponentDidCatchEmulator { - trackBoundaries: boolean; - // `null` means the app layout threw before any routes rendered - renderBoundaryRouteId: string | null; - loaderBoundaryRouteId: string | null; - error?: SerializedError; -} - -export interface SerializedError { - message: string; - stack?: string; -} - -export function serializeError(error: Error): SerializedError { - return { - message: error.message, - stack: - error.stack && - error.stack.replace( - /\((.+?)\)/g, - (_match: string, file: string) => `(file://${file})` - ) - }; + routes: RouteManifest; + url: string; + version: string; } -export function createMatches( - matches: ServerRouteMatch[], - routes: EntryRouteManifest -): EntryRouteMatch[] { +export function createEntryMatches( + matches: RouteMatch[], + routes: RouteManifest +): RouteMatch[] { return matches.map(match => ({ params: match.params, pathname: match.pathname, @@ -86,45 +39,11 @@ export function createMatches( })); } -export async function createRouteData( - matches: ServerRouteMatch[], - loadResults: Response[] -): Promise { - let data = await Promise.all(loadResults.map(extractData)); - - return matches.reduce((memo, match, index) => { - memo[match.route.id] = data[index]; +export function createEntryRouteModules( + manifest: ServerRouteManifest +): RouteModules { + return Object.keys(manifest).reduce((memo, routeId) => { + memo[routeId] = manifest[routeId].module; return memo; - }, {} as RouteData); -} - -function extractData(response: Response): Promise { - let contentType = response.headers.get("Content-Type"); - - if (contentType && /\bapplication\/json\b/.test(contentType)) { - return response.json(); - } - - // What other data types do we need to handle here? What other kinds of - // responses are people going to be returning from their loaders? - // - application/x-www-form-urlencoded ? - // - multipart/form-data ? - // - binary (audio/video) ? - - return response.text(); -} - -export function createRouteModules( - routeManifest: ServerRouteManifest -): RouteModules { - return Object.keys(routeManifest).reduce((memo, routeId) => { - memo[routeId] = routeManifest[routeId].module; - return memo; - }, {} as RouteModules); -} - -export function createServerHandoffString(serverHandoff: any): string { - // Use jsesc to escape data returned from the loaders. This string is - // inserted directly into the HTML in the `` element. - return jsesc(serverHandoff, { isScriptContext: true }); + }, {} as RouteModules); } diff --git a/packages/remix-node/errors.ts b/packages/remix-node/errors.ts new file mode 100644 index 0000000000..d8a3f2bb4c --- /dev/null +++ b/packages/remix-node/errors.ts @@ -0,0 +1,24 @@ +export interface ComponentDidCatchEmulator { + error?: SerializedError; + loaderBoundaryRouteId: string | null; + // `null` means the app layout threw before any routes rendered + renderBoundaryRouteId: string | null; + trackBoundaries: boolean; +} + +export interface SerializedError { + message: string; + stack?: string; +} + +export function serializeError(error: Error): SerializedError { + return { + message: error.message, + stack: + error.stack && + error.stack.replace( + /\((.+?)\)/g, + (_match: string, file: string) => `(file://${file})` + ) + }; +} diff --git a/packages/remix-node/headers.ts b/packages/remix-node/headers.ts index 650e2cbb8e..a326d9da36 100644 --- a/packages/remix-node/headers.ts +++ b/packages/remix-node/headers.ts @@ -1,11 +1,12 @@ import type { ServerBuild } from "./build"; import type { Response } from "./fetch"; import { Headers } from "./fetch"; -import type { ServerRouteMatch } from "./match"; +import type { ServerRoute } from "./routes"; +import type { RouteMatch } from "./routeMatching"; export function getDocumentHeaders( build: ServerBuild, - matches: ServerRouteMatch[], + matches: RouteMatch[], routeLoaderResponses: Response[] ): Headers { return matches.reduce((parentHeaders, match, index) => { diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index 963192a7f6..ae40448279 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -11,15 +11,7 @@ export type { } from "./cookies"; export { createCookie, isCookie } from "./cookies"; -export type { - EntryContext, - AssetsManifest, - EntryRoute, - EntryRouteManifest, - EntryRouteMatch, - ComponentDidCatchEmulator, - SerializedError -} from "./entry"; +export type { AppLoadContext, AppData } from "./data"; export type { HeadersInit, @@ -39,20 +31,15 @@ export type { } from "./links"; export type { - AppLoadContext, - AppData, - RouteComponent, + ActionFunction, ErrorBoundaryComponent, HeadersFunction, - MetaFunction, LinksFunction, LoaderFunction, - ActionFunction, - RouteModule, - RouteManifest, - RouteData, - RouteModules -} from "./routes"; + MetaFunction, + RouteComponent, + RouteHandle +} from "./routeModules"; export { json, redirect } from "./responses"; @@ -69,5 +56,3 @@ export { createSession, isSession, createSessionStorage } from "./sessions"; export { createCookieSessionStorage } from "./sessions/cookieStorage"; export { createFileSessionStorage } from "./sessions/fileStorage"; export { createMemorySessionStorage } from "./sessions/memoryStorage"; - -export { warnOnce } from "./warnings"; diff --git a/packages/remix-node/links.ts b/packages/remix-node/links.ts index 4d83f918ba..5fa7e8fc5e 100644 --- a/packages/remix-node/links.ts +++ b/packages/remix-node/links.ts @@ -1,5 +1,5 @@ /** - * Remix Link descriptor, an object representation of the HTML `` element. + * Represents a `` element. * * WHATWG Specification: https://html.spec.whatwg.org/multipage/semantics.html#the-link-element */ diff --git a/packages/remix-node/match.ts b/packages/remix-node/match.ts deleted file mode 100644 index dceba36749..0000000000 --- a/packages/remix-node/match.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { RouteObject, Params } from "react-router"; -import { matchRoutes as match } from "react-router"; - -import type { ServerRoute, ServerRouteManifest } from "./routes"; - -export interface RouteMatch { - params: Params; - pathname: string; - route: Route; -} - -export type ServerRouteMatch = RouteMatch; - -export function createRoutes( - routeManifest: ServerRouteManifest, - parentId?: string -): ServerRoute[] { - return Object.keys(routeManifest) - .filter(key => routeManifest[key].parentId === parentId) - .map(id => ({ - ...routeManifest[id], - children: createRoutes(routeManifest, id) - })); -} - -export function matchRoutes( - routes: ServerRoute[], - pathname: string -): ServerRouteMatch[] | null { - let matches = match((routes as unknown) as RouteObject[], pathname); - - if (!matches) return null; - - return matches.map(match => ({ - params: match.params, - pathname: match.pathname, - route: (match.route as unknown) as ServerRoute - })); -} diff --git a/packages/remix-node/routeData.ts b/packages/remix-node/routeData.ts new file mode 100644 index 0000000000..87753309e9 --- /dev/null +++ b/packages/remix-node/routeData.ts @@ -0,0 +1,21 @@ +import type { AppData } from "./data"; +import { extractData } from "./data"; +import type { Response } from "./fetch"; +import type { ServerRoute } from "./routes"; +import type { RouteMatch } from "./routeMatching"; + +export interface RouteData { + [routeId: string]: AppData; +} + +export async function createRouteData( + matches: RouteMatch[], + responses: Response[] +): Promise { + let data = await Promise.all(responses.map(extractData)); + + return matches.reduce((memo, match, index) => { + memo[match.route.id] = data[index]; + return memo; + }, {} as RouteData); +} diff --git a/packages/remix-node/routeMatching.ts b/packages/remix-node/routeMatching.ts new file mode 100644 index 0000000000..9e76f653a9 --- /dev/null +++ b/packages/remix-node/routeMatching.ts @@ -0,0 +1,24 @@ +import type { Params, RouteObject } from "react-router"; // TODO: export/import from react-router-dom +import { matchRoutes } from "react-router-dom"; + +import type { ServerRoute } from "./routes"; + +export interface RouteMatch { + params: Params; + pathname: string; + route: Route; +} + +export function matchServerRoutes( + routes: ServerRoute[], + pathname: string +): RouteMatch[] | null { + let matches = matchRoutes((routes as unknown) as RouteObject[], pathname); + if (!matches) return null; + + return matches.map(match => ({ + params: match.params, + pathname: match.pathname, + route: (match.route as unknown) as ServerRoute + })); +} diff --git a/packages/remix-node/routeModules.ts b/packages/remix-node/routeModules.ts new file mode 100644 index 0000000000..7ec670d938 --- /dev/null +++ b/packages/remix-node/routeModules.ts @@ -0,0 +1,93 @@ +import type { Location } from "history"; +import type { ComponentType } from "react"; +import type { Params } from "react-router"; // TODO: import/export from react-router-dom + +import type { AppLoadContext, AppData } from "./data"; +import type { Headers, HeadersInit, Request, Response } from "./fetch"; +import type { LinkDescriptor } from "./links"; +import type { RouteData } from "./routeData"; + +export interface RouteModules { + [routeId: string]: RouteModule; +} + +/** + * A function that handles data mutations for a route. + */ +export interface ActionFunction { + (args: { request: Request; context: AppLoadContext; params: Params }): + | Promise + | Response; +} + +/** + * A React component that is rendered when there is an error on a route. + */ +export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; + +/** + * A function that returns HTTP headers to be used for a route. These headers + * will be merged with (and take precedence over) headers from parent routes. + */ +export interface HeadersFunction { + (args: { loaderHeaders: Headers; parentHeaders: Headers }): + | Headers + | HeadersInit; +} + +/** + * A function that defines `` tags to be inserted into the `` of + * the document on route transitions. + */ +export interface LinksFunction { + (args: { data: AppData }): LinkDescriptor[]; +} + +/** + * A function that loads data for a route. + */ +export interface LoaderFunction { + (args: { request: Request; context: AppLoadContext; params: Params }): + | Promise + | Response + | Promise + | AppData; +} + +/** + * A function that returns an object of name + content pairs to use for + * `` tags for a route. These tags will be merged with (and take + * precedence over) tags from parent routes. + */ +export interface MetaFunction { + (args: { + data: AppData; + parentsData: RouteData; + params: Params; + location: Location; + }): { [name: string]: string }; +} + +/** + * A React component that is rendered for a route. + */ +export type RouteComponent = ComponentType<{}>; + +/** + * An arbitrary object that is associated with a route. + */ +export type RouteHandle = any; + +export interface EntryRouteModule { + ErrorBoundary?: ErrorBoundaryComponent; + default: RouteComponent; + handle?: RouteHandle; + links?: LinksFunction; + meta?: MetaFunction; +} + +export interface ServerRouteModule extends EntryRouteModule { + action?: ActionFunction; + headers?: HeadersFunction; + loader?: LoaderFunction; +} diff --git a/packages/remix-node/routes.ts b/packages/remix-node/routes.ts index 59705273dc..4ac4957a9b 100644 --- a/packages/remix-node/routes.ts +++ b/packages/remix-node/routes.ts @@ -1,113 +1,38 @@ -import type { Location } from "history"; -import type { ComponentType } from "react"; -import type { Params } from "react-router"; - -import type { HeadersInit, Headers, Request, Response } from "./fetch"; -import type { LinkDescriptor } from "./links"; - -/** - * An object of data returned from the server's `getLoadContext` function. This - * will be passed to the data loaders. - */ -export type AppLoadContext = any; - -/** - * Some data that was returned from a route data loader. - */ -export type AppData = any; - -/** - * A React component that is rendered for a route. - */ -export type RouteComponent = ComponentType; - -/** - * A React component that is rendered when there is an error on a route. - */ -export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; - -/** - * A function that returns HTTP headers to be used for a route. These headers - * will be merged with (and take precedence over) headers from parent routes. - */ -export interface HeadersFunction { - (args: { loaderHeaders: Headers; parentHeaders: Headers }): - | Headers - | HeadersInit; -} - -/** - * A function that returns an object of name + content pairs to use for - * `` tags for a route. These tags will be merged with (and take - * precedence over) tags from parent routes. - */ -export interface MetaFunction { - (args: { - data: AppData; - parentsData: RouteData; - params: Params; - location: Location; - }): { [name: string]: string }; -} - -/** - * A function that defines `` tags to be inserted into the `` of - * the document on route transitions. - */ -export interface LinksFunction { - (args: { data: AppData }): LinkDescriptor[]; -} - -/** - * A function that loads data for a route. - */ -export interface LoaderFunction { - (args: { request: Request; context: AppLoadContext; params: Params }): - | Promise - | AppData; -} - -/** - * A function that handles data mutations for a route. - */ -export interface ActionFunction { - (args: { request: Request; context: AppLoadContext; params: Params }): - | Promise - | Response; -} - -/** - * A module that contains info about a route including headers, meta tags, and - * the route component for rendering HTML markup. - */ -export interface RouteModule { - default: RouteComponent; - ErrorBoundary?: ErrorBoundaryComponent; - headers?: HeadersFunction; - meta?: MetaFunction; - loader?: LoaderFunction; - action?: ActionFunction; - links?: LinksFunction; - handle?: any; -} +import type { ServerRouteModule } from "./routeModules"; export interface RouteManifest { [routeId: string]: Route; } -export type RouteData = RouteManifest; -export type RouteModules = RouteManifest; +export type ServerRouteManifest = RouteManifest>; -export interface Route { - path: string; +interface Route { caseSensitive?: boolean; id: string; parentId?: string; + path: string; +} + +export interface EntryRoute extends Route { + hasAction?: boolean; + hasLoader?: boolean; + imports?: string[]; + module: string; } export interface ServerRoute extends Route { - module: RouteModule; children: ServerRoute[]; + module: ServerRouteModule; } -export type ServerRouteManifest = RouteManifest>; +export function createRoutes( + manifest: ServerRouteManifest, + parentId?: string +): ServerRoute[] { + return Object.keys(manifest) + .filter(key => manifest[key].parentId === parentId) + .map(id => ({ + ...manifest[id], + children: createRoutes(manifest, id) + })); +} diff --git a/packages/remix-node/server.ts b/packages/remix-node/server.ts index 51941a7802..a29a51a85a 100644 --- a/packages/remix-node/server.ts +++ b/packages/remix-node/server.ts @@ -1,15 +1,21 @@ +import type { AppLoadContext } from "./data"; import { loadRouteData, callRouteAction } from "./data"; +import type { ComponentDidCatchEmulator } from "./errors"; +import { serializeError } from "./errors"; import type { ServerBuild } from "./build"; -import type { ComponentDidCatchEmulator, EntryContext } from "./entry"; -import * as entry from "./entry"; +import type { EntryContext } from "./entry"; +import { createEntryMatches, createEntryRouteModules } from "./entry"; import type { Request } from "./fetch"; import { Response } from "./fetch"; import { getDocumentHeaders } from "./headers"; -import type { ServerRouteMatch } from "./match"; -import { createRoutes, matchRoutes } from "./match"; +import type { RouteMatch } from "./routeMatching"; +import { matchServerRoutes } from "./routeMatching"; import { ServerMode, isServerMode } from "./mode"; -import type { AppLoadContext, ServerRoute } from "./routes"; +import type { ServerRoute } from "./routes"; +import { createRoutes } from "./routes"; +import { createRouteData } from "./routeData"; import { json } from "./responses"; +import { createServerHandoffString } from "./serverHandoff"; /** * The main request handler for a Remix server. This handler runs in the context @@ -44,12 +50,12 @@ async function handleDataRequest( ): Promise { let url = new URL(request.url); - let matches = matchRoutes(routes, url.pathname); + let matches = matchServerRoutes(routes, url.pathname); if (!matches) { return jsonError(`No route matches URL "${url.pathname}"`, 404); } - let routeMatch: ServerRouteMatch; + let routeMatch: RouteMatch; if (isActionRequest(request)) { routeMatch = matches[matches.length - 1]; } else { @@ -87,7 +93,7 @@ async function handleDataRequest( routeMatch.params ); } catch (error) { - return json(entry.serializeError(error), { + return json(serializeError(error), { status: 500, headers: { "X-Remix-Error": "unfortunately, yes" @@ -123,7 +129,7 @@ async function handleDocumentRequest( ): Promise { let url = new URL(request.url); - let matches = matchRoutes(routes, url.pathname); + let matches = matchServerRoutes(routes, url.pathname); if (!matches) { // TODO: Provide a default 404 page throw new Error( @@ -188,7 +194,7 @@ async function handleDocumentRequest( ); } - componentDidCatchEmulator.error = entry.serializeError(response); + componentDidCatchEmulator.error = serializeError(response); routeLoaderResults[index] = json(null, { status: 500 }); } else if (isRedirectResponse(response)) { return response; @@ -212,19 +218,19 @@ async function handleDocumentRequest( let serverEntryModule = build.entry.module; let headers = getDocumentHeaders(build, matches, routeLoaderResponses); - let entryMatches = entry.createMatches(matches, build.assets.routes); - let routeData = await entry.createRouteData(matches, routeLoaderResponses); - let routeModules = entry.createRouteModules(build.routes); + let entryMatches = createEntryMatches(matches, build.assets.routes); + let routeData = await createRouteData(matches, routeLoaderResponses); + let routeModules = createEntryRouteModules(build.routes); let serverHandoff = { matches: entryMatches, componentDidCatchEmulator, routeData }; - let serverEntryContext: EntryContext = { + let entryContext: EntryContext = { ...serverHandoff, manifest: build.assets, routeModules, - serverHandoffString: entry.createServerHandoffString(serverHandoff) + serverHandoffString: createServerHandoffString(serverHandoff) }; let response: Response | Promise; @@ -233,7 +239,7 @@ async function handleDocumentRequest( request, statusCode, headers, - serverEntryContext + entryContext ); } catch (error) { if (serverMode !== ServerMode.Test) { @@ -251,17 +257,15 @@ async function handleDocumentRequest( // now. I'm okay with tracking our position in the route tree while // rendering, that's pretty much how hooks work 😂) componentDidCatchEmulator.trackBoundaries = false; - componentDidCatchEmulator.error = entry.serializeError(error); - serverEntryContext.serverHandoffString = entry.createServerHandoffString( - serverHandoff - ); + componentDidCatchEmulator.error = serializeError(error); + entryContext.serverHandoffString = createServerHandoffString(serverHandoff); try { response = serverEntryModule.default( request, statusCode, headers, - serverEntryContext + entryContext ); } catch (error) { if (serverMode !== ServerMode.Test) { diff --git a/packages/remix-node/serverHandoff.ts b/packages/remix-node/serverHandoff.ts new file mode 100644 index 0000000000..6f20a8d145 --- /dev/null +++ b/packages/remix-node/serverHandoff.ts @@ -0,0 +1,7 @@ +import jsesc from "jsesc"; + +export function createServerHandoffString(serverHandoff: any): string { + // Use jsesc to escape data returned from the loaders. This string is + // inserted directly into the HTML in the `` element. + return jsesc(serverHandoff, { isScriptContext: true }); +} From 955598dfde41a16a094be43d2e7cfca42f3952ae Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 27 Apr 2021 08:02:54 -0700 Subject: [PATCH 0010/1690] Re-add EntryContext export to remix-node --- packages/remix-node/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index ae40448279..9f44d8b389 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -13,6 +13,8 @@ export { createCookie, isCookie } from "./cookies"; export type { AppLoadContext, AppData } from "./data"; +export type { EntryContext } from "./entry"; + export type { HeadersInit, RequestInfo, From fb6df779a4f3936d6555aa1d78fe443d68d7df65 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 27 Apr 2021 19:23:09 -0700 Subject: [PATCH 0011/1690] Add magic `remix` package --- packages/remix-dev/compiler.ts | 17 ++++++++++++----- packages/remix-dev/compiler2.ts | 7 ++----- packages/remix-dev/package.json | 1 + packages/remix-node/package.json | 3 +++ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 4fc52bb3b6..1e0181ae77 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -12,6 +12,7 @@ import type { TreeshakingOptions } from "rollup"; import * as rollup from "rollup"; +import alias from "@rollup/plugin-alias"; import babel from "@rollup/plugin-babel"; import commonjs from "@rollup/plugin-commonjs"; import json from "@rollup/plugin-json"; @@ -174,14 +175,10 @@ function isLocalModuleId(id: string): boolean { function getExternalOption(target: string): ExternalOption | undefined { return target === BuildTarget.Server ? (id: string) => - // We need to bundle @remix-run/react since it is ESM and we - // are building CommonJS output. - id !== "@remix-run/react" && // Exclude non-local module identifiers from the server bundles. // This includes identifiers like "react" which will be resolved // dynamically at runtime using require(). - !isLocalModuleId(id) && - !isImportHint(id) + !isLocalModuleId(id) && !isImportHint(id) : // Exclude packages we know we don't want in the browser bundles. // These *should* be stripped from the browser bundles anyway when // tree-shaking kicks in, so making them external just saves Rollup @@ -256,6 +253,16 @@ function getBuildPlugins({ mode, target }: Required): Plugin[] { }) ]; + if (target === BuildTarget.Browser) { + plugins.push( + alias({ + entries: [ + { find: "@remix-run/react", replacement: "@remix-run/react/browser" } + ] + }) + ); + } + plugins.push( clientServer({ target }), mdx(), diff --git a/packages/remix-dev/compiler2.ts b/packages/remix-dev/compiler2.ts index ac21039078..03efa1a87c 100644 --- a/packages/remix-dev/compiler2.ts +++ b/packages/remix-dev/compiler2.ts @@ -210,6 +210,7 @@ async function createBrowserBuild( return esbuild.build({ entryPoints, outdir: config.assetsBuildDirectory, + platform: "browser", format: "esm", external: externals, inject: [reactShim], @@ -245,8 +246,8 @@ async function createServerBuild( resolveDir: config.serverBuildDirectory }, outfile: path.resolve(config.serverBuildDirectory, "index.js"), - format: "cjs", platform: "node", + format: "cjs", target: options.target, inject: [reactShim], loader: loaders, @@ -264,10 +265,6 @@ async function createServerBuild( // browser build and it's not there yet. if (id === "./assets.json" && importer === "") return true; - // We need to bundle @remix-run/react because it is ESM and we can't - // require it from the CommonJS output. - if (id === "@remix-run/react") return false; - // Mark all bare imports as external. They will be require()'d at // runtime from node_modules. if (isBareModuleId(id)) { diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index e4a1434feb..1ee6df412c 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -13,6 +13,7 @@ "@babel/preset-typescript": "^7.13.0", "@mdx-js/mdx": "^1.6.22", "@mdx-js/react": "^1.6.22", + "@rollup/plugin-alias": "^3.1.2", "@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-json": "^4.1.0", diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index d5a76bc448..83ecd96312 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -21,5 +21,8 @@ "@types/cookie-signature": "^1.0.3", "@types/jsesc": "^2.5.1" }, + "scripts": { + "postinstall": "remix-setup server=@remix-run/node" + }, "sideEffects": false } From 1a3c0cbb6b9a0bf499c96804aa237e2e8f136430 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 27 Apr 2021 20:27:24 -0700 Subject: [PATCH 0012/1690] Remove `remix-setup` cli --- packages/remix-node/package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 83ecd96312..d5a76bc448 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -21,8 +21,5 @@ "@types/cookie-signature": "^1.0.3", "@types/jsesc": "^2.5.1" }, - "scripts": { - "postinstall": "remix-setup server=@remix-run/node" - }, "sideEffects": false } From d89d74efbd18460444a39805f679e14f5be86e53 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 28 Apr 2021 11:43:11 -0700 Subject: [PATCH 0013/1690] Remove alias plugin, don't need it --- packages/remix-dev/compiler.ts | 12 ------------ packages/remix-dev/package.json | 1 - 2 files changed, 13 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 1e0181ae77..6cee5d8d70 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -1,4 +1,3 @@ -import fs from "fs"; import path from "path"; import type { ExternalOption, @@ -12,7 +11,6 @@ import type { TreeshakingOptions } from "rollup"; import * as rollup from "rollup"; -import alias from "@rollup/plugin-alias"; import babel from "@rollup/plugin-babel"; import commonjs from "@rollup/plugin-commonjs"; import json from "@rollup/plugin-json"; @@ -253,16 +251,6 @@ function getBuildPlugins({ mode, target }: Required): Plugin[] { }) ]; - if (target === BuildTarget.Browser) { - plugins.push( - alias({ - entries: [ - { find: "@remix-run/react", replacement: "@remix-run/react/browser" } - ] - }) - ); - } - plugins.push( clientServer({ target }), mdx(), diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 1ee6df412c..e4a1434feb 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -13,7 +13,6 @@ "@babel/preset-typescript": "^7.13.0", "@mdx-js/mdx": "^1.6.22", "@mdx-js/react": "^1.6.22", - "@rollup/plugin-alias": "^3.1.2", "@rollup/plugin-babel": "^5.3.0", "@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-json": "^4.1.0", From 673943c708cce006b9d71c4334da1d6dca6f015f Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 28 Apr 2021 12:44:39 -0700 Subject: [PATCH 0014/1690] Remove old (Rollup) compiler - Also removed old cli commands --- packages/remix-dev/__tests__/mdx-test.ts | 152 ---- packages/remix-dev/cli.ts | 39 +- packages/remix-dev/cli/commands.ts | 84 +- packages/remix-dev/compiler.ts | 744 +++++++++++------- .../{compiler2 => compiler}/assets.ts | 0 packages/remix-dev/compiler/browserIgnore.ts | 41 - packages/remix-dev/compiler/createUrl.ts | 8 - packages/remix-dev/compiler/crypto.ts | 39 - .../{compiler2 => compiler}/dependencies.ts | 0 packages/remix-dev/compiler/importHints.ts | 5 - .../{compiler2 => compiler}/loaders.ts | 0 .../compiler/rollup/assetsManifest.ts | 213 ----- .../remix-dev/compiler/rollup/clientServer.ts | 38 - packages/remix-dev/compiler/rollup/css.ts | 108 --- packages/remix-dev/compiler/rollup/empty.ts | 24 - packages/remix-dev/compiler/rollup/img.ts | 270 ------- packages/remix-dev/compiler/rollup/mdx.ts | 126 --- .../remix-dev/compiler/rollup/remixConfig.ts | 52 -- .../remix-dev/compiler/rollup/remixInputs.ts | 22 - .../remix-dev/compiler/rollup/routeModules.ts | 111 --- .../compiler/rollup/serverManifest.ts | 121 --- packages/remix-dev/compiler/rollup/url.ts | 52 -- .../compiler/rollup/watchDirectory.ts | 49 -- .../{compiler2 => compiler}/routes.ts | 0 .../{compiler2 => compiler}/shims/react.ts | 0 .../{compiler2 => compiler}/utils/crypto.ts | 0 .../{compiler2 => compiler}/utils/fs.ts | 0 .../{compiler2 => compiler}/utils/url.ts | 0 packages/remix-dev/compiler2.ts | 518 ------------ packages/remix-dev/config.ts | 12 - packages/remix-dev/package.json | 24 - packages/remix-dev/server.ts | 88 --- 32 files changed, 472 insertions(+), 2468 deletions(-) delete mode 100644 packages/remix-dev/__tests__/mdx-test.ts rename packages/remix-dev/{compiler2 => compiler}/assets.ts (100%) delete mode 100644 packages/remix-dev/compiler/browserIgnore.ts delete mode 100644 packages/remix-dev/compiler/createUrl.ts delete mode 100644 packages/remix-dev/compiler/crypto.ts rename packages/remix-dev/{compiler2 => compiler}/dependencies.ts (100%) delete mode 100644 packages/remix-dev/compiler/importHints.ts rename packages/remix-dev/{compiler2 => compiler}/loaders.ts (100%) delete mode 100644 packages/remix-dev/compiler/rollup/assetsManifest.ts delete mode 100644 packages/remix-dev/compiler/rollup/clientServer.ts delete mode 100644 packages/remix-dev/compiler/rollup/css.ts delete mode 100644 packages/remix-dev/compiler/rollup/empty.ts delete mode 100644 packages/remix-dev/compiler/rollup/img.ts delete mode 100644 packages/remix-dev/compiler/rollup/mdx.ts delete mode 100644 packages/remix-dev/compiler/rollup/remixConfig.ts delete mode 100644 packages/remix-dev/compiler/rollup/remixInputs.ts delete mode 100644 packages/remix-dev/compiler/rollup/routeModules.ts delete mode 100644 packages/remix-dev/compiler/rollup/serverManifest.ts delete mode 100644 packages/remix-dev/compiler/rollup/url.ts delete mode 100644 packages/remix-dev/compiler/rollup/watchDirectory.ts rename packages/remix-dev/{compiler2 => compiler}/routes.ts (100%) rename packages/remix-dev/{compiler2 => compiler}/shims/react.ts (100%) rename packages/remix-dev/{compiler2 => compiler}/utils/crypto.ts (100%) rename packages/remix-dev/{compiler2 => compiler}/utils/fs.ts (100%) rename packages/remix-dev/{compiler2 => compiler}/utils/url.ts (100%) delete mode 100644 packages/remix-dev/compiler2.ts delete mode 100644 packages/remix-dev/server.ts diff --git a/packages/remix-dev/__tests__/mdx-test.ts b/packages/remix-dev/__tests__/mdx-test.ts deleted file mode 100644 index b46eb995ad..0000000000 --- a/packages/remix-dev/__tests__/mdx-test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import path from "path"; -import type { RollupBuild } from "rollup"; -import { rollup } from "rollup"; -import babel from "@rollup/plugin-babel"; -import nodeResolve from "@rollup/plugin-node-resolve"; -import commonjs from "@rollup/plugin-commonjs"; -import React from "react"; -import ReactDOMServer from "react-dom/server"; - -import type { MdxConfig, MdxFunctionOption } from "../compiler/rollup/mdx"; -import mdxPlugin from "../compiler/rollup/mdx"; - -import { getRemixConfig } from "../compiler/rollup/remixConfig"; -let mockedGetRemixConfig = (getRemixConfig as unknown) as jest.MockedFunction< - () => any ->; - -jest.mock("../compiler/rollup/remixConfig"); - -describe("mdx rollup plugin", () => { - beforeEach(() => { - mockedGetRemixConfig.mockImplementation(async () => { - return {}; - }); - }); - - afterEach(() => { - mockedGetRemixConfig.mockReset(); - }); - - it("renders", async () => { - let mod = await bundleMdxFile("basic.mdx"); - expect(renderMdxModule(mod)).toMatchInlineSnapshot(`"

I am mdx

"`); - }); - - it("exports meta and headers", async () => { - let mod = await bundleMdxFile("basic.mdx"); - expect(mod.headers()).toMatchInlineSnapshot(` - Object { - "cache-control": "max-age=60", - } - `); - expect(mod.meta()).toMatchInlineSnapshot(` - Object { - "title": "Some title", - } - `); - }); - - it("supports a function as config", async () => { - expect.assertions(3); - let options: MdxFunctionOption = (attributes, filename) => { - expect(attributes).toMatchInlineSnapshot(` - Object { - "headers": Object { - "cache-control": "max-age=60", - }, - "meta": Object { - "title": "Some title", - }, - } - `); - expect(filename.endsWith("basic.mdx")).toBeTruthy(); - return { - rehypePlugins: [fakeRehypePlugin] - }; - }; - let mod = await bundleMdxFile("basic.mdx", options); - expect(renderMdxModule(mod)).toMatchInlineSnapshot( - `"

I am mdx

injected!
"` - ); - }); -}); - -//////////////////////////////////////////////////////////////////////////////// -function fakeRehypePlugin(): any { - return (root: any) => { - root.children.push({ - type: "element", - tagName: "footer", - children: [ - { - type: "text", - value: "injected!" - } - ] - }); - return root; - }; -} -interface MdxTestModule { - default: () => React.ReactElement; - meta: () => { [key: string]: string }; - headers: () => { [key: string]: string }; -} - -function renderMdxModule(mod: MdxTestModule) { - return ReactDOMServer.renderToString(React.createElement(mod.default)); -} - -async function bundleMdxFile( - filename: string, - mdxConfig: MdxConfig = {} -): Promise { - let filepath = path.resolve(__dirname, "fixtures", filename); - let bundle = await rollup({ - input: filepath, - plugins: [mdxPlugin({ mdxConfig }), ...getPlugins()], - external: ["@mdx-js/react"] - }); - let code = await getBundleOutput(bundle); - let module = { exports: {} }; - let params = [ - "module", - "exports", - "require", - `${code}; return module.exports;` - ]; - let evalFunc = new Function(...params); // eslint-disable-line - return evalFunc(module, module.exports, require); -} - -async function getBundleOutput(bundle: RollupBuild) { - let { output } = await bundle.generate({ format: "cjs", exports: "named" }); - return output[0].code; -} - -function getPlugins() { - return [ - babel({ - babelHelpers: "bundled", - configFile: false, - exclude: /node_modules/, - extensions: [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"], - presets: [ - ["@babel/preset-react", { runtime: "automatic" }], - ["@babel/preset-env", { targets: { node: true } }], - [ - "@babel/preset-typescript", - { - allExtensions: true, - isTSX: true - } - ] - ] - }), - nodeResolve({ - extensions: [".js", ".json", ".ts", ".tsx"] - }), - commonjs() - ]; -} diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index 605d129215..e91878d114 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -35,41 +35,18 @@ if (cli.flags.version) { cli.showVersion(); } -// In 0.17 we only have -// - remix build (build2) -// - remix dev (dev) -// - remix run (run3) switch (cli.input[0]) { - // rollup - case "build": // gone in 0.17 + case "build": commands.build(cli.input[1], process.env.NODE_ENV); break; - case "run": // gone in 0.17 - commands.run(cli.input[1]); + case "dev": // `remix dev` is alias for `remix watch` + case "watch": + commands.watch(cli.input[1], process.env.NODE_ENV); break; - - // esbuild - case "build2": // becomes `remix build` in 0.17 - commands.build2(cli.input[1], process.env.NODE_ENV); - break; - - // these three are all the same - case "watch2": // gone in 0.17 - commands.watch2(cli.input[1], process.env.NODE_ENV); - break; - case "run2": // gone in 0.17 - commands.run2(cli.input[1]); + case "run": + commands.run(cli.input[1], process.env.NODE_ENV); break; - case "dev": // stays in 0.17 - commands.watch2(cli.input[1], process.env.NODE_ENV); - break; - - // built-in app/dev server - case "run3": // becomes `remix run` in 0.17 - commands.run3(cli.input[1]); - break; - default: - // `remix my-project` is shorthand for `remix run3 my-project` - commands.run3(cli.input[0]); + // `remix my-project` is shorthand for `remix run my-project` + commands.run(cli.input[0]); } diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index de138e70ac..b1c25c03b5 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -1,72 +1,14 @@ -//////////////////////////////////////////////////////////////////////////////// -// In 0.17 -// -// - fn run3() -> fn run() -// - fn watch2() -> fn dev() -// - fn build2() -> fn build() import * as path from "path"; import signalExit from "signal-exit"; import prettyMs from "pretty-ms"; import WebSocket from "ws"; -import { BuildMode, isBuildMode, BuildTarget } from "../build"; +import { BuildMode, isBuildMode } from "../build"; import * as compiler from "../compiler"; -import * as compiler2 from "../compiler2"; import type { RemixConfig } from "../config"; import { readConfig } from "../config"; -import { startDevServer } from "../server"; -/** - * Runs the build for a Remix app with the old rollup compiler - */ -export async function build(remixRoot: string, mode?: string) { - let buildMode = isBuildMode(mode) ? mode : BuildMode.Production; - - console.log(`Building Remix app for ${buildMode}...`); - - let config = await readConfig(remixRoot); - - await Promise.all([ - compiler.write( - await compiler.build(config, { - mode: buildMode, - target: BuildTarget.Server - }), - config.serverBuildDirectory - ), - compiler.write( - await compiler.build(config, { - mode: buildMode, - target: BuildTarget.Browser - }), - config.assetsBuildDirectory - ) - ]); - - console.log("done!"); -} - -/** - * Runs the old rollup dev watcher. - */ -export async function run(remixRoot: string) { - let config = await readConfig(remixRoot); - - startDevServer(config, { - onListen() { - console.log( - `Remix dev server running on port ${config.devServerPort}...` - ); - } - }); -} - -/////////////////////////////////////////////////////////////////////////////// - -/** - * Runs the new esbuild compiler - */ -export async function build2( +export async function build( remixRoot: string, modeArg?: string ): Promise { @@ -76,15 +18,12 @@ export async function build2( let start = Date.now(); let config = await readConfig(remixRoot); - await compiler2.build(config, { mode: mode }); + await compiler.build(config, { mode: mode }); console.log(`Built in ${prettyMs(Date.now() - start)}`); } -/** - * Watches with the new esbuild compiler - */ -export async function watch2( +export async function watch( remixRootOrConfig: string | RemixConfig, modeArg?: string, onRebuildStart?: () => void @@ -114,7 +53,7 @@ export async function watch2( } signalExit( - await compiler2.watch(config, { + await compiler.watch(config, { mode, // TODO: esbuild compiler just blows up on syntax errors in the app // onError(errorMessage) { @@ -144,26 +83,19 @@ export async function watch2( console.log(`💿 Built in ${prettyMs(Date.now() - start)}`); } -export function run2(remixRoot: string): Promise { - return watch2(remixRoot); -} - -/** - * Runs the built-in remix app server and dev asset server - */ -export async function run3(remixRoot: string) { +export async function run(remixRoot: string, modeArg?: string) { // TODO: Warn about the need to install @remix-run/serve if it isn't there? let { createApp } = require("@remix-run/serve"); let config = await readConfig(remixRoot); - let mode = process.env.NODE_ENV || "development"; + let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Development; let port = process.env.PORT || 3000; createApp(config.serverBuildDirectory, mode).listen(port, () => { console.log(`Remix App Server started at http://localhost:${port}`); }); - watch2(config, BuildMode.Development, () => { + watch(config, mode, () => { purgeAppRequireCache(config.serverBuildDirectory); }); } diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 6cee5d8d70..0ec7ada831 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -1,350 +1,518 @@ -import path from "path"; -import type { - ExternalOption, - InputOption, - InputOptions, - OutputOptions, - Plugin, - RollupBuild, - RollupError, - RollupOutput, - TreeshakingOptions -} from "rollup"; -import * as rollup from "rollup"; -import babel from "@rollup/plugin-babel"; -import commonjs from "@rollup/plugin-commonjs"; -import json from "@rollup/plugin-json"; -import nodeResolve from "@rollup/plugin-node-resolve"; -import replace from "@rollup/plugin-replace"; -import { terser } from "rollup-plugin-terser"; +import { promises as fsp } from "fs"; +import * as path from "path"; +import { builtinModules as nodeBuiltins } from "module"; +import * as esbuild from "esbuild"; +import debounce from "lodash.debounce"; +import chokidar from "chokidar"; import { BuildMode, BuildTarget } from "./build"; import type { RemixConfig } from "./config"; -import { isImportHint } from "./compiler/importHints"; -import { ignorePackages } from "./compiler/browserIgnore"; - -import assetsManifest from "./compiler/rollup/assetsManifest"; -import clientServer from "./compiler/rollup/clientServer"; -import css from "./compiler/rollup/css"; -import img from "./compiler/rollup/img"; -import mdx from "./compiler/rollup/mdx"; -import remixConfig from "./compiler/rollup/remixConfig"; -import remixInputs from "./compiler/rollup/remixInputs"; -import routeModules from "./compiler/rollup/routeModules"; -import serverManifest from "./compiler/rollup/serverManifest"; -import url from "./compiler/rollup/url"; -import watchDirectory from "./compiler/rollup/watchDirectory"; - -export interface RemixBuild extends RollupBuild { - options: Required; +import { readConfig } from "./config"; +import invariant from "./invariant"; +import { warnOnce } from "./warnings"; +import { createAssetsManifest } from "./compiler/assets"; +import { getAppDependencies } from "./compiler/dependencies"; +import { loaders, getLoaderForFile } from "./compiler/loaders"; +import { getRouteModuleExportsCached } from "./compiler/routes"; +import { writeFileSafe } from "./compiler/utils/fs"; + +// When we build Remix, this shim file is copied directly into the output +// directory in the same place relative to this file. It is eventually injected +// as a source file when building the app. +const reactShim = path.resolve(__dirname, "compiler/shims/react.ts"); + +interface BuildConfig { + mode: BuildMode; + target: BuildTarget; } -export function createBuild( - rollupBuild: RollupBuild, - options: Required -): RemixBuild { - let build = (rollupBuild as unknown) as RemixBuild; - build.options = options; - return build; +function defaultWarningHandler(message: string, key: string) { + warnOnce(false, message, key); } -export interface BuildOptions { - mode?: BuildMode; - target?: BuildTarget; +function defaultErrorHandler(message: string) { + console.error(message); +} + +interface BuildOptions extends Partial { + onWarning?(message: string, key: string): void; + onError?(message: string): void; } -/** - * Runs the build. - */ export async function build( config: RemixConfig, { mode = BuildMode.Production, - target = BuildTarget.Server + target = BuildTarget.Node14, + onWarning = defaultWarningHandler, + onError = defaultErrorHandler }: BuildOptions = {} -): Promise { - let buildOptions = { mode, target }; - let plugins = [ - remixConfig({ rootDir: config.rootDirectory }), - ...getBuildPlugins(buildOptions) - ]; - - let rollupBuild = await rollup.rollup({ - external: getExternalOption(target), - treeshake: getTreeshakeOption(target), - onwarn: getOnWarnOption(target), - plugins - }); - - return createBuild(rollupBuild, buildOptions); +): Promise { + await buildEverything(config, { mode, target, onWarning, onError }); } -export interface WatchOptions extends BuildOptions { - onBuildStart?: () => void; - onBuildEnd?: (build: RemixBuild) => void; - onError?: (error: RollupError) => void; +interface WatchOptions extends BuildOptions { + onRebuildStart?(): void; + onRebuildFinish?(): void; + onFileCreated?(file: string): void; + onFileChanged?(file: string): void; + onFileDeleted?(file: string): void; } -/** - * Runs the build in watch mode. - */ -export function watch( +export async function watch( config: RemixConfig, { mode = BuildMode.Development, - target = BuildTarget.Browser, - onBuildStart, - onBuildEnd, - onError + target = BuildTarget.Node14, + onWarning = defaultWarningHandler, + onError = defaultErrorHandler, + onRebuildStart, + onRebuildFinish, + onFileCreated, + onFileChanged, + onFileDeleted }: WatchOptions = {} -): () => void { - let buildOptions = { mode, target }; - let plugins = [ - remixConfig({ rootDir: config.rootDirectory }), - // Watch for newly created route files. - watchDirectory({ dir: config.appDirectory }), - ...getBuildPlugins(buildOptions) - ]; - - let watcher = rollup.watch({ - external: getExternalOption(target), - treeshake: getTreeshakeOption(target), - onwarn: getOnWarnOption(target), - plugins, - watch: { - buildDelay: 100, - // Skip the write here and do it in a callback instead. This gives us - // a more consistent interface between `build` and `watch`. Both of them - // give you access to the raw build and let you do the generate/write - // step separately. - skipWrite: true - } - }); +): Promise<() => void> { + let options = { mode, target, onWarning, onError, incremental: true }; + let [browserBuild, serverBuild] = await buildEverything(config, options); + + async function disposeBuilders() { + await Promise.all([ + browserBuild.rebuild?.dispose(), + serverBuild.rebuild?.dispose() + ]); + } - watcher.on("event", async event => { - if (event.code === "ERROR") { - if (onError) { - onError(event.error); + let restartBuilders = debounce(async (newConfig?: RemixConfig) => { + await disposeBuilders(); + config = newConfig || (await readConfig(config.rootDirectory)); + if (onRebuildStart) onRebuildStart(); + let builders = await buildEverything(config, options); + if (onRebuildFinish) onRebuildFinish(); + browserBuild = builders[0]; + serverBuild = builders[1]; + }, 500); + + let rebuildEverything = debounce(async () => { + if (onRebuildStart) onRebuildStart(); + await Promise.all([ + browserBuild.rebuild!().then(build => + generateManifests(config, build.metafile!) + ), + serverBuild.rebuild!() + ]); + if (onRebuildFinish) onRebuildFinish(); + }, 100); + + let watcher = chokidar + .watch(config.appDirectory, { + persistent: true, + ignoreInitial: true, + awaitWriteFinish: { + stabilityThreshold: 100, + pollInterval: 100 + } + }) + .on("error", error => console.error(error)) + .on("change", async file => { + if (onFileChanged) onFileChanged(file); + await rebuildEverything(); + }) + .on("add", async file => { + if (onFileCreated) onFileCreated(file); + let newConfig = await readConfig(config.rootDirectory); + if (isEntryPoint(newConfig, file)) { + await restartBuilders(newConfig); } else { - console.error(event.error); + await rebuildEverything(); } - } else if (event.code === "BUNDLE_START") { - if (onBuildStart) onBuildStart(); - } else if (event.code === "BUNDLE_END") { - if (onBuildEnd) { - let rollupBuild = event.result; - onBuildEnd(createBuild(rollupBuild, buildOptions)); + }) + .on("unlink", async file => { + if (onFileDeleted) onFileDeleted(file); + if (isEntryPoint(config, file)) { + await restartBuilders(); + } else { + await rebuildEverything(); } - } - }); + }); - return () => { - watcher.close(); + return async () => { + await watcher.close(); + await disposeBuilders(); }; } -/** - * Creates an in-memory build. This is useful in both the asset server and the - * main server in dev mode to avoid writing the builds to disk. - */ -export function generate(build: RemixBuild): Promise { - return build.generate(getOutputOptions(build)); -} - -/** - * Writes the build to disk. - */ -export function write(build: RemixBuild, dir: string): Promise { - return build.write({ ...getOutputOptions(build), dir }); -} +function isEntryPoint(config: RemixConfig, file: string) { + let appFile = path.relative(config.appDirectory, file); -//////////////////////////////////////////////////////////////////////////////// + if ( + appFile === config.entryClientFile || + appFile === config.entryServerFile + ) { + return true; + } + for (let key in config.routes) { + if (appFile === config.routes[key].file) return true; + } -function isLocalModuleId(id: string): boolean { - return ( - // This is a relative id that hasn't been resolved yet, e.g. "./App" - id.startsWith(".") || - // This is an absolute filesystem path that has already been resolved, e.g. - // "/path/to/node_modules/react/index.js" - path.isAbsolute(id) - ); + return false; } -function getExternalOption(target: string): ExternalOption | undefined { - return target === BuildTarget.Server - ? (id: string) => - // Exclude non-local module identifiers from the server bundles. - // This includes identifiers like "react" which will be resolved - // dynamically at runtime using require(). - !isLocalModuleId(id) && !isImportHint(id) - : // Exclude packages we know we don't want in the browser bundles. - // These *should* be stripped from the browser bundles anyway when - // tree-shaking kicks in, so making them external just saves Rollup - // some time having to load and parse them and their dependencies. - ignorePackages; -} +/////////////////////////////////////////////////////////////////////////////// -function getInputOption(config: RemixConfig, target: BuildTarget): InputOption { - let input: InputOption = {}; - - if (target === BuildTarget.Browser) { - input["entry.client"] = path.resolve( - config.appDirectory, - config.entryClientFile - ); - } else if (target === BuildTarget.Server) { - input["entry.server"] = path.resolve( - config.appDirectory, - config.entryServerFile - ); - } +async function buildEverything( + config: RemixConfig, + options: Required & { incremental?: boolean } +): Promise { + // TODO: + // When building for node, we build both the browser and server builds in + // parallel and emit the asset manifest as a separate file in the output + // directory. + // When building for Cloudflare Workers, we need to run the browser and server + // builds serially so we can inline the asset manifest into the server build + // in a single JavaScript file. + + let browserBuildPromise = createBrowserBuild(config, options); + let serverBuildPromise = createServerBuild(config, options); + + return Promise.all([ + browserBuildPromise.then(async build => { + await generateManifests(config, build.metafile!); + return build; + }), + serverBuildPromise + ]); +} - for (let key of Object.keys(config.routes)) { - let route = config.routes[key]; - input[route.id] = path.resolve(config.appDirectory, route.file); +async function createBrowserBuild( + config: RemixConfig, + options: BuildOptions & { incremental?: boolean } +): Promise { + // For the browser build, exclude node built-ins that don't have a + // browser-safe alternative installed in node_modules. Nothing should + // *actually* be external in the browser build (we want to bundle all deps) so + // this is really just making sure we don't accidentally have any dependencies + // on node built-ins in browser bundles. + let dependencies = Object.keys(await getAppDependencies(config)); + let externals = nodeBuiltins.filter(mod => !dependencies.includes(mod)); + + let entryPoints: esbuild.BuildOptions["entryPoints"] = { + "entry.client": path.resolve(config.appDirectory, config.entryClientFile) + }; + for (let id of Object.keys(config.routes)) { + // All route entry points are virtual modules that will be loaded by the + // browserEntryPointsPlugin. This allows us to tree-shake server-only code + // that we don't want to run in the browser (i.e. action & loader). + entryPoints[id] = + path.resolve(config.appDirectory, config.routes[id].file) + "?browser"; } - return input; + return esbuild.build({ + entryPoints, + outdir: config.assetsBuildDirectory, + platform: "browser", + format: "esm", + external: externals, + inject: [reactShim], + loader: loaders, + bundle: true, + splitting: true, + metafile: true, + incremental: options.incremental, + minify: options.mode === BuildMode.Production, + entryNames: "[dir]/[name]-[hash]", + chunkNames: "_shared/[name]-[hash]", + assetNames: "_assets/[name]-[hash]", + publicPath: config.publicPath, + define: { + "process.env.NODE_ENV": JSON.stringify(options.mode) + }, + plugins: [ + browserRouteModulesPlugin(config, /\?browser$/), + emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/) + ] + }); } -function getTreeshakeOption( - target: BuildTarget -): TreeshakingOptions | undefined { - return target === BuildTarget.Browser - ? // When building for the browser, we need to be very aggressive with code - // removal so we can be sure all imports of server-only code are removed. - { - moduleSideEffects(id) { - // Allow node_modules to have side effects. Everything else (all app - // modules) should be pure. This allows weird dependencies like - // "firebase/auth" to have side effects. - return /\bnode_modules\b/.test(id); +async function createServerBuild( + config: RemixConfig, + options: Required & { incremental?: boolean } +): Promise { + let dependencies = Object.keys(await getAppDependencies(config)); + + return esbuild.build({ + stdin: { + contents: getServerEntryPointModule(config, options), + resolveDir: config.serverBuildDirectory + }, + outfile: path.resolve(config.serverBuildDirectory, "index.js"), + platform: "node", + format: "cjs", + target: options.target, + inject: [reactShim], + loader: loaders, + bundle: true, + incremental: options.incremental, + // The server build needs to know how to generate asset URLs for imports + // of CSS and other files. + assetNames: "_assets/[name]-[hash]", + publicPath: config.publicPath, + plugins: [ + serverRouteModulesPlugin(config), + emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), + manualExternalsPlugin((id, importer) => { + // assets.json is external because this build runs in parallel with the + // browser build and it's not there yet. + if (id === "./assets.json" && importer === "") return true; + + // Mark all bare imports as external. They will be require()'d at + // runtime from node_modules. + if (isBareModuleId(id)) { + let packageName = getNpmPackageName(id); + if ( + !/\bnode_modules\b/.test(importer) && + !nodeBuiltins.includes(packageName) && + !dependencies.includes(packageName) + ) { + options.onWarning( + `The path "${id}" is imported in ` + + `${path.relative(process.cwd(), importer)} but ` + + `${packageName} is not listed in your package.json dependencies. ` + + `Did you forget to install it?`, + packageName + ); + } + return true; } - } - : undefined; + + return false; + }) + ] + }); } -function getOnWarnOption( - target: BuildTarget -): InputOptions["onwarn"] | undefined { - return target === BuildTarget.Browser - ? (warning, warn) => { - if (warning.code === "EMPTY_BUNDLE") { - // Ignore "Generated an empty chunk: blah" warnings when building for - // the browser. There may be quite a few of them because we are - // aggressively removing server-only packages from the build. - // TODO: Can we get Rollup to avoid generating these chunks entirely? - return; - } +function isBareModuleId(id: string): boolean { + return !id.startsWith(".") && !path.isAbsolute(id); +} - warn(warning); - } - : undefined; +function getNpmPackageName(id: string): string { + let split = id.split("/"); + let packageName = split[0]; + if (packageName.startsWith("@")) packageName += `/${split[1]}`; + return packageName; } -function getBuildPlugins({ mode, target }: Required): Plugin[] { - let plugins: Plugin[] = [ - remixInputs({ - getInput(config) { - return getInputOption(config, target); - } +async function generateManifests( + config: RemixConfig, + metafile: esbuild.Metafile +): Promise { + let assetsManifest = await createAssetsManifest(config, metafile); + + let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; + assetsManifest.url = config.publicPath + filename; + + return Promise.all([ + writeFileSafe( + path.join(config.assetsBuildDirectory, filename), + `window.__remixManifest=${JSON.stringify(assetsManifest)}` + ), + writeFileSafe( + path.join(config.serverBuildDirectory, "assets.json"), + JSON.stringify(assetsManifest, null, 2) + ) + ]); +} + +function getServerEntryPointModule( + config: RemixConfig, + options: BuildOptions +): string { + switch (options.target) { + case BuildTarget.Node14: + return ` +import * as entryServer from ${JSON.stringify( + path.resolve(config.appDirectory, config.entryServerFile) + )}; +${Object.keys(config.routes) + .map((key, index) => { + let route = config.routes[key]; + return `import * as route${index} from ${JSON.stringify( + path.resolve(config.appDirectory, route.file) + )};`; + }) + .join("\n")} +export { default as assets } from "./assets.json"; +export const entry = { module: entryServer }; +export const routes = { + ${Object.keys(config.routes) + .map((key, index) => { + let route = config.routes[key]; + return `${JSON.stringify(key)}: { + id: ${JSON.stringify(route.id)}, + parentId: ${JSON.stringify(route.parentId)}, + path: ${JSON.stringify(route.path)}, + caseSensitive: ${JSON.stringify(route.caseSensitive)}, + module: route${index} + }`; }) - ]; - - plugins.push( - clientServer({ target }), - mdx(), - routeModules({ target }), - json(), - img({ target }), - css({ target, mode }), - url({ target }), - babel({ - babelHelpers: "bundled", - configFile: false, - exclude: /node_modules/, - extensions: [".md", ".mdx", ".js", ".jsx", ".ts", ".tsx"], - presets: [ - ["@babel/preset-react", { runtime: "automatic" }], - // TODO: Different targets for browsers vs. node. - ["@babel/preset-env", { bugfixes: true, targets: { node: "12" } }], - [ - "@babel/preset-typescript", - { - allExtensions: true, - isTSX: true - } - ] - ] - }), - nodeResolve({ - browser: target === BuildTarget.Browser, - extensions: [".js", ".json", ".jsx", ".ts", ".tsx"], - preferBuiltins: target !== BuildTarget.Browser - }), - commonjs() - ); - - if (target !== BuildTarget.Server) { - plugins.push( - replace({ - preventAssignment: true, - values: { - "process.env.NODE_ENV": JSON.stringify(mode) - } - }) - ); + .join(",\n ")} +};`; + default: + throw new Error( + `Cannot generate server entry point module for target: ${options.target}` + ); } +} - if (target !== BuildTarget.Server && mode === BuildMode.Production) { - plugins.push(terser({ ecma: 2017 })); - } +type Route = RemixConfig["routes"][string]; - if (target === BuildTarget.Browser) { - plugins.push(assetsManifest()); - } else if (target === BuildTarget.Server) { - plugins.push(serverManifest()); - } +const browserSafeRouteExports: { [name: string]: boolean } = { + ErrorBoundary: true, + default: true, + handle: true, + links: true, + meta: true +}; - return plugins; +/** + * This plugin loads route modules for the browser build, using module shims + * that re-export only the route module exports that are safe for the browser. + */ +function browserRouteModulesPlugin( + config: RemixConfig, + suffixMatcher: RegExp +): esbuild.Plugin { + return { + name: "browser-route-modules", + async setup(build) { + let routesByFile: Map = Object.keys(config.routes).reduce( + (map, key) => { + let route = config.routes[key]; + map.set(path.resolve(config.appDirectory, route.file), route); + return map; + }, + new Map() + ); + + build.onResolve({ filter: suffixMatcher }, args => { + return { path: args.path, namespace: "browser-route-module" }; + }); + + build.onLoad( + { filter: suffixMatcher, namespace: "browser-route-module" }, + async args => { + let file = args.path.replace(suffixMatcher, ""); + let route = routesByFile.get(file); + invariant(route, `Cannot get route by path: ${args.path}`); + + let exports = ( + await getRouteModuleExportsCached(config, route.id) + ).filter(ex => !!browserSafeRouteExports[ex]); + let spec = exports.length > 0 ? `{ ${exports.join(", ")} }` : "*"; + let contents = `export ${spec} from ${JSON.stringify(file)};`; + + return { + contents, + resolveDir: path.dirname(file), + loader: "js" + }; + } + ); + } + }; } -function getOutputOptions(build: RemixBuild): OutputOptions { - let { mode, target } = build.options; - +/** + * This plugin substitutes an empty module for any modules in the `app` + * directory that match the given `filter`. + */ +function emptyModulesPlugin( + config: RemixConfig, + filter: RegExp +): esbuild.Plugin { return { - format: target === BuildTarget.Server ? "cjs" : "esm", - exports: target === BuildTarget.Server ? "named" : undefined, - assetFileNames: - mode === BuildMode.Production && target === BuildTarget.Browser - ? "[name]-[hash][extname]" - : "[name][extname]", - chunkFileNames: "_shared/[name]-[hash].js", - entryFileNames: - mode === BuildMode.Production && target === BuildTarget.Browser - ? "[name]-[hash].js" - : "[name].js", - manualChunks(id) { - return getNpmPackageName(id); + name: "empty-modules", + setup(build) { + build.onResolve({ filter }, args => { + let resolved = path.resolve(args.resolveDir, args.path); + if ( + // Limit this behavior to modules found in only the `app` directory. + // This allows node_modules to use the `.server.js` and `.client.js` + // naming conventions with different semantics. + resolved.startsWith(config.appDirectory) + ) { + return { path: args.path, namespace: "empty-module" }; + } + }); + + build.onLoad({ filter: /.*/, namespace: "empty-module" }, () => { + return { + // Use an empty CommonJS module here instead of ESM to avoid "No + // matching export" errors in esbuild for stuff that is imported + // from this file. + contents: "module.exports = {};", + loader: "js" + }; + }); } }; } -function getNpmPackageName(id: string): string | undefined { - let pieces = id.split(path.sep); - let index = pieces.lastIndexOf("node_modules"); - - if (index !== -1 && pieces.length > index + 1) { - let packageName = pieces[index + 1]; +/** + * This plugin loads route modules for the server build. + */ +function serverRouteModulesPlugin(config: RemixConfig): esbuild.Plugin { + return { + name: "server-route-modules", + setup(build) { + let routeFiles = new Set( + Object.keys(config.routes).map(key => + path.resolve(config.appDirectory, config.routes[key].file) + ) + ); + + build.onResolve({ filter: /.*/ }, args => { + if (routeFiles.has(args.path)) { + return { path: args.path, namespace: "route-module" }; + } + }); + + build.onLoad({ filter: /.*/, namespace: "route-module" }, async args => { + let file = args.path; + let contents = await fsp.readFile(file, "utf-8"); + + // Default to `export {}` if the file is empty so esbuild interprets + // this file as ESM instead of CommonJS with `default: {}`. This helps + // in development when creating new files. + // See https://github.com/evanw/esbuild/issues/1043 + if (!/\S/.test(contents)) { + return { contents: "export {}", loader: "js" }; + } - if (packageName.startsWith("@") && pieces.length > index + 2) { - packageName = - // S3 hates @folder, so we switch it to __ - packageName.replace("@", "__") + "/" + pieces[index + 2]; + return { + contents, + resolveDir: path.dirname(file), + loader: getLoaderForFile(file) + }; + }); } + }; +} - return packageName; - } - - return undefined; +/** + * This plugin marks paths external using a callback function. + */ +function manualExternalsPlugin( + isExternal: (id: string, importer: string) => boolean +): esbuild.Plugin { + return { + name: "manual-externals", + setup(build) { + build.onResolve({ filter: /.*/ }, args => { + if (isExternal(args.path, args.importer)) { + return { path: args.path, external: true }; + } + }); + } + }; } diff --git a/packages/remix-dev/compiler2/assets.ts b/packages/remix-dev/compiler/assets.ts similarity index 100% rename from packages/remix-dev/compiler2/assets.ts rename to packages/remix-dev/compiler/assets.ts diff --git a/packages/remix-dev/compiler/browserIgnore.ts b/packages/remix-dev/compiler/browserIgnore.ts deleted file mode 100644 index d6d06422a9..0000000000 --- a/packages/remix-dev/compiler/browserIgnore.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file is an optimization so that rollup won't try to bundle any of these -// modules, which greatly speeds up the browser tree-shaking -import builtins from "builtin-modules"; - -const remixServerPackages = [ - "@remix-run/architect", - "@remix-run/express", - "@remix-run/node", - "@remix-run/vercel" -]; - -const thirdPartyPackages = [ - "@databases/mysql", - "@databases/pg", - "@databases/sqlite", - "@prisma/client", - "apollo-server", - "better-sqlite3", - "bookshelf", - "dynamodb", - "firebase-admin", - "mariadb", - "mongoose", - "mysql", - "mysql2", - "pg", - "pg-hstore", - "pg-native", - "pg-pool", - "postgres", - "sequelize", - "sqlite", - "sqlite3", - "tedious" -]; - -export let ignorePackages = [ - ...builtins, - ...remixServerPackages, - ...thirdPartyPackages -]; diff --git a/packages/remix-dev/compiler/createUrl.ts b/packages/remix-dev/compiler/createUrl.ts deleted file mode 100644 index e6a52be318..0000000000 --- a/packages/remix-dev/compiler/createUrl.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as path from "path"; - -export default function createUrl( - publicPath: string, - fileName: string -): string { - return publicPath + fileName.split(path.win32.sep).join("/"); -} diff --git a/packages/remix-dev/compiler/crypto.ts b/packages/remix-dev/compiler/crypto.ts deleted file mode 100644 index 1e1cbd0478..0000000000 --- a/packages/remix-dev/compiler/crypto.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { BinaryLike } from "crypto"; -import { createHash } from "crypto"; -import { createReadStream } from "fs"; -import type { OutputBundle } from "rollup"; - -export function getHash(source: BinaryLike): string { - return createHash("sha1").update(source).digest("hex"); -} - -export async function getFileHash(file: string): Promise { - return new Promise((accept, reject) => { - let hash = createHash("sha1"); - let stream = createReadStream(file); - - stream - .on("error", reject) - .on("data", data => { - hash.update(data); - }) - .on("end", () => { - accept(hash.digest("hex")); - }); - }); -} - -export function getBundleHash(bundle: OutputBundle): string { - let hash = createHash("sha1"); - - for (let key of Object.keys(bundle).sort()) { - let output = bundle[key]; - hash.update(output.type === "asset" ? output.source : output.code); - } - - return hash.digest("hex"); -} - -export function addHash(fileName: string, hash: string): string { - return fileName.replace(/(\.\w+)?$/, `-${hash}$1`); -} diff --git a/packages/remix-dev/compiler2/dependencies.ts b/packages/remix-dev/compiler/dependencies.ts similarity index 100% rename from packages/remix-dev/compiler2/dependencies.ts rename to packages/remix-dev/compiler/dependencies.ts diff --git a/packages/remix-dev/compiler/importHints.ts b/packages/remix-dev/compiler/importHints.ts deleted file mode 100644 index 28ae5cff76..0000000000 --- a/packages/remix-dev/compiler/importHints.ts +++ /dev/null @@ -1,5 +0,0 @@ -const importHints = ["css:", "img:", "url:"]; - -export function isImportHint(id: string): boolean { - return importHints.some(hint => id.startsWith(hint)); -} diff --git a/packages/remix-dev/compiler2/loaders.ts b/packages/remix-dev/compiler/loaders.ts similarity index 100% rename from packages/remix-dev/compiler2/loaders.ts rename to packages/remix-dev/compiler/loaders.ts diff --git a/packages/remix-dev/compiler/rollup/assetsManifest.ts b/packages/remix-dev/compiler/rollup/assetsManifest.ts deleted file mode 100644 index 45b9f8389e..0000000000 --- a/packages/remix-dev/compiler/rollup/assetsManifest.ts +++ /dev/null @@ -1,213 +0,0 @@ -import path from "path"; -import { promises as fsp } from "fs"; -import type { OutputBundle, Plugin, RenderedModule } from "rollup"; - -import invariant from "../../invariant"; -import { getBundleHash } from "../crypto"; -import { routeModuleProxy, emptyRouteModule } from "./routeModules"; -import type { RemixConfig } from "./remixConfig"; -import { getRemixConfig } from "./remixConfig"; - -/** - * Generates 2 files: - * - * - An "assets manifest" file in the assets build directory that contains the - * URLs for all bundles needed in the browser - * - An `assets.json` file in the server build directory that contains the URL - * for the assets manifest file - */ -export default function assetsManifestPlugin({ - fileName = "manifest-[hash].js", - globalVar = "__remixManifest" -}: { - fileName?: string; - globalVar?: string; -} = {}): Plugin { - let config: RemixConfig; - - return { - name: "assetsManifest", - - async buildStart({ plugins }) { - config = await getRemixConfig(plugins); - }, - - async generateBundle(_options, bundle) { - let manifest = getAssetsManifest( - bundle, - config.routes, - config.publicPath - ); - - fileName = fileName.replace("[hash]", manifest.version); - - manifest.url = config.publicPath + fileName; - - // Emit the manifest for direct consumption by the browser. - let source = getGlobalScript(manifest, globalVar); - this.emitFile({ type: "asset", fileName, source }); - - // Write the manifest to the server build directory so it knows the asset - // URLs when server rendering and the URL of the manifest. - let assetsFile = path.join(config.serverBuildDirectory, "assets.json"); - await fsp.mkdir(path.dirname(assetsFile), { recursive: true }); - await fsp.writeFile(assetsFile, JSON.stringify(manifest, null, 2)); - } - }; -} - -interface AssetsManifest { - version: string; - url?: string; - entry: { - module: string; - imports: string[]; - }; - routes: { - [routeId: string]: { - id: string; - parentId?: string; - path: string; - caseSensitive?: boolean; - module: string; - imports?: string[]; - hasAction?: boolean; - hasLoader?: boolean; - }; - }; -} - -function getAssetsManifest( - bundle: OutputBundle, - routeManifest: RemixConfig["routes"], - publicPath: string -): AssetsManifest { - let version = getBundleHash(bundle).slice(0, 8); - - let routeIds = Object.keys(routeManifest); - let entry: AssetsManifest["entry"] | undefined; - let routes: AssetsManifest["routes"] = Object.create(null); - - for (let key in bundle) { - let chunk = bundle[key]; - if (chunk.type !== "chunk") continue; - - if (chunk.name === "entry.client") { - entry = { - module: publicPath + chunk.fileName, - imports: chunk.imports.map(path => publicPath + path) - }; - } else if ( - routeIds.includes(chunk.name) && - chunk.facadeModuleId?.endsWith(routeModuleProxy) - ) { - let route = routeManifest[chunk.name]; - - // When we build route modules, we put a shim in front that ends with a - // ?route-module-proxy string. Removing this suffix gets us back to the - // original source module id. - let sourceModuleId = chunk.facadeModuleId.replace(routeModuleProxy, ""); - - // Usually the source module will be contained in this chunk, but if - // someone imports a route module from within another route module, Rollup - // will place the source module in a shared chunk. So we have to go find - // the chunk with the source module in it. If the source module was empty, - // it will have the ?empty-route-module suffix on it. - let sourceModule = - chunk.modules[sourceModuleId] || - chunk.modules[sourceModuleId + emptyRouteModule] || - findRenderedModule(bundle, sourceModuleId) || - findRenderedModule(bundle, sourceModuleId + emptyRouteModule); - - invariant(sourceModule, `Cannot find source module for ${route.id}`); - - routes[route.id] = { - path: route.path, - caseSensitive: route.caseSensitive, - id: route.id, - parentId: route.parentId, - module: publicPath + chunk.fileName, - imports: chunk.imports.map(path => publicPath + path), - hasAction: sourceModule.removedExports.includes("action") - ? true - : // Using `undefined` here prevents this from showing up in the - // manifest JSON when there is no action. - undefined, - hasLoader: sourceModule.removedExports.includes("loader") - ? true - : // Using `undefined` here prevents this from showing up in the - // manifest JSON when there is no loader. - undefined - }; - } - } - - invariant(entry, `Missing entry.client chunk`); - - // Slim down the overall size of the manifest by pruning imports from child - // routes that their parents will have loaded already by the time they render. - optimizeRoutes(routes, entry.imports); - - return { version, entry, routes }; -} - -function findRenderedModule( - bundle: OutputBundle, - name: string -): RenderedModule | undefined { - for (let key in bundle) { - let chunk = bundle[key]; - if (chunk.type === "chunk" && name in chunk.modules) { - return chunk.modules[name]; - } - } -} - -type ImportsCache = { [routeId: string]: string[] }; - -function optimizeRoutes( - routes: AssetsManifest["routes"], - entryImports: string[] -): void { - // This cache is an optimization that allows us to avoid pruning the same - // route's imports more than once. - let importsCache: ImportsCache = Object.create(null); - - for (let key in routes) { - optimizeRouteImports(key, routes, entryImports, importsCache); - } -} - -function optimizeRouteImports( - routeId: string, - routes: AssetsManifest["routes"], - parentImports: string[], - importsCache: ImportsCache -): string[] { - if (importsCache[routeId]) return importsCache[routeId]; - - let route = routes[routeId]; - - if (route.parentId) { - parentImports = parentImports.concat( - optimizeRouteImports(route.parentId, routes, parentImports, importsCache) - ); - } - - let routeImports = (route.imports || []).filter( - url => !parentImports.includes(url) - ); - - // Setting `route.imports = undefined` prevents `imports: []` from showing up - // in the manifest JSON when there are no imports. - route.imports = routeImports.length > 0 ? routeImports : undefined; - - // Cache so the next lookup for this route is faster. - importsCache[routeId] = routeImports; - - return routeImports; -} - -function getGlobalScript(manifest: AssetsManifest, globalVar: string): string { - return `window.${globalVar} = ${JSON.stringify(manifest)}`; -} diff --git a/packages/remix-dev/compiler/rollup/clientServer.ts b/packages/remix-dev/compiler/rollup/clientServer.ts deleted file mode 100644 index de1396fdc7..0000000000 --- a/packages/remix-dev/compiler/rollup/clientServer.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Plugin } from "rollup"; - -import { BuildTarget } from "../../build"; -import empty from "./empty"; - -/** - * All file extensions we support for JavaScript modules. - */ -const moduleExts = [".md", ".mdx", ".js", ".jsx", ".ts", ".tsx"]; - -function isClientOnlyModuleId(id: string): boolean { - return moduleExts.some(ext => id.endsWith(`.client${ext}`)); -} - -function isServerOnlyModuleId(id: string): boolean { - return moduleExts.some(ext => id.endsWith(`.server${ext}`)); -} - -/** - * Rollup plugin that excludes `*.client.js` files from the server build and - * `*.server.js` files from the browser build. - */ -export default function clientServerPlugin({ - target -}: { - target: string; -}): Plugin { - return empty({ - isEmptyModuleId(id) { - if (/\bnode_modules\b/.test(id)) return false; - - return ( - (isClientOnlyModuleId(id) && target === BuildTarget.Server) || - (isServerOnlyModuleId(id) && target === BuildTarget.Browser) - ); - } - }); -} diff --git a/packages/remix-dev/compiler/rollup/css.ts b/packages/remix-dev/compiler/rollup/css.ts deleted file mode 100644 index a6d8dc7a62..0000000000 --- a/packages/remix-dev/compiler/rollup/css.ts +++ /dev/null @@ -1,108 +0,0 @@ -import path from "path"; -import { promises as fsp } from "fs"; -import cacache from "cacache"; -import postcss from "postcss"; -import type Processor from "postcss/lib/processor"; -import type { Plugin } from "rollup"; -import prettyBytes from "pretty-bytes"; -import prettyMs from "pretty-ms"; - -import { BuildTarget } from "../../build"; -import createUrl from "../createUrl"; -import { getHash, addHash } from "../crypto"; -import type { RemixConfig } from "./remixConfig"; -import { getRemixConfig } from "./remixConfig"; - -export default function cssPlugin({ - target, - mode -}: { - target: string; - mode: string; -}): Plugin { - let config: RemixConfig; - let processor: Processor; - - return { - name: "css", - - async buildStart({ plugins }) { - config = await getRemixConfig(plugins); - - if (!processor) { - let postCssConfig = await getPostCssConfig(config.rootDirectory, mode); - processor = postcss(postCssConfig.plugins); - } - }, - - async resolveId(id, importer) { - if (!id.startsWith("css:")) return null; - - let resolved = await this.resolve(id.slice(4), importer, { - skipSelf: true - }); - - return resolved && `\0css:${resolved.id}`; - }, - - async load(id) { - if (!id.startsWith("\0css:")) return; - - let file = id.slice(5); - let originalSource = await fsp.readFile(file); - let hash = getHash(originalSource).slice(0, 8); - let fileName = addHash( - path.relative(config.appDirectory, file), - hash - ).replace(/(\.\w+)?$/, ".css"); - - this.addWatchFile(file); - - if (target === BuildTarget.Browser) { - let source: string | Uint8Array; - try { - let cached = await cacache.get(config.cacheDirectory, hash); - source = cached.data; - } catch (error) { - if (error.code !== "ENOENT") throw error; - source = await generateCssSource(file, originalSource, processor); - await cacache.put(config.cacheDirectory, hash, source); - } - - this.emitFile({ type: "asset", fileName, source }); - } - - return `export default ${JSON.stringify( - createUrl(config.publicPath, fileName) - )}`; - } - }; -} - -async function generateCssSource( - file: string, - content: Buffer, - processor: Processor -): Promise { - let start = Date.now(); - let result = await processor.process(content, { from: file }); - - console.log( - 'Built CSS for "%s", %s, %s', - path.basename(file), - prettyBytes(Buffer.byteLength(result.css)), - prettyMs(Date.now() - start) - ); - - return result.css; -} - -async function getPostCssConfig(appDirectory: string, mode: string) { - let requirePath = path.resolve(appDirectory, "postcss.config.js"); - try { - await fsp.access(requirePath); - return require(requirePath); - } catch (e) { - return { plugins: mode ? [] : [] }; - } -} diff --git a/packages/remix-dev/compiler/rollup/empty.ts b/packages/remix-dev/compiler/rollup/empty.ts deleted file mode 100644 index 33ca61ad79..0000000000 --- a/packages/remix-dev/compiler/rollup/empty.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Plugin } from "rollup"; - -/** - * Rollup plugin that uses an empty shim for any module id that is considered - * "empty" according to the given `isEmptyModuleId` test function. - */ -export default function emptyPlugin({ - isEmptyModuleId -}: { - isEmptyModuleId: (id: string) => boolean; -}): Plugin { - return { - name: "empty", - - load(id) { - if (!isEmptyModuleId(id)) return null; - - return { - code: "export default {}", - syntheticNamedExports: true - }; - } - }; -} diff --git a/packages/remix-dev/compiler/rollup/img.ts b/packages/remix-dev/compiler/rollup/img.ts deleted file mode 100644 index 8764d6070c..0000000000 --- a/packages/remix-dev/compiler/rollup/img.ts +++ /dev/null @@ -1,270 +0,0 @@ -import * as path from "path"; -import cacache from "cacache"; -import type { Plugin } from "rollup"; -import sharp from "sharp"; -import prettyBytes from "pretty-bytes"; -import prettyMs from "pretty-ms"; - -import invariant from "../../invariant"; -import { BuildTarget } from "../../build"; -import { addHash, getFileHash, getHash } from "../crypto"; -import type { RemixConfig } from "./remixConfig"; -import { getRemixConfig } from "./remixConfig"; - -// Don't use the sharp cache, we use Rollup's built-in cache so we don't process -// images between restarts of the dev server. Also, through some experimenting, -// the sharp cache seems to be based on filenames, not the content of the file, -// so replacing an image with a new one by the same name didn't work. -sharp.cache(false); - -const transparent1x1gif = - "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; - -const imageFormats = ["avif", "jpeg", "png", "webp"]; - -export default function imgPlugin({ target }: { target: string }): Plugin { - let config: RemixConfig; - - return { - name: "img", - - async buildStart({ plugins }) { - config = await getRemixConfig(plugins); - }, - - async resolveId(id, importer) { - if (!id.startsWith("img:")) return null; - - let { baseId, search } = parseId(id.slice(4)); - - let resolved = await this.resolve(baseId, importer, { skipSelf: true }); - - return resolved && `\0img:${resolved.id}${search}`; - }, - - async load(id) { - if (!id.startsWith("\0img:")) return; - - let { baseId: file, search } = parseId(id.slice(5)); - let params = new URLSearchParams(search); - let hash = (await getFileHash(file)).slice(0, 8); - - this.addWatchFile(file); - - let assets = await getImageAssets( - config.appDirectory, - file, - hash, - params - ); - - if (target === BuildTarget.Browser) { - for (let asset of assets) { - let { fileName, hash } = asset; - - let source: string | Uint8Array; - try { - let cached = await cacache.get(config.cacheDirectory, hash); - source = cached.data; - } catch (error) { - if (error.code !== "ENOENT") throw error; - source = await generateImageAssetSource(file, asset); - await cacache.put(config.cacheDirectory, hash, source); - } - - this.emitFile({ type: "asset", fileName, source }); - } - } - - let placeholder = - params.get("placeholder") != null - ? await generateImagePlaceholder(file, hash, config.cacheDirectory) - : transparent1x1gif; - - let images = assets.map(asset => ({ - src: config.publicPath + asset.fileName, - width: asset.width, - height: asset.height, - format: asset.transform.format - })); - - return ` - export let images = ${JSON.stringify(images, null, 2)}; - let primaryImage = images[images.length - 1]; - let srcset = images.map(image => image.src + " " + image.width + "w").join(","); - let placeholder = ${JSON.stringify(placeholder)}; - let mod = { ...primaryImage, srcset, placeholder }; - export default mod; - `; - } - }; -} - -function parseId(id: string): { baseId: string; search: string } { - let searchIndex = id.indexOf("?"); - return searchIndex === -1 - ? { baseId: id, search: "" } - : { - baseId: id.slice(0, searchIndex), - search: id.slice(searchIndex) - }; -} - -interface ImageTransform { - width?: number; - height?: number; - quality?: number; - format: string; -} - -function getImageTransforms( - params: URLSearchParams, - defaultFormat: string -): ImageTransform[] { - let width = params.get("width"); - let height = params.get("height"); - let quality = params.get("quality"); - let format = params.get("format") || defaultFormat; - - if (format === "jpg") { - format = "jpeg"; - } else if (!imageFormats.includes(format)) { - throw new Error(`Invalid image format: ${format}`); - } - - let transform = { - width: width ? parseInt(width, 10) : undefined, - height: height ? parseInt(height, 10) : undefined, - quality: quality ? parseInt(quality, 10) : undefined, - format - }; - - let srcset = params.get("srcset"); - - return srcset - ? srcset.split(",").map(width => ({ - ...transform, - width: parseInt(width, 10) - })) - : [transform]; -} - -interface ImageAsset { - fileName: string; - hash: string; - width: number; - height: number; - transform: ImageTransform; -} - -async function getImageAssets( - dir: string, - file: string, - sourceHash: string, - params: URLSearchParams -): Promise { - let defaultFormat = path.extname(file).slice(1); - let transforms = getImageTransforms(params, defaultFormat); - - return Promise.all( - transforms.map(async transform => { - let width: number; - let height: number; - - if (transform.width && transform.height) { - width = transform.width; - height = transform.height; - } else { - let meta = await sharp(file).metadata(); - - invariant( - typeof meta.width === "number" && typeof meta.height === "number", - `Cannot get image metadata: ${file}` - ); - - if (transform.width) { - width = transform.width; - height = Math.round(transform.width / (meta.width / meta.height)); - } else if (transform.height) { - width = Math.round(transform.height / (meta.height / meta.width)); - height = transform.height; - } else { - width = meta.width; - height = meta.height; - } - } - - let hash = getHash( - sourceHash + - transform.width + - transform.height + - transform.quality + - transform.format - ).slice(0, 8); - let fileName = addHash( - addHash(path.relative(dir, file), `${width}x${height}`), - hash - ); - - return { fileName, hash, width, height, transform }; - }) - ); -} - -async function generateImageAssetSource( - file: string, - asset: ImageAsset -): Promise { - let start = Date.now(); - let image = sharp(file); - - if (asset.width || asset.height) { - image.resize({ width: asset.width, height: asset.height }); - } - - // image.jpeg(), image.png(), etc. - // @ts-ignore - image[asset.transform.format]({ quality: asset.transform.quality }); - - let buffer = await image.toBuffer(); - - console.log( - 'Built image "%s", %s, %s', - asset.fileName, - prettyBytes(buffer.byteLength), - prettyMs(Date.now() - start) - ); - - return buffer; -} - -async function generateImagePlaceholder( - file: string, - hash: string, - cacheDir: string -): Promise { - let cacheKey = `placeholder-${hash}`; - - let buffer: Buffer; - try { - let cached = await cacache.get(cacheDir, cacheKey); - buffer = cached.data; - } catch (error) { - if (error.code !== "ENOENT") throw error; - - let start = Date.now(); - let image = sharp(file).resize({ width: 50 }).jpeg({ quality: 25 }); - buffer = await image.toBuffer(); - - console.log( - 'Built placeholder image for "%s", %s, %s', - path.basename(file), - prettyBytes(buffer.byteLength), - prettyMs(Date.now() - start) - ); - - await cacache.put(cacheDir, cacheKey, buffer); - } - - return `data:image/jpeg;base64,${buffer.toString("base64")}`; -} diff --git a/packages/remix-dev/compiler/rollup/mdx.ts b/packages/remix-dev/compiler/rollup/mdx.ts deleted file mode 100644 index 6a383b28ed..0000000000 --- a/packages/remix-dev/compiler/rollup/mdx.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { promises as fsp } from "fs"; -import * as path from "path"; -import cacache from "cacache"; -import type { Plugin } from "rollup"; -import parseFrontMatter from "front-matter"; -import mdx from "@mdx-js/mdx"; -import prettyMs from "pretty-ms"; - -import { getRemixConfig } from "./remixConfig"; -import { getHash } from "../crypto"; - -const imports = ` -import { mdx } from "@mdx-js/react"; -`; - -let regex = /\.mdx?$/; - -interface RemixFrontMatter { - meta?: { [name: string]: string }; - headers?: { [header: string]: string }; -} - -// They don't have types, so we could go figure it all out and add it as an -// interface here -export type MdxOptions = any; -export type MdxFunctionOption = ( - attributes: { [key: string]: any }, - filename: string -) => MdxOptions; - -export type MdxConfig = MdxFunctionOption | MdxOptions; - -/** - * Loads .mdx files as JavaScript modules with support for Remix's `headers` - * and `meta` route module functions as static object declarations in the - * frontmatter. - */ -export default function mdxPlugin({ - mdxConfig: mdxConfigArg, - cache: cacheArg -}: { - mdxConfig?: MdxConfig; - cache?: string; -} = {}): Plugin { - let mdxConfig: MdxConfig; - let cache: string; - - return { - name: "mdx", - - async buildStart({ plugins }) { - let config = await getRemixConfig(plugins); - mdxConfig = mdxConfigArg || config.mdx; - cache = cacheArg || config.cacheDirectory; - }, - - async load(id) { - if (id.startsWith("\0") || !regex.test(id)) return null; - - let file = id; - let source = await fsp.readFile(file, "utf-8"); - let hash = getHash(source).slice(0, 8); - - let code: string; - if (cache) { - try { - let cached = await cacache.get(cache, hash); - code = cached.data.toString("utf-8"); - } catch (error) { - if (error.code !== "ENOENT") throw error; - code = await generateRouteModule(file, source, mdxConfig); - await cacache.put(cache, hash, code); - } - } else { - code = await generateRouteModule(file, source, mdxConfig); - } - - return code; - } - }; -} - -async function generateRouteModule( - file: string, - source: string, - mdxConfig: MdxConfig -): Promise { - let start = Date.now(); - - let { - body, - attributes - }: { - body: string; - attributes: RemixFrontMatter; - } = parseFrontMatter(source); - - let code = imports; - - if (attributes && attributes.meta) { - code += `export function meta() { return ${JSON.stringify( - attributes.meta - )}}\n`; - } - - if (attributes && attributes.headers) { - code += `export function headers() { return ${JSON.stringify( - attributes.headers - )}}\n`; - } - - let mdxOptions = - typeof mdxConfig === "function" ? mdxConfig(attributes, file) : mdxConfig; - - code += await mdx(body, mdxOptions); - - if (process.env.NODE_ENV !== "test") { - console.log( - 'Built MDX for "%s", %s', - path.basename(file), - prettyMs(Date.now() - start) - ); - } - - return code; -} diff --git a/packages/remix-dev/compiler/rollup/remixConfig.ts b/packages/remix-dev/compiler/rollup/remixConfig.ts deleted file mode 100644 index 9f24d226a2..0000000000 --- a/packages/remix-dev/compiler/rollup/remixConfig.ts +++ /dev/null @@ -1,52 +0,0 @@ -import path from "path"; -import type { Plugin } from "rollup"; - -import type { RemixConfig } from "../../config"; -import { readConfig } from "../../config"; -import invariant from "../../invariant"; -import { purgeModuleCache } from "../../modules"; - -export type { RemixConfig }; - -export default function remixConfigPlugin({ - rootDir -}: { - rootDir: string; -}): Plugin { - let configPromise: Promise | null = null; - - return { - name: "remixConfig", - - options(options) { - configPromise = null; - return options; - }, - - buildStart() { - this.addWatchFile(path.join(rootDir, "remix.config.js")); - }, - - api: { - getConfig(): Promise { - if (!configPromise) { - // Purge the cache in case remix.config.js loads any other files. - purgeModuleCache(rootDir); - configPromise = readConfig(rootDir); - } - - return configPromise; - } - } - }; -} - -export function findConfigPlugin(plugins?: Plugin[]): Plugin | undefined { - return plugins && plugins.find(plugin => plugin.name === "remixConfig"); -} - -export function getRemixConfig(plugins?: Plugin[]): Promise { - let plugin = findConfigPlugin(plugins); - invariant(plugin, `Missing remixConfig plugin`); - return plugin.api.getConfig(); -} diff --git a/packages/remix-dev/compiler/rollup/remixInputs.ts b/packages/remix-dev/compiler/rollup/remixInputs.ts deleted file mode 100644 index e5d6e87496..0000000000 --- a/packages/remix-dev/compiler/rollup/remixInputs.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { InputOption, Plugin } from "rollup"; - -import type { RemixConfig } from "./remixConfig"; -import { getRemixConfig } from "./remixConfig"; - -/** - * Enables setting the compiler's input dynamically via a hook function. - */ -export default function remixInputsPlugin({ - getInput -}: { - getInput: (config: RemixConfig) => InputOption; -}): Plugin { - return { - name: "remixInputs", - - async options(options) { - let config = await getRemixConfig(options.plugins || []); - return { ...options, input: getInput(config) }; - } - }; -} diff --git a/packages/remix-dev/compiler/rollup/routeModules.ts b/packages/remix-dev/compiler/rollup/routeModules.ts deleted file mode 100644 index d8d7bbe5cf..0000000000 --- a/packages/remix-dev/compiler/rollup/routeModules.ts +++ /dev/null @@ -1,111 +0,0 @@ -import * as fs from "fs"; -import type { Plugin } from "rollup"; - -import { BuildTarget } from "../../build"; -import { getRemixConfig } from "./remixConfig"; - -export const routeModuleProxy = "?route-module-proxy"; -export const emptyRouteModule = "?empty-route-module"; - -/** - * A resolver/loader for route modules that does a few things: - * - * - when building for the browser, it excludes server-only code from the build - * - when new route files are created in development (watch) mode, it creates - * an empty shim for the module so Rollup doesn't complain and the build - * doesn't break - */ -export default function routeModulesPlugin({ - target -}: { - target: string; -}): Plugin { - return { - name: "routeModules", - - async options(options) { - let input = options.input; - - if (input && typeof input === "object" && !Array.isArray(input)) { - let config = await getRemixConfig(options.plugins); - let routeIds = Object.keys(config.routes); - - for (let alias in input) { - if (routeIds.includes(alias)) { - input[alias] = input[alias] + routeModuleProxy; - } - } - } - - return options; - }, - - async resolveId(id, importer) { - if (id.endsWith(routeModuleProxy) || id.endsWith(emptyRouteModule)) { - return id; - } - - if ( - importer && - importer.endsWith(routeModuleProxy) && - importer.slice(0, -routeModuleProxy.length) === id - ) { - let resolved = await this.resolve(id, importer, { skipSelf: true }); - - if (resolved) { - if (isEmptyFile(resolved.id)) { - resolved.id = resolved.id + emptyRouteModule; - } - - // Using syntheticNamedExports here prevents Rollup from complaining - // when the route source module may not have some of the properties - // we explicitly list in the proxy module. - resolved.syntheticNamedExports = true; - - return resolved; - } - } - - return null; - }, - - load(id) { - if (id.endsWith(emptyRouteModule)) { - let source = id.slice(0, -emptyRouteModule.length); - - this.addWatchFile(source); - - // In a new file, default to an empty component. This prevents errors in - // dev (watch) mode when creating new routes. - return `export default function () { throw new Error('Route "${source}" is empty, put a default export in there!') }`; - } - - if (id.endsWith(routeModuleProxy)) { - let source = id.slice(0, -routeModuleProxy.length); - - if (target === BuildTarget.Browser) { - // Create a proxy module that re-exports only the things we want to be - // available in the browser. All the rest will be tree-shaken out so - // we don't end up with server-only code (and its dependencies) in the - // browser bundles. - return `export { ErrorBoundary, default, handle, links, meta } from ${JSON.stringify( - source - )};`; - } - - // Create a proxy module that transparently re-exports everything from - // the original module. - return ( - `export { default } from ${JSON.stringify(source)};\n` + - `export * from ${JSON.stringify(source)};` - ); - } - - return null; - } - }; -} - -function isEmptyFile(file: string): boolean { - return fs.existsSync(file) && fs.statSync(file).size === 0; -} diff --git a/packages/remix-dev/compiler/rollup/serverManifest.ts b/packages/remix-dev/compiler/rollup/serverManifest.ts deleted file mode 100644 index 2b631d39ad..0000000000 --- a/packages/remix-dev/compiler/rollup/serverManifest.ts +++ /dev/null @@ -1,121 +0,0 @@ -import path from "path"; -import type { OutputBundle, Plugin } from "rollup"; - -import invariant from "../../invariant"; -import { getBundleHash } from "../crypto"; -import type { RemixConfig } from "./remixConfig"; -import { getRemixConfig } from "./remixConfig"; - -/** - * Generates a server module that loads all build artifacts. - */ -export default function serverManifestPlugin({ - fileName = "index.js" -}: { - fileName?: string; -} = {}): Plugin { - let config: RemixConfig; - - return { - name: "serverManifest", - - async buildStart({ plugins }) { - config = await getRemixConfig(plugins); - }, - - async generateBundle(_options, bundle) { - let manifest = getServerManifest(bundle, config.routes); - let source = getCommonjsModule(manifest); - this.emitFile({ type: "asset", fileName, source }); - } - }; -} - -interface ServerManifest { - version: string; - assets: { - moduleId: string; - }; - entry: { - moduleId: string; - }; - routes: { - [routeId: string]: { - id: string; - parentId?: string; - path: string; - caseSensitive?: boolean; - moduleId: string; - }; - }; -} - -function getServerManifest( - bundle: OutputBundle, - routeManifest: RemixConfig["routes"] -): ServerManifest { - let version = getBundleHash(bundle).slice(0, 8); - - let relModuleIdPrefix = "." + path.sep; - let assets = { - moduleId: relModuleIdPrefix + "assets.json" - }; - - let routeIds = Object.keys(routeManifest); - let entry: ServerManifest["entry"] | undefined; - let routes: ServerManifest["routes"] = Object.create(null); - - for (let key in bundle) { - let chunk = bundle[key]; - - if (chunk.type === "chunk") { - if (chunk.name === "entry.server") { - entry = { - moduleId: relModuleIdPrefix + chunk.fileName - }; - } else if (routeIds.includes(chunk.name)) { - let route = routeManifest[chunk.name]; - - routes[chunk.name] = { - id: route.id, - parentId: route.parentId, - path: route.path, - caseSensitive: route.caseSensitive, - moduleId: relModuleIdPrefix + chunk.fileName - }; - } - } - } - - invariant(entry, `Missing entry.server chunk`); - - return { version, assets, entry, routes }; -} - -function getCommonjsModule(manifest: ServerManifest): string { - return ( - `module.exports = { - "version": ${JSON.stringify(manifest.version)}, - "assets": require(${JSON.stringify(manifest.assets.moduleId)}), - "entry": { - "module": require(${JSON.stringify(manifest.entry.moduleId)}) - }, - "routes": { - ` + - Object.keys(manifest.routes) - .map(key => { - let route = manifest.routes[key]; - return `${JSON.stringify(route.id)}: { - "id": ${JSON.stringify(route.id)}, - "parentId": ${JSON.stringify(route.parentId)}, - "path": ${JSON.stringify(route.path)}, - "caseSensitive": ${JSON.stringify(route.caseSensitive)}, - "module": require(${JSON.stringify(route.moduleId)}) - }`; - }) - .join(",\n ") + - ` - } -}` - ); -} diff --git a/packages/remix-dev/compiler/rollup/url.ts b/packages/remix-dev/compiler/rollup/url.ts deleted file mode 100644 index cd6db197d0..0000000000 --- a/packages/remix-dev/compiler/rollup/url.ts +++ /dev/null @@ -1,52 +0,0 @@ -import path from "path"; -import { promises as fsp } from "fs"; -import type { Plugin } from "rollup"; - -import { BuildTarget } from "../../build"; -import createUrl from "../createUrl"; -import { getHash, addHash } from "../crypto"; -import type { RemixConfig } from "./remixConfig"; -import { getRemixConfig } from "./remixConfig"; - -export default function urlPlugin({ target }: { target: string }): Plugin { - let config: RemixConfig; - - return { - name: "url", - - async buildStart({ plugins }) { - config = await getRemixConfig(plugins); - }, - - async resolveId(id, importer) { - if (!id.startsWith("url:")) return null; - - let resolved = await this.resolve(id.slice(4), importer, { - skipSelf: true - }); - - return resolved && `\0url:${resolved.id}`; - }, - - async load(id) { - if (!id.startsWith("\0url:")) return; - - let file = id.slice(5); - let source = await fsp.readFile(file); - let fileName = addHash( - path.relative(config.appDirectory, file), - getHash(source).slice(0, 8) - ); - - this.addWatchFile(file); - - if (target === BuildTarget.Browser) { - this.emitFile({ type: "asset", fileName, source }); - } - - return `export default ${JSON.stringify( - createUrl(config.publicPath, fileName) - )}`; - } - }; -} diff --git a/packages/remix-dev/compiler/rollup/watchDirectory.ts b/packages/remix-dev/compiler/rollup/watchDirectory.ts deleted file mode 100644 index f691912ebe..0000000000 --- a/packages/remix-dev/compiler/rollup/watchDirectory.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { promises as fsp } from "fs"; -import type { Plugin } from "rollup"; -import chokidar from "chokidar"; -import tmp from "tmp"; - -/** - * Triggers a rebuild whenever anything in the given `dir` changes, including - * adding new files. - */ -export default function watchDirectoryPlugin({ dir }: { dir: string }): Plugin { - let tmpfile = tmp.fileSync(); - let startedWatcher = false; - - function startWatcher() { - return new Promise((accept, reject) => { - chokidar - .watch(dir, { - ignoreInitial: true, - ignored: /node_modules/, - followSymlinks: false - }) - .on("add", triggerRebuild) - .on("ready", accept) - .on("error", reject); - }); - } - - async function triggerRebuild() { - let now = new Date(); - await fsp.utimes(tmpfile.name, now, now); - } - - return { - name: "watchDirectory", - - async buildStart() { - // We have to use our own watcher because `this.addWatchFile` does not - // listen for the `add` event, and we want to know when new files show up - // in the `app` directory. - // See https://github.com/rollup/rollup/issues/3704 - if (!startedWatcher) { - await startWatcher(); - startedWatcher = true; - } - - this.addWatchFile(tmpfile.name); - } - }; -} diff --git a/packages/remix-dev/compiler2/routes.ts b/packages/remix-dev/compiler/routes.ts similarity index 100% rename from packages/remix-dev/compiler2/routes.ts rename to packages/remix-dev/compiler/routes.ts diff --git a/packages/remix-dev/compiler2/shims/react.ts b/packages/remix-dev/compiler/shims/react.ts similarity index 100% rename from packages/remix-dev/compiler2/shims/react.ts rename to packages/remix-dev/compiler/shims/react.ts diff --git a/packages/remix-dev/compiler2/utils/crypto.ts b/packages/remix-dev/compiler/utils/crypto.ts similarity index 100% rename from packages/remix-dev/compiler2/utils/crypto.ts rename to packages/remix-dev/compiler/utils/crypto.ts diff --git a/packages/remix-dev/compiler2/utils/fs.ts b/packages/remix-dev/compiler/utils/fs.ts similarity index 100% rename from packages/remix-dev/compiler2/utils/fs.ts rename to packages/remix-dev/compiler/utils/fs.ts diff --git a/packages/remix-dev/compiler2/utils/url.ts b/packages/remix-dev/compiler/utils/url.ts similarity index 100% rename from packages/remix-dev/compiler2/utils/url.ts rename to packages/remix-dev/compiler/utils/url.ts diff --git a/packages/remix-dev/compiler2.ts b/packages/remix-dev/compiler2.ts deleted file mode 100644 index 03efa1a87c..0000000000 --- a/packages/remix-dev/compiler2.ts +++ /dev/null @@ -1,518 +0,0 @@ -import { promises as fsp } from "fs"; -import * as path from "path"; -import { builtinModules as nodeBuiltins } from "module"; -import * as esbuild from "esbuild"; -import debounce from "lodash.debounce"; -import chokidar from "chokidar"; - -import { BuildMode, BuildTarget } from "./build"; -import type { RemixConfig } from "./config"; -import { readConfig } from "./config"; -import invariant from "./invariant"; -import { warnOnce } from "./warnings"; -import { createAssetsManifest } from "./compiler2/assets"; -import { getAppDependencies } from "./compiler2/dependencies"; -import { loaders, getLoaderForFile } from "./compiler2/loaders"; -import { getRouteModuleExportsCached } from "./compiler2/routes"; -import { writeFileSafe } from "./compiler2/utils/fs"; - -// When we build Remix, this shim file is copied directly into the output -// directory in the same place relative to this file. It is eventually injected -// as a source file when building the app. -const reactShim = path.resolve(__dirname, "compiler2/shims/react.ts"); - -interface BuildConfig { - mode: BuildMode; - target: BuildTarget; -} - -function defaultWarningHandler(message: string, key: string) { - warnOnce(false, message, key); -} - -function defaultErrorHandler(message: string) { - console.error(message); -} - -interface BuildOptions extends Partial { - onWarning?(message: string, key: string): void; - onError?(message: string): void; -} - -export async function build( - config: RemixConfig, - { - mode = BuildMode.Production, - target = BuildTarget.Node14, - onWarning = defaultWarningHandler, - onError = defaultErrorHandler - }: BuildOptions = {} -): Promise { - await buildEverything(config, { mode, target, onWarning, onError }); -} - -interface WatchOptions extends BuildOptions { - onRebuildStart?(): void; - onRebuildFinish?(): void; - onFileCreated?(file: string): void; - onFileChanged?(file: string): void; - onFileDeleted?(file: string): void; -} - -export async function watch( - config: RemixConfig, - { - mode = BuildMode.Development, - target = BuildTarget.Node14, - onWarning = defaultWarningHandler, - onError = defaultErrorHandler, - onRebuildStart, - onRebuildFinish, - onFileCreated, - onFileChanged, - onFileDeleted - }: WatchOptions = {} -): Promise<() => void> { - let options = { mode, target, onWarning, onError, incremental: true }; - let [browserBuild, serverBuild] = await buildEverything(config, options); - - async function disposeBuilders() { - await Promise.all([ - browserBuild.rebuild?.dispose(), - serverBuild.rebuild?.dispose() - ]); - } - - let restartBuilders = debounce(async (newConfig?: RemixConfig) => { - await disposeBuilders(); - config = newConfig || (await readConfig(config.rootDirectory)); - if (onRebuildStart) onRebuildStart(); - let builders = await buildEverything(config, options); - if (onRebuildFinish) onRebuildFinish(); - browserBuild = builders[0]; - serverBuild = builders[1]; - }, 500); - - let rebuildEverything = debounce(async () => { - if (onRebuildStart) onRebuildStart(); - await Promise.all([ - browserBuild.rebuild!().then(build => - generateManifests(config, build.metafile!) - ), - serverBuild.rebuild!() - ]); - if (onRebuildFinish) onRebuildFinish(); - }, 100); - - let watcher = chokidar - .watch(config.appDirectory, { - persistent: true, - ignoreInitial: true, - awaitWriteFinish: { - stabilityThreshold: 100, - pollInterval: 100 - } - }) - .on("error", error => console.error(error)) - .on("change", async file => { - if (onFileChanged) onFileChanged(file); - await rebuildEverything(); - }) - .on("add", async file => { - if (onFileCreated) onFileCreated(file); - let newConfig = await readConfig(config.rootDirectory); - if (isEntryPoint(newConfig, file)) { - await restartBuilders(newConfig); - } else { - await rebuildEverything(); - } - }) - .on("unlink", async file => { - if (onFileDeleted) onFileDeleted(file); - if (isEntryPoint(config, file)) { - await restartBuilders(); - } else { - await rebuildEverything(); - } - }); - - return async () => { - await watcher.close(); - await disposeBuilders(); - }; -} - -function isEntryPoint(config: RemixConfig, file: string) { - let appFile = path.relative(config.appDirectory, file); - - if ( - appFile === config.entryClientFile || - appFile === config.entryServerFile - ) { - return true; - } - for (let key in config.routes) { - if (appFile === config.routes[key].file) return true; - } - - return false; -} - -/////////////////////////////////////////////////////////////////////////////// - -async function buildEverything( - config: RemixConfig, - options: Required & { incremental?: boolean } -): Promise { - // TODO: - // When building for node, we build both the browser and server builds in - // parallel and emit the asset manifest as a separate file in the output - // directory. - // When building for Cloudflare Workers, we need to run the browser and server - // builds serially so we can inline the asset manifest into the server build - // in a single JavaScript file. - - let browserBuildPromise = createBrowserBuild(config, options); - let serverBuildPromise = createServerBuild(config, options); - - return Promise.all([ - browserBuildPromise.then(async build => { - await generateManifests(config, build.metafile!); - return build; - }), - serverBuildPromise - ]); -} - -async function createBrowserBuild( - config: RemixConfig, - options: BuildOptions & { incremental?: boolean } -): Promise { - // For the browser build, exclude node built-ins that don't have a - // browser-safe alternative installed in node_modules. Nothing should - // *actually* be external in the browser build (we want to bundle all deps) so - // this is really just making sure we don't accidentally have any dependencies - // on node built-ins in browser bundles. - let dependencies = Object.keys(await getAppDependencies(config)); - let externals = nodeBuiltins.filter(mod => !dependencies.includes(mod)); - - let entryPoints: esbuild.BuildOptions["entryPoints"] = { - "entry.client": path.resolve(config.appDirectory, config.entryClientFile) - }; - for (let id of Object.keys(config.routes)) { - // All route entry points are virtual modules that will be loaded by the - // browserEntryPointsPlugin. This allows us to tree-shake server-only code - // that we don't want to run in the browser (i.e. action & loader). - entryPoints[id] = - path.resolve(config.appDirectory, config.routes[id].file) + "?browser"; - } - - return esbuild.build({ - entryPoints, - outdir: config.assetsBuildDirectory, - platform: "browser", - format: "esm", - external: externals, - inject: [reactShim], - loader: loaders, - bundle: true, - splitting: true, - metafile: true, - incremental: options.incremental, - minify: options.mode === BuildMode.Production, - entryNames: "[dir]/[name]-[hash]", - chunkNames: "_shared/[name]-[hash]", - assetNames: "_assets/[name]-[hash]", - publicPath: config.publicPath, - define: { - "process.env.NODE_ENV": JSON.stringify(options.mode) - }, - plugins: [ - browserRouteModulesPlugin(config, /\?browser$/), - emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/) - ] - }); -} - -async function createServerBuild( - config: RemixConfig, - options: Required & { incremental?: boolean } -): Promise { - let dependencies = Object.keys(await getAppDependencies(config)); - - return esbuild.build({ - stdin: { - contents: getServerEntryPointModule(config, options), - resolveDir: config.serverBuildDirectory - }, - outfile: path.resolve(config.serverBuildDirectory, "index.js"), - platform: "node", - format: "cjs", - target: options.target, - inject: [reactShim], - loader: loaders, - bundle: true, - incremental: options.incremental, - // The server build needs to know how to generate asset URLs for imports - // of CSS and other files. - assetNames: "_assets/[name]-[hash]", - publicPath: config.publicPath, - plugins: [ - serverRouteModulesPlugin(config), - emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), - manualExternalsPlugin((id, importer) => { - // assets.json is external because this build runs in parallel with the - // browser build and it's not there yet. - if (id === "./assets.json" && importer === "") return true; - - // Mark all bare imports as external. They will be require()'d at - // runtime from node_modules. - if (isBareModuleId(id)) { - let packageName = getNpmPackageName(id); - if ( - !/\bnode_modules\b/.test(importer) && - !nodeBuiltins.includes(packageName) && - !dependencies.includes(packageName) - ) { - options.onWarning( - `The path "${id}" is imported in ` + - `${path.relative(process.cwd(), importer)} but ` + - `${packageName} is not listed in your package.json dependencies. ` + - `Did you forget to install it?`, - packageName - ); - } - return true; - } - - return false; - }) - ] - }); -} - -function isBareModuleId(id: string): boolean { - return !id.startsWith(".") && !path.isAbsolute(id); -} - -function getNpmPackageName(id: string): string { - let split = id.split("/"); - let packageName = split[0]; - if (packageName.startsWith("@")) packageName += `/${split[1]}`; - return packageName; -} - -async function generateManifests( - config: RemixConfig, - metafile: esbuild.Metafile -): Promise { - let assetsManifest = await createAssetsManifest(config, metafile); - - let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; - assetsManifest.url = config.publicPath + filename; - - return Promise.all([ - writeFileSafe( - path.join(config.assetsBuildDirectory, filename), - `window.__remixManifest=${JSON.stringify(assetsManifest)}` - ), - writeFileSafe( - path.join(config.serverBuildDirectory, "assets.json"), - JSON.stringify(assetsManifest, null, 2) - ) - ]); -} - -function getServerEntryPointModule( - config: RemixConfig, - options: BuildOptions -): string { - switch (options.target) { - case BuildTarget.Node14: - return ` -import * as entryServer from ${JSON.stringify( - path.resolve(config.appDirectory, config.entryServerFile) - )}; -${Object.keys(config.routes) - .map((key, index) => { - let route = config.routes[key]; - return `import * as route${index} from ${JSON.stringify( - path.resolve(config.appDirectory, route.file) - )};`; - }) - .join("\n")} -export { default as assets } from "./assets.json"; -export const entry = { module: entryServer }; -export const routes = { - ${Object.keys(config.routes) - .map((key, index) => { - let route = config.routes[key]; - return `${JSON.stringify(key)}: { - id: ${JSON.stringify(route.id)}, - parentId: ${JSON.stringify(route.parentId)}, - path: ${JSON.stringify(route.path)}, - caseSensitive: ${JSON.stringify(route.caseSensitive)}, - module: route${index} - }`; - }) - .join(",\n ")} -};`; - default: - throw new Error( - `Cannot generate server entry point module for target: ${options.target}` - ); - } -} - -type Route = RemixConfig["routes"][string]; - -const browserSafeRouteExports: { [name: string]: boolean } = { - ErrorBoundary: true, - default: true, - handle: true, - links: true, - meta: true -}; - -/** - * This plugin loads route modules for the browser build, using module shims - * that re-export only the route module exports that are safe for the browser. - */ -function browserRouteModulesPlugin( - config: RemixConfig, - suffixMatcher: RegExp -): esbuild.Plugin { - return { - name: "browser-route-modules", - async setup(build) { - let routesByFile: Map = Object.keys(config.routes).reduce( - (map, key) => { - let route = config.routes[key]; - map.set(path.resolve(config.appDirectory, route.file), route); - return map; - }, - new Map() - ); - - build.onResolve({ filter: suffixMatcher }, args => { - return { path: args.path, namespace: "browser-route-module" }; - }); - - build.onLoad( - { filter: suffixMatcher, namespace: "browser-route-module" }, - async args => { - let file = args.path.replace(suffixMatcher, ""); - let route = routesByFile.get(file); - invariant(route, `Cannot get route by path: ${args.path}`); - - let exports = ( - await getRouteModuleExportsCached(config, route.id) - ).filter(ex => !!browserSafeRouteExports[ex]); - let spec = exports.length > 0 ? `{ ${exports.join(", ")} }` : "*"; - let contents = `export ${spec} from ${JSON.stringify(file)};`; - - return { - contents, - resolveDir: path.dirname(file), - loader: "js" - }; - } - ); - } - }; -} - -/** - * This plugin substitutes an empty module for any modules in the `app` - * directory that match the given `filter`. - */ -function emptyModulesPlugin( - config: RemixConfig, - filter: RegExp -): esbuild.Plugin { - return { - name: "empty-modules", - setup(build) { - build.onResolve({ filter }, args => { - let resolved = path.resolve(args.resolveDir, args.path); - if ( - // Limit this behavior to modules found in only the `app` directory. - // This allows node_modules to use the `.server.js` and `.client.js` - // naming conventions with different semantics. - resolved.startsWith(config.appDirectory) - ) { - return { path: args.path, namespace: "empty-module" }; - } - }); - - build.onLoad({ filter: /.*/, namespace: "empty-module" }, () => { - return { - // Use an empty CommonJS module here instead of ESM to avoid "No - // matching export" errors in esbuild for stuff that is imported - // from this file. - contents: "module.exports = {};", - loader: "js" - }; - }); - } - }; -} - -/** - * This plugin loads route modules for the server build. - */ -function serverRouteModulesPlugin(config: RemixConfig): esbuild.Plugin { - return { - name: "server-route-modules", - setup(build) { - let routeFiles = new Set( - Object.keys(config.routes).map(key => - path.resolve(config.appDirectory, config.routes[key].file) - ) - ); - - build.onResolve({ filter: /.*/ }, args => { - if (routeFiles.has(args.path)) { - return { path: args.path, namespace: "route-module" }; - } - }); - - build.onLoad({ filter: /.*/, namespace: "route-module" }, async args => { - let file = args.path; - let contents = await fsp.readFile(file, "utf-8"); - - // Default to `export {}` if the file is empty so esbuild interprets - // this file as ESM instead of CommonJS with `default: {}`. This helps - // in development when creating new files. - // See https://github.com/evanw/esbuild/issues/1043 - if (!/\S/.test(contents)) { - return { contents: "export {}", loader: "js" }; - } - - return { - contents, - resolveDir: path.dirname(file), - loader: getLoaderForFile(file) - }; - }); - } - }; -} - -/** - * This plugin marks paths external using a callback function. - */ -function manualExternalsPlugin( - isExternal: (id: string, importer: string) => boolean -): esbuild.Plugin { - return { - name: "manual-externals", - setup(build) { - build.onResolve({ filter: /.*/ }, args => { - if (isExternal(args.path, args.importer)) { - return { path: args.path, external: true }; - } - }); - } - }; -} diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 29d86dc858..d155d404ee 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -1,6 +1,5 @@ import * as fs from "fs"; import * as path from "path"; -import type { MdxOptions } from "@mdx-js/mdx"; import { loadModule } from "./modules"; import type { RouteManifest, DefineRoutesFunction } from "./config/routes"; @@ -62,11 +61,6 @@ export interface AppConfig { * The port number to use for the dev server. Defaults to 8002. */ devServerPort?: number; - - /** - * Options to use when compiling MDX. - */ - mdx?: MdxOptions; } /** @@ -127,11 +121,6 @@ export interface RemixConfig { * The port number to use for the dev (asset) server. */ devServerPort: number; - - /** - * Options to use when compiling MDX. - */ - mdx?: MdxOptions; } /** @@ -227,7 +216,6 @@ export async function readConfig( entryClientFile, entryServerFile, devServerPort, - mdx: appConfig.mdx, assetsBuildDirectory, publicPath, rootDirectory, diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index e4a1434feb..692cc6a60e 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -7,44 +7,20 @@ "remix": "cli.js" }, "dependencies": { - "@babel/core": "^7.13.10", - "@babel/preset-env": "^7.13.10", - "@babel/preset-react": "^7.12.13", - "@babel/preset-typescript": "^7.13.0", - "@mdx-js/mdx": "^1.6.22", - "@mdx-js/react": "^1.6.22", - "@rollup/plugin-babel": "^5.3.0", - "@rollup/plugin-commonjs": "^17.1.0", - "@rollup/plugin-json": "^4.1.0", - "@rollup/plugin-node-resolve": "^11.2.0", - "@rollup/plugin-replace": "^2.4.1", "cacache": "^15.0.5", "chokidar": "^3.5.1", "esbuild": "0.11.4", - "express": "^4.17.1", - "front-matter": "^4.0.2", "lodash.debounce": "^4.0.8", "meow": "^7.1.1", - "morgan": "^1.10.0", - "postcss": "^8.2.6", - "pretty-bytes": "^5.5.0", "pretty-ms": "^7.0.1", "read-package-json-fast": "^2.0.2", - "rollup": "^2.39.0", - "rollup-plugin-terser": "^7.0.2", - "sharp": "^0.27.1", "signal-exit": "^3.0.3", - "tmp": "^0.2.1", "ws": "^7.4.5" }, "devDependencies": { "@types/cacache": "^15.0.0", - "@types/express": "^4.17.11", "@types/lodash.debounce": "^4.0.6", - "@types/morgan": "^1.9.2", - "@types/sharp": "^0.27.1", "@types/signal-exit": "^3.0.0", - "@types/tmp": "^0.2.0", "@types/ws": "^7.4.1", "semver": "^7.3.4" } diff --git a/packages/remix-dev/server.ts b/packages/remix-dev/server.ts deleted file mode 100644 index 8955e89eaa..0000000000 --- a/packages/remix-dev/server.ts +++ /dev/null @@ -1,88 +0,0 @@ -import http from "http"; -import path from "path"; -import type { Request, Response } from "express"; -import express from "express"; -import morgan from "morgan"; -import signalExit from "signal-exit"; - -import { BuildMode, BuildTarget } from "./build"; -import * as compiler from "./compiler"; -import type { RemixConfig } from "./config"; - -export function startDevServer( - config: RemixConfig, - { - onListen - }: { - onListen?: () => void; - } = {} -) { - let requestHandler = createRequestHandler(config); - let server = http.createServer(requestHandler); - - server.listen(config.devServerPort, onListen); - - signalExit(() => { - server.close(); - }); -} - -function createRequestHandler(config: RemixConfig) { - let serverBuildStart = 0; - let assetsBuildStart = 0; - - signalExit( - compiler.watch(config, { - mode: BuildMode.Development, - target: BuildTarget.Server, - onBuildStart() { - console.log("Building Remix..."); - serverBuildStart = Date.now(); - }, - async onBuildEnd(build) { - await compiler.write(build, config.serverBuildDirectory); - - let dir = path.relative(process.cwd(), config.serverBuildDirectory); - let time = Date.now() - serverBuildStart; - console.log(`Wrote server build to ./${dir} in ${time}ms`); - }, - onError(error) { - console.error(error); - } - }) - ); - - signalExit( - compiler.watch(config, { - mode: BuildMode.Development, - target: BuildTarget.Browser, - onBuildStart() { - assetsBuildStart = Date.now(); - }, - async onBuildEnd(build) { - await compiler.write(build, config.assetsBuildDirectory); - - let dir = path.relative(process.cwd(), config.assetsBuildDirectory); - let time = Date.now() - assetsBuildStart; - console.log(`Wrote assets build to ./${dir} in ${time}ms`); - }, - onError(error) { - console.error(error); - } - }) - ); - - function handleRequest(_req: Request, res: Response) { - res.status(200).send(); - } - - let app = express(); - - app.disable("x-powered-by"); - - app.use(morgan("dev")); - - app.get("*", handleRequest); - - return app; -} From 3679f3b1a77fb9d32b02ece97bda524a7f0cdd80 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 28 Apr 2021 22:04:34 -0700 Subject: [PATCH 0015/1690] Use esbuild 0.11.16 --- .../remix-dev/__tests__/readConfig-test.ts | 59 +++++++------------ packages/remix-dev/compiler.ts | 2 +- packages/remix-dev/package.json | 2 +- 3 files changed, 24 insertions(+), 39 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 915050c89a..51258af772 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -26,76 +26,75 @@ describe("readConfig", () => { "assetsBuildDirectory": Any, "cacheDirectory": Any, "devServerPort": 8002, - "entryClientFile": "entry.client.js", - "entryServerFile": "entry.server.js", - "mdx": undefined, + "entryClientFile": "entry.client.jsx", + "entryServerFile": "entry.server.jsx", "publicPath": "/build/", "rootDirectory": Any, "routes": Object { - "pages/one": Object { + "pages/four": Object { "caseSensitive": false, - "file": "pages/one.mdx", - "id": "pages/one", + "file": "pages/four.jsx", + "id": "pages/four", "parentId": "root", - "path": "/page/one", + "path": "/page/four", }, - "pages/two": Object { + "pages/three": Object { "caseSensitive": false, - "file": "pages/two.mdx", - "id": "pages/two", + "file": "pages/three.jsx", + "id": "pages/three", "parentId": "root", - "path": "/page/two", + "path": "/page/three", }, "root": Object { - "file": "root.js", + "file": "root.jsx", "id": "root", "path": "/", }, "routes/404": Object { "caseSensitive": false, - "file": "routes/404.js", + "file": "routes/404.jsx", "id": "routes/404", "parentId": "root", "path": "*", }, "routes/empty": Object { "caseSensitive": false, - "file": "routes/empty.js", + "file": "routes/empty.jsx", "id": "routes/empty", "parentId": "root", "path": "empty", }, "routes/gists": Object { "caseSensitive": false, - "file": "routes/gists.js", + "file": "routes/gists.jsx", "id": "routes/gists", "parentId": "root", "path": "gists", }, "routes/gists.mine": Object { "caseSensitive": false, - "file": "routes/gists.mine.js", + "file": "routes/gists.mine.jsx", "id": "routes/gists.mine", "parentId": "root", "path": "gists/mine", }, "routes/gists/$username": Object { "caseSensitive": false, - "file": "routes/gists/$username.js", + "file": "routes/gists/$username.jsx", "id": "routes/gists/$username", "parentId": "routes/gists", "path": ":username", }, "routes/gists/index": Object { "caseSensitive": false, - "file": "routes/gists/index.js", + "file": "routes/gists/index.jsx", "id": "routes/gists/index", "parentId": "routes/gists", "path": "/", }, "routes/index": Object { "caseSensitive": false, - "file": "routes/index.js", + "file": "routes/index.jsx", "id": "routes/index", "parentId": "root", "path": "/", @@ -109,14 +108,14 @@ describe("readConfig", () => { }, "routes/loader-errors": Object { "caseSensitive": false, - "file": "routes/loader-errors.js", + "file": "routes/loader-errors.jsx", "id": "routes/loader-errors", "parentId": "root", "path": "loader-errors", }, "routes/loader-errors/nested": Object { "caseSensitive": false, - "file": "routes/loader-errors/nested.js", + "file": "routes/loader-errors/nested.jsx", "id": "routes/loader-errors/nested", "parentId": "routes/loader-errors", "path": "nested", @@ -128,20 +127,6 @@ describe("readConfig", () => { "parentId": "root", "path": "methods", }, - "routes/page/four": Object { - "caseSensitive": false, - "file": "routes/page/four.mdx", - "id": "routes/page/four", - "parentId": "root", - "path": "page/four", - }, - "routes/page/three": Object { - "caseSensitive": false, - "file": "routes/page/three.md", - "id": "routes/page/three", - "parentId": "root", - "path": "page/three", - }, "routes/prefs": Object { "caseSensitive": false, "file": "routes/prefs.tsx", @@ -151,14 +136,14 @@ describe("readConfig", () => { }, "routes/render-errors": Object { "caseSensitive": false, - "file": "routes/render-errors.js", + "file": "routes/render-errors.jsx", "id": "routes/render-errors", "parentId": "root", "path": "render-errors", }, "routes/render-errors/nested": Object { "caseSensitive": false, - "file": "routes/render-errors/nested.js", + "file": "routes/render-errors/nested.jsx", "id": "routes/render-errors/nested", "parentId": "routes/render-errors", "path": "nested", diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 0ec7ada831..45a5505354 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -314,7 +314,7 @@ async function generateManifests( return Promise.all([ writeFileSafe( path.join(config.assetsBuildDirectory, filename), - `window.__remixManifest=${JSON.stringify(assetsManifest)}` + `window.__remixManifest=${JSON.stringify(assetsManifest)};` ), writeFileSafe( path.join(config.serverBuildDirectory, "assets.json"), diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 692cc6a60e..430caf41f6 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -9,7 +9,7 @@ "dependencies": { "cacache": "^15.0.5", "chokidar": "^3.5.1", - "esbuild": "0.11.4", + "esbuild": "0.11.16", "lodash.debounce": "^4.0.8", "meow": "^7.1.1", "pretty-ms": "^7.0.1", From e961c43f7b1246f17e88fd47e65bcea675b17112 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 28 Apr 2021 23:39:02 -0700 Subject: [PATCH 0016/1690] Version 0.0.0-experimental-2d01141 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 430caf41f6..e172ee4339 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.16.5", + "version": "0.0.0-experimental-2d01141", "repository": "https://github.com/remix-run/remix", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 11bc1f712d..af1eb3fbeb 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.16.5", + "version": "0.0.0-experimental-2d01141", "repository": "https://github.com/remix-run/remix", "dependencies": { - "@remix-run/node": "0.16.5" + "@remix-run/node": "0.0.0-experimental-2d01141" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index d5a76bc448..f3e433815d 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.16.5", + "version": "0.0.0-experimental-2d01141", "repository": "https://github.com/remix-run/remix", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 068b89989b..475231d59f 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.16.5", + "version": "0.0.0-experimental-2d01141", "repository": "https://github.com/remix-run/remix", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.16.5", + "@remix-run/express": "0.0.0-experimental-2d01141", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From d814c453e123e21deba1da99d089e38a126a1190 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 29 Apr 2021 09:59:39 -0700 Subject: [PATCH 0017/1690] Set NODE_ENV in `remix run` --- packages/remix-dev/cli.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index e91878d114..302cb346c4 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -44,9 +44,11 @@ switch (cli.input[0]) { commands.watch(cli.input[1], process.env.NODE_ENV); break; case "run": + if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; commands.run(cli.input[1], process.env.NODE_ENV); break; default: - // `remix my-project` is shorthand for `remix run my-project` - commands.run(cli.input[0]); + // `remix ./my-project` is shorthand for `remix run ./my-project` + if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; + commands.run(cli.input[0], process.env.NODE_ENV); } From 548490605d4981da54c0f26b6b9c023398c5543c Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 29 Apr 2021 11:33:33 -0700 Subject: [PATCH 0018/1690] Version 0.17.0 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index e172ee4339..ed3ac825da 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.0.0-experimental-2d01141", + "version": "0.17.0", "repository": "https://github.com/remix-run/remix", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index af1eb3fbeb..92a036ca6b 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.0.0-experimental-2d01141", + "version": "0.17.0", "repository": "https://github.com/remix-run/remix", "dependencies": { - "@remix-run/node": "0.0.0-experimental-2d01141" + "@remix-run/node": "0.17.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index f3e433815d..997840baf9 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.0.0-experimental-2d01141", + "version": "0.17.0", "repository": "https://github.com/remix-run/remix", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 475231d59f..3662426381 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.0.0-experimental-2d01141", + "version": "0.17.0", "repository": "https://github.com/remix-run/remix", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.0.0-experimental-2d01141", + "@remix-run/express": "0.17.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From fe7a0c83c91c31afc7437a3856a3a4f0f2418472 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 1 May 2021 09:51:41 -0700 Subject: [PATCH 0019/1690] Update packages repo name --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 2 +- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index ed3ac825da..1934147923 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -2,7 +2,7 @@ "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", "version": "0.17.0", - "repository": "https://github.com/remix-run/remix", + "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" }, diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 92a036ca6b..791d5d0039 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -2,7 +2,7 @@ "name": "@remix-run/express", "description": "Express server request handler for Remix", "version": "0.17.0", - "repository": "https://github.com/remix-run/remix", + "repository": "https://github.com/remix-run/packages", "dependencies": { "@remix-run/node": "0.17.0" }, diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 997840baf9..867d6df41b 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -2,7 +2,7 @@ "name": "@remix-run/node", "description": "Node.js bindings for Remix", "version": "0.17.0", - "repository": "https://github.com/remix-run/remix", + "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", "@types/node-fetch": "^2.5.7", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 3662426381..12cd7cf10e 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -2,7 +2,7 @@ "name": "@remix-run/serve", "description": "Production application server for Remix", "version": "0.17.0", - "repository": "https://github.com/remix-run/remix", + "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, From d504dc1cca6eb777dcbb7e47cc42e1972427621f Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 3 May 2021 16:47:42 -0700 Subject: [PATCH 0020/1690] Speed up conventional routes code --- packages/remix-dev/config/routesConvention.ts | 50 +++++++------------ 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 3ac7c392ed..4fb9d92748 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -4,10 +4,7 @@ import * as path from "path"; import type { RouteManifest, DefineRouteFunction } from "./routes"; import { defineRoutes, createRouteId } from "./routes"; -/** - * All file extensions we support for route modules. - */ -export const routeModuleExts = [".js", ".jsx", ".md", ".mdx", ".ts", ".tsx"]; +const routeModuleExts = [".js", ".jsx", ".ts", ".tsx"]; export function isRouteModuleFile(filename: string): boolean { return routeModuleExts.includes(path.extname(filename)); @@ -25,15 +22,28 @@ export function isRouteModuleFile(filename: string): boolean { * with a path of `gists/:username`. */ export function defineConventionalRoutes(appDir: string): RouteManifest { - let files: { - [routeId: string]: string; - } = {}; + let files: { [routeId: string]: string } = {}; + + // First, find all route modules in app/routes + visitFiles(path.join(appDir, "routes"), file => { + let routeId = createRouteId(path.join("routes", file)); + + if (isRouteModuleFile(file)) { + files[routeId] = path.join("routes", file); + } else { + throw new Error( + `Invalid route module file: ${path.join(appDir, "routes", file)}` + ); + } + }); + let routeIds = Object.keys(files).sort(byLongestFirst); + + // Then, recurse through all routes using the public defineRoutes() API function defineNestedRoutes( defineRoute: DefineRouteFunction, parentId?: string ): void { - let routeIds = Object.keys(files); let childRouteIds = routeIds.filter( id => findParentRouteId(routeIds, id) === parentId ); @@ -50,19 +60,6 @@ export function defineConventionalRoutes(appDir: string): RouteManifest { } } - // First, find all route modules in app/routes - visitFiles(path.join(appDir, "routes"), file => { - let routeId = createRouteId(path.join("routes", file)); - - if (isRouteModuleFile(file)) { - files[routeId] = path.join("routes", file); - } else { - throw new Error( - `Invalid route module file: ${path.join(appDir, "routes", file)}` - ); - } - }); - return defineRoutes(defineNestedRoutes); } @@ -75,16 +72,7 @@ function findParentRouteId( routeIds: string[], childRouteId: string ): string | undefined { - return ( - routeIds - .slice(0) - .sort(byLongestFirst) - // FIXME: this will probably break with two routes like foo/ and foo-bar/, - // we use `startsWith` with we also need to factor in the segment `/` - // boundaries. There are bugs in React Router NavLink with this too. - // Probably need to ditch all uses of `startsWith` in route matching. - .find(id => childRouteId.startsWith(`${id}/`)) - ); + return routeIds.find(id => childRouteId.startsWith(`${id}/`)); } function byLongestFirst(a: string, b: string): number { From 4ec04fb76c2902485b01d740214508a39d397257 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 3 May 2021 17:11:44 -0700 Subject: [PATCH 0021/1690] Remove unneeded module --- packages/remix-dev/config.ts | 3 +-- packages/remix-dev/modules.ts | 29 ----------------------------- 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 packages/remix-dev/modules.ts diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index d155d404ee..e2e0b8e7ae 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -1,7 +1,6 @@ import * as fs from "fs"; import * as path from "path"; -import { loadModule } from "./modules"; import type { RouteManifest, DefineRoutesFunction } from "./config/routes"; import { defineRoutes } from "./config/routes"; import { defineConventionalRoutes } from "./config/routesConvention"; @@ -144,7 +143,7 @@ export async function readConfig( let appConfig: AppConfig; try { - appConfig = loadModule(configFile); + appConfig = require(configFile); } catch (error) { console.error(`Error loading Remix config in ${configFile}`); console.error(error); diff --git a/packages/remix-dev/modules.ts b/packages/remix-dev/modules.ts deleted file mode 100644 index cf1b6c3371..0000000000 --- a/packages/remix-dev/modules.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Loads a CommonJS module from the filesystem using node's `require` function. - */ -export function loadModule(file: string): any { - return require(file); -} - -/** - * Purges all entries that begin with the given prefix from node's internal - * `require` cache. - * - * This is useful when running the Remix build in watch mode because we - * currently load remix.config.js using CommonJS require, which means that it - * can require() other files it might need. So we just purge them all to make - * sure we pick up the latest changes. - */ -export function purgeModuleCache( - prefix: string, - includeNodeModules = false -): void { - for (let key of Object.keys(require.cache)) { - if ( - key.startsWith(prefix) && - (includeNodeModules || !/\bnode_modules\b/.test(key)) - ) { - delete require.cache[key]; - } - } -} From c2f8d1dfb7a4cb4a99f356f06781622c48075f3f Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 3 May 2021 17:55:31 -0700 Subject: [PATCH 0022/1690] Auto-add exports to `remix` package in postinstall --- packages/remix-node/magicExports/server.ts | 55 ++++++++++++++++++++++ packages/remix-node/package.json | 3 ++ packages/remix-node/scripts/postinstall.ts | 25 ++++++++++ 3 files changed, 83 insertions(+) create mode 100644 packages/remix-node/magicExports/server.ts create mode 100644 packages/remix-node/scripts/postinstall.ts diff --git a/packages/remix-node/magicExports/server.ts b/packages/remix-node/magicExports/server.ts new file mode 100644 index 0000000000..0304990134 --- /dev/null +++ b/packages/remix-node/magicExports/server.ts @@ -0,0 +1,55 @@ +// This file lists all exports from this package that are available to `import +// "remix"`. + +export type { + ServerBuild, + ServerEntryModule, + CookieParseOptions, + CookieSerializeOptions, + CookieSignatureOptions, + CookieOptions, + Cookie, + AppLoadContext, + AppData, + EntryContext, + HeadersInit, + RequestInfo, + RequestInit, + ResponseInit, + LinkDescriptor, + HTMLLinkDescriptor, + BlockLinkDescriptor, + PageLinkDescriptor, + ActionFunction, + ErrorBoundaryComponent, + HeadersFunction, + LinksFunction, + LoaderFunction, + MetaFunction, + RouteComponent, + RouteHandle, + RequestHandler, + SessionData, + Session, + SessionStorage, + SessionIdStorageStrategy +} from "@remix-run/node"; + +export { + createCookie, + isCookie, + Headers, + Request, + Response, + fetch, + // installGlobals, // only needed by adapters + json, + redirect, + // createRequestHandler, // only needed by adapters + createSession, + isSession, + createSessionStorage, + createCookieSessionStorage, + createFileSessionStorage, + createMemorySessionStorage +} from "@remix-run/node"; diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 867d6df41b..795d89f6d0 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -21,5 +21,8 @@ "@types/cookie-signature": "^1.0.3", "@types/jsesc": "^2.5.1" }, + "scripts": { + "postinstall": "node ./scripts/postinstall.js" + }, "sideEffects": false } diff --git a/packages/remix-node/scripts/postinstall.ts b/packages/remix-node/scripts/postinstall.ts new file mode 100644 index 0000000000..6cdcbc68b2 --- /dev/null +++ b/packages/remix-node/scripts/postinstall.ts @@ -0,0 +1,25 @@ +import * as path from "path"; + +async function run() { + try { + await require("remix/setup").installMagicExports( + path.resolve(__dirname, "..", "magicExports") + ); + } catch (error) { + if (error.code === "MODULE_NOT_FOUND") { + // ignore missing "remix" package + } else { + throw error; + } + } +} + +run().then( + () => { + process.exit(0); + }, + error => { + console.error(error); + process.exit(1); + } +); From 0ae2b841b83f5e8d474efb244d25a9b1a1d235f7 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 4 May 2021 18:45:40 -0700 Subject: [PATCH 0023/1690] Add postinstall step during build --- packages/remix-node/package.json | 3 --- packages/remix-node/scripts/postinstall.ts | 2 +- packages/remix-node/tsconfig.json | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 795d89f6d0..867d6df41b 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -21,8 +21,5 @@ "@types/cookie-signature": "^1.0.3", "@types/jsesc": "^2.5.1" }, - "scripts": { - "postinstall": "node ./scripts/postinstall.js" - }, "sideEffects": false } diff --git a/packages/remix-node/scripts/postinstall.ts b/packages/remix-node/scripts/postinstall.ts index 6cdcbc68b2..5342365cfe 100644 --- a/packages/remix-node/scripts/postinstall.ts +++ b/packages/remix-node/scripts/postinstall.ts @@ -2,7 +2,7 @@ import * as path from "path"; async function run() { try { - await require("remix/setup").installMagicExports( + await require("remix/magic").installMagicExports( path.resolve(__dirname, "..", "magicExports") ); } catch (error) { diff --git a/packages/remix-node/tsconfig.json b/packages/remix-node/tsconfig.json index a7a4ed1724..a5c064d8e5 100644 --- a/packages/remix-node/tsconfig.json +++ b/packages/remix-node/tsconfig.json @@ -1,5 +1,5 @@ { - "exclude": ["__tests__/**/*"], + "exclude": ["__tests__/**/*", "scripts/**/*"], "compilerOptions": { "lib": ["ES2019"], "target": "ES2019", From 0553fa610cd8ea5a3f1a44da1209696b694d221b Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Sat, 8 May 2021 08:54:20 -0700 Subject: [PATCH 0024/1690] Version 0.17.1-pre.0 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 1934147923..d4c06bc6e7 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.0", + "version": "0.17.1-pre.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 791d5d0039..7dfb4d1f7f 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.0", + "version": "0.17.1-pre.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.0" + "@remix-run/node": "0.17.1-pre.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 867d6df41b..8f6a8e62ff 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.0", + "version": "0.17.1-pre.0", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 12cd7cf10e..ebbb6b382c 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.0", + "version": "0.17.1-pre.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.0", + "@remix-run/express": "0.17.1-pre.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From c3edeb0d98b5c3df082a25f35d39c667b7c5d383 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 10 May 2021 15:07:23 -0700 Subject: [PATCH 0025/1690] Version 0.17.1-pre.1 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index d4c06bc6e7..17157f9893 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.1-pre.0", + "version": "0.17.1-pre.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 7dfb4d1f7f..614c2ae576 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.1-pre.0", + "version": "0.17.1-pre.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.1-pre.0" + "@remix-run/node": "0.17.1-pre.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 8f6a8e62ff..0fbf78aab6 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.1-pre.0", + "version": "0.17.1-pre.1", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index ebbb6b382c..d4af092641 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.1-pre.0", + "version": "0.17.1-pre.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.1-pre.0", + "@remix-run/express": "0.17.1-pre.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From 8297b7fc098602eb9a9f42abde83c20841cb44ec Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 10 May 2021 15:11:26 -0700 Subject: [PATCH 0026/1690] Version 0.17.1-pre.2 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 17157f9893..c6d0d422f3 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.1-pre.1", + "version": "0.17.1-pre.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 614c2ae576..a1c8b7b5b7 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.1-pre.1", + "version": "0.17.1-pre.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.1-pre.1" + "@remix-run/node": "0.17.1-pre.2" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 0fbf78aab6..264d987130 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.1-pre.1", + "version": "0.17.1-pre.2", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index d4af092641..5526d2c468 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.1-pre.1", + "version": "0.17.1-pre.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.1-pre.1", + "@remix-run/express": "0.17.1-pre.2", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From 7fc041425f696bcd06b0108d3aea15b0e43f88e4 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 10 May 2021 15:19:34 -0700 Subject: [PATCH 0027/1690] Version 0.17.1-pre.3 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index c6d0d422f3..0b027f5aba 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.1-pre.2", + "version": "0.17.1-pre.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index a1c8b7b5b7..ff7d3efc40 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.1-pre.2", + "version": "0.17.1-pre.3", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.1-pre.2" + "@remix-run/node": "0.17.1-pre.3" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 264d987130..41fc57f9c7 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.1-pre.2", + "version": "0.17.1-pre.3", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 5526d2c468..2655e614ca 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.1-pre.2", + "version": "0.17.1-pre.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.1-pre.2", + "@remix-run/express": "0.17.1-pre.3", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From 63f3c5b01c5c0559e112310aa0114bf2cf2c3717 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 10 May 2021 19:08:05 -0700 Subject: [PATCH 0028/1690] Add deps to remix package.json in postinstall hook --- packages/remix-node/scripts/postinstall.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/remix-node/scripts/postinstall.ts b/packages/remix-node/scripts/postinstall.ts index 5342365cfe..09d05b114c 100644 --- a/packages/remix-node/scripts/postinstall.ts +++ b/packages/remix-node/scripts/postinstall.ts @@ -1,9 +1,12 @@ import * as path from "path"; async function run() { + let packageJson = require("../package.json"); + try { await require("remix/magic").installMagicExports( - path.resolve(__dirname, "..", "magicExports") + path.resolve(__dirname, "..", "magicExports"), + { [packageJson.name]: packageJson.version } ); } catch (error) { if (error.code === "MODULE_NOT_FOUND") { From 83415a44d829a9e04352da3d45b310aac46bbf7a Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 10 May 2021 19:27:07 -0700 Subject: [PATCH 0029/1690] Add dependencies to remix package in postinstall hook --- packages/remix-node/scripts/postinstall.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix-node/scripts/postinstall.ts b/packages/remix-node/scripts/postinstall.ts index 09d05b114c..2d8fb3e493 100644 --- a/packages/remix-node/scripts/postinstall.ts +++ b/packages/remix-node/scripts/postinstall.ts @@ -5,8 +5,8 @@ async function run() { try { await require("remix/magic").installMagicExports( - path.resolve(__dirname, "..", "magicExports"), - { [packageJson.name]: packageJson.version } + { [packageJson.name]: packageJson.version }, + path.resolve(__dirname, "..", "magicExports") ); } catch (error) { if (error.code === "MODULE_NOT_FOUND") { From 54158d5dbe4a83c65d682e079840f6a16e43d940 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 10 May 2021 20:27:24 -0700 Subject: [PATCH 0030/1690] Version 0.17.1-pre.4 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 0b027f5aba..e91c760c19 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.1-pre.3", + "version": "0.17.1-pre.4", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index ff7d3efc40..63b7f96732 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.1-pre.3", + "version": "0.17.1-pre.4", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.1-pre.3" + "@remix-run/node": "0.17.1-pre.4" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 41fc57f9c7..54f45f08bd 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.1-pre.3", + "version": "0.17.1-pre.4", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 2655e614ca..c3a17746b4 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.1-pre.3", + "version": "0.17.1-pre.4", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.1-pre.3", + "@remix-run/express": "0.17.1-pre.4", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From 5086602240175ff268ec58d215c619d72eb2ef04 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 11 May 2021 10:48:58 -0700 Subject: [PATCH 0031/1690] Version 0.17.1-pre.5 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index e91c760c19..2f798bfff1 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.1-pre.4", + "version": "0.17.1-pre.5", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 63b7f96732..8d1be99bbb 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.1-pre.4", + "version": "0.17.1-pre.5", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.1-pre.4" + "@remix-run/node": "0.17.1-pre.5" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 54f45f08bd..c281a9bfbd 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.1-pre.4", + "version": "0.17.1-pre.5", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index c3a17746b4..2461d5ca2b 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.1-pre.4", + "version": "0.17.1-pre.5", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.1-pre.4", + "@remix-run/express": "0.17.1-pre.5", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From b16c2eb923eb0f1f4c9a9c63b6a89cb6eddc982a Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 11 May 2021 12:09:35 -0700 Subject: [PATCH 0032/1690] Version 0.17.1 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 2f798bfff1..f6988073a4 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.1-pre.5", + "version": "0.17.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 8d1be99bbb..301f2f52bf 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.1-pre.5", + "version": "0.17.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.1-pre.5" + "@remix-run/node": "0.17.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index c281a9bfbd..329e9491de 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.1-pre.5", + "version": "0.17.1", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 2461d5ca2b..4d1716b915 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.1-pre.5", + "version": "0.17.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.1-pre.5", + "@remix-run/express": "0.17.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From 24aae58a9c07667a33261c1a1ee7ffa50a17c2a5 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 11 May 2021 12:26:00 -0700 Subject: [PATCH 0033/1690] Version 0.17.2 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index f6988073a4..d48d4f4e81 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.1", + "version": "0.17.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 301f2f52bf..11caff12cf 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.1", + "version": "0.17.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.1" + "@remix-run/node": "0.17.2" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 329e9491de..59f288c831 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.1", + "version": "0.17.2", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 4d1716b915..d36adcab53 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.1", + "version": "0.17.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.1", + "@remix-run/express": "0.17.2", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From f9ed7bb6ef4694d583f03ef4b47b976088a9b450 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 12 May 2021 14:42:14 -0600 Subject: [PATCH 0034/1690] Remove _data from fetch requests This param is internal implementation that shouldn't leak out into loaders, apps shouldn't care if it's a document or fetch request. It also complicated redirect flows that preserved the search params, causing the _data param to show up in the browser address bar. Fixes #150 --- packages/remix-node/__tests__/data-test.ts | 40 ++++++++++++++++++++++ packages/remix-node/server.ts | 23 ++++++++++--- 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 packages/remix-node/__tests__/data-test.ts diff --git a/packages/remix-node/__tests__/data-test.ts b/packages/remix-node/__tests__/data-test.ts new file mode 100644 index 0000000000..ac9ee09f98 --- /dev/null +++ b/packages/remix-node/__tests__/data-test.ts @@ -0,0 +1,40 @@ +import { Request } from "node-fetch"; +import { ServerBuild } from "../build"; +import { createRequestHandler } from "../server"; + +describe("data", () => { + // so that HTML/Fetch requests are the same, and so redirects don't hang on to + // this param for no reason + it("removes _data from request.url", async () => { + let loader = async ({ request }) => { + return new URL(request.url).search; + }; + + let routeId = "routes/random"; + let build = ({ + routes: { + [routeId]: { + id: routeId, + path: "/random", + module: { + loader + } + } + } + } as unknown) as ServerBuild; + + let handler = createRequestHandler(build); + + let request = new Request( + "http://example.com/random?_data=routes/random&foo=bar", + { + headers: { + "Content-Type": "application/json" + } + } + ); + + let res = await handler(request); + expect(await res.json()).toMatchInlineSnapshot(`"?foo=bar"`); + }); +}); diff --git a/packages/remix-node/server.ts b/packages/remix-node/server.ts index a29a51a85a..f69235e792 100644 --- a/packages/remix-node/server.ts +++ b/packages/remix-node/server.ts @@ -5,8 +5,7 @@ import { serializeError } from "./errors"; import type { ServerBuild } from "./build"; import type { EntryContext } from "./entry"; import { createEntryMatches, createEntryRouteModules } from "./entry"; -import type { Request } from "./fetch"; -import { Response } from "./fetch"; +import { Response, Request } from "./fetch"; import { getDocumentHeaders } from "./headers"; import type { RouteMatch } from "./routeMatching"; import { matchServerRoutes } from "./routeMatching"; @@ -16,6 +15,7 @@ import { createRoutes } from "./routes"; import { createRouteData } from "./routeData"; import { json } from "./responses"; import { createServerHandoffString } from "./serverHandoff"; +import { RequestInit } from "node-fetch"; /** * The main request handler for a Remix server. This handler runs in the context @@ -75,20 +75,22 @@ async function handleDataRequest( routeMatch = match; } + let clonedRequest = await stripDataParam(request); + let response: Response; try { response = isActionRequest(request) ? await callRouteAction( build, routeMatch.route.id, - request, + clonedRequest, loadContext, routeMatch.params ) : await loadRouteData( build, routeMatch.route.id, - request, + clonedRequest, loadContext, routeMatch.params ); @@ -303,3 +305,16 @@ const redirectStatusCodes = new Set([301, 302, 303, 307, 308]); function isRedirectResponse(response: Response): boolean { return redirectStatusCodes.has(response.status); } + +async function stripDataParam(og: Request) { + let url = new URL(og.url); + url.searchParams.delete("_data"); + let init: RequestInit = { + method: og.method, + headers: og.headers + }; + if (og.method.toLowerCase() !== "get") { + init.body = await og.text(); + } + return new Request(url, init); +} From 074db9c9e7e7882e8651fec6654db8ddc04d3edf Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 12 May 2021 16:17:47 -0600 Subject: [PATCH 0035/1690] broken test --- packages/remix-node/__tests__/data-test.ts | 33 +++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/remix-node/__tests__/data-test.ts b/packages/remix-node/__tests__/data-test.ts index ac9ee09f98..8ad199b897 100644 --- a/packages/remix-node/__tests__/data-test.ts +++ b/packages/remix-node/__tests__/data-test.ts @@ -2,7 +2,38 @@ import { Request } from "node-fetch"; import { ServerBuild } from "../build"; import { createRequestHandler } from "../server"; -describe("data", () => { +describe("actions", () => { + it("returns a redirect when actions return a string", async () => { + let location = "/just/a/string"; + let action = async () => location; + + let routeId = "routes/random"; + let build = ({ + routes: { + [routeId]: { + id: routeId, + path: "/random", + module: { action } + } + } + } as unknown) as ServerBuild; + + let handler = createRequestHandler(build); + + let request = new Request("http://example.com/random", { + method: "POST", + headers: { + "Content-Type": "application/json" + } + }); + + let res = await handler(request); + expect(res.status).toBe(303); + expect(res.headers.get("location")).toBe(location); + }); +}); + +describe("loaders", () => { // so that HTML/Fetch requests are the same, and so redirects don't hang on to // this param for no reason it("removes _data from request.url", async () => { From 64246251a1f6f11e4a58de8d3f975ce7c878085f Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 12 May 2021 16:26:42 -0600 Subject: [PATCH 0036/1690] Let actions return strings --- packages/remix-node/data.ts | 11 +++++++++-- packages/remix-node/routeModules.ts | 5 +++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/remix-node/data.ts b/packages/remix-node/data.ts index 9e4c197d30..806bde9b2f 100644 --- a/packages/remix-node/data.ts +++ b/packages/remix-node/data.ts @@ -60,11 +60,18 @@ export async function callRouteAction( let result = await routeModule.action({ request, context, params }); + if (typeof result === "string") { + return new Response("", { + status: 303, + headers: { Location: result } + }); + } + if (!isResponse(result) || result.headers.get("Location") == null) { throw new Error( `You made a ${request.method} request to ${request.url} but did not return ` + - `a redirect. Please \`return redirect(newUrl)\` from your \`action\` ` + - `function to avoid reposts when users click the back button.` + `a redirect. Please \`return newUrlString\` or \`return redirect(newUrl)\` from ` + + `your \`action\` function to avoid reposts when users click the back button.` ); } diff --git a/packages/remix-node/routeModules.ts b/packages/remix-node/routeModules.ts index 7ec670d938..b8306fe56e 100644 --- a/packages/remix-node/routeModules.ts +++ b/packages/remix-node/routeModules.ts @@ -16,8 +16,9 @@ export interface RouteModules { */ export interface ActionFunction { (args: { request: Request; context: AppLoadContext; params: Params }): - | Promise - | Response; + | Promise + | Response + | string; } /** From 20c0acf3e24635fa16972e0947e930c315bec78a Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 19 May 2021 12:37:25 -0700 Subject: [PATCH 0037/1690] Version 0.17.3 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index d48d4f4e81..c2070870b9 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.2", + "version": "0.17.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 11caff12cf..703d4597c5 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.2", + "version": "0.17.3", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.2" + "@remix-run/node": "0.17.3" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 59f288c831..ffde5f2dc6 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.2", + "version": "0.17.3", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index d36adcab53..011385d857 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.2", + "version": "0.17.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.2", + "@remix-run/express": "0.17.3", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From 693ea8c3f9b4b8baea1c9e31acd22ff6bd843bce Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 13 May 2021 08:47:14 -0600 Subject: [PATCH 0038/1690] Handle uncaught action errors --- .../remix-dev/__tests__/readConfig-test.ts | 14 ++ packages/remix-node/errors.ts | 42 +++++ packages/remix-node/headers.ts | 5 +- packages/remix-node/server.ts | 165 ++++++++++++++---- 4 files changed, 188 insertions(+), 38 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 51258af772..94b1d3df0c 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -57,6 +57,20 @@ describe("readConfig", () => { "parentId": "root", "path": "*", }, + "routes/action-errors": Object { + "caseSensitive": false, + "file": "routes/action-errors.jsx", + "id": "routes/action-errors", + "parentId": "root", + "path": "action-errors", + }, + "routes/action-errors-self-boundary": Object { + "caseSensitive": false, + "file": "routes/action-errors-self-boundary.jsx", + "id": "routes/action-errors-self-boundary", + "parentId": "root", + "path": "action-errors-self-boundary", + }, "routes/empty": Object { "caseSensitive": false, "file": "routes/empty.jsx", diff --git a/packages/remix-node/errors.ts b/packages/remix-node/errors.ts index d8a3f2bb4c..f4a49da028 100644 --- a/packages/remix-node/errors.ts +++ b/packages/remix-node/errors.ts @@ -1,3 +1,45 @@ +/** + * This thing probably warrants some explanation. + * + * The whole point here is to emulate componentDidCatch for server rendering and + * data loading. It can get tricky. React can do this on component boundaries + * but doesn't support it for server rendering or data loading. We know enough + * with nested routes to be able to emulate the behavior (because we know them + * statically before rendering.) + * + * Each route can export an `ErrorBoundary`. + * + * - When rendering throws an error, the nearest error boundary will render + * (normal react componentDidCatch). This will be the route's own boundary, but + * if none is provided, it will bubble up to the parents. + * - When data loading throws an error, the nearest error boundary will render + * - When performing an action, the nearest error boundary for the action's + * route tree will render (no redirect happens) + * + * During normal react rendering, we do nothing special, just normal + * componentDidCatch. + * + * For server rendering, we mutate `renderBoundaryRouteId` to know the last + * layout that has an error boundary that tried to render. This emulates which + * layout would catch a thrown error. If the rendering fails, we catch the error + * on the server, and go again a second time with the emulator holding on to the + * information it needs to render the same error boundary as a dynamically + * thrown render error. + * + * When data loading, server or client side, we use the emulator to likewise + * hang on to the error and re-render at the appropriate layout (where a thrown + * error would have been caught by cDC). + * + * When actions throw, it all works the same. There's an edge case to be aware + * of though. Actions normally are required to redirect, but in the case of + * errors, we render the action's route with the emulator holding on to the + * error. If during this render a parent route/loader throws we ignore that new + * error and render the action's original error as deeply as possible. In other + * words, we simply ignore the new error and use the action's error in place + * because it came first, and that just wouldn't be fair to let errors cut in + * line. + */ + export interface ComponentDidCatchEmulator { error?: SerializedError; loaderBoundaryRouteId: string | null; diff --git a/packages/remix-node/headers.ts b/packages/remix-node/headers.ts index a326d9da36..491c4a676c 100644 --- a/packages/remix-node/headers.ts +++ b/packages/remix-node/headers.ts @@ -11,8 +11,9 @@ export function getDocumentHeaders( ): Headers { return matches.reduce((parentHeaders, match, index) => { let routeModule = build.routes[match.route.id].module; - let loaderResponse = routeLoaderResponses[index]; - let loaderHeaders = loaderResponse.headers; + let loaderHeaders = routeLoaderResponses[index] + ? routeLoaderResponses[index].headers + : new Headers(); let headers = new Headers( routeModule.headers diff --git a/packages/remix-node/server.ts b/packages/remix-node/server.ts index f69235e792..600532bdec 100644 --- a/packages/remix-node/server.ts +++ b/packages/remix-node/server.ts @@ -1,6 +1,7 @@ import type { AppLoadContext } from "./data"; import { loadRouteData, callRouteAction } from "./data"; import type { ComponentDidCatchEmulator } from "./errors"; + import { serializeError } from "./errors"; import type { ServerBuild } from "./build"; import type { EntryContext } from "./entry"; @@ -140,21 +141,6 @@ async function handleDocumentRequest( ); } - if (isActionRequest(request)) { - let leafMatch = matches[matches.length - 1]; - let response = await callRouteAction( - build, - leafMatch.route.id, - request, - loadContext, - leafMatch.params - ); - - // TODO: How do we handle errors here? - - return response; - } - let componentDidCatchEmulator: ComponentDidCatchEmulator = { trackBoundaries: true, renderBoundaryRouteId: null, @@ -162,11 +148,43 @@ async function handleDocumentRequest( error: undefined }; - // Run all data loaders in parallel. Await them in series below. - // Note: This code is a little weird due to the way unhandled promise - // rejections are handled in node. We use a .catch() handler on each - // promise to avoid the warning, then handle errors manually afterwards. - let routeLoaderPromises: Promise[] = matches.map(match => + let actionErrored: boolean = false; + + if (isActionRequest(request)) { + let leafMatch = matches[matches.length - 1]; + try { + let response = await callRouteAction( + build, + leafMatch.route.id, + request, + loadContext, + leafMatch.params + ); + + return response; + } catch (error) { + actionErrored = true; + let withBoundaries = getMatchesUpToDeepestErrorBoundary(matches); + componentDidCatchEmulator.loaderBoundaryRouteId = + withBoundaries[withBoundaries.length - 1].route.id; + componentDidCatchEmulator.error = serializeError(error); + } + } + + let matchesToLoad = actionErrored + ? getMatchesUpToDeepestErrorBoundary( + // get rid of the action, we know we don't want to call it's loader + matches.slice(0, -1) + ) + : matches; + + // Run all data loaders in parallel. Await them in series below. Note: This + // code is a little weird due to the way unhandled promise rejections are + // handled in node. We use a .catch() handler on each promise to avoid the + // warning, then handle errors manually afterwards. + let routeLoaderPromises: Promise< + Response | Error + >[] = matchesToLoad.map(match => loadRouteData( build, match.route.id, @@ -178,13 +196,32 @@ async function handleDocumentRequest( let routeLoaderResults = await Promise.all(routeLoaderPromises); for (let [index, response] of routeLoaderResults.entries()) { + let route = matches[index].route; + let routeModule = build.routes[route.id].module; + + // Rare case where an action throws an error, and then when we try to render + // the action's page to tell the user about the the error, a loader above + // the action route *also* threw an error or tried to redirect! + // + // Instead of rendering the loader error or redirecting like usual, we + // ignore the loader error or redirect because the action error was first + // and is higher priority to surface. Perhaps the action error is the + // reason the loader blows up now! It happened first and is more important + // to address. + // + // We just give up and move on with rendering the error as deeply as we can, + // which is the previous iteration of this loop + if ( + actionErrored && + (response instanceof Error || isRedirectResponse(response)) + ) { + break; + } + if (componentDidCatchEmulator.error) { continue; } - let route = matches[index].route; - let routeModule = build.routes[route.id].module; - if (routeModule.ErrorBoundary) { componentDidCatchEmulator.loaderBoundaryRouteId = route.id; } @@ -212,16 +249,29 @@ async function handleDocumentRequest( response => response.status !== 200 ); - let statusCode = notOkResponse + let statusCode = actionErrored + ? 500 + : notOkResponse ? notOkResponse.status : matches[matches.length - 1].route.id === "routes/404" ? 404 : 200; + let renderableMatches = getRenderableMatches( + matches, + componentDidCatchEmulator + ); let serverEntryModule = build.entry.module; - let headers = getDocumentHeaders(build, matches, routeLoaderResponses); - let entryMatches = createEntryMatches(matches, build.assets.routes); - let routeData = await createRouteData(matches, routeLoaderResponses); + let headers = getDocumentHeaders( + build, + renderableMatches, + routeLoaderResponses + ); + let entryMatches = createEntryMatches(renderableMatches, build.assets.routes); + let routeData = await createRouteData( + renderableMatches, + routeLoaderResponses + ); let routeModules = createEntryRouteModules(build.routes); let serverHandoff = { matches: entryMatches, @@ -250,14 +300,12 @@ async function handleDocumentRequest( statusCode = 500; - // Go again, this time with the componentDidCatch emulation. Remember, the - // routes `componentDidCatch.routeId` because we can't know that here. (Well - // ... maybe we could, we could search the error.stack lines for the first - // file matching the id of a route from the route manifest, but that would - // require us to have source maps installed so the filenames don't get - // changed when we bundle, and just feels a little too shakey for me right - // now. I'm okay with tracking our position in the route tree while - // rendering, that's pretty much how hooks work 😂) + // Go again, this time with the componentDidCatch emulation. As it rendered + // last time we mutated `componentDidCatch.routeId` for the last rendered + // route, now we know where to render the error boundary (feels a little + // hacky but that's how hooks work). This tells the emulator to stop + // tracking the `routeId` as we render because we already have an error to + // render. componentDidCatchEmulator.trackBoundaries = false; componentDidCatchEmulator.error = serializeError(error); entryContext.serverHandoffString = createServerHandoffString(serverHandoff); @@ -275,7 +323,6 @@ async function handleDocumentRequest( } // Good grief folks, get your act together 😂! - // TODO: Something is wrong in serverEntryModule, use the default root error handler response = new Response(`Unexpected Server Error\n\n${error.message}`, { status: 500, headers: { @@ -318,3 +365,49 @@ async function stripDataParam(og: Request) { } return new Request(url, init); } + +// This ensures we only load the data for the routes above an action error +function getMatchesUpToDeepestErrorBoundary( + matches: RouteMatch[] +) { + let deepestErrorBoundaryIndex: number = -1; + + matches.forEach((match, index) => { + if (match.route.module.ErrorBoundary) { + deepestErrorBoundaryIndex = index; + } + }); + + if (deepestErrorBoundaryIndex === -1) { + // no route error boundaries, don't need to call any loaders + return []; + } + + return matches.slice(0, deepestErrorBoundaryIndex + 1); +} + +// This prevents `` from rendering anything below where the error threw +// TODO: maybe do this in +function getRenderableMatches( + matches: RouteMatch[], + componentDidCatchEmulator: ComponentDidCatchEmulator +) { + // no error, no worries + if (!componentDidCatchEmulator.error) { + return matches; + } + + let lastRenderableIndex: number = -1; + + matches.forEach((match, index) => { + let id = match.route.id; + if ( + componentDidCatchEmulator.renderBoundaryRouteId === id || + componentDidCatchEmulator.loaderBoundaryRouteId === id + ) { + lastRenderableIndex = index; + } + }); + + return matches.slice(0, lastRenderableIndex + 1); +} From 9daf60d010a3591d7130fe3fda466c62770581ca Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Mon, 24 May 2021 14:39:07 -0600 Subject: [PATCH 0039/1690] Version 0.17.4 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index c2070870b9..5758d9bae7 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.3", + "version": "0.17.4", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 703d4597c5..0ca285ceb2 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.3", + "version": "0.17.4", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.3" + "@remix-run/node": "0.17.4" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index ffde5f2dc6..b6e6ca57af 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.3", + "version": "0.17.4", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 011385d857..26775ade8a 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.3", + "version": "0.17.4", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.3", + "@remix-run/express": "0.17.4", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From 74358e9e1b201e13d70aaef287663d8011e1d8ba Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Mon, 24 May 2021 15:42:46 -0600 Subject: [PATCH 0040/1690] Version 0.17.5 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 5758d9bae7..0e3b0fdcc0 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.4", + "version": "0.17.5", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 0ca285ceb2..10367b76f8 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.4", + "version": "0.17.5", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.4" + "@remix-run/node": "0.17.5" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index b6e6ca57af..da857ca8e3 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.4", + "version": "0.17.5", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 26775ade8a..81746736bb 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.4", + "version": "0.17.5", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.4", + "@remix-run/express": "0.17.5", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From 5fac0f6c78944be4b876ff91d552f3387adc41e1 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Mon, 14 Jun 2021 14:09:38 -0600 Subject: [PATCH 0041/1690] `useActionData`: actions can return anything API changes: - adds `useActionData` - actions can now return anything, redirects aren't enforced - can no longer return a string for redirects from actions since strings are valid route data - s/useRouteData/useLoaderData/ for better parity with `useActionData` since both are "route data" - `usePendingFormSubmit().data` is now a `URLSearchParams` instead of `FormBody` because it serializes better, unlikely to break any apps except for type extensions they may have created Implementation notes: - All the "pending form" stuff is no longer persisted in memory, but instead the form submit information lives on `location.state`, which simplified some code and enabled `useActionData` to even work at all. This is also why `usePendingFormSubmit().data` is a `URLSearchParams` instead of `FormData`, we needed to serialize the form data to a string (the same way the browser does) to be able to resubmit on pop events. Extra benefit is that reposts will work even across domain navigation :D - I took some shortcuts on types, need to go clean those up but I have some questions about the types on history/react-router that are related. Closes #187 --- .../remix-dev/__tests__/readConfig-test.ts | 7 +++++ packages/remix-node/__tests__/data-test.ts | 31 ------------------- packages/remix-node/data.ts | 21 +++---------- packages/remix-node/entry.ts | 1 + packages/remix-node/headers.ts | 13 ++++++-- packages/remix-node/routeData.ts | 4 +++ packages/remix-node/routeModules.ts | 8 +++-- packages/remix-node/server.ts | 26 +++++++++++----- 8 files changed, 50 insertions(+), 61 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 94b1d3df0c..57686ae185 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -71,6 +71,13 @@ describe("readConfig", () => { "parentId": "root", "path": "action-errors-self-boundary", }, + "routes/actions": Object { + "caseSensitive": false, + "file": "routes/actions.tsx", + "id": "routes/actions", + "parentId": "root", + "path": "actions", + }, "routes/empty": Object { "caseSensitive": false, "file": "routes/empty.jsx", diff --git a/packages/remix-node/__tests__/data-test.ts b/packages/remix-node/__tests__/data-test.ts index 8ad199b897..1cf4813f15 100644 --- a/packages/remix-node/__tests__/data-test.ts +++ b/packages/remix-node/__tests__/data-test.ts @@ -2,37 +2,6 @@ import { Request } from "node-fetch"; import { ServerBuild } from "../build"; import { createRequestHandler } from "../server"; -describe("actions", () => { - it("returns a redirect when actions return a string", async () => { - let location = "/just/a/string"; - let action = async () => location; - - let routeId = "routes/random"; - let build = ({ - routes: { - [routeId]: { - id: routeId, - path: "/random", - module: { action } - } - } - } as unknown) as ServerBuild; - - let handler = createRequestHandler(build); - - let request = new Request("http://example.com/random", { - method: "POST", - headers: { - "Content-Type": "application/json" - } - }); - - let res = await handler(request); - expect(res.status).toBe(303); - expect(res.headers.get("location")).toBe(location); - }); -}); - describe("loaders", () => { // so that HTML/Fetch requests are the same, and so redirects don't hang on to // this param for no reason diff --git a/packages/remix-node/data.ts b/packages/remix-node/data.ts index 806bde9b2f..363c9fc391 100644 --- a/packages/remix-node/data.ts +++ b/packages/remix-node/data.ts @@ -34,7 +34,7 @@ export async function loadRouteData( if (result === undefined) { throw new Error( `You defined a loader for route "${routeId}" but didn't return ` + - `anything from your \`loader\` function. We can't do everything for you! 😅` + `anything from your \`loader\` function. Please return a value or \`null\`.` ); } @@ -60,25 +60,14 @@ export async function callRouteAction( let result = await routeModule.action({ request, context, params }); - if (typeof result === "string") { - return new Response("", { - status: 303, - headers: { Location: result } - }); - } - - if (!isResponse(result) || result.headers.get("Location") == null) { + if (result === undefined) { throw new Error( - `You made a ${request.method} request to ${request.url} but did not return ` + - `a redirect. Please \`return newUrlString\` or \`return redirect(newUrl)\` from ` + - `your \`action\` function to avoid reposts when users click the back button.` + `You defined an action for route "${routeId}" but didn't return ` + + `anything from your \`action\` function. Please return a value or \`null\`.` ); } - return new Response("", { - status: 303, - headers: result.headers - }); + return isResponse(result) ? result : json(result); } function isResponse(value: any): value is Response { diff --git a/packages/remix-node/entry.ts b/packages/remix-node/entry.ts index 64b0e72e16..25d0be08ed 100644 --- a/packages/remix-node/entry.ts +++ b/packages/remix-node/entry.ts @@ -14,6 +14,7 @@ export interface EntryContext { manifest: AssetsManifest; matches: RouteMatch[]; routeData: RouteData; + actionData?: RouteData; routeModules: RouteModules; serverHandoffString?: string; } diff --git a/packages/remix-node/headers.ts b/packages/remix-node/headers.ts index 491c4a676c..cf388d81fa 100644 --- a/packages/remix-node/headers.ts +++ b/packages/remix-node/headers.ts @@ -7,9 +7,12 @@ import type { RouteMatch } from "./routeMatching"; export function getDocumentHeaders( build: ServerBuild, matches: RouteMatch[], - routeLoaderResponses: Response[] + routeLoaderResponses: Response[], + actionResponse?: Response ): Headers { - return matches.reduce((parentHeaders, match, index) => { + let actionHeaders = actionResponse?.headers || new Headers(); + + let finalHeaders = matches.reduce((parentHeaders, match, index) => { let routeModule = build.routes[match.route.id].module; let loaderHeaders = routeLoaderResponses[index] ? routeLoaderResponses[index].headers @@ -17,7 +20,7 @@ export function getDocumentHeaders( let headers = new Headers( routeModule.headers - ? routeModule.headers({ loaderHeaders, parentHeaders }) + ? routeModule.headers({ loaderHeaders, parentHeaders, actionHeaders }) : undefined ); @@ -28,6 +31,10 @@ export function getDocumentHeaders( return headers; }, new Headers()); + + prependCookies(finalHeaders, actionHeaders); + + return finalHeaders; } function prependCookies(parentHeaders: Headers, childHeaders: Headers): void { diff --git a/packages/remix-node/routeData.ts b/packages/remix-node/routeData.ts index 87753309e9..f2a24911ec 100644 --- a/packages/remix-node/routeData.ts +++ b/packages/remix-node/routeData.ts @@ -19,3 +19,7 @@ export async function createRouteData( return memo; }, {} as RouteData); } + +export async function createActionData(response: Response): Promise { + return extractData(response); +} diff --git a/packages/remix-node/routeModules.ts b/packages/remix-node/routeModules.ts index b8306fe56e..00d3602c33 100644 --- a/packages/remix-node/routeModules.ts +++ b/packages/remix-node/routeModules.ts @@ -31,9 +31,11 @@ export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; * will be merged with (and take precedence over) headers from parent routes. */ export interface HeadersFunction { - (args: { loaderHeaders: Headers; parentHeaders: Headers }): - | Headers - | HeadersInit; + (args: { + loaderHeaders: Headers; + parentHeaders: Headers; + actionHeaders: Headers; + }): Headers | HeadersInit; } /** diff --git a/packages/remix-node/server.ts b/packages/remix-node/server.ts index 600532bdec..846ce92b2c 100644 --- a/packages/remix-node/server.ts +++ b/packages/remix-node/server.ts @@ -13,7 +13,7 @@ import { matchServerRoutes } from "./routeMatching"; import { ServerMode, isServerMode } from "./mode"; import type { ServerRoute } from "./routes"; import { createRoutes } from "./routes"; -import { createRouteData } from "./routeData"; +import { createActionData, createRouteData } from "./routeData"; import { json } from "./responses"; import { createServerHandoffString } from "./serverHandoff"; import { RequestInit } from "node-fetch"; @@ -149,19 +149,21 @@ async function handleDocumentRequest( }; let actionErrored: boolean = false; + let actionResponse: Response | undefined; if (isActionRequest(request)) { let leafMatch = matches[matches.length - 1]; try { - let response = await callRouteAction( + actionResponse = await callRouteAction( build, leafMatch.route.id, - request, + request.clone(), loadContext, leafMatch.params ); - - return response; + if (isRedirectResponse(actionResponse)) { + return actionResponse; + } } catch (error) { actionErrored = true; let withBoundaries = getMatchesUpToDeepestErrorBoundary(matches); @@ -173,7 +175,10 @@ async function handleDocumentRequest( let matchesToLoad = actionErrored ? getMatchesUpToDeepestErrorBoundary( - // get rid of the action, we know we don't want to call it's loader + // get rid of the action, we don't want to call it's loader either + // because we'll be rendering the error boundary, if you can get access + // to the loader data in the error boundary then how the heck is it + // supposed to deal with errors in the loader, too? matches.slice(0, -1) ) : matches; @@ -265,18 +270,23 @@ async function handleDocumentRequest( let headers = getDocumentHeaders( build, renderableMatches, - routeLoaderResponses + routeLoaderResponses, + actionResponse ); let entryMatches = createEntryMatches(renderableMatches, build.assets.routes); let routeData = await createRouteData( renderableMatches, routeLoaderResponses ); + let actionData = actionResponse + ? await createActionData(actionResponse) + : undefined; let routeModules = createEntryRouteModules(build.routes); let serverHandoff = { matches: entryMatches, componentDidCatchEmulator, - routeData + routeData, + actionData }; let entryContext: EntryContext = { ...serverHandoff, From 47a3e5c0f268ded8683edc15f127e3bdb722ebf5 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Fri, 18 Jun 2021 13:05:49 -0600 Subject: [PATCH 0042/1690] Rewrote client transition code with transitionManager - adds `useTransition` for finer grained control over pending indicators and optimistic UI - adds "submission keys" to get the transition for a specific form's submissions rather than the global transition, allowing apps to create complex UIs with a lot of mutations happening at the same time - adds support for concurrent form submissions (#151) - adds route module `shouldReload` to optimize which routes should reload on form submission reloads or search param changes (#181, #175) - reloads data when links to the current url are clicked (#128) - provides state change updates on navigations (#54-ish) - fixed issues with submitting a form multiple times quickly that completely broke the app before - is mostly Remix agnostic, so we should be able to bring it over to React Router without to much headache Closes #151, #181, #175, #128, #54, #208 --- packages/remix-dev/__tests__/readConfig-test.ts | 7 +++++++ packages/remix-dev/compiler.ts | 3 ++- packages/remix-dev/compiler/assets.ts | 8 +++++--- packages/remix-node/routeModules.ts | 4 ++-- packages/remix-node/routes.ts | 5 +++-- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 57686ae185..5659d86299 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -148,6 +148,13 @@ describe("readConfig", () => { "parentId": "root", "path": "methods", }, + "routes/pending-forms": Object { + "caseSensitive": false, + "file": "routes/pending-forms.tsx", + "id": "routes/pending-forms", + "parentId": "root", + "path": "pending-forms", + }, "routes/prefs": Object { "caseSensitive": false, "file": "routes/prefs.tsx", diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 45a5505354..d2773a72b3 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -371,7 +371,8 @@ const browserSafeRouteExports: { [name: string]: boolean } = { default: true, handle: true, links: true, - meta: true + meta: true, + shouldReload: true }; /** diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index 9b6156db4a..93c6259227 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -24,8 +24,9 @@ interface AssetsManifest { caseSensitive?: boolean; module: string; imports?: string[]; - hasAction?: boolean; - hasLoader?: boolean; + hasAction: boolean; + hasLoader: boolean; + hasErrorBoundary: boolean; }; }; } @@ -89,7 +90,8 @@ export async function createAssetsManifest( module: resolveUrl(key), imports: resolveImports(output.imports), hasAction: sourceExports.includes("action"), - hasLoader: sourceExports.includes("loader") + hasLoader: sourceExports.includes("loader"), + hasErrorBoundary: sourceExports.includes("ErrorBoundary") }; } } diff --git a/packages/remix-node/routeModules.ts b/packages/remix-node/routeModules.ts index 00d3602c33..b671f4470a 100644 --- a/packages/remix-node/routeModules.ts +++ b/packages/remix-node/routeModules.ts @@ -16,9 +16,9 @@ export interface RouteModules { */ export interface ActionFunction { (args: { request: Request; context: AppLoadContext; params: Params }): - | Promise + | Promise | Response - | string; + | AppData; } /** diff --git a/packages/remix-node/routes.ts b/packages/remix-node/routes.ts index 4ac4957a9b..405ab87dc8 100644 --- a/packages/remix-node/routes.ts +++ b/packages/remix-node/routes.ts @@ -14,8 +14,9 @@ interface Route { } export interface EntryRoute extends Route { - hasAction?: boolean; - hasLoader?: boolean; + hasAction: boolean; + hasLoader: boolean; + hasErrorBoundary: boolean; imports?: string[]; module: string; } From 961f46258742ad64e38ba37832bc9b3b9b1cbdc5 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 4 Aug 2021 15:45:49 -0600 Subject: [PATCH 0043/1690] Version 0.18.0-pre.0 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 0e3b0fdcc0..a737918788 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.5", + "version": "0.18.0-pre.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 10367b76f8..83c887f6b5 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.5", + "version": "0.18.0-pre.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.5" + "@remix-run/node": "0.18.0-pre.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index da857ca8e3..c26201787a 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.17.5", + "version": "0.18.0-pre.0", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 81746736bb..ab25d9834b 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.5", + "version": "0.18.0-pre.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.5", + "@remix-run/express": "0.18.0-pre.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From e70638b03799f7db142302f9475c80e18ae4f472 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 9 Aug 2021 16:35:55 -0700 Subject: [PATCH 0044/1690] fix: cli no longer crashes on syntax errors (#239) feat: print syntax error location --- packages/remix-dev/cli/commands.ts | 4 -- packages/remix-dev/compiler.ts | 93 ++++++++++++++++++++++----- packages/remix-dev/compiler/assets.ts | 3 +- packages/remix-dev/compiler/routes.ts | 3 +- 4 files changed, 82 insertions(+), 21 deletions(-) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index b1c25c03b5..cfbc09f6a8 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -55,10 +55,6 @@ export async function watch( signalExit( await compiler.watch(config, { mode, - // TODO: esbuild compiler just blows up on syntax errors in the app - // onError(errorMessage) { - // console.error(errorMessage); - // }, onRebuildStart() { start = Date.now(); onRebuildStart && onRebuildStart(); diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index d2773a72b3..52995097dc 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -30,13 +30,31 @@ function defaultWarningHandler(message: string, key: string) { warnOnce(false, message, key); } -function defaultErrorHandler(message: string) { - console.error(message); +function defaultBuildFailureHandler(failure: Error | esbuild.BuildFailure) { + if ("warnings" in failure || "errors" in failure) { + if (failure.warnings) { + let messages = esbuild.formatMessagesSync(failure.warnings, { + kind: "warning", + color: true + }); + console.warn(...messages); + } + + if (failure.errors) { + let messages = esbuild.formatMessagesSync(failure.errors, { + kind: "error", + color: true + }); + console.error(...messages); + } + } + + console.error(failure?.message || "An unknown build error occured"); } interface BuildOptions extends Partial { onWarning?(message: string, key: string): void; - onError?(message: string): void; + onBuildFailure?(failure: Error | esbuild.BuildFailure): void; } export async function build( @@ -45,10 +63,15 @@ export async function build( mode = BuildMode.Production, target = BuildTarget.Node14, onWarning = defaultWarningHandler, - onError = defaultErrorHandler + onBuildFailure = defaultBuildFailureHandler }: BuildOptions = {} ): Promise { - await buildEverything(config, { mode, target, onWarning, onError }); + await buildEverything(config, { + mode, + target, + onWarning, + onBuildFailure + }); } interface WatchOptions extends BuildOptions { @@ -65,7 +88,7 @@ export async function watch( mode = BuildMode.Development, target = BuildTarget.Node14, onWarning = defaultWarningHandler, - onError = defaultErrorHandler, + onBuildFailure = defaultBuildFailureHandler, onRebuildStart, onRebuildFinish, onFileCreated, @@ -73,13 +96,19 @@ export async function watch( onFileDeleted }: WatchOptions = {} ): Promise<() => void> { - let options = { mode, target, onWarning, onError, incremental: true }; + let options = { + mode, + target, + onBuildFailure, + onWarning, + incremental: true + }; let [browserBuild, serverBuild] = await buildEverything(config, options); async function disposeBuilders() { await Promise.all([ - browserBuild.rebuild?.dispose(), - serverBuild.rebuild?.dispose() + browserBuild?.rebuild?.dispose(), + serverBuild?.rebuild?.dispose() ]); } @@ -95,12 +124,29 @@ export async function watch( let rebuildEverything = debounce(async () => { if (onRebuildStart) onRebuildStart(); + + if (!browserBuild || !serverBuild) { + await disposeBuilders(); + + try { + [browserBuild, serverBuild] = await buildEverything(config, options); + if (onRebuildFinish) onRebuildFinish(); + } catch (err) { + onBuildFailure(err); + } + return; + } + await Promise.all([ + // If we get here and can't call rebuild something went wrong and we + // should probably blow as it's not really recoverable. browserBuild.rebuild!().then(build => generateManifests(config, build.metafile!) ), serverBuild.rebuild!() - ]); + ]).catch(err => { + onBuildFailure(err); + }); if (onRebuildFinish) onRebuildFinish(); }, 100); @@ -163,7 +209,7 @@ function isEntryPoint(config: RemixConfig, file: string) { async function buildEverything( config: RemixConfig, options: Required & { incremental?: boolean } -): Promise { +): Promise<(esbuild.BuildResult | undefined)[]> { // TODO: // When building for node, we build both the browser and server builds in // parallel and emit the asset manifest as a separate file in the output @@ -181,7 +227,10 @@ async function buildEverything( return build; }), serverBuildPromise - ]); + ]).catch(err => { + options.onBuildFailure(err); + return [undefined, undefined]; + }); } async function createBrowserBuild( @@ -216,6 +265,7 @@ async function createBrowserBuild( inject: [reactShim], loader: loaders, bundle: true, + logLevel: "silent", splitting: true, metafile: true, incremental: options.incremental, @@ -252,6 +302,7 @@ async function createServerBuild( inject: [reactShim], loader: loaders, bundle: true, + logLevel: "silent", incremental: options.incremental, // The server build needs to know how to generate asset URLs for imports // of CSS and other files. @@ -406,9 +457,21 @@ function browserRouteModulesPlugin( let route = routesByFile.get(file); invariant(route, `Cannot get route by path: ${args.path}`); - let exports = ( - await getRouteModuleExportsCached(config, route.id) - ).filter(ex => !!browserSafeRouteExports[ex]); + let exports; + try { + exports = ( + await getRouteModuleExportsCached(config, route.id) + ).filter(ex => !!browserSafeRouteExports[ex]); + } catch (error) { + return { + errors: [ + { + text: error.message, + pluginName: "browser-route-module" + } + ] + }; + } let spec = exports.length > 0 ? `{ ${exports.join(", ")} }` : "*"; let contents = `export ${spec} from ${JSON.stringify(file)};`; diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index 93c6259227..a8fba0d34f 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -78,7 +78,8 @@ export async function createAssetsManifest( module: resolveUrl(key), imports: resolveImports(output.imports) }; - } else { + // Only parse routes otherwise dynamic imports can fall into here and fail the build + } else if (output.entryPoint.startsWith("browser-route-module:")) { let route = routesByFile.get(entryPointFile); invariant(route, `Cannot get route for entry point ${output.entryPoint}`); let sourceExports = await getRouteModuleExportsCached(config, route.id); diff --git a/packages/remix-dev/compiler/routes.ts b/packages/remix-dev/compiler/routes.ts index 624848a993..a502531a3f 100644 --- a/packages/remix-dev/compiler/routes.ts +++ b/packages/remix-dev/compiler/routes.ts @@ -46,7 +46,8 @@ export async function getRouteModuleExports( platform: "neutral", format: "esm", metafile: true, - write: false + write: false, + logLevel: "silent" }); let metafile = result.metafile!; From 6d0bafe6801c583d29cfd841ae0d26e1b5fbf011 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 12 Aug 2021 15:22:27 -0600 Subject: [PATCH 0045/1690] Revert "Version 0.18.0-pre.0" This reverts commit 961f46258742ad64e38ba37832bc9b3b9b1cbdc5. --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 2 +- packages/remix-serve/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index a737918788..0e3b0fdcc0 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.18.0-pre.0", + "version": "0.17.5", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 83c887f6b5..10367b76f8 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.18.0-pre.0", + "version": "0.17.5", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.18.0-pre.0" + "@remix-run/node": "0.17.5" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index c26201787a..da857ca8e3 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js bindings for Remix", - "version": "0.18.0-pre.0", + "version": "0.17.5", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index ab25d9834b..81746736bb 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.18.0-pre.0", + "version": "0.17.5", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.18.0-pre.0", + "@remix-run/express": "0.17.5", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" From aa91beaacfe0779f450161ede831245f8dd4efd0 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 12 Aug 2021 15:22:41 -0600 Subject: [PATCH 0046/1690] Revert "Rewrote client transition code with transitionManager" This reverts commit 47a3e5c0f268ded8683edc15f127e3bdb722ebf5. --- packages/remix-dev/__tests__/readConfig-test.ts | 7 ------- packages/remix-dev/compiler.ts | 3 +-- packages/remix-dev/compiler/assets.ts | 8 +++----- packages/remix-node/routeModules.ts | 4 ++-- packages/remix-node/routes.ts | 5 ++--- 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 5659d86299..57686ae185 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -148,13 +148,6 @@ describe("readConfig", () => { "parentId": "root", "path": "methods", }, - "routes/pending-forms": Object { - "caseSensitive": false, - "file": "routes/pending-forms.tsx", - "id": "routes/pending-forms", - "parentId": "root", - "path": "pending-forms", - }, "routes/prefs": Object { "caseSensitive": false, "file": "routes/prefs.tsx", diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 52995097dc..5d5ae8aa8a 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -422,8 +422,7 @@ const browserSafeRouteExports: { [name: string]: boolean } = { default: true, handle: true, links: true, - meta: true, - shouldReload: true + meta: true }; /** diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index a8fba0d34f..e4e39b5357 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -24,9 +24,8 @@ interface AssetsManifest { caseSensitive?: boolean; module: string; imports?: string[]; - hasAction: boolean; - hasLoader: boolean; - hasErrorBoundary: boolean; + hasAction?: boolean; + hasLoader?: boolean; }; }; } @@ -91,8 +90,7 @@ export async function createAssetsManifest( module: resolveUrl(key), imports: resolveImports(output.imports), hasAction: sourceExports.includes("action"), - hasLoader: sourceExports.includes("loader"), - hasErrorBoundary: sourceExports.includes("ErrorBoundary") + hasLoader: sourceExports.includes("loader") }; } } diff --git a/packages/remix-node/routeModules.ts b/packages/remix-node/routeModules.ts index b671f4470a..00d3602c33 100644 --- a/packages/remix-node/routeModules.ts +++ b/packages/remix-node/routeModules.ts @@ -16,9 +16,9 @@ export interface RouteModules { */ export interface ActionFunction { (args: { request: Request; context: AppLoadContext; params: Params }): - | Promise + | Promise | Response - | AppData; + | string; } /** diff --git a/packages/remix-node/routes.ts b/packages/remix-node/routes.ts index 405ab87dc8..4ac4957a9b 100644 --- a/packages/remix-node/routes.ts +++ b/packages/remix-node/routes.ts @@ -14,9 +14,8 @@ interface Route { } export interface EntryRoute extends Route { - hasAction: boolean; - hasLoader: boolean; - hasErrorBoundary: boolean; + hasAction?: boolean; + hasLoader?: boolean; imports?: string[]; module: string; } From 47055f5af48c41a9064403c934e27cb7a6f61bbc Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 12 Aug 2021 15:23:04 -0600 Subject: [PATCH 0047/1690] Revert "`useActionData`: actions can return anything" This reverts commit 5fac0f6c78944be4b876ff91d552f3387adc41e1. --- .../remix-dev/__tests__/readConfig-test.ts | 7 ----- packages/remix-node/__tests__/data-test.ts | 31 +++++++++++++++++++ packages/remix-node/data.ts | 21 ++++++++++--- packages/remix-node/entry.ts | 1 - packages/remix-node/headers.ts | 13 ++------ packages/remix-node/routeData.ts | 4 --- packages/remix-node/routeModules.ts | 8 ++--- packages/remix-node/server.ts | 26 +++++----------- 8 files changed, 61 insertions(+), 50 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 57686ae185..94b1d3df0c 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -71,13 +71,6 @@ describe("readConfig", () => { "parentId": "root", "path": "action-errors-self-boundary", }, - "routes/actions": Object { - "caseSensitive": false, - "file": "routes/actions.tsx", - "id": "routes/actions", - "parentId": "root", - "path": "actions", - }, "routes/empty": Object { "caseSensitive": false, "file": "routes/empty.jsx", diff --git a/packages/remix-node/__tests__/data-test.ts b/packages/remix-node/__tests__/data-test.ts index 1cf4813f15..8ad199b897 100644 --- a/packages/remix-node/__tests__/data-test.ts +++ b/packages/remix-node/__tests__/data-test.ts @@ -2,6 +2,37 @@ import { Request } from "node-fetch"; import { ServerBuild } from "../build"; import { createRequestHandler } from "../server"; +describe("actions", () => { + it("returns a redirect when actions return a string", async () => { + let location = "/just/a/string"; + let action = async () => location; + + let routeId = "routes/random"; + let build = ({ + routes: { + [routeId]: { + id: routeId, + path: "/random", + module: { action } + } + } + } as unknown) as ServerBuild; + + let handler = createRequestHandler(build); + + let request = new Request("http://example.com/random", { + method: "POST", + headers: { + "Content-Type": "application/json" + } + }); + + let res = await handler(request); + expect(res.status).toBe(303); + expect(res.headers.get("location")).toBe(location); + }); +}); + describe("loaders", () => { // so that HTML/Fetch requests are the same, and so redirects don't hang on to // this param for no reason diff --git a/packages/remix-node/data.ts b/packages/remix-node/data.ts index 363c9fc391..806bde9b2f 100644 --- a/packages/remix-node/data.ts +++ b/packages/remix-node/data.ts @@ -34,7 +34,7 @@ export async function loadRouteData( if (result === undefined) { throw new Error( `You defined a loader for route "${routeId}" but didn't return ` + - `anything from your \`loader\` function. Please return a value or \`null\`.` + `anything from your \`loader\` function. We can't do everything for you! 😅` ); } @@ -60,14 +60,25 @@ export async function callRouteAction( let result = await routeModule.action({ request, context, params }); - if (result === undefined) { + if (typeof result === "string") { + return new Response("", { + status: 303, + headers: { Location: result } + }); + } + + if (!isResponse(result) || result.headers.get("Location") == null) { throw new Error( - `You defined an action for route "${routeId}" but didn't return ` + - `anything from your \`action\` function. Please return a value or \`null\`.` + `You made a ${request.method} request to ${request.url} but did not return ` + + `a redirect. Please \`return newUrlString\` or \`return redirect(newUrl)\` from ` + + `your \`action\` function to avoid reposts when users click the back button.` ); } - return isResponse(result) ? result : json(result); + return new Response("", { + status: 303, + headers: result.headers + }); } function isResponse(value: any): value is Response { diff --git a/packages/remix-node/entry.ts b/packages/remix-node/entry.ts index 25d0be08ed..64b0e72e16 100644 --- a/packages/remix-node/entry.ts +++ b/packages/remix-node/entry.ts @@ -14,7 +14,6 @@ export interface EntryContext { manifest: AssetsManifest; matches: RouteMatch[]; routeData: RouteData; - actionData?: RouteData; routeModules: RouteModules; serverHandoffString?: string; } diff --git a/packages/remix-node/headers.ts b/packages/remix-node/headers.ts index cf388d81fa..491c4a676c 100644 --- a/packages/remix-node/headers.ts +++ b/packages/remix-node/headers.ts @@ -7,12 +7,9 @@ import type { RouteMatch } from "./routeMatching"; export function getDocumentHeaders( build: ServerBuild, matches: RouteMatch[], - routeLoaderResponses: Response[], - actionResponse?: Response + routeLoaderResponses: Response[] ): Headers { - let actionHeaders = actionResponse?.headers || new Headers(); - - let finalHeaders = matches.reduce((parentHeaders, match, index) => { + return matches.reduce((parentHeaders, match, index) => { let routeModule = build.routes[match.route.id].module; let loaderHeaders = routeLoaderResponses[index] ? routeLoaderResponses[index].headers @@ -20,7 +17,7 @@ export function getDocumentHeaders( let headers = new Headers( routeModule.headers - ? routeModule.headers({ loaderHeaders, parentHeaders, actionHeaders }) + ? routeModule.headers({ loaderHeaders, parentHeaders }) : undefined ); @@ -31,10 +28,6 @@ export function getDocumentHeaders( return headers; }, new Headers()); - - prependCookies(finalHeaders, actionHeaders); - - return finalHeaders; } function prependCookies(parentHeaders: Headers, childHeaders: Headers): void { diff --git a/packages/remix-node/routeData.ts b/packages/remix-node/routeData.ts index f2a24911ec..87753309e9 100644 --- a/packages/remix-node/routeData.ts +++ b/packages/remix-node/routeData.ts @@ -19,7 +19,3 @@ export async function createRouteData( return memo; }, {} as RouteData); } - -export async function createActionData(response: Response): Promise { - return extractData(response); -} diff --git a/packages/remix-node/routeModules.ts b/packages/remix-node/routeModules.ts index 00d3602c33..b8306fe56e 100644 --- a/packages/remix-node/routeModules.ts +++ b/packages/remix-node/routeModules.ts @@ -31,11 +31,9 @@ export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; * will be merged with (and take precedence over) headers from parent routes. */ export interface HeadersFunction { - (args: { - loaderHeaders: Headers; - parentHeaders: Headers; - actionHeaders: Headers; - }): Headers | HeadersInit; + (args: { loaderHeaders: Headers; parentHeaders: Headers }): + | Headers + | HeadersInit; } /** diff --git a/packages/remix-node/server.ts b/packages/remix-node/server.ts index 846ce92b2c..600532bdec 100644 --- a/packages/remix-node/server.ts +++ b/packages/remix-node/server.ts @@ -13,7 +13,7 @@ import { matchServerRoutes } from "./routeMatching"; import { ServerMode, isServerMode } from "./mode"; import type { ServerRoute } from "./routes"; import { createRoutes } from "./routes"; -import { createActionData, createRouteData } from "./routeData"; +import { createRouteData } from "./routeData"; import { json } from "./responses"; import { createServerHandoffString } from "./serverHandoff"; import { RequestInit } from "node-fetch"; @@ -149,21 +149,19 @@ async function handleDocumentRequest( }; let actionErrored: boolean = false; - let actionResponse: Response | undefined; if (isActionRequest(request)) { let leafMatch = matches[matches.length - 1]; try { - actionResponse = await callRouteAction( + let response = await callRouteAction( build, leafMatch.route.id, - request.clone(), + request, loadContext, leafMatch.params ); - if (isRedirectResponse(actionResponse)) { - return actionResponse; - } + + return response; } catch (error) { actionErrored = true; let withBoundaries = getMatchesUpToDeepestErrorBoundary(matches); @@ -175,10 +173,7 @@ async function handleDocumentRequest( let matchesToLoad = actionErrored ? getMatchesUpToDeepestErrorBoundary( - // get rid of the action, we don't want to call it's loader either - // because we'll be rendering the error boundary, if you can get access - // to the loader data in the error boundary then how the heck is it - // supposed to deal with errors in the loader, too? + // get rid of the action, we know we don't want to call it's loader matches.slice(0, -1) ) : matches; @@ -270,23 +265,18 @@ async function handleDocumentRequest( let headers = getDocumentHeaders( build, renderableMatches, - routeLoaderResponses, - actionResponse + routeLoaderResponses ); let entryMatches = createEntryMatches(renderableMatches, build.assets.routes); let routeData = await createRouteData( renderableMatches, routeLoaderResponses ); - let actionData = actionResponse - ? await createActionData(actionResponse) - : undefined; let routeModules = createEntryRouteModules(build.routes); let serverHandoff = { matches: entryMatches, componentDidCatchEmulator, - routeData, - actionData + routeData }; let entryContext: EntryContext = { ...serverHandoff, From a0df4392332cb19eac3e7e74862bab146351104c Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 18 Aug 2021 09:26:37 -0700 Subject: [PATCH 0048/1690] feat: added sourcemap support (#242) feat: added sourcemap support feat!: remix-node has been renamed remix-server. remix-node has been repurposed to house the abstractions of the node runtime. feat!: added abstraction for runtimes (#244) feat!: moved fileStorage session to remix-node this removes the last node specific platform modules from remix-server-runtime feat: added cloudflare worker handler (untested) feat: moved to webcrypto instead of "crypto" to support more platforms feat!: updated .npmrc to minimum required version for webcrypto API feat: Use node-fetch types in node adapters feat: abstracted req and res types through the platform feat: Add base64 encoding primitives to node globals Co-authored-by: Michael Jackson --- packages/remix-dev/compiler.ts | 2 + .../remix-express/__tests__/server-test.ts | 6 +- packages/remix-express/package.json | 3 +- packages/remix-express/server.ts | 40 +++-- packages/remix-node/__tests__/cookies-test.ts | 95 ---------- packages/remix-node/__tests__/errors-test.ts | 61 +++++++ .../remix-node/__tests__/sessions-test.ts | 121 ------------- packages/remix-node/base64.ts | 7 + packages/remix-node/errors.ts | 165 +++++++++++------- packages/remix-node/globals.ts | 36 ++-- packages/remix-node/index.ts | 49 +----- packages/remix-node/magicExports/platform.ts | 10 ++ packages/remix-node/package.json | 20 +-- packages/remix-node/responses.ts | 47 ++--- packages/remix-node/routeModules.ts | 97 +--------- packages/remix-node/sessions/fileStorage.ts | 12 +- .../__tests__/cookies-test.ts | 121 +++++++++++++ .../__tests__/data-test.ts | 0 .../__tests__/responses-test.ts | 0 .../__tests__/sessions-test.ts | 125 +++++++++++++ .../remix-server-runtime/__tests__/utils.ts | 5 + .../assetImportTypes.ts | 0 .../build.ts | 1 - .../remix-server-runtime/cookieSigning.ts | 52 ++++++ .../cookies.ts | 40 +++-- .../data.ts | 2 - .../entry.ts | 0 packages/remix-server-runtime/errors.ts | 61 +++++++ .../headers.ts | 2 - packages/remix-server-runtime/index.ts | 51 ++++++ .../invariant.ts | 0 .../links.ts | 0 .../magicExports/server.ts | 20 +-- .../mode.ts | 0 packages/remix-server-runtime/package.json | 23 +++ packages/remix-server-runtime/platform.ts | 17 ++ packages/remix-server-runtime/responses.ts | 43 +++++ .../routeData.ts | 1 - .../routeMatching.ts | 0 packages/remix-server-runtime/routeModules.ts | 98 +++++++++++ .../routes.ts | 0 .../scripts/postinstall.ts | 28 +++ .../server.ts | 83 +++++---- .../serverHandoff.ts | 0 .../sessions.ts | 2 +- .../sessions/cookieStorage.ts | 2 +- .../sessions/memoryStorage.ts | 0 packages/remix-server-runtime/tsconfig.json | 20 +++ .../warnings.ts | 0 49 files changed, 987 insertions(+), 581 deletions(-) delete mode 100644 packages/remix-node/__tests__/cookies-test.ts create mode 100644 packages/remix-node/__tests__/errors-test.ts create mode 100644 packages/remix-node/base64.ts create mode 100644 packages/remix-node/magicExports/platform.ts create mode 100644 packages/remix-server-runtime/__tests__/cookies-test.ts rename packages/{remix-node => remix-server-runtime}/__tests__/data-test.ts (100%) rename packages/{remix-node => remix-server-runtime}/__tests__/responses-test.ts (100%) create mode 100644 packages/remix-server-runtime/__tests__/sessions-test.ts create mode 100644 packages/remix-server-runtime/__tests__/utils.ts rename packages/{remix-node => remix-server-runtime}/assetImportTypes.ts (100%) rename packages/{remix-node => remix-server-runtime}/build.ts (90%) create mode 100644 packages/remix-server-runtime/cookieSigning.ts rename packages/{remix-node => remix-server-runtime}/cookies.ts (80%) rename packages/{remix-node => remix-server-runtime}/data.ts (97%) rename packages/{remix-node => remix-server-runtime}/entry.ts (100%) create mode 100644 packages/remix-server-runtime/errors.ts rename packages/{remix-node => remix-server-runtime}/headers.ts (96%) create mode 100644 packages/remix-server-runtime/index.ts rename packages/{remix-node => remix-server-runtime}/invariant.ts (100%) rename packages/{remix-node => remix-server-runtime}/links.ts (100%) rename packages/{remix-node => remix-server-runtime}/magicExports/server.ts (66%) rename packages/{remix-node => remix-server-runtime}/mode.ts (100%) create mode 100644 packages/remix-server-runtime/package.json create mode 100644 packages/remix-server-runtime/platform.ts create mode 100644 packages/remix-server-runtime/responses.ts rename packages/{remix-node => remix-server-runtime}/routeData.ts (93%) rename packages/{remix-node => remix-server-runtime}/routeMatching.ts (100%) create mode 100644 packages/remix-server-runtime/routeModules.ts rename packages/{remix-node => remix-server-runtime}/routes.ts (100%) create mode 100644 packages/remix-server-runtime/scripts/postinstall.ts rename packages/{remix-node => remix-server-runtime}/server.ts (85%) rename packages/{remix-node => remix-server-runtime}/serverHandoff.ts (100%) rename packages/{remix-node => remix-server-runtime}/sessions.ts (98%) rename packages/{remix-node => remix-server-runtime}/sessions/cookieStorage.ts (95%) rename packages/{remix-node => remix-server-runtime}/sessions/memoryStorage.ts (100%) create mode 100644 packages/remix-server-runtime/tsconfig.json rename packages/{remix-node => remix-server-runtime}/warnings.ts (100%) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 5d5ae8aa8a..67963b72a0 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -267,6 +267,7 @@ async function createBrowserBuild( bundle: true, logLevel: "silent", splitting: true, + sourcemap: true, metafile: true, incremental: options.incremental, minify: options.mode === BuildMode.Production, @@ -304,6 +305,7 @@ async function createServerBuild( bundle: true, logLevel: "silent", incremental: options.incremental, + sourcemap: true, // The server build needs to know how to generate asset URLs for imports // of CSS and other files. assetNames: "_assets/[name]-[hash]", diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index f55dd44ac3..c16cd2f491 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -3,13 +3,11 @@ import supertest from "supertest"; import { createRequestHandler } from "../server"; -import { Response } from "@remix-run/node"; - -import { createRequestHandler as createRemixRequestHandler } from "@remix-run/node/server"; +import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; // We don't want to test that the remix server works here (that's what the // puppetteer tests do), we just want to test the express adapter -jest.mock("@remix-run/node/server"); +jest.mock("@remix-run/server-runtime/server"); let mockedCreateRequestHandler = createRemixRequestHandler as jest.MockedFunction< typeof createRemixRequestHandler >; diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 10367b76f8..ccbfa45fc3 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -4,7 +4,8 @@ "version": "0.17.5", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.5" + "@remix-run/node": "0.17.5", + "@remix-run/server-runtime": "0.17.5" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index 3d7bd7b06b..e4d12a206f 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -1,16 +1,19 @@ import { PassThrough } from "stream"; -import { URL } from "url"; import type * as express from "express"; import type { AppLoadContext, - RequestInit, - Response, - ServerBuild + ServerBuild, + ServerPlatform +} from "@remix-run/server-runtime"; +import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; +import type { + RequestInit as NodeRequestInit, + Response as NodeResponse } from "@remix-run/node"; import { - Headers, - Request, - createRequestHandler as createRemixRequestHandler + Headers as NodeHeaders, + Request as NodeRequest, + formatServerError } from "@remix-run/node"; /** @@ -39,7 +42,8 @@ export function createRequestHandler({ getLoadContext?: GetLoadContextFunction; mode?: string; }) { - let handleRequest = createRemixRequestHandler(build, mode); + let platform: ServerPlatform = { formatServerError }; + let handleRequest = createRemixRequestHandler(build, platform, mode); return async ( req: express.Request, @@ -53,7 +57,10 @@ export function createRequestHandler({ ? getLoadContext(req, res) : undefined; - let response = await handleRequest(request, loadContext); + let response = ((await handleRequest( + (request as unknown) as Request, + loadContext + )) as unknown) as NodeResponse; sendRemixResponse(res, response); } catch (error) { @@ -66,8 +73,8 @@ export function createRequestHandler({ function createRemixHeaders( requestHeaders: express.Request["headers"] -): Headers { - return new Headers( +): NodeHeaders { + return new NodeHeaders( Object.keys(requestHeaders).reduce((memo, key) => { let value = requestHeaders[key]; @@ -82,11 +89,11 @@ function createRemixHeaders( ); } -function createRemixRequest(req: express.Request): Request { +function createRemixRequest(req: express.Request): NodeRequest { let origin = `${req.protocol}://${req.hostname}`; let url = new URL(req.url, origin); - let init: RequestInit = { + let init: NodeRequestInit = { method: req.method, headers: createRemixHeaders(req.headers) }; @@ -95,10 +102,13 @@ function createRemixRequest(req: express.Request): Request { init.body = req.pipe(new PassThrough({ highWaterMark: 16384 })); } - return new Request(url.toString(), init); + return new NodeRequest(url.toString(), init); } -function sendRemixResponse(res: express.Response, response: Response): void { +function sendRemixResponse( + res: express.Response, + response: NodeResponse +): void { res.status(response.status); for (let [key, value] of response.headers.entries()) { diff --git a/packages/remix-node/__tests__/cookies-test.ts b/packages/remix-node/__tests__/cookies-test.ts deleted file mode 100644 index f53a947487..0000000000 --- a/packages/remix-node/__tests__/cookies-test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { createCookie, isCookie } from "../cookies"; - -function getCookieFromSetCookie(setCookie: string): string { - return setCookie.split(/;\s*/)[0]; -} - -describe("isCookie", () => { - it("returns `true` for Cookie objects", () => { - expect(isCookie(createCookie("my-cookie"))).toBe(true); - }); - - it("returns `false` for non-Cookie objects", () => { - expect(isCookie({})).toBe(false); - expect(isCookie([])).toBe(false); - expect(isCookie("")).toBe(false); - expect(isCookie(true)).toBe(false); - }); -}); - -describe("cookies", () => { - it("parses/serializes empty string values", () => { - let cookie = createCookie("my-cookie"); - let setCookie = cookie.serialize(""); - let value = cookie.parse(getCookieFromSetCookie(setCookie)); - - expect(value).toMatchInlineSnapshot(`""`); - }); - - it("parses/serializes unsigned string values", () => { - let cookie = createCookie("my-cookie"); - let setCookie = cookie.serialize("hello world"); - let value = cookie.parse(getCookieFromSetCookie(setCookie)); - - expect(value).toEqual("hello world"); - }); - - it("parses/serializes unsigned boolean values", () => { - let cookie = createCookie("my-cookie"); - let setCookie = cookie.serialize(true); - let value = cookie.parse(getCookieFromSetCookie(setCookie)); - - expect(value).toBe(true); - }); - - it("parses/serializes signed string values", () => { - let cookie = createCookie("my-cookie", { - secrets: ["secret1"] - }); - let setCookie = cookie.serialize("hello michael"); - let value = cookie.parse(getCookieFromSetCookie(setCookie)); - - expect(value).toMatchInlineSnapshot(`"hello michael"`); - }); - - it("parses/serializes signed object values", () => { - let cookie = createCookie("my-cookie", { - secrets: ["secret1"] - }); - let setCookie = cookie.serialize({ hello: "mjackson" }); - let value = cookie.parse(getCookieFromSetCookie(setCookie)); - - expect(value).toMatchInlineSnapshot(` - Object { - "hello": "mjackson", - } - `); - }); - - it("supports secret rotation", () => { - let cookie = createCookie("my-cookie", { - secrets: ["secret1"] - }); - let setCookie = cookie.serialize({ hello: "mjackson" }); - let value = cookie.parse(getCookieFromSetCookie(setCookie)); - - expect(value).toMatchInlineSnapshot(` - Object { - "hello": "mjackson", - } - `); - - // A new secret enters the rotation... - cookie = createCookie("my-cookie", { - secrets: ["secret2", "secret1"] - }); - - // cookie should still be able to parse old cookies. - let oldValue = cookie.parse(getCookieFromSetCookie(setCookie)); - expect(oldValue).toMatchObject(value); - - // New Set-Cookie should be different, it uses a differet secret. - let setCookie2 = cookie.serialize(value); - expect(setCookie).not.toEqual(setCookie2); - }); -}); diff --git a/packages/remix-node/__tests__/errors-test.ts b/packages/remix-node/__tests__/errors-test.ts new file mode 100644 index 0000000000..e3584007ca --- /dev/null +++ b/packages/remix-node/__tests__/errors-test.ts @@ -0,0 +1,61 @@ +import { + getSourceContentForPosition, + relativeFilename, + UNKNOWN_LOCATION_POSITION +} from "../errors"; + +describe("getSourceContentForPosition", () => { + it("returns unknown position when no pos", () => { + expect(getSourceContentForPosition(null!, null)).toBe( + UNKNOWN_LOCATION_POSITION + ); + }); + + it("returns unknown position when no source", () => { + expect(getSourceContentForPosition(null!, {} as any)).toBe( + UNKNOWN_LOCATION_POSITION + ); + }); + + it("returns unknown position when no line", () => { + expect(getSourceContentForPosition(null!, { source: "yay!" } as any)).toBe( + UNKNOWN_LOCATION_POSITION + ); + }); + + it("returns trimmed source", () => { + let smc = { + sourceContentFor: jest.fn(() => "\n test() \n") + }; + + expect( + getSourceContentForPosition( + smc as any, + { source: "yay!", line: 2 } as any + ) + ).toBe("test()"); + }); +}); + +describe("relativeFilename", () => { + let root = process.cwd() + "/"; + let baseFilename = "./app/test.jsx"; + it("returns original filename", () => { + expect(relativeFilename(baseFilename)).toBe(baseFilename); + }); + + it("returns clean filename for route-module: prefix", () => { + let filename = "route-module:./app/test.jsx"; + expect(relativeFilename(filename)).toBe(baseFilename); + }); + + it("returns clean filename for absolute path route-module: prefix", () => { + let filename = `route-module:${root}app/test.jsx`; + expect(relativeFilename(filename)).toBe(baseFilename); + }); + + it("returns clean filename for absolute path route-module: prefix with extra stuff", () => { + let filename = `extra-stuff:route-module:${root}app/test.jsx`; + expect(relativeFilename(filename)).toBe(baseFilename); + }); +}); diff --git a/packages/remix-node/__tests__/sessions-test.ts b/packages/remix-node/__tests__/sessions-test.ts index 3ba2393a73..6e617c96af 100644 --- a/packages/remix-node/__tests__/sessions-test.ts +++ b/packages/remix-node/__tests__/sessions-test.ts @@ -2,133 +2,12 @@ import path from "path"; import { promises as fsp } from "fs"; import os from "os"; -import { createSession, isSession } from "../sessions"; -import { createCookieSessionStorage } from "../sessions/cookieStorage"; import { createFileSessionStorage } from "../sessions/fileStorage"; -import { createMemorySessionStorage } from "../sessions/memoryStorage"; function getCookieFromSetCookie(setCookie: string): string { return setCookie.split(/;\s*/)[0]; } -describe("Session", () => { - it("has an empty id by default", () => { - expect(createSession().id).toEqual(""); - }); - - it("correctly stores and retrieves values", () => { - let session = createSession(); - - session.set("user", "mjackson"); - session.flash("error", "boom"); - - expect(session.has("user")).toBe(true); - expect(session.get("user")).toBe("mjackson"); - // Normal values should remain in the session after get() - expect(session.has("user")).toBe(true); - expect(session.get("user")).toBe("mjackson"); - - expect(session.has("error")).toBe(true); - expect(session.get("error")).toBe("boom"); - // Flash values disappear after the first get() - expect(session.has("error")).toBe(false); - expect(session.get("error")).toBeUndefined(); - - session.unset("user"); - - expect(session.has("user")).toBe(false); - expect(session.get("user")).toBeUndefined(); - }); -}); - -describe("isSession", () => { - it("returns `true` for Session objects", () => { - expect(isSession(createSession())).toBe(true); - }); - - it("returns `false` for non-Session objects", () => { - expect(isSession({})).toBe(false); - expect(isSession([])).toBe(false); - expect(isSession("")).toBe(false); - expect(isSession(true)).toBe(false); - }); -}); - -describe("In-memory session storage", () => { - it("persists session data across requests", async () => { - let { getSession, commitSession } = createMemorySessionStorage({ - cookie: { secrets: ["secret1"] } - }); - let session = await getSession(); - session.set("user", "mjackson"); - let setCookie = await commitSession(session); - session = await getSession(getCookieFromSetCookie(setCookie)); - - expect(session.get("user")).toEqual("mjackson"); - }); -}); - -describe("Cookie session storage", () => { - it("persists session data across requests", async () => { - let { getSession, commitSession } = createCookieSessionStorage({ - cookie: { secrets: ["secret1"] } - }); - let session = await getSession(); - session.set("user", "mjackson"); - let setCookie = await commitSession(session); - session = await getSession(getCookieFromSetCookie(setCookie)); - - expect(session.get("user")).toEqual("mjackson"); - }); - - it("returns an empty session for cookies that are not signed properly", async () => { - let { getSession, commitSession } = createCookieSessionStorage({ - cookie: { secrets: ["secret1"] } - }); - let session = await getSession(); - session.set("user", "mjackson"); - - expect(session.get("user")).toEqual("mjackson"); - - let setCookie = await commitSession(session); - session = await getSession( - // Tamper with the session cookie... - getCookieFromSetCookie(setCookie).slice(0, -1) - ); - - expect(session.get("user")).toBeUndefined(); - }); - - describe("when a new secret shows up in the rotation", () => { - it("unsigns old session cookies using the old secret and encodes new cookies using the new secret", async () => { - let { getSession, commitSession } = createCookieSessionStorage({ - cookie: { secrets: ["secret1"] } - }); - let session = await getSession(); - session.set("user", "mjackson"); - let setCookie = await commitSession(session); - session = await getSession(getCookieFromSetCookie(setCookie)); - - expect(session.get("user")).toEqual("mjackson"); - - // A new secret enters the rotation... - let storage = createCookieSessionStorage({ - cookie: { secrets: ["secret2", "secret1"] } - }); - getSession = storage.getSession; - commitSession = storage.commitSession; - - // Old cookies should still work with the old secret. - session = await storage.getSession(getCookieFromSetCookie(setCookie)); - expect(session.get("user")).toEqual("mjackson"); - - // New cookies should be signed using the new secret. - let setCookie2 = await storage.commitSession(session); - expect(setCookie2).not.toEqual(setCookie); - }); - }); -}); - describe("File session storage", () => { let dir = path.join(os.tmpdir(), "file-session-storage"); diff --git a/packages/remix-node/base64.ts b/packages/remix-node/base64.ts new file mode 100644 index 0000000000..f4d2f50fff --- /dev/null +++ b/packages/remix-node/base64.ts @@ -0,0 +1,7 @@ +export function atob(a: string): string { + return Buffer.from(a, "base64").toString("binary"); +} + +export function btoa(b: string): string { + return Buffer.from(b, "binary").toString("base64"); +} diff --git a/packages/remix-node/errors.ts b/packages/remix-node/errors.ts index f4a49da028..dd9f72688f 100644 --- a/packages/remix-node/errors.ts +++ b/packages/remix-node/errors.ts @@ -1,66 +1,109 @@ -/** - * This thing probably warrants some explanation. - * - * The whole point here is to emulate componentDidCatch for server rendering and - * data loading. It can get tricky. React can do this on component boundaries - * but doesn't support it for server rendering or data loading. We know enough - * with nested routes to be able to emulate the behavior (because we know them - * statically before rendering.) - * - * Each route can export an `ErrorBoundary`. - * - * - When rendering throws an error, the nearest error boundary will render - * (normal react componentDidCatch). This will be the route's own boundary, but - * if none is provided, it will bubble up to the parents. - * - When data loading throws an error, the nearest error boundary will render - * - When performing an action, the nearest error boundary for the action's - * route tree will render (no redirect happens) - * - * During normal react rendering, we do nothing special, just normal - * componentDidCatch. - * - * For server rendering, we mutate `renderBoundaryRouteId` to know the last - * layout that has an error boundary that tried to render. This emulates which - * layout would catch a thrown error. If the rendering fails, we catch the error - * on the server, and go again a second time with the emulator holding on to the - * information it needs to render the same error boundary as a dynamically - * thrown render error. - * - * When data loading, server or client side, we use the emulator to likewise - * hang on to the error and re-render at the appropriate layout (where a thrown - * error would have been caught by cDC). - * - * When actions throw, it all works the same. There's an edge case to be aware - * of though. Actions normally are required to redirect, but in the case of - * errors, we render the action's route with the emulator holding on to the - * error. If during this render a parent route/loader throws we ignore that new - * error and render the action's original error as deeply as possible. In other - * words, we simply ignore the new error and use the action's error in place - * because it came first, and that just wouldn't be fair to let errors cut in - * line. - */ - -export interface ComponentDidCatchEmulator { - error?: SerializedError; - loaderBoundaryRouteId: string | null; - // `null` means the app layout threw before any routes rendered - renderBoundaryRouteId: string | null; - trackBoundaries: boolean; +import fs from "fs"; +import fsp from "fs/promises"; +import path from "path"; + +import type { NullableMappedPosition } from "source-map"; +import { SourceMapConsumer } from "source-map"; + +const ROOT = process.cwd() + path.sep; +const SOURCE_PATTERN = /(?\s+at.+)\((?.+):(?\d+):(?\d+)\)/; + +export const UNKNOWN_LOCATION_POSITION = ""; + +export async function formatServerError(error: Error): Promise { + error.stack = await formatStackTrace(error); + return error; } -export interface SerializedError { - message: string; - stack?: string; +export async function formatStackTrace(error: Error) { + const cache = new Map(); + const lines = error.stack?.split("\n") || []; + const promises = lines.map(line => mapToSourceFile(cache, line)); + const stack = (await Promise.all(promises)).join("\n") || error.stack; + + return stack; +} + +export async function mapToSourceFile( + cache: Map, + stackLine: string +) { + let match = SOURCE_PATTERN.exec(stackLine); + + if (!match?.groups) { + // doesn't match pattern but may still have a filename + return relativeFilename(stackLine); + } + + let { at, filename } = match.groups; + let line: number | string = match.groups.line; + let column: number | string = match.groups.column; + let mapFilename = `${filename}.map`; + let smc = cache.get(mapFilename); + filename = relativeFilename(filename); + + if (!smc) { + if (await fileExists(mapFilename)) { + // read source map and setup consumer + const map = JSON.parse(await fsp.readFile(mapFilename, "utf-8")); + map.sourceRoot = path.dirname(mapFilename); + smc = await new SourceMapConsumer(map); + cache.set(mapFilename, smc); + } + } + + if (smc) { + const pos = getOriginalSourcePosition( + smc, + parseInt(line, 10), + parseInt(column, 10) + ); + + if (pos.source) { + filename = relativeFilename(pos.source); + line = pos.line || "?"; + column = pos.column || "?"; + at = ` at \`${getSourceContentForPosition(smc, pos)}\` `; + } + } + + return `${at}(${filename}:${line}:${column})`; +} + +export function relativeFilename(filename: string) { + if (filename.includes("route-module:")) { + filename = filename.substring(filename.indexOf("route-module:")); + } + return filename.replace("route-module:", "").replace(ROOT, "./"); +} + +export function getOriginalSourcePosition( + smc: SourceMapConsumer, + line: number, + column: number +) { + return smc.originalPositionFor({ line, column }); +} + +export function getSourceContentForPosition( + smc: SourceMapConsumer, + pos: NullableMappedPosition +) { + let src: string | null = null; + if (pos?.source && typeof pos.line === "number") { + src = smc.sourceContentFor(pos.source); + } + + if (!src) { + return UNKNOWN_LOCATION_POSITION; + } + + return src.split("\n")[pos.line! - 1].trim(); } -export function serializeError(error: Error): SerializedError { - return { - message: error.message, - stack: - error.stack && - error.stack.replace( - /\((.+?)\)/g, - (_match: string, file: string) => `(file://${file})` - ) - }; +function fileExists(filename: string) { + return fsp + .access(filename, fs.constants.F_OK) + .then(() => true) + .catch(() => false); } diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts index ea6186748b..2a62e6a408 100644 --- a/packages/remix-node/globals.ts +++ b/packages/remix-node/globals.ts @@ -1,24 +1,32 @@ -import { - Headers as NodeHeaders, - Request as NodeRequest, - Response as NodeResponse, - fetch as nodeFetch -} from "./fetch"; +import crypto from "crypto"; + +import { atob, btoa } from "./base64"; +import { Headers, Request, Response, fetch } from "./fetch"; declare global { namespace NodeJS { interface Global { - Headers: typeof NodeHeaders; - Request: typeof NodeRequest; - Response: typeof NodeResponse; - fetch: typeof nodeFetch; + atob: typeof atob; + btoa: typeof btoa; + Headers: typeof Headers; + Request: typeof Request; + Response: typeof Response; + fetch: typeof fetch; + crypto: Crypto; } } } export function installGlobals() { - (global as NodeJS.Global).Headers = NodeHeaders; - (global as NodeJS.Global).Request = NodeRequest; - (global as NodeJS.Global).Response = NodeResponse; - (global as NodeJS.Global).fetch = nodeFetch; + global.atob = atob; + global.btoa = btoa; + + (global as NodeJS.Global).Headers = Headers; + (global as NodeJS.Global).Request = Request; + (global as NodeJS.Global).Response = Response; + (global as NodeJS.Global).fetch = fetch; + + // TODO: Missing types + // @ts-expect-error + global.crypto = crypto.webcrypto; } diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index 9f44d8b389..98a151df67 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -1,19 +1,4 @@ -import "./assetImportTypes"; - -export type { ServerBuild, ServerEntryModule } from "./build"; - -export type { - CookieParseOptions, - CookieSerializeOptions, - CookieSignatureOptions, - CookieOptions, - Cookie -} from "./cookies"; -export { createCookie, isCookie } from "./cookies"; - -export type { AppLoadContext, AppData } from "./data"; - -export type { EntryContext } from "./entry"; +export { formatServerError } from "./errors"; export type { HeadersInit, @@ -25,36 +10,12 @@ export { Headers, Request, Response, fetch } from "./fetch"; export { installGlobals } from "./globals"; -export type { - LinkDescriptor, - HTMLLinkDescriptor, - BlockLinkDescriptor, - PageLinkDescriptor -} from "./links"; +export { createFileSessionStorage } from "./sessions/fileStorage"; + +export { json, redirect } from "./responses"; export type { ActionFunction, - ErrorBoundaryComponent, HeadersFunction, - LinksFunction, - LoaderFunction, - MetaFunction, - RouteComponent, - RouteHandle + LoaderFunction } from "./routeModules"; - -export { json, redirect } from "./responses"; - -export type { RequestHandler } from "./server"; -export { createRequestHandler } from "./server"; - -export type { - SessionData, - Session, - SessionStorage, - SessionIdStorageStrategy -} from "./sessions"; -export { createSession, isSession, createSessionStorage } from "./sessions"; -export { createCookieSessionStorage } from "./sessions/cookieStorage"; -export { createFileSessionStorage } from "./sessions/fileStorage"; -export { createMemorySessionStorage } from "./sessions/memoryStorage"; diff --git a/packages/remix-node/magicExports/platform.ts b/packages/remix-node/magicExports/platform.ts new file mode 100644 index 0000000000..fef73ca92d --- /dev/null +++ b/packages/remix-node/magicExports/platform.ts @@ -0,0 +1,10 @@ +// This file lists all exports from this package that are available to `import +// "remix"`. + +export type { + ActionFunction, + HeadersFunction, + LoaderFunction +} from "@remix-run/node"; + +export { createFileSessionStorage, json, redirect } from "@remix-run/node"; diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index da857ca8e3..e3a3b76b20 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,25 +1,11 @@ { "name": "@remix-run/node", - "description": "Node.js bindings for Remix", + "description": "Node.js platform abstractions for Remix", "version": "0.17.5", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@types/cookie": "^0.4.0", - "@types/node-fetch": "^2.5.7", - "cookie": "^0.4.1", - "cookie-signature": "^1.1.0", - "history": "^5.0.0", - "jsesc": "^3.0.1", - "node-fetch": "^2.6.1", - "react-router-dom": "^6.0.0-beta.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - }, - "devDependencies": { - "@types/cookie-signature": "^1.0.3", - "@types/jsesc": "^2.5.1" + "@remix-run/server-runtime": "0.17.5", + "source-map": "^0.7.3" }, "sideEffects": false } diff --git a/packages/remix-node/responses.ts b/packages/remix-node/responses.ts index a3931aec69..f0bfb584d5 100644 --- a/packages/remix-node/responses.ts +++ b/packages/remix-node/responses.ts @@ -1,38 +1,15 @@ -import type { ResponseInit } from "./fetch"; -import { Headers, Response } from "./fetch"; +import { + json as coreJson, + redirect as coreRedirect +} from "@remix-run/server-runtime"; -/** - * A JSON response. Converts `data` to JSON and sets the `Content-Type` header. - */ -export function json(data: any, init: number | ResponseInit = {}): Response { - if (typeof init === "number") { - init = { status: init }; - } +import type { + Response as NodeResponse, + ResponseInit as NodeResponseInit +} from "./fetch"; - let headers = new Headers(init.headers); - if (!headers.has("Content-Type")) { - headers.set("Content-Type", "application/json; charset=utf-8"); - } +export let json = (data: any, init: NodeResponseInit = {}) => + (coreJson(data, init as ResponseInit) as unknown) as NodeResponse; - return new Response(JSON.stringify(data), { ...init, headers }); -} - -/** - * A redirect response. Sets the status code and the `Location` header. - * Defaults to "302 Found". - */ -export function redirect( - url: string, - init: number | ResponseInit = 302 -): Response { - if (typeof init === "number") { - init = { status: init }; - } else if (typeof init.status === "undefined") { - init.status = 302; - } - - let headers = new Headers(init.headers); - headers.set("Location", url); - - return new Response("", { ...init, headers }); -} +export let redirect = (url: string, init: number | NodeResponseInit = 302) => + (coreRedirect(url, init as ResponseInit) as unknown) as NodeResponse; diff --git a/packages/remix-node/routeModules.ts b/packages/remix-node/routeModules.ts index b8306fe56e..594d0c2eb0 100644 --- a/packages/remix-node/routeModules.ts +++ b/packages/remix-node/routeModules.ts @@ -1,94 +1,13 @@ -import type { Location } from "history"; -import type { ComponentType } from "react"; -import type { Params } from "react-router"; // TODO: import/export from react-router-dom +import type { + ActionFunction as CoreActionFunction, + HeadersFunction as CoreHeadersFunction, + LoaderFunction as CoreLoaderFunction +} from "@remix-run/server-runtime"; -import type { AppLoadContext, AppData } from "./data"; import type { Headers, HeadersInit, Request, Response } from "./fetch"; -import type { LinkDescriptor } from "./links"; -import type { RouteData } from "./routeData"; -export interface RouteModules { - [routeId: string]: RouteModule; -} +export type ActionFunction = CoreActionFunction; -/** - * A function that handles data mutations for a route. - */ -export interface ActionFunction { - (args: { request: Request; context: AppLoadContext; params: Params }): - | Promise - | Response - | string; -} +export type HeadersFunction = CoreHeadersFunction; -/** - * A React component that is rendered when there is an error on a route. - */ -export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; - -/** - * A function that returns HTTP headers to be used for a route. These headers - * will be merged with (and take precedence over) headers from parent routes. - */ -export interface HeadersFunction { - (args: { loaderHeaders: Headers; parentHeaders: Headers }): - | Headers - | HeadersInit; -} - -/** - * A function that defines `` tags to be inserted into the `` of - * the document on route transitions. - */ -export interface LinksFunction { - (args: { data: AppData }): LinkDescriptor[]; -} - -/** - * A function that loads data for a route. - */ -export interface LoaderFunction { - (args: { request: Request; context: AppLoadContext; params: Params }): - | Promise - | Response - | Promise - | AppData; -} - -/** - * A function that returns an object of name + content pairs to use for - * `` tags for a route. These tags will be merged with (and take - * precedence over) tags from parent routes. - */ -export interface MetaFunction { - (args: { - data: AppData; - parentsData: RouteData; - params: Params; - location: Location; - }): { [name: string]: string }; -} - -/** - * A React component that is rendered for a route. - */ -export type RouteComponent = ComponentType<{}>; - -/** - * An arbitrary object that is associated with a route. - */ -export type RouteHandle = any; - -export interface EntryRouteModule { - ErrorBoundary?: ErrorBoundaryComponent; - default: RouteComponent; - handle?: RouteHandle; - links?: LinksFunction; - meta?: MetaFunction; -} - -export interface ServerRouteModule extends EntryRouteModule { - action?: ActionFunction; - headers?: HeadersFunction; - loader?: LoaderFunction; -} +export type LoaderFunction = CoreLoaderFunction; diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts index 46153723b6..600b60ba42 100644 --- a/packages/remix-node/sessions/fileStorage.ts +++ b/packages/remix-node/sessions/fileStorage.ts @@ -1,9 +1,11 @@ -import { randomBytes } from "crypto"; import { promises as fsp } from "fs"; import * as path from "path"; -import type { SessionStorage, SessionIdStorageStrategy } from "../sessions"; -import { createSessionStorage } from "../sessions"; +import type { + SessionStorage, + SessionIdStorageStrategy +} from "@remix-run/server-runtime"; +import { createSessionStorage } from "@remix-run/server-runtime"; interface FileSessionStorageOptions { /** @@ -34,11 +36,13 @@ export function createFileSessionStorage({ let content = JSON.stringify({ data, expires }); while (true) { + let randomBytes = new Uint8Array(8); + crypto.getRandomValues(randomBytes); // This storage manages an id space of 2^64 ids, which is far greater // than the maximum number of files allowed on an NTFS or ext4 volume // (2^32). However, the larger id space should help to avoid collisions // with existing ids when creating new sessions, which speeds things up. - let id = randomBytes(8).toString("hex"); + let id = Buffer.from(randomBytes).toString("hex"); try { let file = getFile(dir, id); diff --git a/packages/remix-server-runtime/__tests__/cookies-test.ts b/packages/remix-server-runtime/__tests__/cookies-test.ts new file mode 100644 index 0000000000..a5de0661c5 --- /dev/null +++ b/packages/remix-server-runtime/__tests__/cookies-test.ts @@ -0,0 +1,121 @@ +import { createCookie, isCookie } from "../cookies"; + +function getCookieFromSetCookie(setCookie: string): string { + return setCookie.split(/;\s*/)[0]; +} + +describe("isCookie", () => { + it("returns `true` for Cookie objects", () => { + expect(isCookie(createCookie("my-cookie"))).toBe(true); + }); + + it("returns `false` for non-Cookie objects", () => { + expect(isCookie({})).toBe(false); + expect(isCookie([])).toBe(false); + expect(isCookie("")).toBe(false); + expect(isCookie(true)).toBe(false); + }); +}); + +describe("cookies", () => { + it("parses/serializes empty string values", async () => { + let cookie = createCookie("my-cookie"); + let setCookie = await cookie.serialize(""); + let value = await cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toMatchInlineSnapshot(`""`); + }); + + it("parses/serializes unsigned string values", async () => { + let cookie = createCookie("my-cookie"); + let setCookie = await cookie.serialize("hello world"); + let value = await cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toEqual("hello world"); + }); + + it("parses/serializes unsigned boolean values", async () => { + let cookie = createCookie("my-cookie"); + let setCookie = await cookie.serialize(true); + let value = await cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toBe(true); + }); + + it("parses/serializes signed string values", async () => { + let cookie = createCookie("my-cookie", { + secrets: ["secret1"] + }); + let setCookie = await cookie.serialize("hello michael"); + let value = await cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toMatchInlineSnapshot(`"hello michael"`); + }); + + it("fails to parses signed string values with invalid signature", async () => { + let cookie = createCookie("my-cookie", { + secrets: ["secret1"] + }); + let setCookie = await cookie.serialize("hello michael"); + let cookie2 = createCookie("my-cookie", { + secrets: ["secret2"] + }); + let value = await cookie2.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toBe(null); + }); + + it("parses/serializes signed object values", async () => { + let cookie = createCookie("my-cookie", { + secrets: ["secret1"] + }); + let setCookie = await cookie.serialize({ hello: "mjackson" }); + let value = await cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toMatchInlineSnapshot(` + Object { + "hello": "mjackson", + } + `); + }); + + it("failes to parses signed object values with invalid signature", async () => { + let cookie = createCookie("my-cookie", { + secrets: ["secret1"] + }); + let setCookie = await cookie.serialize({ hello: "mjackson" }); + let cookie2 = createCookie("my-cookie", { + secrets: ["secret2"] + }); + let value = await cookie2.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toBeNull(); + }); + + it("supports secret rotation", async () => { + let cookie = createCookie("my-cookie", { + secrets: ["secret1"] + }); + let setCookie = await cookie.serialize({ hello: "mjackson" }); + let value = await cookie.parse(getCookieFromSetCookie(setCookie)); + + expect(value).toMatchInlineSnapshot(` + Object { + "hello": "mjackson", + } + `); + + // A new secret enters the rotation... + cookie = createCookie("my-cookie", { + secrets: ["secret2", "secret1"] + }); + + // cookie should still be able to parse old cookies. + let oldValue = await cookie.parse(getCookieFromSetCookie(setCookie)); + expect(oldValue).toMatchObject(value); + + // New Set-Cookie should be different, it uses a differet secret. + let setCookie2 = await cookie.serialize(value); + expect(setCookie).not.toEqual(setCookie2); + }); +}); diff --git a/packages/remix-node/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts similarity index 100% rename from packages/remix-node/__tests__/data-test.ts rename to packages/remix-server-runtime/__tests__/data-test.ts diff --git a/packages/remix-node/__tests__/responses-test.ts b/packages/remix-server-runtime/__tests__/responses-test.ts similarity index 100% rename from packages/remix-node/__tests__/responses-test.ts rename to packages/remix-server-runtime/__tests__/responses-test.ts diff --git a/packages/remix-server-runtime/__tests__/sessions-test.ts b/packages/remix-server-runtime/__tests__/sessions-test.ts new file mode 100644 index 0000000000..4dd7abf163 --- /dev/null +++ b/packages/remix-server-runtime/__tests__/sessions-test.ts @@ -0,0 +1,125 @@ +import { createSession, isSession } from "../sessions"; +import { createCookieSessionStorage } from "../sessions/cookieStorage"; +import { createMemorySessionStorage } from "../sessions/memoryStorage"; + +function getCookieFromSetCookie(setCookie: string): string { + return setCookie.split(/;\s*/)[0]; +} + +describe("Session", () => { + it("has an empty id by default", () => { + expect(createSession().id).toEqual(""); + }); + + it("correctly stores and retrieves values", () => { + let session = createSession(); + + session.set("user", "mjackson"); + session.flash("error", "boom"); + + expect(session.has("user")).toBe(true); + expect(session.get("user")).toBe("mjackson"); + // Normal values should remain in the session after get() + expect(session.has("user")).toBe(true); + expect(session.get("user")).toBe("mjackson"); + + expect(session.has("error")).toBe(true); + expect(session.get("error")).toBe("boom"); + // Flash values disappear after the first get() + expect(session.has("error")).toBe(false); + expect(session.get("error")).toBeUndefined(); + + session.unset("user"); + + expect(session.has("user")).toBe(false); + expect(session.get("user")).toBeUndefined(); + }); +}); + +describe("isSession", () => { + it("returns `true` for Session objects", () => { + expect(isSession(createSession())).toBe(true); + }); + + it("returns `false` for non-Session objects", () => { + expect(isSession({})).toBe(false); + expect(isSession([])).toBe(false); + expect(isSession("")).toBe(false); + expect(isSession(true)).toBe(false); + }); +}); + +describe("In-memory session storage", () => { + it("persists session data across requests", async () => { + let { getSession, commitSession } = createMemorySessionStorage({ + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + let setCookie = await commitSession(session); + session = await getSession(getCookieFromSetCookie(setCookie)); + + expect(session.get("user")).toEqual("mjackson"); + }); +}); + +describe("Cookie session storage", () => { + it("persists session data across requests", async () => { + let { getSession, commitSession } = createCookieSessionStorage({ + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + let setCookie = await commitSession(session); + session = await getSession(getCookieFromSetCookie(setCookie)); + + expect(session.get("user")).toEqual("mjackson"); + }); + + it("returns an empty session for cookies that are not signed properly", async () => { + let { getSession, commitSession } = createCookieSessionStorage({ + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + + expect(session.get("user")).toEqual("mjackson"); + + let setCookie = await commitSession(session); + session = await getSession( + // Tamper with the session cookie... + getCookieFromSetCookie(setCookie).slice(0, -1) + ); + + expect(session.get("user")).toBeUndefined(); + }); + + describe("when a new secret shows up in the rotation", () => { + it("unsigns old session cookies using the old secret and encodes new cookies using the new secret", async () => { + let { getSession, commitSession } = createCookieSessionStorage({ + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + let setCookie = await commitSession(session); + session = await getSession(getCookieFromSetCookie(setCookie)); + + expect(session.get("user")).toEqual("mjackson"); + + // A new secret enters the rotation... + let storage = createCookieSessionStorage({ + cookie: { secrets: ["secret2", "secret1"] } + }); + getSession = storage.getSession; + commitSession = storage.commitSession; + + // Old cookies should still work with the old secret. + session = await storage.getSession(getCookieFromSetCookie(setCookie)); + expect(session.get("user")).toEqual("mjackson"); + + // New cookies should be signed using the new secret. + let setCookie2 = await storage.commitSession(session); + expect(setCookie2).not.toEqual(setCookie); + }); + }); +}); diff --git a/packages/remix-server-runtime/__tests__/utils.ts b/packages/remix-server-runtime/__tests__/utils.ts new file mode 100644 index 0000000000..06ad0653dd --- /dev/null +++ b/packages/remix-server-runtime/__tests__/utils.ts @@ -0,0 +1,5 @@ +import prettier from "prettier"; + +export function prettyHtml(source: string): string { + return prettier.format(source, { parser: "html" }); +} diff --git a/packages/remix-node/assetImportTypes.ts b/packages/remix-server-runtime/assetImportTypes.ts similarity index 100% rename from packages/remix-node/assetImportTypes.ts rename to packages/remix-server-runtime/assetImportTypes.ts diff --git a/packages/remix-node/build.ts b/packages/remix-server-runtime/build.ts similarity index 90% rename from packages/remix-node/build.ts rename to packages/remix-server-runtime/build.ts index 4d55a75d47..afc1f5c073 100644 --- a/packages/remix-node/build.ts +++ b/packages/remix-server-runtime/build.ts @@ -1,5 +1,4 @@ import type { EntryContext, AssetsManifest } from "./entry"; -import type { Headers, Request, Response } from "./fetch"; import type { ServerRouteManifest } from "./routes"; /** diff --git a/packages/remix-server-runtime/cookieSigning.ts b/packages/remix-server-runtime/cookieSigning.ts new file mode 100644 index 0000000000..fed5cef6ae --- /dev/null +++ b/packages/remix-server-runtime/cookieSigning.ts @@ -0,0 +1,52 @@ +const encoder = new TextEncoder(); + +export async function sign(value: string, secret: string): Promise { + let key = await crypto.subtle.importKey( + "raw", + encoder.encode(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"] + ); + + let data = encoder.encode(value); + let signature = await crypto.subtle.sign("HMAC", key, data); + let hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace( + /=+$/, + "" + ); + + return value + "." + hash; +} + +export async function unsign( + cookie: string, + secret: string +): Promise { + let key = await crypto.subtle.importKey( + "raw", + encoder.encode(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["verify"] + ); + + let value = cookie.slice(0, cookie.lastIndexOf(".")); + let hash = cookie.slice(cookie.lastIndexOf(".") + 1); + + let data = encoder.encode(value); + let signature = byteStringToUint8Array(atob(hash)); + let valid = await crypto.subtle.verify("HMAC", key, signature, data); + + return valid ? value : false; +} + +function byteStringToUint8Array(byteString: string): Uint8Array { + let array = new Uint8Array(byteString.length); + + for (let i = 0; i < byteString.length; i++) { + array[i] = byteString.charCodeAt(i); + } + + return array; +} diff --git a/packages/remix-node/cookies.ts b/packages/remix-server-runtime/cookies.ts similarity index 80% rename from packages/remix-node/cookies.ts rename to packages/remix-server-runtime/cookies.ts index f8ac7a56c9..df1475725c 100644 --- a/packages/remix-node/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -1,6 +1,7 @@ import type { CookieParseOptions, CookieSerializeOptions } from "cookie"; import { parse, serialize } from "cookie"; -import { sign, unsign } from "cookie-signature"; + +import { sign, unsign } from "./cookieSigning"; export type { CookieParseOptions, CookieSerializeOptions }; @@ -53,13 +54,16 @@ export interface Cookie { * Parses a raw `Cookie` header and returns the value of this cookie or * `null` if it's not present. */ - parse(cookieHeader: string | null, options?: CookieParseOptions): any; + parse( + cookieHeader: string | null, + options?: CookieParseOptions + ): Promise; /** * Serializes the given value to a string and returns the `Set-Cookie` * header. */ - serialize(value: any, options?: CookieSerializeOptions): string; + serialize(value: any, options?: CookieSerializeOptions): Promise; } /** @@ -82,19 +86,19 @@ export function createCookie( ? new Date(Date.now() + options.maxAge * 1000) : options.expires; }, - parse(cookieHeader, parseOptions) { + async parse(cookieHeader, parseOptions) { if (!cookieHeader) return null; let cookies = parse(cookieHeader, { ...options, ...parseOptions }); return name in cookies ? cookies[name] === "" ? "" - : decodeCookieValue(cookies[name], secrets) + : await decodeCookieValue(cookies[name], secrets) : null; }, - serialize(value, serializeOptions) { + async serialize(value, serializeOptions) { return serialize( name, - value === "" ? "" : encodeCookieValue(value, secrets), + value === "" ? "" : await encodeCookieValue(value, secrets), { ...options, ...serializeOptions @@ -114,20 +118,26 @@ export function isCookie(object: any): object is Cookie { ); } -function encodeCookieValue(value: any, secrets: string[]): string { +async function encodeCookieValue( + value: any, + secrets: string[] +): Promise { let encoded = encodeData(value); if (secrets.length > 0) { - encoded = sign(encoded, secrets[0]); + encoded = await sign(encoded, secrets[0]); } return encoded; } -function decodeCookieValue(value: string, secrets: string[]): any { +async function decodeCookieValue( + value: string, + secrets: string[] +): Promise { if (secrets.length > 0) { for (let secret of secrets) { - let unsignedValue = unsign(value, secret); + let unsignedValue = await unsign(value, secret); if (unsignedValue !== false) { return decodeData(unsignedValue); } @@ -150,11 +160,3 @@ function decodeData(value: string): any { return {}; } } - -function btoa(b: string): string { - return Buffer.from(b, "binary").toString("base64"); -} - -function atob(a: string): string { - return Buffer.from(a, "base64").toString("binary"); -} diff --git a/packages/remix-node/data.ts b/packages/remix-server-runtime/data.ts similarity index 97% rename from packages/remix-node/data.ts rename to packages/remix-server-runtime/data.ts index 806bde9b2f..cd5184af91 100644 --- a/packages/remix-node/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -1,8 +1,6 @@ import type { Params } from "react-router"; import type { ServerBuild } from "./build"; -import type { Request } from "./fetch"; -import { Response } from "./fetch"; import { json } from "./responses"; /** diff --git a/packages/remix-node/entry.ts b/packages/remix-server-runtime/entry.ts similarity index 100% rename from packages/remix-node/entry.ts rename to packages/remix-server-runtime/entry.ts diff --git a/packages/remix-server-runtime/errors.ts b/packages/remix-server-runtime/errors.ts new file mode 100644 index 0000000000..9cea8a329a --- /dev/null +++ b/packages/remix-server-runtime/errors.ts @@ -0,0 +1,61 @@ +/** + * This thing probably warrants some explanation. + * + * The whole point here is to emulate componentDidCatch for server rendering and + * data loading. It can get tricky. React can do this on component boundaries + * but doesn't support it for server rendering or data loading. We know enough + * with nested routes to be able to emulate the behavior (because we know them + * statically before rendering.) + * + * Each route can export an `ErrorBoundary`. + * + * - When rendering throws an error, the nearest error boundary will render + * (normal react componentDidCatch). This will be the route's own boundary, but + * if none is provided, it will bubble up to the parents. + * - When data loading throws an error, the nearest error boundary will render + * - When performing an action, the nearest error boundary for the action's + * route tree will render (no redirect happens) + * + * During normal react rendering, we do nothing special, just normal + * componentDidCatch. + * + * For server rendering, we mutate `renderBoundaryRouteId` to know the last + * layout that has an error boundary that tried to render. This emulates which + * layout would catch a thrown error. If the rendering fails, we catch the error + * on the server, and go again a second time with the emulator holding on to the + * information it needs to render the same error boundary as a dynamically + * thrown render error. + * + * When data loading, server or client side, we use the emulator to likewise + * hang on to the error and re-render at the appropriate layout (where a thrown + * error would have been caught by cDC). + * + * When actions throw, it all works the same. There's an edge case to be aware + * of though. Actions normally are required to redirect, but in the case of + * errors, we render the action's route with the emulator holding on to the + * error. If during this render a parent route/loader throws we ignore that new + * error and render the action's original error as deeply as possible. In other + * words, we simply ignore the new error and use the action's error in place + * because it came first, and that just wouldn't be fair to let errors cut in + * line. + */ + +export interface ComponentDidCatchEmulator { + error?: SerializedError; + loaderBoundaryRouteId: string | null; + // `null` means the app layout threw before any routes rendered + renderBoundaryRouteId: string | null; + trackBoundaries: boolean; +} + +export interface SerializedError { + message: string; + stack?: string; +} + +export async function serializeError(error: Error): Promise { + return { + message: error.message, + stack: error.stack + }; +} diff --git a/packages/remix-node/headers.ts b/packages/remix-server-runtime/headers.ts similarity index 96% rename from packages/remix-node/headers.ts rename to packages/remix-server-runtime/headers.ts index 491c4a676c..3bd683c294 100644 --- a/packages/remix-node/headers.ts +++ b/packages/remix-server-runtime/headers.ts @@ -1,6 +1,4 @@ import type { ServerBuild } from "./build"; -import type { Response } from "./fetch"; -import { Headers } from "./fetch"; import type { ServerRoute } from "./routes"; import type { RouteMatch } from "./routeMatching"; diff --git a/packages/remix-server-runtime/index.ts b/packages/remix-server-runtime/index.ts new file mode 100644 index 0000000000..f96775d95f --- /dev/null +++ b/packages/remix-server-runtime/index.ts @@ -0,0 +1,51 @@ +import "./assetImportTypes"; + +export type { ServerBuild, ServerEntryModule } from "./build"; + +export type { + CookieParseOptions, + CookieSerializeOptions, + CookieSignatureOptions, + CookieOptions, + Cookie +} from "./cookies"; +export { createCookie, isCookie } from "./cookies"; + +export type { AppLoadContext, AppData } from "./data"; + +export type { EntryContext } from "./entry"; + +export type { + LinkDescriptor, + HTMLLinkDescriptor, + BlockLinkDescriptor, + PageLinkDescriptor +} from "./links"; + +export type { ServerPlatform } from "./platform"; + +export type { + ActionFunction, + ErrorBoundaryComponent, + HeadersFunction, + LinksFunction, + LoaderFunction, + MetaFunction, + RouteComponent, + RouteHandle +} from "./routeModules"; + +export { json, redirect } from "./responses"; + +export type { RequestHandler } from "./server"; +export { createRequestHandler } from "./server"; + +export type { + SessionData, + Session, + SessionStorage, + SessionIdStorageStrategy +} from "./sessions"; +export { createSession, isSession, createSessionStorage } from "./sessions"; +export { createCookieSessionStorage } from "./sessions/cookieStorage"; +export { createMemorySessionStorage } from "./sessions/memoryStorage"; diff --git a/packages/remix-node/invariant.ts b/packages/remix-server-runtime/invariant.ts similarity index 100% rename from packages/remix-node/invariant.ts rename to packages/remix-server-runtime/invariant.ts diff --git a/packages/remix-node/links.ts b/packages/remix-server-runtime/links.ts similarity index 100% rename from packages/remix-node/links.ts rename to packages/remix-server-runtime/links.ts diff --git a/packages/remix-node/magicExports/server.ts b/packages/remix-server-runtime/magicExports/server.ts similarity index 66% rename from packages/remix-node/magicExports/server.ts rename to packages/remix-server-runtime/magicExports/server.ts index 0304990134..ce0557a4bf 100644 --- a/packages/remix-node/magicExports/server.ts +++ b/packages/remix-server-runtime/magicExports/server.ts @@ -12,19 +12,12 @@ export type { AppLoadContext, AppData, EntryContext, - HeadersInit, - RequestInfo, - RequestInit, - ResponseInit, LinkDescriptor, HTMLLinkDescriptor, BlockLinkDescriptor, PageLinkDescriptor, - ActionFunction, ErrorBoundaryComponent, - HeadersFunction, LinksFunction, - LoaderFunction, MetaFunction, RouteComponent, RouteHandle, @@ -33,23 +26,14 @@ export type { Session, SessionStorage, SessionIdStorageStrategy -} from "@remix-run/node"; +} from "@remix-run/server-runtime"; export { createCookie, isCookie, - Headers, - Request, - Response, - fetch, - // installGlobals, // only needed by adapters - json, - redirect, - // createRequestHandler, // only needed by adapters createSession, isSession, createSessionStorage, createCookieSessionStorage, - createFileSessionStorage, createMemorySessionStorage -} from "@remix-run/node"; +} from "@remix-run/server-runtime"; diff --git a/packages/remix-node/mode.ts b/packages/remix-server-runtime/mode.ts similarity index 100% rename from packages/remix-node/mode.ts rename to packages/remix-server-runtime/mode.ts diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json new file mode 100644 index 0000000000..c1985e79fa --- /dev/null +++ b/packages/remix-server-runtime/package.json @@ -0,0 +1,23 @@ +{ + "name": "@remix-run/server-runtime", + "description": "Server runtime for Remix", + "version": "0.17.5", + "repository": "https://github.com/remix-run/packages", + "dependencies": { + "@types/cookie": "^0.4.0", + "@types/node-fetch": "^2.5.7", + "cookie": "^0.4.1", + "history": "^5.0.0", + "jsesc": "^3.0.1", + "react-router-dom": "^6.0.0-beta.0", + "source-map": "^0.7.3" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + }, + "devDependencies": { + "@types/jsesc": "^2.5.1" + }, + "sideEffects": false +} diff --git a/packages/remix-server-runtime/platform.ts b/packages/remix-server-runtime/platform.ts new file mode 100644 index 0000000000..e0da0cc21f --- /dev/null +++ b/packages/remix-server-runtime/platform.ts @@ -0,0 +1,17 @@ +/** + * This also probably warrants some explanation. + * + * The whole point here is to abstract out the server functionality that is required + * by the server runtime but is dependant on the platform runtime. + * + * An example of this is error beautification as it depends on loading sourcemaps from + * the file system in node, while functions hosted on cloudflare workers will not need + * to format as they have built in sourcemap support. + */ + +/** + * Abstracts functionality that is platform specific (node vs workers, etc.) + */ +export interface ServerPlatform { + formatServerError?(error: Error): Promise; +} diff --git a/packages/remix-server-runtime/responses.ts b/packages/remix-server-runtime/responses.ts new file mode 100644 index 0000000000..ca20c40492 --- /dev/null +++ b/packages/remix-server-runtime/responses.ts @@ -0,0 +1,43 @@ +/** + * A JSON response. Converts `data` to JSON and sets the `Content-Type` header. + */ +export function json(data: any, init: number | ResponseInit = {}): Response { + let responseInit: any = init; + if (typeof init === "number") { + responseInit = { status: init }; + } + + let headers = new Headers(responseInit.headers); + if (!headers.has("Content-Type")) { + headers.set("Content-Type", "application/json; charset=utf-8"); + } + + return new Response(JSON.stringify(data), { + ...responseInit, + headers + }); +} + +/** + * A redirect response. Sets the status code and the `Location` header. + * Defaults to "302 Found". + */ +export function redirect( + url: string, + init: number | ResponseInit = 302 +): Response { + let responseInit: any = init; + if (typeof init === "number") { + responseInit = { status: init }; + } else if (typeof responseInit.status === "undefined") { + responseInit.status = 302; + } + + let headers = new Headers(responseInit.headers); + headers.set("Location", url); + + return new Response("", { + ...responseInit, + headers + }); +} diff --git a/packages/remix-node/routeData.ts b/packages/remix-server-runtime/routeData.ts similarity index 93% rename from packages/remix-node/routeData.ts rename to packages/remix-server-runtime/routeData.ts index 87753309e9..d5315a4892 100644 --- a/packages/remix-node/routeData.ts +++ b/packages/remix-server-runtime/routeData.ts @@ -1,6 +1,5 @@ import type { AppData } from "./data"; import { extractData } from "./data"; -import type { Response } from "./fetch"; import type { ServerRoute } from "./routes"; import type { RouteMatch } from "./routeMatching"; diff --git a/packages/remix-node/routeMatching.ts b/packages/remix-server-runtime/routeMatching.ts similarity index 100% rename from packages/remix-node/routeMatching.ts rename to packages/remix-server-runtime/routeMatching.ts diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts new file mode 100644 index 0000000000..0c4154eceb --- /dev/null +++ b/packages/remix-server-runtime/routeModules.ts @@ -0,0 +1,98 @@ +import type { Location } from "history"; +import type { ComponentType } from "react"; +import type { Params } from "react-router"; // TODO: import/export from react-router-dom + +import type { AppLoadContext, AppData } from "./data"; +import type { LinkDescriptor } from "./links"; +import type { RouteData } from "./routeData"; + +export interface RouteModules { + [routeId: string]: RouteModule; +} + +/** + * A function that handles data mutations for a route. + */ +export interface ActionFunction { + (args: { request: TRequest; context: AppLoadContext; params: Params }): + | Promise + | TResponse + | Response + | string; +} + +/** + * A React component that is rendered when there is an error on a route. + */ +export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; + +/** + * A function that returns HTTP headers to be used for a route. These headers + * will be merged with (and take precedence over) headers from parent routes. + */ +export interface HeadersFunction< + THeaders = Headers, + THeadersInit = HeadersInit +> { + (args: { loaderHeaders: THeaders; parentHeaders: THeaders }): + | THeaders + | THeadersInit; +} + +/** + * A function that defines `` tags to be inserted into the `` of + * the document on route transitions. + */ +export interface LinksFunction { + (args: { data: AppData }): LinkDescriptor[]; +} + +/** + * A function that loads data for a route. + */ +export interface LoaderFunction { + (args: { request: TRequest; context: AppLoadContext; params: Params }): + | Promise + | TResponse + | Response + | Promise + | AppData; +} + +/** + * A function that returns an object of name + content pairs to use for + * `` tags for a route. These tags will be merged with (and take + * precedence over) tags from parent routes. + */ +export interface MetaFunction { + (args: { + data: AppData; + parentsData: RouteData; + params: Params; + location: Location; + }): { [name: string]: string }; +} + +/** + * A React component that is rendered for a route. + */ +export type RouteComponent = ComponentType<{}>; + +/** + * An arbitrary object that is associated with a route. + */ +export type RouteHandle = any; + +export interface EntryRouteModule { + ErrorBoundary?: ErrorBoundaryComponent; + default: RouteComponent; + handle?: RouteHandle; + links?: LinksFunction; + meta?: MetaFunction; +} + +export interface ServerRouteModule extends EntryRouteModule { + action?: ActionFunction; + headers?: HeadersFunction; + loader?: LoaderFunction; +} diff --git a/packages/remix-node/routes.ts b/packages/remix-server-runtime/routes.ts similarity index 100% rename from packages/remix-node/routes.ts rename to packages/remix-server-runtime/routes.ts diff --git a/packages/remix-server-runtime/scripts/postinstall.ts b/packages/remix-server-runtime/scripts/postinstall.ts new file mode 100644 index 0000000000..2d8fb3e493 --- /dev/null +++ b/packages/remix-server-runtime/scripts/postinstall.ts @@ -0,0 +1,28 @@ +import * as path from "path"; + +async function run() { + let packageJson = require("../package.json"); + + try { + await require("remix/magic").installMagicExports( + { [packageJson.name]: packageJson.version }, + path.resolve(__dirname, "..", "magicExports") + ); + } catch (error) { + if (error.code === "MODULE_NOT_FOUND") { + // ignore missing "remix" package + } else { + throw error; + } + } +} + +run().then( + () => { + process.exit(0); + }, + error => { + console.error(error); + process.exit(1); + } +); diff --git a/packages/remix-node/server.ts b/packages/remix-server-runtime/server.ts similarity index 85% rename from packages/remix-node/server.ts rename to packages/remix-server-runtime/server.ts index 600532bdec..de0b8b06df 100644 --- a/packages/remix-node/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -2,12 +2,12 @@ import type { AppLoadContext } from "./data"; import { loadRouteData, callRouteAction } from "./data"; import type { ComponentDidCatchEmulator } from "./errors"; -import { serializeError } from "./errors"; import type { ServerBuild } from "./build"; import type { EntryContext } from "./entry"; import { createEntryMatches, createEntryRouteModules } from "./entry"; -import { Response, Request } from "./fetch"; +import { serializeError } from "./errors"; import { getDocumentHeaders } from "./headers"; +import type { ServerPlatform } from "./platform"; import type { RouteMatch } from "./routeMatching"; import { matchServerRoutes } from "./routeMatching"; import { ServerMode, isServerMode } from "./mode"; @@ -16,7 +16,6 @@ import { createRoutes } from "./routes"; import { createRouteData } from "./routeData"; import { json } from "./responses"; import { createServerHandoffString } from "./serverHandoff"; -import { RequestInit } from "node-fetch"; /** * The main request handler for a Remix server. This handler runs in the context @@ -32,6 +31,7 @@ export interface RequestHandler { */ export function createRequestHandler( build: ServerBuild, + platform: ServerPlatform, mode?: string ): RequestHandler { let routes = createRoutes(build.routes); @@ -39,14 +39,22 @@ export function createRequestHandler( return (request, loadContext = {}) => isDataRequest(request) - ? handleDataRequest(request, loadContext, build, routes) - : handleDocumentRequest(request, loadContext, build, routes, serverMode); + ? handleDataRequest(request, loadContext, build, platform, routes) + : handleDocumentRequest( + request, + loadContext, + build, + platform, + routes, + serverMode + ); } async function handleDataRequest( request: Request, loadContext: AppLoadContext, build: ServerBuild, + platform: ServerPlatform, routes: ServerRoute[] ): Promise { let url = new URL(request.url); @@ -76,7 +84,7 @@ async function handleDataRequest( routeMatch = match; } - let clonedRequest = await stripDataParam(request); + let clonedRequest = stripDataParam(request); let response: Response; try { @@ -96,7 +104,8 @@ async function handleDataRequest( routeMatch.params ); } catch (error) { - return json(serializeError(error), { + let formattedError = (await platform.formatServerError?.(error)) || error; + return json(await serializeError(formattedError), { status: 500, headers: { "X-Remix-Error": "unfortunately, yes" @@ -108,15 +117,13 @@ async function handleDataRequest( // We don't have any way to prevent a fetch request from following // redirects. So we use the `X-Remix-Redirect` header to indicate the // next URL, and then "follow" the redirect manually on the client. - let locationHeader = response.headers.get("Location"); - response.headers.delete("Location"); + let headers = new Headers(response.headers); + headers.set("X-Remix-Redirect", headers.get("Location")!); + headers.delete("Location"); return new Response("", { status: 204, - headers: { - ...Object.fromEntries(response.headers), - "X-Remix-Redirect": locationHeader! - } + headers }); } @@ -127,6 +134,7 @@ async function handleDocumentRequest( request: Request, loadContext: AppLoadContext, build: ServerBuild, + platform: ServerPlatform, routes: ServerRoute[], serverMode: ServerMode ): Promise { @@ -153,21 +161,23 @@ async function handleDocumentRequest( if (isActionRequest(request)) { let leafMatch = matches[matches.length - 1]; try { - let response = await callRouteAction( + let actionResponse = await callRouteAction( build, leafMatch.route.id, request, loadContext, leafMatch.params ); - - return response; + if (actionResponse && isRedirectResponse(actionResponse)) { + return actionResponse; + } } catch (error) { + let formattedError = (await platform.formatServerError?.(error)) || error; actionErrored = true; let withBoundaries = getMatchesUpToDeepestErrorBoundary(matches); componentDidCatchEmulator.loaderBoundaryRouteId = withBoundaries[withBoundaries.length - 1].route.id; - componentDidCatchEmulator.error = serializeError(error); + componentDidCatchEmulator.error = await serializeError(formattedError); } } @@ -233,7 +243,10 @@ async function handleDocumentRequest( ); } - componentDidCatchEmulator.error = serializeError(response); + let formattedError = + (await platform.formatServerError?.(response)) || response; + + componentDidCatchEmulator.error = await serializeError(formattedError); routeLoaderResults[index] = json(null, { status: 500 }); } else if (isRedirectResponse(response)) { return response; @@ -294,8 +307,9 @@ async function handleDocumentRequest( entryContext ); } catch (error) { + let formattedError = (await platform.formatServerError?.(error)) || error; if (serverMode !== ServerMode.Test) { - console.error(error); + console.error(formattedError); } statusCode = 500; @@ -307,7 +321,7 @@ async function handleDocumentRequest( // tracking the `routeId` as we render because we already have an error to // render. componentDidCatchEmulator.trackBoundaries = false; - componentDidCatchEmulator.error = serializeError(error); + componentDidCatchEmulator.error = await serializeError(formattedError); entryContext.serverHandoffString = createServerHandoffString(serverHandoff); try { @@ -318,17 +332,21 @@ async function handleDocumentRequest( entryContext ); } catch (error) { + let formattedError = (await platform.formatServerError?.(error)) || error; if (serverMode !== ServerMode.Test) { - console.error(error); + console.error(formattedError); } // Good grief folks, get your act together 😂! - response = new Response(`Unexpected Server Error\n\n${error.message}`, { - status: 500, - headers: { - "Content-Type": "text/plain" + response = new Response( + `Unexpected Server Error\n\n${formattedError.message}`, + { + status: 500, + headers: { + "Content-Type": "text/plain" + } } - }); + ); } } @@ -353,17 +371,10 @@ function isRedirectResponse(response: Response): boolean { return redirectStatusCodes.has(response.status); } -async function stripDataParam(og: Request) { - let url = new URL(og.url); +function stripDataParam(request: Request) { + let url = new URL(request.url); url.searchParams.delete("_data"); - let init: RequestInit = { - method: og.method, - headers: og.headers - }; - if (og.method.toLowerCase() !== "get") { - init.body = await og.text(); - } - return new Request(url, init); + return new Request(url.toString(), request); } // This ensures we only load the data for the routes above an action error diff --git a/packages/remix-node/serverHandoff.ts b/packages/remix-server-runtime/serverHandoff.ts similarity index 100% rename from packages/remix-node/serverHandoff.ts rename to packages/remix-server-runtime/serverHandoff.ts diff --git a/packages/remix-node/sessions.ts b/packages/remix-server-runtime/sessions.ts similarity index 98% rename from packages/remix-node/sessions.ts rename to packages/remix-server-runtime/sessions.ts index ea26f6c690..b03596ab87 100644 --- a/packages/remix-node/sessions.ts +++ b/packages/remix-server-runtime/sessions.ts @@ -214,7 +214,7 @@ export function createSessionStorage({ return { async getSession(cookieHeader, options) { - let id = cookieHeader && cookie.parse(cookieHeader, options); + let id = cookieHeader && (await cookie.parse(cookieHeader, options)); let data = id && (await readData(id)); return createSession(data || {}, id || ""); }, diff --git a/packages/remix-node/sessions/cookieStorage.ts b/packages/remix-server-runtime/sessions/cookieStorage.ts similarity index 95% rename from packages/remix-node/sessions/cookieStorage.ts rename to packages/remix-server-runtime/sessions/cookieStorage.ts index cdaa221804..44f63dba10 100644 --- a/packages/remix-node/sessions/cookieStorage.ts +++ b/packages/remix-server-runtime/sessions/cookieStorage.ts @@ -31,7 +31,7 @@ export function createCookieSessionStorage({ return { async getSession(cookieHeader, options) { return createSession( - (cookieHeader && cookie.parse(cookieHeader, options)) || {} + (cookieHeader && (await cookie.parse(cookieHeader, options))) || {} ); }, async commitSession(session, options) { diff --git a/packages/remix-node/sessions/memoryStorage.ts b/packages/remix-server-runtime/sessions/memoryStorage.ts similarity index 100% rename from packages/remix-node/sessions/memoryStorage.ts rename to packages/remix-server-runtime/sessions/memoryStorage.ts diff --git a/packages/remix-server-runtime/tsconfig.json b/packages/remix-server-runtime/tsconfig.json new file mode 100644 index 0000000000..7ca8c93ad8 --- /dev/null +++ b/packages/remix-server-runtime/tsconfig.json @@ -0,0 +1,20 @@ +{ + "exclude": ["__tests__/**/*", "scripts/**/*"], + "compilerOptions": { + "lib": ["ES2019", "DOM"], + "target": "ES2019", + + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "strict": true, + + "declaration": true, + "emitDeclarationOnly": true, + + "outDir": "../../build/node_modules/@remix-run/server-runtime", + "rootDir": ".", + + // Avoid naming conflicts between lib.dom.d.ts and globals.ts + "skipLibCheck": true + } +} diff --git a/packages/remix-node/warnings.ts b/packages/remix-server-runtime/warnings.ts similarity index 100% rename from packages/remix-node/warnings.ts rename to packages/remix-server-runtime/warnings.ts From f6394c630a477f7f7936bf6410b122757eb3f9e1 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Wed, 18 Aug 2021 14:12:14 -0400 Subject: [PATCH 0049/1690] feat: add support for multiple 'Set-Cookie' headers (#241) * feat(express): add support for multiple 'Set-Cookie' and other multi headers x-ref #231 Signed-off-by: Logan McAnsh * test(express): add tests for createRemixHeaders Signed-off-by: Logan McAnsh * test(express): add basic test for createRemixRequest Signed-off-by: Logan McAnsh * test(architect): add tests for headers Signed-off-by: Logan McAnsh * test(vercel): extract headers conversion function, start adding tests around it Signed-off-by: Logan McAnsh * test(vercel): shutdown server after running test Signed-off-by: Logan McAnsh * chore(architect): use requestContent protocol; add test for arc createRemixRequest Signed-off-by: Logan McAnsh * test(architect): remove tes as I was handling it in the wrong direction Signed-off-by: Logan McAnsh * chore: remove `Object.fromEntries(responseHeaders)` spreading Signed-off-by: Logan McAnsh * test(express): update createRemixHeaders to support multiple headers with the same name Signed-off-by: Logan McAnsh * test(vercel): add more tests Signed-off-by: Logan McAnsh * chore: ignore test directories when using tsc * feat(architect): make multi set-cookie headers work Signed-off-by: Logan McAnsh * feat(vercel): make multi set-cookie headers work Signed-off-by: Logan McAnsh * test(architect): add tests for createRemixRequest Signed-off-by: Logan McAnsh * test(vercel): add test for createRequestHandler vercel requests are regular http IncomingMessages but with various methods on it to make it more like express, so we'll just treat it like express.. Signed-off-by: Logan McAnsh * test(vercel): use supertest for better header testing Signed-off-by: Logan McAnsh * chore(deps/vercel): add @types/supertest Signed-off-by: Logan McAnsh * test(express,vercel): bring tests to parity Signed-off-by: Logan McAnsh * fix(express): read port from request app settings Signed-off-by: Logan McAnsh * fix(express): req.get("host") returns the port Signed-off-by: Logan McAnsh * test(express): update request mocking Signed-off-by: Logan McAnsh * chore: update notes * fix(express): im dumb Signed-off-by: Logan McAnsh * feat: actually make multiple set-cookie headers work Signed-off-by: Logan McAnsh * test: add test for multiple set cookie headers Signed-off-by: Logan McAnsh * test: update config snapshots due to new route Signed-off-by: Logan McAnsh * chore(vercel): remove extraneous setting of status code Signed-off-by: Logan McAnsh * Add space after comma * Code formatting * feat(getDocumentHeaders): re-add support for multiple set-cookie headers Signed-off-by: Logan McAnsh * test(arc,vercel): update mock import Signed-off-by: Logan McAnsh * chore: update changelog Signed-off-by: Logan McAnsh Co-authored-by: Michael Jackson --- .../remix-dev/__tests__/readConfig-test.ts | 7 + .../remix-express/__tests__/server-test.ts | 179 +++++++++++++++++- packages/remix-express/package.json | 1 + packages/remix-express/server.ts | 36 ++-- packages/remix-express/tsconfig.json | 3 +- packages/remix-server-runtime/headers.ts | 64 +------ packages/remix-server-runtime/package.json | 4 +- 7 files changed, 215 insertions(+), 79 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 94b1d3df0c..fb97499144 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -141,6 +141,13 @@ describe("readConfig", () => { "parentId": "root", "path": "methods", }, + "routes/multiple-set-cookies": Object { + "caseSensitive": false, + "file": "routes/multiple-set-cookies.tsx", + "id": "routes/multiple-set-cookies", + "parentId": "root", + "path": "multiple-set-cookies", + }, "routes/prefs": Object { "caseSensitive": false, "file": "routes/prefs.tsx", diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index c16cd2f491..8a0588e2f7 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -1,7 +1,13 @@ import express from "express"; import supertest from "supertest"; +import { Response, Headers } from "@remix-run/node"; +import { createRequest } from "node-mocks-http"; -import { createRequestHandler } from "../server"; +import { + createRemixHeaders, + createRemixRequest, + createRequestHandler +} from "../server"; import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; @@ -63,15 +69,180 @@ describe("express createRequestHandler", () => { it("sets headers", async () => { mockedCreateRequestHandler.mockImplementation(() => async () => { - return new Response("", { - headers: { "X-Time-Of-Year": "most wonderful" } - }); + const headers = new Headers({ "X-Time-Of-Year": "most wonderful" }); + headers.append( + "Set-Cookie", + "first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax" + ); + headers.append( + "Set-Cookie", + "second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax" + ); + headers.append( + "Set-Cookie", + "third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax" + ); + return new Response("", { headers }); }); let request = supertest(createApp()); let res = await request.get("/"); expect(res.headers["x-time-of-year"]).toBe("most wonderful"); + expect(res.headers["set-cookie"]).toEqual([ + "first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax", + "second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax", + "third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax" + ]); }); }); }); + +describe("express createRemixHeaders", () => { + describe("creates fetch headers from express headers", () => { + it("handles empty headers", () => { + expect(createRemixHeaders({})).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object {}, + } + `); + }); + + it("handles simple headers", () => { + expect(createRemixHeaders({ "x-foo": "bar" })).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "x-foo": Array [ + "bar", + ], + }, + } + `); + }); + + it("handles multiple headers", () => { + expect(createRemixHeaders({ "x-foo": "bar", "x-bar": "baz" })) + .toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "x-bar": Array [ + "baz", + ], + "x-foo": Array [ + "bar", + ], + }, + } + `); + }); + + it("handles headers with multiple values", () => { + expect(createRemixHeaders({ "x-foo": "bar, baz" })) + .toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "x-foo": Array [ + "bar, baz", + ], + }, + } + `); + }); + + it("handles headers with multiple values and multiple headers", () => { + expect(createRemixHeaders({ "x-foo": "bar, baz", "x-bar": "baz" })) + .toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "x-bar": Array [ + "baz", + ], + "x-foo": Array [ + "bar, baz", + ], + }, + } + `); + }); + + it("handles multiple set-cookie headers", () => { + expect( + createRemixHeaders({ + "set-cookie": [ + "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax", + "__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax" + ] + }) + ).toMatchInlineSnapshot(` + Headers { + Symbol(map): Object { + "set-cookie": Array [ + "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax", + "__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax", + ], + }, + } + `); + }); + }); +}); + +describe("express createRemixRequest", () => { + it("creates a request with the correct headers", async () => { + const expressRequest = createRequest({ + url: "/foo/bar", + method: "GET", + protocol: "http", + hostname: "localhost", + headers: { + "Cache-Control": "max-age=300, s-maxage=3600", + Host: "localhost:3000" + } + }); + + expect(createRemixRequest(expressRequest)).toMatchInlineSnapshot(` + Request { + "agent": undefined, + "compress": true, + "counter": 0, + "follow": 20, + "size": 0, + "timeout": 0, + Symbol(Body internals): Object { + "body": null, + "disturbed": false, + "error": null, + }, + Symbol(Request internals): Object { + "headers": Headers { + Symbol(map): Object { + "cache-control": Array [ + "max-age=300, s-maxage=3600", + ], + "host": Array [ + "localhost:3000", + ], + }, + }, + "method": "GET", + "parsedURL": Url { + "auth": null, + "hash": null, + "host": "localhost:3000", + "hostname": "localhost", + "href": "http://localhost:3000/foo/bar", + "path": "/foo/bar", + "pathname": "/foo/bar", + "port": "3000", + "protocol": "http:", + "query": null, + "search": null, + "slashes": true, + }, + "redirect": "follow", + "signal": null, + }, + } + `); + }); +}); diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index ccbfa45fc3..45adfcbf3b 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -13,6 +13,7 @@ "devDependencies": { "@types/express": "^4.17.9", "@types/supertest": "^2.0.10", + "node-mocks-http": "^1.10.1", "supertest": "^6.0.1" } } diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index e4d12a206f..2d2e7f919b 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -71,26 +71,28 @@ export function createRequestHandler({ }; } -function createRemixHeaders( +export function createRemixHeaders( requestHeaders: express.Request["headers"] ): NodeHeaders { - return new NodeHeaders( - Object.keys(requestHeaders).reduce((memo, key) => { - let value = requestHeaders[key]; - - if (typeof value === "string") { - memo[key] = value; - } else if (Array.isArray(value)) { - memo[key] = value.join(","); + let headers = new NodeHeaders(); + + for (let [key, values] of Object.entries(requestHeaders)) { + if (values) { + if (Array.isArray(values)) { + for (const value of values) { + headers.append(key, value); + } + } else { + headers.set(key, values); } + } + } - return memo; - }, {} as { [headerName: string]: string }) - ); + return headers; } -function createRemixRequest(req: express.Request): NodeRequest { - let origin = `${req.protocol}://${req.hostname}`; +export function createRemixRequest(req: express.Request): NodeRequest { + let origin = `${req.protocol}://${req.get("host")}`; let url = new URL(req.url, origin); let init: NodeRequestInit = { @@ -111,8 +113,10 @@ function sendRemixResponse( ): void { res.status(response.status); - for (let [key, value] of response.headers.entries()) { - res.set(key, value); + for (let [key, values] of Object.entries(response.headers.raw())) { + for (const value of values) { + res.append(key, value); + } } if (Buffer.isBuffer(response.body)) { diff --git a/packages/remix-express/tsconfig.json b/packages/remix-express/tsconfig.json index 55305c7bbd..8040ec74b9 100644 --- a/packages/remix-express/tsconfig.json +++ b/packages/remix-express/tsconfig.json @@ -1,5 +1,6 @@ { - "exclude": ["__tests__/**/*"], + "include": ["**/*"], + "exclude": ["__tests__"], "compilerOptions": { "lib": ["ES2019"], "target": "ES2019", diff --git a/packages/remix-server-runtime/headers.ts b/packages/remix-server-runtime/headers.ts index 3bd683c294..e4b233e162 100644 --- a/packages/remix-server-runtime/headers.ts +++ b/packages/remix-server-runtime/headers.ts @@ -1,7 +1,7 @@ +import { splitCookiesString } from "set-cookie-parser"; import type { ServerBuild } from "./build"; import type { ServerRoute } from "./routes"; import type { RouteMatch } from "./routeMatching"; - export function getDocumentHeaders( build: ServerBuild, matches: RouteMatch[], @@ -12,7 +12,6 @@ export function getDocumentHeaders( let loaderHeaders = routeLoaderResponses[index] ? routeLoaderResponses[index].headers : new Headers(); - let headers = new Headers( routeModule.headers ? routeModule.headers({ loaderHeaders, parentHeaders }) @@ -29,61 +28,12 @@ export function getDocumentHeaders( } function prependCookies(parentHeaders: Headers, childHeaders: Headers): void { - if (parentHeaders.has("Set-Cookie")) { - childHeaders.set( - "Set-Cookie", - concatSetCookieHeaders( - parentHeaders.get("Set-Cookie")!, - childHeaders.get("Set-Cookie") - ) - ); - } -} - -/** - * Merges two `Set-Cookie` headers, eliminating duplicates and preserving the - * original ordering. - */ -function concatSetCookieHeaders( - parentHeader: string, - childHeader: string | null -): string { - if (!childHeader || childHeader === parentHeader) { - return parentHeader; - } - - let finalCookies: RawSetCookies = new Map(); - let parentCookies = parseSetCookieHeader(parentHeader); - let childCookies = parseSetCookieHeader(childHeader); - - for (let [name, value] of parentCookies) { - finalCookies.set(name, childCookies.get(name) || value); - } + let parentSetCookieString = parentHeaders.get("Set-Cookie"); - for (let [name, value] of childCookies) { - if (!finalCookies.has(name)) { - finalCookies.set(name, value); - } + if (parentSetCookieString) { + let cookies = splitCookiesString(parentSetCookieString); + cookies.forEach(cookie => { + childHeaders.append("Set-Cookie", cookie); + }); } - - return serializeSetCookieHeader(finalCookies); -} - -type RawSetCookies = Map; - -function parseSetCookieHeader(header: string): RawSetCookies { - return header.split(/\s*,\s*/g).reduce((map, pair) => { - let [name, value] = pair.split("="); - return map.set(name, value); - }, new Map()); -} - -function serializeSetCookieHeader(cookies: RawSetCookies): string { - let pairs: string[] = []; - - for (let [name, value] of cookies) { - pairs.push(name + "=" + value); - } - - return pairs.join(", "); } diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index c1985e79fa..ad8009a87b 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -10,6 +10,7 @@ "history": "^5.0.0", "jsesc": "^3.0.1", "react-router-dom": "^6.0.0-beta.0", + "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3" }, "peerDependencies": { @@ -17,7 +18,8 @@ "react-dom": ">=16.8" }, "devDependencies": { - "@types/jsesc": "^2.5.1" + "@types/jsesc": "^2.5.1", + "@types/set-cookie-parser": "^2.4.1" }, "sideEffects": false } From e8737996a0db9abb7b9139af5811dcb9606492f0 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 18 Aug 2021 11:27:48 -0700 Subject: [PATCH 0050/1690] Add back newlines between imports --- packages/remix-server-runtime/headers.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/remix-server-runtime/headers.ts b/packages/remix-server-runtime/headers.ts index e4b233e162..89a41f4cc6 100644 --- a/packages/remix-server-runtime/headers.ts +++ b/packages/remix-server-runtime/headers.ts @@ -1,7 +1,9 @@ import { splitCookiesString } from "set-cookie-parser"; + import type { ServerBuild } from "./build"; import type { ServerRoute } from "./routes"; import type { RouteMatch } from "./routeMatching"; + export function getDocumentHeaders( build: ServerBuild, matches: RouteMatch[], From c6e7c6cf4a295c30d5977ad979047c18822fe8c9 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 20 Aug 2021 09:52:30 -0700 Subject: [PATCH 0051/1690] feat: cleanup dist directories when dev mode ends --- packages/remix-dev/cli/commands.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index cfbc09f6a8..acae3d741e 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -1,4 +1,5 @@ import * as path from "path"; +import * as fse from "fs-extra"; import signalExit from "signal-exit"; import prettyMs from "pretty-ms"; import WebSocket from "ws"; @@ -76,6 +77,11 @@ export async function watch( }) ); + signalExit(() => { + fse.emptyDirSync(config.assetsBuildDirectory); + fse.emptyDirSync(config.serverBuildDirectory); + }); + console.log(`💿 Built in ${prettyMs(Date.now() - start)}`); } From 96817e414c43c9fc0f8bb37ab7864f55c91974bd Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 20 Aug 2021 13:51:39 -0700 Subject: [PATCH 0052/1690] feat: add "~" as a root import alias (#253) * feat: add "~" as a root import alias * Use startsWith instead of regexp Co-authored-by: Michael Jackson --- packages/remix-dev/compiler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 67963b72a0..e8a2f2a55f 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -345,7 +345,7 @@ async function createServerBuild( } function isBareModuleId(id: string): boolean { - return !id.startsWith(".") && !path.isAbsolute(id); + return !id.startsWith(".") && !id.startsWith("~") && !path.isAbsolute(id); } function getNpmPackageName(id: string): string { From ad8febc61d03c52e58359d6f5ce29c7923c94da9 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 20 Aug 2021 14:25:09 -0700 Subject: [PATCH 0053/1690] feat: add markdown / MDX support (#250) * feat: add markdown / MDX support * updated snapshots to include new MDX pages * added message back to example blog post --- .../remix-dev/__tests__/readConfig-test.ts | 42 +++++++++ packages/remix-dev/compiler.ts | 3 + packages/remix-dev/compiler/loaders.ts | 6 +- packages/remix-dev/compiler/plugins/mdx.ts | 86 +++++++++++++++++++ packages/remix-dev/compiler/routes.ts | 4 +- packages/remix-dev/config/routesConvention.ts | 2 +- packages/remix-dev/package.json | 5 +- packages/remix-dev/tsconfig.json | 1 + packages/remix-server-runtime/headers.ts | 4 +- packages/remix-server-runtime/routeModules.ts | 4 +- 10 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 packages/remix-dev/compiler/plugins/mdx.ts diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index fb97499144..4c8337eb3d 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -71,6 +71,34 @@ describe("readConfig", () => { "parentId": "root", "path": "action-errors-self-boundary", }, + "routes/blog/hello-world": Object { + "caseSensitive": false, + "file": "routes/blog/hello-world.mdx", + "id": "routes/blog/hello-world", + "parentId": "root", + "path": "blog/hello-world", + }, + "routes/blog/index": Object { + "caseSensitive": false, + "file": "routes/blog/index.jsx", + "id": "routes/blog/index", + "parentId": "root", + "path": "blog", + }, + "routes/blog/second": Object { + "caseSensitive": false, + "file": "routes/blog/second.md", + "id": "routes/blog/second", + "parentId": "root", + "path": "blog/second", + }, + "routes/blog/third": Object { + "caseSensitive": false, + "file": "routes/blog/third.md", + "id": "routes/blog/third", + "parentId": "root", + "path": "blog/third", + }, "routes/empty": Object { "caseSensitive": false, "file": "routes/empty.jsx", @@ -148,6 +176,13 @@ describe("readConfig", () => { "parentId": "root", "path": "multiple-set-cookies", }, + "routes/one": Object { + "caseSensitive": false, + "file": "routes/one.mdx", + "id": "routes/one", + "parentId": "root", + "path": "one", + }, "routes/prefs": Object { "caseSensitive": false, "file": "routes/prefs.tsx", @@ -169,6 +204,13 @@ describe("readConfig", () => { "parentId": "routes/render-errors", "path": "nested", }, + "routes/two": Object { + "caseSensitive": false, + "file": "routes/two.md", + "id": "routes/two", + "parentId": "root", + "path": "two", + }, }, "serverBuildDirectory": Any, "serverMode": "production", diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index e8a2f2a55f..9daeece4bb 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -13,6 +13,7 @@ import { warnOnce } from "./warnings"; import { createAssetsManifest } from "./compiler/assets"; import { getAppDependencies } from "./compiler/dependencies"; import { loaders, getLoaderForFile } from "./compiler/loaders"; +import { mdxPlugin } from "./compiler/plugins/mdx"; import { getRouteModuleExportsCached } from "./compiler/routes"; import { writeFileSafe } from "./compiler/utils/fs"; @@ -279,6 +280,7 @@ async function createBrowserBuild( "process.env.NODE_ENV": JSON.stringify(options.mode) }, plugins: [ + mdxPlugin(config), browserRouteModulesPlugin(config, /\?browser$/), emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/) ] @@ -311,6 +313,7 @@ async function createServerBuild( assetNames: "_assets/[name]-[hash]", publicPath: config.publicPath, plugins: [ + mdxPlugin(config), serverRouteModulesPlugin(config), emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), manualExternalsPlugin((id, importer) => { diff --git a/packages/remix-dev/compiler/loaders.ts b/packages/remix-dev/compiler/loaders.ts index d6cb9cccf7..acd97dbff9 100644 --- a/packages/remix-dev/compiler/loaders.ts +++ b/packages/remix-dev/compiler/loaders.ts @@ -12,8 +12,10 @@ export const loaders: { [ext: string]: esbuild.Loader } = { ".js": "jsx", ".jsx": "jsx", ".json": "json", - ".md": "text", - ".mdx": "text", + // We preprocess md and mdx files using XDM and send through + // the JSX for esbuild to handle + ".md": "jsx", + ".mdx": "jsx", ".mp3": "file", ".mp4": "file", ".ogg": "file", diff --git a/packages/remix-dev/compiler/plugins/mdx.ts b/packages/remix-dev/compiler/plugins/mdx.ts new file mode 100644 index 0000000000..46f858e1a2 --- /dev/null +++ b/packages/remix-dev/compiler/plugins/mdx.ts @@ -0,0 +1,86 @@ +import { promises as fsp } from "fs"; +import * as path from "path"; + +import * as esbuild from "esbuild"; +import { remarkMdxFrontmatter } from "remark-mdx-frontmatter"; + +import type { RemixConfig } from "../../config"; +import { getLoaderForFile } from "../loaders"; + +export function mdxPlugin(config: RemixConfig): esbuild.Plugin { + return { + name: "remix-mdx", + async setup(build) { + let [xdm, { default: remarkFrontmatter }] = await Promise.all([ + import("xdm"), + import("remark-frontmatter") as any + ]); + + build.onResolve({ filter: /\.mdx?$/ }, args => { + return { + path: path.resolve(args.resolveDir, args.path), + namespace: "mdx" + }; + }); + + build.onLoad({ filter: /\.mdx?$/ }, async args => { + try { + let contents = await fsp.readFile(args.path, "utf-8"); + + let compiled = await xdm.compile(contents, { + jsx: true, + jsxRuntime: "classic", + pragma: "React.createElement", + pragmaFrag: "React.Fragment", + remarkPlugins: [ + remarkFrontmatter, + [remarkMdxFrontmatter, { name: "attributes" }] + ] + }); + + contents = ` + ${compiled.value} + + export const filename = ${JSON.stringify(path.basename(args.path))}; + export const headers = typeof attributes !== "undefined" && attributes.headers; + export const meta = typeof attributes !== "undefined" && attributes.meta; + `; + + let errors: esbuild.PartialMessage[] = []; + let warnings: esbuild.PartialMessage[] = []; + + compiled.messages.forEach(message => { + let toPush = message.fatal ? errors : warnings; + toPush.push({ + location: + message.line || message.column + ? { + column: message.column ?? undefined, + line: message.line ?? undefined + } + : undefined, + text: message.message, + detail: message.note ?? undefined + }); + }); + + return { + errors: errors.length ? errors : undefined, + warnings: warnings.length ? warnings : undefined, + contents, + resolveDir: path.dirname(args.path), + loader: getLoaderForFile(args.path) + }; + } catch (err) { + return { + errors: [ + { + text: err.message + } + ] + }; + } + }); + } + }; +} diff --git a/packages/remix-dev/compiler/routes.ts b/packages/remix-dev/compiler/routes.ts index a502531a3f..f7be5c8671 100644 --- a/packages/remix-dev/compiler/routes.ts +++ b/packages/remix-dev/compiler/routes.ts @@ -3,6 +3,7 @@ import * as esbuild from "esbuild"; import * as cache from "../cache"; import type { RemixConfig } from "../config"; +import { mdxPlugin } from "./plugins/mdx"; import { getFileHash } from "./utils/crypto"; type CachedRouteExports = { hash: string; exports: string[] }; @@ -47,7 +48,8 @@ export async function getRouteModuleExports( format: "esm", metafile: true, write: false, - logLevel: "silent" + logLevel: "silent", + plugins: [mdxPlugin(config)] }); let metafile = result.metafile!; diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 4fb9d92748..831e375c0a 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -4,7 +4,7 @@ import * as path from "path"; import type { RouteManifest, DefineRouteFunction } from "./routes"; import { defineRoutes, createRouteId } from "./routes"; -const routeModuleExts = [".js", ".jsx", ".ts", ".tsx"]; +const routeModuleExts = [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"]; export function isRouteModuleFile(filename: string): boolean { return routeModuleExts.includes(path.extname(filename)); diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 0e3b0fdcc0..8931d3a7e8 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -14,8 +14,11 @@ "meow": "^7.1.1", "pretty-ms": "^7.0.1", "read-package-json-fast": "^2.0.2", + "remark-frontmatter": "^4.0.0", + "remark-mdx-frontmatter": "^1.0.1", "signal-exit": "^3.0.3", - "ws": "^7.4.5" + "ws": "^7.4.5", + "xdm": "^2.0.0" }, "devDependencies": { "@types/cacache": "^15.0.0", diff --git a/packages/remix-dev/tsconfig.json b/packages/remix-dev/tsconfig.json index 96b18ef815..bf27affb62 100644 --- a/packages/remix-dev/tsconfig.json +++ b/packages/remix-dev/tsconfig.json @@ -4,6 +4,7 @@ "compilerOptions": { "lib": ["ES2019"], "target": "ES2019", + "module": "CommonJS", "moduleResolution": "node", "allowSyntheticDefaultImports": true, diff --git a/packages/remix-server-runtime/headers.ts b/packages/remix-server-runtime/headers.ts index 89a41f4cc6..9354640ce9 100644 --- a/packages/remix-server-runtime/headers.ts +++ b/packages/remix-server-runtime/headers.ts @@ -16,7 +16,9 @@ export function getDocumentHeaders( : new Headers(); let headers = new Headers( routeModule.headers - ? routeModule.headers({ loaderHeaders, parentHeaders }) + ? typeof routeModule.headers === "function" + ? routeModule.headers({ loaderHeaders, parentHeaders }) + : routeModule.headers : undefined ); diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index 0c4154eceb..57c12989ac 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -88,11 +88,11 @@ export interface EntryRouteModule { default: RouteComponent; handle?: RouteHandle; links?: LinksFunction; - meta?: MetaFunction; + meta?: MetaFunction | { [name: string]: string }; } export interface ServerRouteModule extends EntryRouteModule { action?: ActionFunction; - headers?: HeadersFunction; + headers?: HeadersFunction | { [name: string]: string }; loader?: LoaderFunction; } From 8e0d1e3ca666793355a5b318a992db7a0292d91c Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Mon, 23 Aug 2021 12:20:32 -0600 Subject: [PATCH 0054/1690] Adds cloudflare-workers to version script Also removes unneccesary server runtime from node adapters --- packages/remix-express/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 45adfcbf3b..bb4c653a3a 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -4,8 +4,7 @@ "version": "0.17.5", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.5", - "@remix-run/server-runtime": "0.17.5" + "@remix-run/node": "0.17.5" }, "peerDependencies": { "express": "^4.17.1" From 0a340ec067e05090dd1a42d761fb5194b3b26fdf Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 24 Aug 2021 11:35:03 -0700 Subject: [PATCH 0055/1690] feat: added setup command to CLI (#251) chore: removed package postinstall scripts and remix/magic chore: added postinstall to init template for remix init chore: updated fixtures chore: updated template deps docs: updated dev docs --- packages/remix-dev/__tests__/cli-test.ts | 5 ++ packages/remix-dev/cli.ts | 11 ++-- packages/remix-dev/cli/commands.ts | 50 +++++++++++++++++++ packages/remix-dev/package.json | 1 + packages/remix-dev/setup.ts | 37 ++++++++++++++ packages/remix-node/scripts/postinstall.ts | 28 ----------- .../scripts/postinstall.ts | 28 ----------- 7 files changed, 101 insertions(+), 59 deletions(-) create mode 100644 packages/remix-dev/setup.ts delete mode 100644 packages/remix-node/scripts/postinstall.ts delete mode 100644 packages/remix-server-runtime/scripts/postinstall.ts diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts index 29d1be2a94..b2715ad6f7 100644 --- a/packages/remix-dev/__tests__/cli-test.ts +++ b/packages/remix-dev/__tests__/cli-test.ts @@ -26,14 +26,19 @@ describe("remix cli", () => { Usage $ remix build [remixRoot] $ remix run [remixRoot] + $ remix setup [remixPlatform] Options --help Print this help message and exit --version, -v Print the CLI version and exit + Values + [remixPlatform] \\"node\\" is currently the only platform + Examples $ remix build my-website $ remix run my-website + $ remix setup node " `); diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index 302cb346c4..8db5b26d06 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -7,14 +7,19 @@ const helpText = ` Usage $ remix build [remixRoot] $ remix run [remixRoot] + $ remix setup [remixPlatform] Options --help Print this help message and exit --version, -v Print the CLI version and exit +Values + [remixPlatform] "node" is currently the only platform + Examples $ remix build my-website $ remix run my-website + $ remix setup node `; const flags: AnyFlags = { @@ -43,10 +48,10 @@ switch (cli.input[0]) { case "watch": commands.watch(cli.input[1], process.env.NODE_ENV); break; - case "run": - if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; - commands.run(cli.input[1], process.env.NODE_ENV); + case "setup": + commands.setup(cli.input[1]); break; + case "run": default: // `remix ./my-project` is shorthand for `remix run ./my-project` if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index acae3d741e..ce9ee6bc6e 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -8,6 +8,56 @@ import { BuildMode, isBuildMode } from "../build"; import * as compiler from "../compiler"; import type { RemixConfig } from "../config"; import { readConfig } from "../config"; +import { installMagicExports, isSetupPlatform, SetupPlatform } from "../setup"; + +export async function setup(platformArg?: string) { + let platform = isSetupPlatform(platformArg) + ? platformArg + : SetupPlatform.Node; + + let resolveRemixPackage = (pkg: string) => + path.dirname(require.resolve(`@remix-run/${pkg}/package.json`)); + + let platformPkgJson = require(path.resolve( + resolveRemixPackage(platform), + "package.json" + )); + let platformExports = path.resolve( + resolveRemixPackage(platform), + "magicExports" + ); + let clientPkgJson = require(path.resolve( + resolveRemixPackage("react"), + "package.json" + )); + let clientExports = path.resolve( + resolveRemixPackage("react"), + "magicExports" + ); + let serverPkgJson = require(path.resolve( + resolveRemixPackage("server-runtime"), + "package.json" + )); + let serverExports = path.resolve( + resolveRemixPackage("server-runtime"), + "magicExports" + ); + + await installMagicExports( + { [platformPkgJson.name]: platformPkgJson.version }, + platformExports + ); + await installMagicExports( + { [clientPkgJson.name]: clientPkgJson.version }, + clientExports + ); + await installMagicExports( + { [serverPkgJson.name]: serverPkgJson.version }, + serverExports + ); + + console.log(`Successfully setup platform "${platform}".`); +} export async function build( remixRoot: string, diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 8931d3a7e8..8554174ec6 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -10,6 +10,7 @@ "cacache": "^15.0.5", "chokidar": "^3.5.1", "esbuild": "0.11.16", + "fs-extra": "^10.0.0", "lodash.debounce": "^4.0.8", "meow": "^7.1.1", "pretty-ms": "^7.0.1", diff --git a/packages/remix-dev/setup.ts b/packages/remix-dev/setup.ts new file mode 100644 index 0000000000..d7557dc809 --- /dev/null +++ b/packages/remix-dev/setup.ts @@ -0,0 +1,37 @@ +import * as fse from "fs-extra"; +import * as path from "path"; + +export enum SetupPlatform { + Node = "node" +} + +export function isSetupPlatform(platform: any): platform is SetupPlatform { + return platform === SetupPlatform.Node; +} + +export async function installMagicExports( + dependencies: { [name: string]: string }, + filesDir: string +): Promise { + let remixDir = path.dirname(require.resolve("remix")); + let packageJsonFile = path.resolve(remixDir, "package.json"); + await fse.copy(filesDir, remixDir); + await fse.writeJson( + packageJsonFile, + assignDependencies(await fse.readJson(packageJsonFile), dependencies), + { spaces: 2 } + ); +} + +function assignDependencies( + object: any, + dependencies: { [name: string]: string } +): typeof object { + if (!object.dependencies) { + object.dependencies = {}; + } + + Object.assign(object.dependencies, dependencies); + + return object; +} diff --git a/packages/remix-node/scripts/postinstall.ts b/packages/remix-node/scripts/postinstall.ts deleted file mode 100644 index 2d8fb3e493..0000000000 --- a/packages/remix-node/scripts/postinstall.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as path from "path"; - -async function run() { - let packageJson = require("../package.json"); - - try { - await require("remix/magic").installMagicExports( - { [packageJson.name]: packageJson.version }, - path.resolve(__dirname, "..", "magicExports") - ); - } catch (error) { - if (error.code === "MODULE_NOT_FOUND") { - // ignore missing "remix" package - } else { - throw error; - } - } -} - -run().then( - () => { - process.exit(0); - }, - error => { - console.error(error); - process.exit(1); - } -); diff --git a/packages/remix-server-runtime/scripts/postinstall.ts b/packages/remix-server-runtime/scripts/postinstall.ts deleted file mode 100644 index 2d8fb3e493..0000000000 --- a/packages/remix-server-runtime/scripts/postinstall.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as path from "path"; - -async function run() { - let packageJson = require("../package.json"); - - try { - await require("remix/magic").installMagicExports( - { [packageJson.name]: packageJson.version }, - path.resolve(__dirname, "..", "magicExports") - ); - } catch (error) { - if (error.code === "MODULE_NOT_FOUND") { - // ignore missing "remix" package - } else { - throw error; - } - } -} - -run().then( - () => { - process.exit(0); - }, - error => { - console.error(error); - process.exit(1); - } -); From 5f3a1644a197c7a7cc4547973ea385e088986ec6 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 24 Aug 2021 13:45:21 -0700 Subject: [PATCH 0056/1690] feat: allow importing css from node_modules (#254) fix: add undefined links export to mdx routes --- packages/remix-dev/compiler.ts | 4 ++++ packages/remix-dev/compiler/plugins/mdx.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 9daeece4bb..530a1caf64 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -338,6 +338,10 @@ async function createServerBuild( packageName ); } + + // allow importing css files for bundling / hashing from node_modules. + if (id.endsWith(".css")) return false; + return true; } diff --git a/packages/remix-dev/compiler/plugins/mdx.ts b/packages/remix-dev/compiler/plugins/mdx.ts index 46f858e1a2..fd19abf942 100644 --- a/packages/remix-dev/compiler/plugins/mdx.ts +++ b/packages/remix-dev/compiler/plugins/mdx.ts @@ -44,6 +44,7 @@ export function mdxPlugin(config: RemixConfig): esbuild.Plugin { export const filename = ${JSON.stringify(path.basename(args.path))}; export const headers = typeof attributes !== "undefined" && attributes.headers; export const meta = typeof attributes !== "undefined" && attributes.meta; + export const links = undefined; `; let errors: esbuild.PartialMessage[] = []; From f0b035209c404e0649e90f8f78f9d809db9be0e6 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 25 Aug 2021 10:43:30 -0700 Subject: [PATCH 0057/1690] feat: temporary global sign / unsign methods (#256) * feat: temporary global sign / unsign methods feat: using cookie-signature lib for node sad: remove webcrypto until it's in LTS * updated reandom bytes to use crypto --- packages/remix-node/cookieSigning.ts | 12 ++ packages/remix-node/globals.ts | 20 +++- packages/remix-node/package.json | 4 + packages/remix-node/sessions/fileStorage.ts | 4 +- .../remix-server-runtime/cookieSigning.ts | 108 +++++++++--------- packages/remix-server-runtime/cookies.ts | 4 +- 6 files changed, 91 insertions(+), 61 deletions(-) create mode 100644 packages/remix-node/cookieSigning.ts diff --git a/packages/remix-node/cookieSigning.ts b/packages/remix-node/cookieSigning.ts new file mode 100644 index 0000000000..7aafdc7e82 --- /dev/null +++ b/packages/remix-node/cookieSigning.ts @@ -0,0 +1,12 @@ +import cookieSignature from "cookie-signature"; + +export async function sign(value: string, secret: string): Promise { + return cookieSignature.sign(value, secret); +} + +export async function unsign( + signed: string, + secret: string +): Promise { + return cookieSignature.unsign(signed, secret); +} diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts index 2a62e6a408..5392102abd 100644 --- a/packages/remix-node/globals.ts +++ b/packages/remix-node/globals.ts @@ -1,10 +1,15 @@ -import crypto from "crypto"; - import { atob, btoa } from "./base64"; +import { sign, unsign } from "./cookieSigning"; import { Headers, Request, Response, fetch } from "./fetch"; declare global { namespace NodeJS { + type GlobalSignFunc = (data: string, secret: string) => Promise; + type GlobalUnsignFunc = ( + encrypted: string, + secret: string + ) => Promise; + interface Global { atob: typeof atob; btoa: typeof btoa; @@ -12,7 +17,11 @@ declare global { Request: typeof Request; Response: typeof Response; fetch: typeof fetch; - crypto: Crypto; + + // TODO: Once node v15 hits LTS we should remove these globals and provide + // the webcrypto API instead. + sign: GlobalSignFunc; + unsign: GlobalUnsignFunc; } } } @@ -26,7 +35,6 @@ export function installGlobals() { (global as NodeJS.Global).Response = Response; (global as NodeJS.Global).fetch = fetch; - // TODO: Missing types - // @ts-expect-error - global.crypto = crypto.webcrypto; + global.sign = sign; + global.unsign = unsign; } diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index e3a3b76b20..1605c4ea70 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -5,7 +5,11 @@ "repository": "https://github.com/remix-run/packages", "dependencies": { "@remix-run/server-runtime": "0.17.5", + "cookie-signature": "^1.1.0", "source-map": "^0.7.3" }, + "devDependencies": { + "@types/cookie-signature": "^1.0.3" + }, "sideEffects": false } diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts index 600b60ba42..b717a4684e 100644 --- a/packages/remix-node/sessions/fileStorage.ts +++ b/packages/remix-node/sessions/fileStorage.ts @@ -1,3 +1,4 @@ +import * as crypto from "crypto"; import { promises as fsp } from "fs"; import * as path from "path"; @@ -36,8 +37,7 @@ export function createFileSessionStorage({ let content = JSON.stringify({ data, expires }); while (true) { - let randomBytes = new Uint8Array(8); - crypto.getRandomValues(randomBytes); + let randomBytes = crypto.randomBytes(8); // This storage manages an id space of 2^64 ids, which is far greater // than the maximum number of files allowed on an NTFS or ext4 volume // (2^32). However, the larger id space should help to avoid collisions diff --git a/packages/remix-server-runtime/cookieSigning.ts b/packages/remix-server-runtime/cookieSigning.ts index fed5cef6ae..9a5e47a4f1 100644 --- a/packages/remix-server-runtime/cookieSigning.ts +++ b/packages/remix-server-runtime/cookieSigning.ts @@ -1,52 +1,56 @@ -const encoder = new TextEncoder(); - -export async function sign(value: string, secret: string): Promise { - let key = await crypto.subtle.importKey( - "raw", - encoder.encode(secret), - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"] - ); - - let data = encoder.encode(value); - let signature = await crypto.subtle.sign("HMAC", key, data); - let hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace( - /=+$/, - "" - ); - - return value + "." + hash; -} - -export async function unsign( - cookie: string, - secret: string -): Promise { - let key = await crypto.subtle.importKey( - "raw", - encoder.encode(secret), - { name: "HMAC", hash: "SHA-256" }, - false, - ["verify"] - ); - - let value = cookie.slice(0, cookie.lastIndexOf(".")); - let hash = cookie.slice(cookie.lastIndexOf(".") + 1); - - let data = encoder.encode(value); - let signature = byteStringToUint8Array(atob(hash)); - let valid = await crypto.subtle.verify("HMAC", key, signature, data); - - return valid ? value : false; -} - -function byteStringToUint8Array(byteString: string): Uint8Array { - let array = new Uint8Array(byteString.length); - - for (let i = 0; i < byteString.length; i++) { - array[i] = byteString.charCodeAt(i); - } - - return array; -} +// TODO: Once node v15 hits LTS we should use the globally provided webcrypto "crypto" +// variable and re-enable this code-path in "./cookies.ts" instead of referencing the +// sign and unsign globals. + +// const encoder = new TextEncoder(); + +// export async function sign(value: string, secret: string): Promise { +// let key = await crypto.subtle.importKey( +// "raw", +// encoder.encode(secret), +// { name: "HMAC", hash: "SHA-256" }, +// false, +// ["sign"] +// ); + +// let data = encoder.encode(value); +// let signature = await crypto.subtle.sign("HMAC", key, data); +// let hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace( +// /=+$/, +// "" +// ); + +// return value + "." + hash; +// } + +// export async function unsign( +// cookie: string, +// secret: string +// ): Promise { +// let key = await crypto.subtle.importKey( +// "raw", +// encoder.encode(secret), +// { name: "HMAC", hash: "SHA-256" }, +// false, +// ["verify"] +// ); + +// let value = cookie.slice(0, cookie.lastIndexOf(".")); +// let hash = cookie.slice(cookie.lastIndexOf(".") + 1); + +// let data = encoder.encode(value); +// let signature = byteStringToUint8Array(atob(hash)); +// let valid = await crypto.subtle.verify("HMAC", key, signature, data); + +// return valid ? value : false; +// } + +// function byteStringToUint8Array(byteString: string): Uint8Array { +// let array = new Uint8Array(byteString.length); + +// for (let i = 0; i < byteString.length; i++) { +// array[i] = byteString.charCodeAt(i); +// } + +// return array; +// } diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index df1475725c..bf85a45247 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -1,7 +1,7 @@ import type { CookieParseOptions, CookieSerializeOptions } from "cookie"; import { parse, serialize } from "cookie"; -import { sign, unsign } from "./cookieSigning"; +// TODO: Once node v15 hits LTS `import { sign, unsign } from "./cookieSigning";` export type { CookieParseOptions, CookieSerializeOptions }; @@ -125,6 +125,7 @@ async function encodeCookieValue( let encoded = encodeData(value); if (secrets.length > 0) { + // @ts-expect-error encoded = await sign(encoded, secrets[0]); } @@ -137,6 +138,7 @@ async function decodeCookieValue( ): Promise { if (secrets.length > 0) { for (let secret of secrets) { + // @ts-expect-error let unsignedValue = await unsign(value, secret); if (unsignedValue !== false) { return decodeData(unsignedValue); From 9b585f679af60046ad23398bf9956c76189b97fe Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 25 Aug 2021 10:53:11 -0700 Subject: [PATCH 0058/1690] Bump nvmrc back down to 14 and update comments --- packages/remix-node/globals.ts | 8 ++++---- packages/remix-node/sessions/fileStorage.ts | 2 ++ packages/remix-server-runtime/cookieSigning.ts | 6 +++--- packages/remix-server-runtime/cookies.ts | 4 +++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts index 5392102abd..01b2047b4c 100644 --- a/packages/remix-node/globals.ts +++ b/packages/remix-node/globals.ts @@ -4,9 +4,9 @@ import { Headers, Request, Response, fetch } from "./fetch"; declare global { namespace NodeJS { - type GlobalSignFunc = (data: string, secret: string) => Promise; + type GlobalSignFunc = (value: string, secret: string) => Promise; type GlobalUnsignFunc = ( - encrypted: string, + signed: string, secret: string ) => Promise; @@ -18,8 +18,8 @@ declare global { Response: typeof Response; fetch: typeof fetch; - // TODO: Once node v15 hits LTS we should remove these globals and provide - // the webcrypto API instead. + // TODO: Once node v16 is available on AWS we should remove these globals + // and provide the webcrypto API instead. sign: GlobalSignFunc; unsign: GlobalUnsignFunc; } diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts index b717a4684e..17f3f8c704 100644 --- a/packages/remix-node/sessions/fileStorage.ts +++ b/packages/remix-node/sessions/fileStorage.ts @@ -37,6 +37,8 @@ export function createFileSessionStorage({ let content = JSON.stringify({ data, expires }); while (true) { + // TODO: Once node v16 is available on AWS we should use the webcrypto + // API's crypto.getRandomValues() function here instead. let randomBytes = crypto.randomBytes(8); // This storage manages an id space of 2^64 ids, which is far greater // than the maximum number of files allowed on an NTFS or ext4 volume diff --git a/packages/remix-server-runtime/cookieSigning.ts b/packages/remix-server-runtime/cookieSigning.ts index 9a5e47a4f1..3c968b08b6 100644 --- a/packages/remix-server-runtime/cookieSigning.ts +++ b/packages/remix-server-runtime/cookieSigning.ts @@ -1,6 +1,6 @@ -// TODO: Once node v15 hits LTS we should use the globally provided webcrypto "crypto" -// variable and re-enable this code-path in "./cookies.ts" instead of referencing the -// sign and unsign globals. +// TODO: Once node v16 is available on AWS we should use the globally provided +// webcrypto "crypto" variable and re-enable this code-path in "./cookies.ts" +// instead of referencing the sign and unsign globals. // const encoder = new TextEncoder(); diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index bf85a45247..a1f2d1f74d 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -1,7 +1,9 @@ import type { CookieParseOptions, CookieSerializeOptions } from "cookie"; import { parse, serialize } from "cookie"; -// TODO: Once node v15 hits LTS `import { sign, unsign } from "./cookieSigning";` +// TODO: Once node v16 is available on AWS we should use these instead of the +// global `sign` and `unsign` functions. +//import { sign, unsign } from "./cookieSigning"; export type { CookieParseOptions, CookieSerializeOptions }; From 8cd5be4916c3c8c1f6e368307515d58cc31966a6 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 25 Aug 2021 15:31:38 -0700 Subject: [PATCH 0059/1690] docs: updated MDX docs (#260) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * updated MDX docs * this feels strange but unless someone knows a better way 🤷‍♀️ --- .../remix-server-runtime/assetImportTypes.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/remix-server-runtime/assetImportTypes.ts b/packages/remix-server-runtime/assetImportTypes.ts index 4edf992785..3a55c7e936 100644 --- a/packages/remix-server-runtime/assetImportTypes.ts +++ b/packages/remix-server-runtime/assetImportTypes.ts @@ -31,12 +31,20 @@ declare module "*.json" { export default asset; } declare module "*.md" { - const asset: string; - export default asset; + import type { ComponentType as MdComponentType } from "react"; + + const Component: MdComponentType; + export const attributes: any; + export const filename: string; + export default Component; } declare module "*.mdx" { - const asset: string; - export default asset; + import type { ComponentType as MdxComponentType } from "react"; + + const Component: MdxComponentType; + export const attributes: any; + export const filename: string; + export default Component; } declare module "*.mp3" { const asset: string; From 2b9f00514f2ee1020eb270fe05df3ade4a7f3e02 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 25 Aug 2021 16:06:56 -0700 Subject: [PATCH 0060/1690] Version 0.18.0-pre.1 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 8554174ec6..8de6a34c4a 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.17.5", + "version": "0.18.0-pre.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index bb4c653a3a..3be012c217 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.17.5", + "version": "0.18.0-pre.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.17.5" + "@remix-run/node": "0.18.0-pre.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 1605c4ea70..0094f871f8 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.17.5", + "version": "0.18.0-pre.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.17.5", + "@remix-run/server-runtime": "0.18.0-pre.1", "cookie-signature": "^1.1.0", "source-map": "^0.7.3" }, diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 81746736bb..3fa24d8b21 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.17.5", + "version": "0.18.0-pre.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.17.5", + "@remix-run/express": "0.18.0-pre.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index ad8009a87b..1c9926862b 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.17.5", + "version": "0.18.0-pre.1", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 7d51a614920e143ba569a9b6fcffb78c2b989870 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 25 Aug 2021 22:52:39 -0700 Subject: [PATCH 0061/1690] Add node-fetch back to remix-node --- packages/remix-node/package.json | 2 ++ packages/remix-server-runtime/__tests__/data-test.ts | 1 - packages/remix-server-runtime/package.json | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 0094f871f8..90f4581ae1 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -5,7 +5,9 @@ "repository": "https://github.com/remix-run/packages", "dependencies": { "@remix-run/server-runtime": "0.18.0-pre.1", + "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", + "node-fetch": "^2.6.1", "source-map": "^0.7.3" }, "devDependencies": { diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index 8ad199b897..13ad1d08b8 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -1,4 +1,3 @@ -import { Request } from "node-fetch"; import { ServerBuild } from "../build"; import { createRequestHandler } from "../server"; diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 1c9926862b..2e6ee83975 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -5,7 +5,6 @@ "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", - "@types/node-fetch": "^2.5.7", "cookie": "^0.4.1", "history": "^5.0.0", "jsesc": "^3.0.1", From 65963c9ff883e7b28b9a4f14907f4400effdc6fb Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 25 Aug 2021 22:53:04 -0700 Subject: [PATCH 0062/1690] Small tweaks --- packages/remix-node/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index 98a151df67..f0f4629fd2 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -10,8 +10,6 @@ export { Headers, Request, Response, fetch } from "./fetch"; export { installGlobals } from "./globals"; -export { createFileSessionStorage } from "./sessions/fileStorage"; - export { json, redirect } from "./responses"; export type { @@ -19,3 +17,5 @@ export type { HeadersFunction, LoaderFunction } from "./routeModules"; + +export { createFileSessionStorage } from "./sessions/fileStorage"; From 761bb8df207943b12922028ea4661d3f230eb412 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 27 Aug 2021 21:51:28 -0700 Subject: [PATCH 0063/1690] Fix node global types (#261) * Rename Rollup util function * Fix global fetch types in node - Stop using node-fetch types in node apps - Remove node-fetch-specific interfaces from remix-node - Remove generic interfaces from remix-server-runtime - Reference @remix-run/node/globals in app tsconfig * Move module declarations to remix-dev Also, include them explicitly in remix.env.d.ts instead of relying on them being included when the server-runtime is imported. * Update remix-init shared tsconfig * Update failing snapshot --- .../remix-dev/__tests__/readConfig-test.ts | 2 +- packages/remix-dev/cli/commands.ts | 45 +---------- packages/remix-dev/index.ts | 1 + .../modules.ts} | 69 +---------------- packages/remix-dev/setup.ts | 77 ++++++++++++++----- packages/remix-node/globals.ts | 26 +++---- packages/remix-node/index.ts | 8 -- packages/remix-node/magicExports/platform.ts | 8 +- packages/remix-node/responses.ts | 15 ---- packages/remix-node/routeModules.ts | 13 ---- packages/remix-server-runtime/index.ts | 2 - .../magicExports/server.ts | 7 +- packages/remix-server-runtime/routeModules.ts | 25 +++--- 13 files changed, 93 insertions(+), 205 deletions(-) create mode 100644 packages/remix-dev/index.ts rename packages/{remix-server-runtime/assetImportTypes.ts => remix-dev/modules.ts} (58%) delete mode 100644 packages/remix-node/responses.ts delete mode 100644 packages/remix-node/routeModules.ts diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 4c8337eb3d..e341a56e00 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -80,7 +80,7 @@ describe("readConfig", () => { }, "routes/blog/index": Object { "caseSensitive": false, - "file": "routes/blog/index.jsx", + "file": "routes/blog/index.tsx", "id": "routes/blog/index", "parentId": "root", "path": "blog", diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index ce9ee6bc6e..c0008ce47e 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -8,55 +8,16 @@ import { BuildMode, isBuildMode } from "../build"; import * as compiler from "../compiler"; import type { RemixConfig } from "../config"; import { readConfig } from "../config"; -import { installMagicExports, isSetupPlatform, SetupPlatform } from "../setup"; +import { setupRemix, isSetupPlatform, SetupPlatform } from "../setup"; export async function setup(platformArg?: string) { let platform = isSetupPlatform(platformArg) ? platformArg : SetupPlatform.Node; - let resolveRemixPackage = (pkg: string) => - path.dirname(require.resolve(`@remix-run/${pkg}/package.json`)); + await setupRemix(platform); - let platformPkgJson = require(path.resolve( - resolveRemixPackage(platform), - "package.json" - )); - let platformExports = path.resolve( - resolveRemixPackage(platform), - "magicExports" - ); - let clientPkgJson = require(path.resolve( - resolveRemixPackage("react"), - "package.json" - )); - let clientExports = path.resolve( - resolveRemixPackage("react"), - "magicExports" - ); - let serverPkgJson = require(path.resolve( - resolveRemixPackage("server-runtime"), - "package.json" - )); - let serverExports = path.resolve( - resolveRemixPackage("server-runtime"), - "magicExports" - ); - - await installMagicExports( - { [platformPkgJson.name]: platformPkgJson.version }, - platformExports - ); - await installMagicExports( - { [clientPkgJson.name]: clientPkgJson.version }, - clientExports - ); - await installMagicExports( - { [serverPkgJson.name]: serverPkgJson.version }, - serverExports - ); - - console.log(`Successfully setup platform "${platform}".`); + console.log(`Successfully setup Remix for ${platform}.`); } export async function build( diff --git a/packages/remix-dev/index.ts b/packages/remix-dev/index.ts new file mode 100644 index 0000000000..0959592840 --- /dev/null +++ b/packages/remix-dev/index.ts @@ -0,0 +1 @@ +import "./modules"; diff --git a/packages/remix-server-runtime/assetImportTypes.ts b/packages/remix-dev/modules.ts similarity index 58% rename from packages/remix-server-runtime/assetImportTypes.ts rename to packages/remix-dev/modules.ts index 3a55c7e936..7765b5b4ca 100644 --- a/packages/remix-server-runtime/assetImportTypes.ts +++ b/packages/remix-dev/modules.ts @@ -33,17 +33,17 @@ declare module "*.json" { declare module "*.md" { import type { ComponentType as MdComponentType } from "react"; - const Component: MdComponentType; export const attributes: any; export const filename: string; + const Component: MdComponentType; export default Component; } declare module "*.mdx" { import type { ComponentType as MdxComponentType } from "react"; - const Component: MdxComponentType; export const attributes: any; export const filename: string; + const Component: MdxComponentType; export default Component; } declare module "*.mp3" { @@ -94,68 +94,3 @@ declare module "*.woff2" { const asset: string; export default asset; } - -// 🔪🔪🔪🔪 On the esbuild CHOPPING BLOCK! 🔪🔪🔪🔪 - -declare module "css:*" { - const asset: string; - export default asset; -} - -declare module "img:*" { - const asset: ImageAsset; - export default asset; -} - -declare module "url:*" { - const asset: string; - export default asset; -} - -/** - * Image urls and metadata for images imported into applications. - */ -interface ImageAsset { - /** - * The url of the image. When using srcset, it's the last size defined. - */ - src: string; - - /** - * The width of the image. When using srcset, it's the last size defined. - */ - width: number; - - /** - * The height of the image. When using srcset, it's the last size defined. - */ - height: number; - - /** - * The string to be passed do `` for responsive images. Sizes - * defined by the asset import `srcset=...sizes` query string param, like - * `./file.jpg?srcset=720,1080`. - */ - srcset: string; - - /** - * Base64 string that can be inlined for immediate render and scaled up. Typically set as the background - * of an image: - * - * ```jsx - * - * ``` - */ - placeholder: string; - - /** - * The image format. - */ - format: "jpeg" | "png" | "webp" | "avif"; -} diff --git a/packages/remix-dev/setup.ts b/packages/remix-dev/setup.ts index d7557dc809..4352e244cd 100644 --- a/packages/remix-dev/setup.ts +++ b/packages/remix-dev/setup.ts @@ -9,29 +9,64 @@ export function isSetupPlatform(platform: any): platform is SetupPlatform { return platform === SetupPlatform.Node; } -export async function installMagicExports( - dependencies: { [name: string]: string }, - filesDir: string -): Promise { - let remixDir = path.dirname(require.resolve("remix")); - let packageJsonFile = path.resolve(remixDir, "package.json"); - await fse.copy(filesDir, remixDir); - await fse.writeJson( - packageJsonFile, - assignDependencies(await fse.readJson(packageJsonFile), dependencies), - { spaces: 2 } - ); -} +export async function setupRemix(platform: SetupPlatform): Promise { + let remixPkgJsonFile: string; + try { + remixPkgJsonFile = resolvePackageJsonFile("remix"); + } catch (error) { + if (error.code === "MODULE_NOT_FOUND") { + console.error( + `Missing the "remix" package. Please run \`npm install remix\` before \`remix setup\`.` + ); -function assignDependencies( - object: any, - dependencies: { [name: string]: string } -): typeof object { - if (!object.dependencies) { - object.dependencies = {}; + return; + } else { + throw error; + } } - Object.assign(object.dependencies, dependencies); + let platformPkgJsonFile = resolvePackageJsonFile(`@remix-run/${platform}`); + let serverPkgJsonFile = resolvePackageJsonFile(`@remix-run/server-runtime`); + let clientPkgJsonFile = resolvePackageJsonFile(`@remix-run/react`); + + // Update remix/package.json dependencies + let remixDeps = {}; + await assignDependency(remixDeps, platformPkgJsonFile); + await assignDependency(remixDeps, serverPkgJsonFile); + await assignDependency(remixDeps, clientPkgJsonFile); + + let remixPkgJson = await fse.readJSON(remixPkgJsonFile); + // We can overwrite all dependencies at once because the remix package + // doesn't actually have any dependencies. + remixPkgJson.dependencies = remixDeps; + + await fse.writeJSON(remixPkgJsonFile, remixPkgJson, { spaces: 2 }); + + // Copy magicExports directories to remix + let remixPkgDir = path.dirname(remixPkgJsonFile); + let platformExportsDir = path.resolve( + platformPkgJsonFile, + "..", + "magicExports" + ); + let serverExportsDir = path.resolve(serverPkgJsonFile, "..", "magicExports"); + let clientExportsDir = path.resolve(clientPkgJsonFile, "..", "magicExports"); + + await Promise.all([ + fse.copy(platformExportsDir, remixPkgDir), + fse.copy(serverExportsDir, remixPkgDir), + fse.copy(clientExportsDir, remixPkgDir) + ]); +} + +function resolvePackageJsonFile(packageName: string): string { + return require.resolve(path.join(packageName, "package.json")); +} - return object; +async function assignDependency( + deps: { [key: string]: string }, + pkgJsonFile: string +) { + let pkgJson = await fse.readJSON(pkgJsonFile); + deps[pkgJson.name] = pkgJson.version; } diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts index 01b2047b4c..7111aeb0d1 100644 --- a/packages/remix-node/globals.ts +++ b/packages/remix-node/globals.ts @@ -1,18 +1,18 @@ import { atob, btoa } from "./base64"; import { sign, unsign } from "./cookieSigning"; -import { Headers, Request, Response, fetch } from "./fetch"; +import { + Headers as NodeHeaders, + Request as NodeRequest, + Response as NodeResponse, + fetch as nodeFetch +} from "./fetch"; declare global { namespace NodeJS { - type GlobalSignFunc = (value: string, secret: string) => Promise; - type GlobalUnsignFunc = ( - signed: string, - secret: string - ) => Promise; - interface Global { atob: typeof atob; btoa: typeof btoa; + Headers: typeof Headers; Request: typeof Request; Response: typeof Response; @@ -20,8 +20,8 @@ declare global { // TODO: Once node v16 is available on AWS we should remove these globals // and provide the webcrypto API instead. - sign: GlobalSignFunc; - unsign: GlobalUnsignFunc; + sign: typeof sign; + unsign: typeof unsign; } } } @@ -30,10 +30,10 @@ export function installGlobals() { global.atob = atob; global.btoa = btoa; - (global as NodeJS.Global).Headers = Headers; - (global as NodeJS.Global).Request = Request; - (global as NodeJS.Global).Response = Response; - (global as NodeJS.Global).fetch = fetch; + global.Headers = (NodeHeaders as unknown) as typeof Headers; + global.Request = (NodeRequest as unknown) as typeof Request; + global.Response = (NodeResponse as unknown) as typeof Response; + global.fetch = (nodeFetch as unknown) as typeof fetch; global.sign = sign; global.unsign = unsign; diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index f0f4629fd2..3acd439515 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -10,12 +10,4 @@ export { Headers, Request, Response, fetch } from "./fetch"; export { installGlobals } from "./globals"; -export { json, redirect } from "./responses"; - -export type { - ActionFunction, - HeadersFunction, - LoaderFunction -} from "./routeModules"; - export { createFileSessionStorage } from "./sessions/fileStorage"; diff --git a/packages/remix-node/magicExports/platform.ts b/packages/remix-node/magicExports/platform.ts index fef73ca92d..253f4412fe 100644 --- a/packages/remix-node/magicExports/platform.ts +++ b/packages/remix-node/magicExports/platform.ts @@ -1,10 +1,4 @@ // This file lists all exports from this package that are available to `import // "remix"`. -export type { - ActionFunction, - HeadersFunction, - LoaderFunction -} from "@remix-run/node"; - -export { createFileSessionStorage, json, redirect } from "@remix-run/node"; +export { createFileSessionStorage } from "@remix-run/node"; diff --git a/packages/remix-node/responses.ts b/packages/remix-node/responses.ts deleted file mode 100644 index f0bfb584d5..0000000000 --- a/packages/remix-node/responses.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - json as coreJson, - redirect as coreRedirect -} from "@remix-run/server-runtime"; - -import type { - Response as NodeResponse, - ResponseInit as NodeResponseInit -} from "./fetch"; - -export let json = (data: any, init: NodeResponseInit = {}) => - (coreJson(data, init as ResponseInit) as unknown) as NodeResponse; - -export let redirect = (url: string, init: number | NodeResponseInit = 302) => - (coreRedirect(url, init as ResponseInit) as unknown) as NodeResponse; diff --git a/packages/remix-node/routeModules.ts b/packages/remix-node/routeModules.ts deleted file mode 100644 index 594d0c2eb0..0000000000 --- a/packages/remix-node/routeModules.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { - ActionFunction as CoreActionFunction, - HeadersFunction as CoreHeadersFunction, - LoaderFunction as CoreLoaderFunction -} from "@remix-run/server-runtime"; - -import type { Headers, HeadersInit, Request, Response } from "./fetch"; - -export type ActionFunction = CoreActionFunction; - -export type HeadersFunction = CoreHeadersFunction; - -export type LoaderFunction = CoreLoaderFunction; diff --git a/packages/remix-server-runtime/index.ts b/packages/remix-server-runtime/index.ts index f96775d95f..49110b70d3 100644 --- a/packages/remix-server-runtime/index.ts +++ b/packages/remix-server-runtime/index.ts @@ -1,5 +1,3 @@ -import "./assetImportTypes"; - export type { ServerBuild, ServerEntryModule } from "./build"; export type { diff --git a/packages/remix-server-runtime/magicExports/server.ts b/packages/remix-server-runtime/magicExports/server.ts index ce0557a4bf..6203b1b923 100644 --- a/packages/remix-server-runtime/magicExports/server.ts +++ b/packages/remix-server-runtime/magicExports/server.ts @@ -17,7 +17,10 @@ export type { BlockLinkDescriptor, PageLinkDescriptor, ErrorBoundaryComponent, + ActionFunction, + HeadersFunction, LinksFunction, + LoaderFunction, MetaFunction, RouteComponent, RouteHandle, @@ -35,5 +38,7 @@ export { isSession, createSessionStorage, createCookieSessionStorage, - createMemorySessionStorage + createMemorySessionStorage, + json, + redirect } from "@remix-run/server-runtime"; diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index 57c12989ac..f7f509a4a6 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -13,10 +13,9 @@ export interface RouteModules { /** * A function that handles data mutations for a route. */ -export interface ActionFunction { - (args: { request: TRequest; context: AppLoadContext; params: Params }): - | Promise - | TResponse +export interface ActionFunction { + (args: { request: Request; context: AppLoadContext; params: Params }): + | Promise | Response | string; } @@ -30,13 +29,10 @@ export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; * A function that returns HTTP headers to be used for a route. These headers * will be merged with (and take precedence over) headers from parent routes. */ -export interface HeadersFunction< - THeaders = Headers, - THeadersInit = HeadersInit -> { - (args: { loaderHeaders: THeaders; parentHeaders: THeaders }): - | THeaders - | THeadersInit; +export interface HeadersFunction { + (args: { loaderHeaders: Headers; parentHeaders: Headers }): + | Headers + | HeadersInit; } /** @@ -50,10 +46,9 @@ export interface LinksFunction { /** * A function that loads data for a route. */ -export interface LoaderFunction { - (args: { request: TRequest; context: AppLoadContext; params: Params }): - | Promise - | TResponse +export interface LoaderFunction { + (args: { request: Request; context: AppLoadContext; params: Params }): + | Promise | Response | Promise | AppData; From 02232287a5cffbfede7add66c82a60c7f40484ce Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 1 Sep 2021 09:37:39 -0700 Subject: [PATCH 0064/1690] Version 0.18.0-pre.2 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 8de6a34c4a..bf474d495e 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.18.0-pre.1", + "version": "0.18.0-pre.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 3be012c217..827c6dd337 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.18.0-pre.1", + "version": "0.18.0-pre.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.18.0-pre.1" + "@remix-run/node": "0.18.0-pre.2" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 90f4581ae1..e9cbf663b1 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.18.0-pre.1", + "version": "0.18.0-pre.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.18.0-pre.1", + "@remix-run/server-runtime": "0.18.0-pre.2", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 3fa24d8b21..51c1c4d6aa 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.18.0-pre.1", + "version": "0.18.0-pre.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.18.0-pre.1", + "@remix-run/express": "0.18.0-pre.2", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 2e6ee83975..458fead657 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.18.0-pre.1", + "version": "0.18.0-pre.2", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 9673f9ab5f93b92c8adf95753ec8579a950dd35b Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 1 Sep 2021 09:52:58 -0700 Subject: [PATCH 0065/1690] Fix `remix run` command regression --- packages/remix-dev/cli.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index 8db5b26d06..d73c130e85 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -52,6 +52,9 @@ switch (cli.input[0]) { commands.setup(cli.input[1]); break; case "run": + if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; + commands.run(cli.input[1], process.env.NODE_ENV); + break; default: // `remix ./my-project` is shorthand for `remix run ./my-project` if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; From cdc29c9611de69095072732c7fb1e319d0f8e2ce Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 1 Sep 2021 09:53:32 -0700 Subject: [PATCH 0066/1690] Version 0.18.0-pre.3 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index bf474d495e..d8f1682e9d 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.18.0-pre.2", + "version": "0.18.0-pre.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 827c6dd337..141475d62d 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.18.0-pre.2", + "version": "0.18.0-pre.3", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.18.0-pre.2" + "@remix-run/node": "0.18.0-pre.3" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index e9cbf663b1..94242f009a 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.18.0-pre.2", + "version": "0.18.0-pre.3", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.18.0-pre.2", + "@remix-run/server-runtime": "0.18.0-pre.3", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 51c1c4d6aa..15e9961c61 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.18.0-pre.2", + "version": "0.18.0-pre.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.18.0-pre.2", + "@remix-run/express": "0.18.0-pre.3", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 458fead657..0dad73b36c 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.18.0-pre.2", + "version": "0.18.0-pre.3", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 677bfc66f6ae8a2da746f7bcfb2b927d54778418 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Mon, 14 Jun 2021 14:09:38 -0600 Subject: [PATCH 0067/1690] useTransition, useActionData, useFetcher, shouldReload This is kinda big but opens up a lot of use cases, - can build better pending navigation UI with useTransition telling app more detailed information (state, type) - replaces usePendingFormSubmit and usePendingLocation - actually aborts stale submissions/loads - fixes bugs around interrupted submissions/navigations - actions can return data now - super useful for form validation, no more screwing around with sessions - allows apps to call loaders and actions outside of navigation - manages cancellation of stale submissions and loads - reloads route data after actions - commits the freshest reloaded data along the way when there are multiple inflight allows route modules to decide if they should reload or not - after submissions - when the search params change - when the same href is navigated to other stuff - reloads route data when the same href is navigated to - does not create ghost history entries on interrupted navigation These old hooks still work, but have been deprecated for the new hooks. - useRouteData -> useLoaderData - usePendingFormSubmit -> useTransition().submission - usePendingLocation -> useTransition().location Also includes a helping of docs updates Closes #169, #151, #175, #128, #54, #208 --- .../remix-dev/__tests__/readConfig-test.ts | 14 +++++++++ packages/remix-dev/compiler.ts | 3 +- packages/remix-dev/compiler/assets.ts | 8 +++-- .../__tests__/data-test.ts | 31 ------------------- packages/remix-server-runtime/data.ts | 21 +++---------- packages/remix-server-runtime/entry.ts | 1 + packages/remix-server-runtime/headers.ts | 7 +++-- packages/remix-server-runtime/routeData.ts | 4 +++ packages/remix-server-runtime/routeModules.ts | 13 ++++---- packages/remix-server-runtime/routes.ts | 5 +-- packages/remix-server-runtime/server.ts | 27 +++++++++++----- 11 files changed, 66 insertions(+), 68 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index e341a56e00..5438ef39ae 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -71,6 +71,13 @@ describe("readConfig", () => { "parentId": "root", "path": "action-errors-self-boundary", }, + "routes/actions": Object { + "caseSensitive": false, + "file": "routes/actions.tsx", + "id": "routes/actions", + "parentId": "root", + "path": "actions", + }, "routes/blog/hello-world": Object { "caseSensitive": false, "file": "routes/blog/hello-world.mdx", @@ -106,6 +113,13 @@ describe("readConfig", () => { "parentId": "root", "path": "empty", }, + "routes/fetchers": Object { + "caseSensitive": false, + "file": "routes/fetchers.tsx", + "id": "routes/fetchers", + "parentId": "root", + "path": "fetchers", + }, "routes/gists": Object { "caseSensitive": false, "file": "routes/gists.jsx", diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 530a1caf64..cd591c6157 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -431,7 +431,8 @@ const browserSafeRouteExports: { [name: string]: boolean } = { default: true, handle: true, links: true, - meta: true + meta: true, + shouldReload: true }; /** diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index e4e39b5357..a8fba0d34f 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -24,8 +24,9 @@ interface AssetsManifest { caseSensitive?: boolean; module: string; imports?: string[]; - hasAction?: boolean; - hasLoader?: boolean; + hasAction: boolean; + hasLoader: boolean; + hasErrorBoundary: boolean; }; }; } @@ -90,7 +91,8 @@ export async function createAssetsManifest( module: resolveUrl(key), imports: resolveImports(output.imports), hasAction: sourceExports.includes("action"), - hasLoader: sourceExports.includes("loader") + hasLoader: sourceExports.includes("loader"), + hasErrorBoundary: sourceExports.includes("ErrorBoundary") }; } } diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index 13ad1d08b8..778d5db2f1 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -1,37 +1,6 @@ import { ServerBuild } from "../build"; import { createRequestHandler } from "../server"; -describe("actions", () => { - it("returns a redirect when actions return a string", async () => { - let location = "/just/a/string"; - let action = async () => location; - - let routeId = "routes/random"; - let build = ({ - routes: { - [routeId]: { - id: routeId, - path: "/random", - module: { action } - } - } - } as unknown) as ServerBuild; - - let handler = createRequestHandler(build); - - let request = new Request("http://example.com/random", { - method: "POST", - headers: { - "Content-Type": "application/json" - } - }); - - let res = await handler(request); - expect(res.status).toBe(303); - expect(res.headers.get("location")).toBe(location); - }); -}); - describe("loaders", () => { // so that HTML/Fetch requests are the same, and so redirects don't hang on to // this param for no reason diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index cd5184af91..b692b08801 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -32,7 +32,7 @@ export async function loadRouteData( if (result === undefined) { throw new Error( `You defined a loader for route "${routeId}" but didn't return ` + - `anything from your \`loader\` function. We can't do everything for you! 😅` + `anything from your \`loader\` function. Please return a value or \`null\`.` ); } @@ -58,25 +58,14 @@ export async function callRouteAction( let result = await routeModule.action({ request, context, params }); - if (typeof result === "string") { - return new Response("", { - status: 303, - headers: { Location: result } - }); - } - - if (!isResponse(result) || result.headers.get("Location") == null) { + if (result === undefined) { throw new Error( - `You made a ${request.method} request to ${request.url} but did not return ` + - `a redirect. Please \`return newUrlString\` or \`return redirect(newUrl)\` from ` + - `your \`action\` function to avoid reposts when users click the back button.` + `You defined an action for route "${routeId}" but didn't return ` + + `anything from your \`action\` function. Please return a value or \`null\`.` ); } - return new Response("", { - status: 303, - headers: result.headers - }); + return isResponse(result) ? result : json(result); } function isResponse(value: any): value is Response { diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index 64b0e72e16..25d0be08ed 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -14,6 +14,7 @@ export interface EntryContext { manifest: AssetsManifest; matches: RouteMatch[]; routeData: RouteData; + actionData?: RouteData; routeModules: RouteModules; serverHandoffString?: string; } diff --git a/packages/remix-server-runtime/headers.ts b/packages/remix-server-runtime/headers.ts index 9354640ce9..05730b96fa 100644 --- a/packages/remix-server-runtime/headers.ts +++ b/packages/remix-server-runtime/headers.ts @@ -7,23 +7,26 @@ import type { RouteMatch } from "./routeMatching"; export function getDocumentHeaders( build: ServerBuild, matches: RouteMatch[], - routeLoaderResponses: Response[] + routeLoaderResponses: Response[], + actionResponse?: Response ): Headers { return matches.reduce((parentHeaders, match, index) => { let routeModule = build.routes[match.route.id].module; let loaderHeaders = routeLoaderResponses[index] ? routeLoaderResponses[index].headers : new Headers(); + let actionHeaders = actionResponse ? actionResponse.headers : new Headers(); let headers = new Headers( routeModule.headers ? typeof routeModule.headers === "function" - ? routeModule.headers({ loaderHeaders, parentHeaders }) + ? routeModule.headers({ loaderHeaders, parentHeaders, actionHeaders }) : routeModule.headers : undefined ); // Automatically preserve Set-Cookie headers that were set either by the // loader or by a parent route. + prependCookies(actionHeaders, headers); prependCookies(loaderHeaders, headers); prependCookies(parentHeaders, headers); diff --git a/packages/remix-server-runtime/routeData.ts b/packages/remix-server-runtime/routeData.ts index d5315a4892..d6da0dba2a 100644 --- a/packages/remix-server-runtime/routeData.ts +++ b/packages/remix-server-runtime/routeData.ts @@ -18,3 +18,7 @@ export async function createRouteData( return memo; }, {} as RouteData); } + +export async function createActionData(response: Response): Promise { + return extractData(response); +} diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index f7f509a4a6..d61f083c6b 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -15,9 +15,8 @@ export interface RouteModules { */ export interface ActionFunction { (args: { request: Request; context: AppLoadContext; params: Params }): - | Promise - | Response - | string; + | Promise + | Response; } /** @@ -30,9 +29,11 @@ export type ErrorBoundaryComponent = ComponentType<{ error: Error }>; * will be merged with (and take precedence over) headers from parent routes. */ export interface HeadersFunction { - (args: { loaderHeaders: Headers; parentHeaders: Headers }): - | Headers - | HeadersInit; + (args: { + loaderHeaders: Headers; + parentHeaders: Headers; + actionHeaders: Headers; + }): Headers | HeadersInit; } /** diff --git a/packages/remix-server-runtime/routes.ts b/packages/remix-server-runtime/routes.ts index 4ac4957a9b..405ab87dc8 100644 --- a/packages/remix-server-runtime/routes.ts +++ b/packages/remix-server-runtime/routes.ts @@ -14,8 +14,9 @@ interface Route { } export interface EntryRoute extends Route { - hasAction?: boolean; - hasLoader?: boolean; + hasAction: boolean; + hasLoader: boolean; + hasErrorBoundary: boolean; imports?: string[]; module: string; } diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index de0b8b06df..897e2776cd 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -13,7 +13,7 @@ import { matchServerRoutes } from "./routeMatching"; import { ServerMode, isServerMode } from "./mode"; import type { ServerRoute } from "./routes"; import { createRoutes } from "./routes"; -import { createRouteData } from "./routeData"; +import { createActionData, createRouteData } from "./routeData"; import { json } from "./responses"; import { createServerHandoffString } from "./serverHandoff"; @@ -157,18 +157,19 @@ async function handleDocumentRequest( }; let actionErrored: boolean = false; + let actionResponse: Response | undefined; if (isActionRequest(request)) { let leafMatch = matches[matches.length - 1]; try { - let actionResponse = await callRouteAction( + actionResponse = await callRouteAction( build, leafMatch.route.id, - request, + request.clone(), loadContext, leafMatch.params ); - if (actionResponse && isRedirectResponse(actionResponse)) { + if (isRedirectResponse(actionResponse)) { return actionResponse; } } catch (error) { @@ -183,7 +184,10 @@ async function handleDocumentRequest( let matchesToLoad = actionErrored ? getMatchesUpToDeepestErrorBoundary( - // get rid of the action, we know we don't want to call it's loader + // get rid of the action, we don't want to call it's loader either + // because we'll be rendering the error boundary, if you can get access + // to the loader data in the error boundary then how the heck is it + // supposed to deal with errors in the loader, too? matches.slice(0, -1) ) : matches; @@ -278,18 +282,27 @@ async function handleDocumentRequest( let headers = getDocumentHeaders( build, renderableMatches, - routeLoaderResponses + routeLoaderResponses, + actionResponse ); let entryMatches = createEntryMatches(renderableMatches, build.assets.routes); let routeData = await createRouteData( renderableMatches, routeLoaderResponses ); + let actionData = actionResponse + ? { + [matches[matches.length - 1].route.id]: await createActionData( + actionResponse + ) + } + : undefined; let routeModules = createEntryRouteModules(build.routes); let serverHandoff = { matches: entryMatches, componentDidCatchEmulator, - routeData + routeData, + actionData }; let entryContext: EntryContext = { ...serverHandoff, From b6fb3eaeafdd1da20517611243e21dc09a7fbfae Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 2 Sep 2021 13:55:11 -0700 Subject: [PATCH 0068/1690] Version 0.18.0 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index d8f1682e9d..9463b901fc 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.18.0-pre.3", + "version": "0.18.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 141475d62d..32167ff0d2 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.18.0-pre.3", + "version": "0.18.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.18.0-pre.3" + "@remix-run/node": "0.18.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 94242f009a..3e51c8dfa4 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.18.0-pre.3", + "version": "0.18.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.18.0-pre.3", + "@remix-run/server-runtime": "0.18.0", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 15e9961c61..fc2ef954ae 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.18.0-pre.3", + "version": "0.18.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.18.0-pre.3", + "@remix-run/express": "0.18.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 0dad73b36c..f207a9672f 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.18.0-pre.3", + "version": "0.18.0", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 03a30a933096b05c1d9d3db2add88975d258af05 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 7 Sep 2021 10:56:32 -0700 Subject: [PATCH 0069/1690] fix: remove nullish coalescing operators --- packages/remix-dev/compiler/plugins/mdx.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/remix-dev/compiler/plugins/mdx.ts b/packages/remix-dev/compiler/plugins/mdx.ts index fd19abf942..97641dac43 100644 --- a/packages/remix-dev/compiler/plugins/mdx.ts +++ b/packages/remix-dev/compiler/plugins/mdx.ts @@ -56,12 +56,19 @@ export function mdxPlugin(config: RemixConfig): esbuild.Plugin { location: message.line || message.column ? { - column: message.column ?? undefined, - line: message.line ?? undefined + column: + typeof message.column === "number" + ? message.column + : undefined, + line: + typeof message.line === "number" + ? message.line + : undefined } : undefined, text: message.message, - detail: message.note ?? undefined + detail: + typeof message.note === "string" ? message.note : undefined }); }); @@ -72,7 +79,7 @@ export function mdxPlugin(config: RemixConfig): esbuild.Plugin { resolveDir: path.dirname(args.path), loader: getLoaderForFile(args.path) }; - } catch (err) { + } catch (err: any) { return { errors: [ { From 6a8f59e12a0230331c5ee7e8379070bf8403a4e5 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 8 Sep 2021 10:16:17 -0600 Subject: [PATCH 0070/1690] Version 0.18.1 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 9463b901fc..e5b7f0c300 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.18.0", + "version": "0.18.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 32167ff0d2..e242ec26d4 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.18.0", + "version": "0.18.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.18.0" + "@remix-run/node": "0.18.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 3e51c8dfa4..f253581a3e 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.18.0", + "version": "0.18.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.18.0", + "@remix-run/server-runtime": "0.18.1", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index fc2ef954ae..b0fd8af9a7 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.18.0", + "version": "0.18.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.18.0", + "@remix-run/express": "0.18.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index f207a9672f..3eec108afe 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.18.0", + "version": "0.18.1", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From cd5d57ab5ee4fbbfce77302fa99d86a9e605466e Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 8 Sep 2021 21:44:42 -0600 Subject: [PATCH 0071/1690] Adds catchall route naming convention Files named: routes/docs/$.jsx routes/docs.$.jsx Become: routes/docs/* --- .../remix-dev/__tests__/readConfig-test.ts | 28 +++++++++++++++++++ packages/remix-dev/config/routesConvention.ts | 12 +++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 5438ef39ae..9565cbd4b1 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -106,6 +106,34 @@ describe("readConfig", () => { "parentId": "root", "path": "blog/third", }, + "routes/catchall-nested": Object { + "caseSensitive": false, + "file": "routes/catchall-nested.jsx", + "id": "routes/catchall-nested", + "parentId": "root", + "path": "catchall-nested", + }, + "routes/catchall-nested-no-layout/$": Object { + "caseSensitive": false, + "file": "routes/catchall-nested-no-layout/$.jsx", + "id": "routes/catchall-nested-no-layout/$", + "parentId": "root", + "path": "catchall-nested-no-layout/*", + }, + "routes/catchall-nested/$": Object { + "caseSensitive": false, + "file": "routes/catchall-nested/$.jsx", + "id": "routes/catchall-nested/$", + "parentId": "routes/catchall-nested", + "path": "*", + }, + "routes/catchall.flat.$": Object { + "caseSensitive": false, + "file": "routes/catchall.flat.$.jsx", + "id": "routes/catchall.flat.$", + "parentId": "root", + "path": "catchall/flat/*", + }, "routes/empty": Object { "caseSensitive": false, "file": "routes/empty.jsx", diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 831e375c0a..c8cd82c4a7 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -64,7 +64,17 @@ export function defineConventionalRoutes(appDir: string): RouteManifest { } function createRoutePath(routeId: string): string { - let path = routeId.replace(/\$/g, ":").replace(/\./g, "/"); + let path = routeId + // routes/$ -> routes/* + // routes/nested/$.tsx (with a "routes/nested.tsx" layout) + .replace(/^\$$/, "*") + // routes/docs.$ -> routes/docs/* + // routes/docs/$ -> routes/docs/* + .replace(/(\/|\.)\$$/, "/*") + // routes/$user -> routes/:user + .replace(/\$/g, ":") + // routes/not.nested -> routes/not/nested + .replace(/\./g, "/"); return /\b\/?index$/.test(path) ? path.replace(/\/?index$/, "") : path; } From 0b6c98dc587b702adc4848acc2244e1602e8cf0b Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 17 Sep 2021 14:41:52 -0700 Subject: [PATCH 0072/1690] Version 0.18.2 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index e5b7f0c300..08f2f75b81 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.18.1", + "version": "0.18.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index e242ec26d4..3b02850707 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.18.1", + "version": "0.18.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.18.1" + "@remix-run/node": "0.18.2" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index f253581a3e..4f242df1fc 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.18.1", + "version": "0.18.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.18.1", + "@remix-run/server-runtime": "0.18.2", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index b0fd8af9a7..6122e2b38e 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.18.1", + "version": "0.18.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.18.1", + "@remix-run/express": "0.18.2", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 3eec108afe..2385b540bb 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.18.1", + "version": "0.18.2", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 6f601883f5631cf7c68900c993dbaec594494324 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 17 Sep 2021 16:56:24 -0700 Subject: [PATCH 0073/1690] Version 0.18.2 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index e5b7f0c300..08f2f75b81 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.18.1", + "version": "0.18.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index e242ec26d4..3b02850707 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.18.1", + "version": "0.18.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.18.1" + "@remix-run/node": "0.18.2" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index f253581a3e..4f242df1fc 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.18.1", + "version": "0.18.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.18.1", + "@remix-run/server-runtime": "0.18.2", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index b0fd8af9a7..6122e2b38e 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.18.1", + "version": "0.18.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.18.1", + "@remix-run/express": "0.18.2", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 3eec108afe..2385b540bb 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.18.1", + "version": "0.18.2", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 1788e128afac0c2b24faea71a652790be4f5b3a0 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 17 Sep 2021 17:09:07 -0700 Subject: [PATCH 0074/1690] Revert "Version 0.18.2" This reverts commit 0b6c98dc587b702adc4848acc2244e1602e8cf0b. --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 08f2f75b81..e5b7f0c300 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.18.2", + "version": "0.18.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 3b02850707..e242ec26d4 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.18.2", + "version": "0.18.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.18.2" + "@remix-run/node": "0.18.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 4f242df1fc..f253581a3e 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.18.2", + "version": "0.18.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.18.2", + "@remix-run/server-runtime": "0.18.1", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 6122e2b38e..b0fd8af9a7 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.18.2", + "version": "0.18.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.18.2", + "@remix-run/express": "0.18.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 2385b540bb..3eec108afe 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.18.2", + "version": "0.18.1", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From bbd2306575d66f84b3361af6508c116df7382824 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 20 Sep 2021 14:26:05 -0700 Subject: [PATCH 0075/1690] =?UTF-8?q?feat:=20added=20CatchBoundary=20suppo?= =?UTF-8?q?rt=20=F0=9F=A7=A4=20(#264)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: removal of routes/404 for root CatchBoundary (#275) chore: added CatchBoundary docs chore: added default CatchBoundary to templates --- .../remix-dev/__tests__/readConfig-test.ts | 38 ++++- packages/remix-dev/compiler.ts | 5 +- packages/remix-dev/compiler/assets.ts | 2 + packages/remix-dev/config.ts | 2 +- packages/remix-dev/config/routesConvention.ts | 7 +- .../__tests__/data-test.ts | 39 ++++- packages/remix-server-runtime/data.ts | 29 +++- packages/remix-server-runtime/errors.ts | 8 + packages/remix-server-runtime/routeModules.ts | 6 + packages/remix-server-runtime/server.ts | 156 +++++++++++++----- 10 files changed, 235 insertions(+), 57 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 9565cbd4b1..a5be24a234 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -48,14 +48,35 @@ describe("readConfig", () => { "root": Object { "file": "root.jsx", "id": "root", - "path": "/", + "path": "", }, - "routes/404": Object { + "routes/action-catches": Object { "caseSensitive": false, - "file": "routes/404.jsx", - "id": "routes/404", + "file": "routes/action-catches.jsx", + "id": "routes/action-catches", "parentId": "root", - "path": "*", + "path": "action-catches", + }, + "routes/action-catches-from-loader": Object { + "caseSensitive": false, + "file": "routes/action-catches-from-loader.jsx", + "id": "routes/action-catches-from-loader", + "parentId": "root", + "path": "action-catches-from-loader", + }, + "routes/action-catches-from-loader-self-boundary": Object { + "caseSensitive": false, + "file": "routes/action-catches-from-loader-self-boundary.jsx", + "id": "routes/action-catches-from-loader-self-boundary", + "parentId": "root", + "path": "action-catches-from-loader-self-boundary", + }, + "routes/action-catches-self-boundary": Object { + "caseSensitive": false, + "file": "routes/action-catches-self-boundary.jsx", + "id": "routes/action-catches-self-boundary", + "parentId": "root", + "path": "action-catches-self-boundary", }, "routes/action-errors": Object { "caseSensitive": false, @@ -204,6 +225,13 @@ describe("readConfig", () => { "parentId": "routes/loader-errors", "path": "nested", }, + "routes/loader-errors/nested-catch": Object { + "caseSensitive": false, + "file": "routes/loader-errors/nested-catch.jsx", + "id": "routes/loader-errors/nested-catch", + "parentId": "routes/loader-errors", + "path": "nested-catch", + }, "routes/methods": Object { "caseSensitive": false, "file": "routes/methods.tsx", diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index cd591c6157..d0372c2bb9 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -132,7 +132,7 @@ export async function watch( try { [browserBuild, serverBuild] = await buildEverything(config, options); if (onRebuildFinish) onRebuildFinish(); - } catch (err) { + } catch (err: any) { onBuildFailure(err); } return; @@ -427,6 +427,7 @@ export const routes = { type Route = RemixConfig["routes"][string]; const browserSafeRouteExports: { [name: string]: boolean } = { + CatchBoundary: true, ErrorBoundary: true, default: true, handle: true, @@ -471,7 +472,7 @@ function browserRouteModulesPlugin( exports = ( await getRouteModuleExportsCached(config, route.id) ).filter(ex => !!browserSafeRouteExports[ex]); - } catch (error) { + } catch (error: any) { return { errors: [ { diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index a8fba0d34f..94d433a3f1 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -26,6 +26,7 @@ interface AssetsManifest { imports?: string[]; hasAction: boolean; hasLoader: boolean; + hasCatchBoundary: boolean; hasErrorBoundary: boolean; }; }; @@ -92,6 +93,7 @@ export async function createAssetsManifest( imports: resolveImports(output.imports), hasAction: sourceExports.includes("action"), hasLoader: sourceExports.includes("loader"), + hasCatchBoundary: sourceExports.includes("CatchBoundary"), hasErrorBoundary: sourceExports.includes("ErrorBoundary") }; } diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index e2e0b8e7ae..0800edfdd2 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -192,7 +192,7 @@ export async function readConfig( } let routes: RouteManifest = { - root: { path: "/", id: "root", file: rootRouteFile } + root: { path: "", id: "root", file: rootRouteFile } }; if (fs.existsSync(path.resolve(appDirectory, "routes"))) { let conventionalRoutes = defineConventionalRoutes(appDirectory); diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index c8cd82c4a7..2af501eb1c 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -49,10 +49,9 @@ export function defineConventionalRoutes(appDir: string): RouteManifest { ); for (let routeId of childRouteIds) { - let routePath = - routeId === "routes/404" - ? "*" - : createRoutePath(routeId.slice((parentId || "routes").length + 1)); + let routePath = createRoutePath( + routeId.slice((parentId || "routes").length + 1) + ); defineRoute(routePath, files[routeId], () => { defineNestedRoutes(defineRoute, routeId); diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index 778d5db2f1..b374c4bcd4 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -22,7 +22,7 @@ describe("loaders", () => { } } as unknown) as ServerBuild; - let handler = createRequestHandler(build); + let handler = createRequestHandler(build, {}); let request = new Request( "http://example.com/random?_data=routes/random&foo=bar", @@ -36,4 +36,41 @@ describe("loaders", () => { let res = await handler(request); expect(await res.json()).toMatchInlineSnapshot(`"?foo=bar"`); }); + + it("sets header for throw responses", async () => { + let loader = async ({ request }) => { + throw new Response("null", { + headers: { + "Content-type": "application/json" + } + }); + }; + + let routeId = "routes/random"; + let build = ({ + routes: { + [routeId]: { + id: routeId, + path: "/random", + module: { + loader + } + } + } + } as unknown) as ServerBuild; + + let handler = createRequestHandler(build, {}); + + let request = new Request( + "http://example.com/random?_data=routes/random&foo=bar", + { + headers: { + "Content-Type": "application/json" + } + } + ); + + let res = await handler(request); + expect(await res.headers.get("X-Remix-Catch")).toBeTruthy(); + }); }); diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index b692b08801..50ef80cf9d 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -27,7 +27,18 @@ export async function loadRouteData( return Promise.resolve(json(null)); } - let result = await routeModule.loader({ request, context, params }); + let result; + + try { + result = await routeModule.loader({ request, context, params }); + } catch (error) { + if (!isResponse(error)) { + throw error; + } + + error.headers.set("X-Remix-Catch", "yes"); + result = error; + } if (result === undefined) { throw new Error( @@ -56,7 +67,17 @@ export async function callRouteAction( ); } - let result = await routeModule.action({ request, context, params }); + let result + try { + result = await routeModule.action({ request, context, params }); + } catch (error) { + if (!isResponse(error)) { + throw error; + } + + error.headers.set("X-Remix-Catch", "yes"); + result = error; + } if (result === undefined) { throw new Error( @@ -68,6 +89,10 @@ export async function callRouteAction( return isResponse(result) ? result : json(result); } +export function isCatchResponse(value: any) { + return isResponse(value) && value.headers.get("X-Remix-Catch") != null; +} + function isResponse(value: any): value is Response { return ( value != null && diff --git a/packages/remix-server-runtime/errors.ts b/packages/remix-server-runtime/errors.ts index 9cea8a329a..9fece4e8a5 100644 --- a/packages/remix-server-runtime/errors.ts +++ b/packages/remix-server-runtime/errors.ts @@ -42,10 +42,18 @@ export interface ComponentDidCatchEmulator { error?: SerializedError; + catch?: ThrownResponse; + catchBoundaryRouteId: string | null; loaderBoundaryRouteId: string | null; // `null` means the app layout threw before any routes rendered renderBoundaryRouteId: string | null; trackBoundaries: boolean; + trackCatchBoundaries: boolean; +} + +export interface ThrownResponse { + status: number; + data: T; } export interface SerializedError { diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index d61f083c6b..140fd7d4db 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -19,6 +19,11 @@ export interface ActionFunction { | Response; } +/** + * A React component that is rendered when the server throws a Response. + */ +export type CatchBoundaryComponent = ComponentType<{}>; + /** * A React component that is rendered when there is an error on a route. */ @@ -80,6 +85,7 @@ export type RouteComponent = ComponentType<{}>; export type RouteHandle = any; export interface EntryRouteModule { + CatchBoundary?: CatchBoundaryComponent; ErrorBoundary?: ErrorBoundaryComponent; default: RouteComponent; handle?: RouteHandle; diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index 897e2776cd..e8735eb860 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -1,4 +1,4 @@ -import type { AppLoadContext } from "./data"; +import { AppLoadContext, extractData, isCatchResponse } from "./data"; import { loadRouteData, callRouteAction } from "./data"; import type { ComponentDidCatchEmulator } from "./errors"; @@ -103,7 +103,7 @@ async function handleDataRequest( loadContext, routeMatch.params ); - } catch (error) { + } catch (error: any) { let formattedError = (await platform.formatServerError?.(error)) || error; return json(await serializeError(formattedError), { status: 500, @@ -141,25 +141,49 @@ async function handleDocumentRequest( let url = new URL(request.url); let matches = matchServerRoutes(routes, url.pathname); + let isNoMatch = false; + if (!matches) { - // TODO: Provide a default 404 page - throw new Error( - `There is no route that matches ${url.pathname}. Please add ` + - `a routes/404.js file` - ); + // If we do not match a user-provided-route, fall back to the root + // to allow the CatchBoundary to take over + isNoMatch = true; + matches = [ + { + params: {}, + pathname: "", + route: routes[0] + } + ]; } let componentDidCatchEmulator: ComponentDidCatchEmulator = { trackBoundaries: true, + trackCatchBoundaries: true, + catchBoundaryRouteId: null, renderBoundaryRouteId: null, loaderBoundaryRouteId: null, - error: undefined + error: undefined, + catch: undefined }; - let actionErrored: boolean = false; + let actionState: "" | "caught" | "error" = ""; let actionResponse: Response | undefined; - if (isActionRequest(request)) { + if (isNoMatch) { + componentDidCatchEmulator.trackCatchBoundaries = false; + let withBoundaries = getMatchesUpToDeepestBoundary( + matches, + "CatchBoundary" + ); + componentDidCatchEmulator.catchBoundaryRouteId = + withBoundaries.length > 0 + ? withBoundaries[withBoundaries.length - 1].route.id + : null; + componentDidCatchEmulator.catch = { + status: 404, + data: null + }; + } else if (isActionRequest(request)) { let leafMatch = matches[matches.length - 1]; try { actionResponse = await callRouteAction( @@ -172,25 +196,58 @@ async function handleDocumentRequest( if (isRedirectResponse(actionResponse)) { return actionResponse; } - } catch (error) { + } catch (error: any) { let formattedError = (await platform.formatServerError?.(error)) || error; - actionErrored = true; - let withBoundaries = getMatchesUpToDeepestErrorBoundary(matches); + actionState = "error"; + let withBoundaries = getMatchesUpToDeepestBoundary( + matches, + "ErrorBoundary" + ); componentDidCatchEmulator.loaderBoundaryRouteId = withBoundaries[withBoundaries.length - 1].route.id; componentDidCatchEmulator.error = await serializeError(formattedError); } } - let matchesToLoad = actionErrored - ? getMatchesUpToDeepestErrorBoundary( + if (actionResponse && isCatchResponse(actionResponse)) { + actionState = "caught"; + let withBoundaries = getMatchesUpToDeepestBoundary( + matches, + "CatchBoundary" + ); + componentDidCatchEmulator.trackCatchBoundaries = false; + componentDidCatchEmulator.catchBoundaryRouteId = + withBoundaries[withBoundaries.length - 1].route.id; + componentDidCatchEmulator.catch = { + status: actionResponse.status, + data: await extractData(actionResponse.clone()) + }; + } + + // If we did not match a route, there is no need to call any loaders + let matchesToLoad = isNoMatch ? [] : matches; + switch (actionState) { + case "caught": + matchesToLoad = getMatchesUpToDeepestBoundary( + // get rid of the action, we don't want to call it's loader either + // because we'll be rendering the catch boundary, if you can get access + // to the loader data in the catch boundary then how the heck is it + // supposed to deal with thrown responses? + matches.slice(0, -1), + "CatchBoundary" + ); + break; + case "error": + matchesToLoad = getMatchesUpToDeepestBoundary( // get rid of the action, we don't want to call it's loader either // because we'll be rendering the error boundary, if you can get access // to the loader data in the error boundary then how the heck is it // supposed to deal with errors in the loader, too? - matches.slice(0, -1) - ) - : matches; + matches.slice(0, -1), + "ErrorBoundary" + ); + break; + } // Run all data loaders in parallel. Await them in series below. Note: This // code is a little weird due to the way unhandled promise rejections are @@ -226,16 +283,21 @@ async function handleDocumentRequest( // We just give up and move on with rendering the error as deeply as we can, // which is the previous iteration of this loop if ( - actionErrored && - (response instanceof Error || isRedirectResponse(response)) + (actionState === "error" && + (response instanceof Error || isRedirectResponse(response))) || + (actionState === "caught" && isCatchResponse(response)) ) { break; } - if (componentDidCatchEmulator.error) { + if (componentDidCatchEmulator.catch || componentDidCatchEmulator.error) { continue; } + if (routeModule.CatchBoundary) { + componentDidCatchEmulator.catchBoundaryRouteId = route.id; + } + if (routeModule.ErrorBoundary) { componentDidCatchEmulator.loaderBoundaryRouteId = route.id; } @@ -254,6 +316,13 @@ async function handleDocumentRequest( routeLoaderResults[index] = json(null, { status: 500 }); } else if (isRedirectResponse(response)) { return response; + } else if (isCatchResponse(response)) { + componentDidCatchEmulator.trackCatchBoundaries = false; + componentDidCatchEmulator.catch = { + status: response.status, + data: await extractData(response.clone()) + }; + routeLoaderResults[index] = json(null, { status: response.status }); } } @@ -262,17 +331,18 @@ async function handleDocumentRequest( // Handle responses with a non-200 status code. The first loader with a // non-200 status code determines the status code for the whole response. - let notOkResponse = routeLoaderResponses.find( - response => response.status !== 200 + let notOkResponse = [actionResponse, ...routeLoaderResponses].find( + response => response && response.status !== 200 ); - let statusCode = actionErrored - ? 500 - : notOkResponse - ? notOkResponse.status - : matches[matches.length - 1].route.id === "routes/404" - ? 404 - : 200; + let statusCode = + actionState === "error" + ? 500 + : notOkResponse + ? notOkResponse.status + : isNoMatch + ? 404 + : 200; let renderableMatches = getRenderableMatches( matches, @@ -319,7 +389,7 @@ async function handleDocumentRequest( headers, entryContext ); - } catch (error) { + } catch (error: any) { let formattedError = (await platform.formatServerError?.(error)) || error; if (serverMode !== ServerMode.Test) { console.error(formattedError); @@ -344,7 +414,7 @@ async function handleDocumentRequest( headers, entryContext ); - } catch (error) { + } catch (error: any) { let formattedError = (await platform.formatServerError?.(error)) || error; if (serverMode !== ServerMode.Test) { console.error(formattedError); @@ -390,24 +460,25 @@ function stripDataParam(request: Request) { return new Request(url.toString(), request); } -// This ensures we only load the data for the routes above an action error -function getMatchesUpToDeepestErrorBoundary( - matches: RouteMatch[] +// TODO: update to use key for lookup +function getMatchesUpToDeepestBoundary( + matches: RouteMatch[], + key: "CatchBoundary" | "ErrorBoundary" ) { - let deepestErrorBoundaryIndex: number = -1; + let deepestBoundaryIndex: number = -1; matches.forEach((match, index) => { - if (match.route.module.ErrorBoundary) { - deepestErrorBoundaryIndex = index; + if (match.route.module[key]) { + deepestBoundaryIndex = index; } }); - if (deepestErrorBoundaryIndex === -1) { + if (deepestBoundaryIndex === -1) { // no route error boundaries, don't need to call any loaders return []; } - return matches.slice(0, deepestErrorBoundaryIndex + 1); + return matches.slice(0, deepestBoundaryIndex + 1); } // This prevents `` from rendering anything below where the error threw @@ -417,7 +488,7 @@ function getRenderableMatches( componentDidCatchEmulator: ComponentDidCatchEmulator ) { // no error, no worries - if (!componentDidCatchEmulator.error) { + if (!componentDidCatchEmulator.catch && !componentDidCatchEmulator.error) { return matches; } @@ -427,7 +498,8 @@ function getRenderableMatches( let id = match.route.id; if ( componentDidCatchEmulator.renderBoundaryRouteId === id || - componentDidCatchEmulator.loaderBoundaryRouteId === id + componentDidCatchEmulator.loaderBoundaryRouteId === id || + componentDidCatchEmulator.catchBoundaryRouteId === id ) { lastRenderableIndex = index; } From 8638a21e2e057a70febf1ca639a7879e05f6deb6 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 20 Sep 2021 15:01:52 -0700 Subject: [PATCH 0076/1690] feat: distinguish between index/layout routes (#284) --- .../remix-dev/__tests__/readConfig-test.ts | 21 ++++++ .../__tests__/data-test.ts | 66 +++++++++++++++++++ packages/remix-server-runtime/server.ts | 63 ++++++++++++++---- 3 files changed, 139 insertions(+), 11 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index a5be24a234..f6112d1199 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -246,6 +246,27 @@ describe("readConfig", () => { "parentId": "root", "path": "multiple-set-cookies", }, + "routes/nested-forms": Object { + "caseSensitive": false, + "file": "routes/nested-forms.tsx", + "id": "routes/nested-forms", + "parentId": "root", + "path": "nested-forms", + }, + "routes/nested-forms/nested": Object { + "caseSensitive": false, + "file": "routes/nested-forms/nested.tsx", + "id": "routes/nested-forms/nested", + "parentId": "routes/nested-forms", + "path": "nested", + }, + "routes/nested-forms/nested/index": Object { + "caseSensitive": false, + "file": "routes/nested-forms/nested/index.tsx", + "id": "routes/nested-forms/nested/index", + "parentId": "routes/nested-forms/nested", + "path": "/", + }, "routes/one": Object { "caseSensitive": false, "file": "routes/one.mdx", diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index b374c4bcd4..a7f1a24dbc 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -73,4 +73,70 @@ describe("loaders", () => { let res = await handler(request); expect(await res.headers.get("X-Remix-Catch")).toBeTruthy(); }); + + it("removes index from request.url", async () => { + let loader = async ({ request }) => { + return new URL(request.url).search; + }; + + let routeId = "routes/random"; + let build = ({ + routes: { + [routeId]: { + id: routeId, + path: "/random", + module: { + loader + } + } + } + } as unknown) as ServerBuild; + + let handler = createRequestHandler(build, {}); + + let request = new Request( + "http://example.com/random?_data=routes/random&index&foo=bar", + { + headers: { + "Content-Type": "application/json" + } + } + ); + + let res = await handler(request); + expect(await res.json()).toMatchInlineSnapshot(`"?foo=bar"`); + }); + + it("removes index from request.url and keeps other values", async () => { + let loader = async ({ request }) => { + return new URL(request.url).search; + }; + + let routeId = "routes/random"; + let build = ({ + routes: { + [routeId]: { + id: routeId, + path: "/random", + module: { + loader + } + } + } + } as unknown) as ServerBuild; + + let handler = createRequestHandler(build, {}); + + let request = new Request( + "http://example.com/random?_data=routes/random&index&foo=bar&index=test", + { + headers: { + "Content-Type": "application/json" + } + } + ); + + let res = await handler(request); + expect(await res.json()).toMatchInlineSnapshot(`"?foo=bar&index=test"`); + }); }); diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index e8735eb860..4d721596d3 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -67,6 +67,13 @@ async function handleDataRequest( let routeMatch: RouteMatch; if (isActionRequest(request)) { routeMatch = matches[matches.length - 1]; + + if ( + !isIndexRequestUrl(url) && + matches[matches.length - 1].route.id.endsWith("/index") + ) { + routeMatch = matches[matches.length - 2]; + } } else { let routeId = url.searchParams.get("_data"); if (!routeId) { @@ -84,7 +91,7 @@ async function handleDataRequest( routeMatch = match; } - let clonedRequest = stripDataParam(request); + let clonedRequest = stripIndexParam(stripDataParam(request)); let response: Response; try { @@ -168,6 +175,7 @@ async function handleDocumentRequest( let actionState: "" | "caught" | "error" = ""; let actionResponse: Response | undefined; + let actionRouteId: string | undefined; if (isNoMatch) { componentDidCatchEmulator.trackCatchBoundaries = false; @@ -184,14 +192,19 @@ async function handleDocumentRequest( data: null }; } else if (isActionRequest(request)) { - let leafMatch = matches[matches.length - 1]; + let actionMatch = matches[matches.length - 1]; + if (!isIndexRequestUrl(url) && actionMatch.route.id.endsWith("/index")) { + actionMatch = matches[matches.length - 2]; + } + actionRouteId = actionMatch.route.id; + try { actionResponse = await callRouteAction( build, - leafMatch.route.id, + actionMatch.route.id, request.clone(), loadContext, - leafMatch.params + actionMatch.params ); if (isRedirectResponse(actionResponse)) { return actionResponse; @@ -360,13 +373,12 @@ async function handleDocumentRequest( renderableMatches, routeLoaderResponses ); - let actionData = actionResponse - ? { - [matches[matches.length - 1].route.id]: await createActionData( - actionResponse - ) - } - : undefined; + let actionData = + actionResponse && actionRouteId + ? { + [actionRouteId]: await createActionData(actionResponse) + } + : undefined; let routeModules = createEntryRouteModules(build.routes); let serverHandoff = { matches: entryMatches, @@ -454,6 +466,35 @@ function isRedirectResponse(response: Response): boolean { return redirectStatusCodes.has(response.status); } +function isIndexRequestUrl(url: URL) { + let indexRequest = false; + + for (let param of url.searchParams.getAll("index")) { + if (!param) { + indexRequest = true; + } + } + + return indexRequest; +} + +function stripIndexParam(request: Request) { + let url = new URL(request.url); + let indexValues = url.searchParams.getAll("index"); + url.searchParams.delete("index"); + let indexValuesToKeep = []; + for (let indexValue of indexValues) { + if (indexValue) { + indexValuesToKeep.push(indexValue); + } + } + for (let toKeep of indexValuesToKeep) { + url.searchParams.append("index", toKeep); + } + + return new Request(url.toString(), request); +} + function stripDataParam(request: Request) { let url = new URL(request.url); url.searchParams.delete("_data"); From ce83ee52a245fec74556a42f94b458593d14c2ec Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 20 Sep 2021 17:56:09 -0700 Subject: [PATCH 0077/1690] feat: added extension point for MDX plugins (#280) --- .../remix-dev/__tests__/readConfig-test.ts | 24 +++++++---- packages/remix-dev/compiler/plugins/mdx.ts | 43 ++++++++++++++----- packages/remix-dev/config.ts | 16 ++++++- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index f6112d1199..698e1dc24a 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -28,6 +28,7 @@ describe("readConfig", () => { "devServerPort": 8002, "entryClientFile": "entry.client.jsx", "entryServerFile": "entry.server.jsx", + "mdx": [Function], "publicPath": "/build/", "rootDirectory": Any, "routes": Object { @@ -99,33 +100,40 @@ describe("readConfig", () => { "parentId": "root", "path": "actions", }, + "routes/blog": Object { + "caseSensitive": false, + "file": "routes/blog.tsx", + "id": "routes/blog", + "parentId": "root", + "path": "blog", + }, "routes/blog/hello-world": Object { "caseSensitive": false, "file": "routes/blog/hello-world.mdx", "id": "routes/blog/hello-world", - "parentId": "root", - "path": "blog/hello-world", + "parentId": "routes/blog", + "path": "hello-world", }, "routes/blog/index": Object { "caseSensitive": false, "file": "routes/blog/index.tsx", "id": "routes/blog/index", - "parentId": "root", - "path": "blog", + "parentId": "routes/blog", + "path": "/", }, "routes/blog/second": Object { "caseSensitive": false, "file": "routes/blog/second.md", "id": "routes/blog/second", - "parentId": "root", - "path": "blog/second", + "parentId": "routes/blog", + "path": "second", }, "routes/blog/third": Object { "caseSensitive": false, "file": "routes/blog/third.md", "id": "routes/blog/third", - "parentId": "root", - "path": "blog/third", + "parentId": "routes/blog", + "path": "third", }, "routes/catchall-nested": Object { "caseSensitive": false, diff --git a/packages/remix-dev/compiler/plugins/mdx.ts b/packages/remix-dev/compiler/plugins/mdx.ts index 97641dac43..90142eb8a2 100644 --- a/packages/remix-dev/compiler/plugins/mdx.ts +++ b/packages/remix-dev/compiler/plugins/mdx.ts @@ -27,25 +27,46 @@ export function mdxPlugin(config: RemixConfig): esbuild.Plugin { try { let contents = await fsp.readFile(args.path, "utf-8"); + let rehypePlugins = []; + let remarkPlugins = [ + remarkFrontmatter, + [remarkMdxFrontmatter, { name: "attributes" }] + ]; + + switch (typeof config.mdx) { + case "object": + rehypePlugins.push(...(config.mdx.rehypePlugins || [])); + remarkPlugins.push(...(config.mdx.remarkPlugins || [])); + + break; + case "function": + let mdxConfig = await config.mdx(args.path); + rehypePlugins.push(...(mdxConfig?.rehypePlugins || [])); + remarkPlugins.push(...(mdxConfig?.remarkPlugins || [])); + break; + } + + console.log(rehypePlugins); + + let remixExports = ` +export const filename = ${JSON.stringify(path.basename(args.path))}; +export const headers = typeof attributes !== "undefined" && attributes.headers; +export const meta = typeof attributes !== "undefined" && attributes.meta; +export const links = undefined; + `; + let compiled = await xdm.compile(contents, { jsx: true, jsxRuntime: "classic", pragma: "React.createElement", pragmaFrag: "React.Fragment", - remarkPlugins: [ - remarkFrontmatter, - [remarkMdxFrontmatter, { name: "attributes" }] - ] + rehypePlugins, + remarkPlugins }); contents = ` - ${compiled.value} - - export const filename = ${JSON.stringify(path.basename(args.path))}; - export const headers = typeof attributes !== "undefined" && attributes.headers; - export const meta = typeof attributes !== "undefined" && attributes.meta; - export const links = undefined; - `; +${compiled.value} +${remixExports}`; let errors: esbuild.PartialMessage[] = []; let warnings: esbuild.PartialMessage[] = []; diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 0800edfdd2..b49474e9d7 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -6,6 +6,15 @@ import { defineRoutes } from "./config/routes"; import { defineConventionalRoutes } from "./config/routesConvention"; import { ServerMode, isValidServerMode } from "./config/serverModes"; +export interface RemixMdxConfig { + rehypePlugins?: any[]; + remarkPlugins?: any[]; +} + +export type RemixMdxConfigFunction = ( + filename: string +) => Promise | RemixMdxConfig | undefined; + /** * The user-provided config in `remix.config.js`. */ @@ -60,6 +69,8 @@ export interface AppConfig { * The port number to use for the dev server. Defaults to 8002. */ devServerPort?: number; + + mdx?: RemixMdxConfig | RemixMdxConfigFunction; } /** @@ -120,6 +131,8 @@ export interface RemixConfig { * The port number to use for the dev (asset) server. */ devServerPort: number; + + mdx?: RemixMdxConfig | RemixMdxConfigFunction; } /** @@ -220,7 +233,8 @@ export async function readConfig( rootDirectory, routes, serverBuildDirectory, - serverMode + serverMode, + mdx: appConfig.mdx }; } From 8f66da26f21bf7c9eb627097373a0112501e5bec Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 24 Sep 2021 16:13:01 -0700 Subject: [PATCH 0078/1690] Tweak tsconfigs --- packages/remix-dev/tsconfig.json | 2 +- packages/remix-express/tsconfig.json | 1 - packages/remix-node/tsconfig.json | 2 +- packages/remix-serve/tsconfig.json | 2 +- packages/remix-server-runtime/tsconfig.json | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/remix-dev/tsconfig.json b/packages/remix-dev/tsconfig.json index bf27affb62..7d7236fdf0 100644 --- a/packages/remix-dev/tsconfig.json +++ b/packages/remix-dev/tsconfig.json @@ -1,6 +1,6 @@ { "include": ["../../types/mdx-js__mdx.d.ts", "**/*"], - "exclude": ["__tests__/**/*"], + "exclude": ["__tests__"], "compilerOptions": { "lib": ["ES2019"], "target": "ES2019", diff --git a/packages/remix-express/tsconfig.json b/packages/remix-express/tsconfig.json index 8040ec74b9..2e44d701e5 100644 --- a/packages/remix-express/tsconfig.json +++ b/packages/remix-express/tsconfig.json @@ -1,5 +1,4 @@ { - "include": ["**/*"], "exclude": ["__tests__"], "compilerOptions": { "lib": ["ES2019"], diff --git a/packages/remix-node/tsconfig.json b/packages/remix-node/tsconfig.json index a5c064d8e5..a3d9fe2987 100644 --- a/packages/remix-node/tsconfig.json +++ b/packages/remix-node/tsconfig.json @@ -1,5 +1,5 @@ { - "exclude": ["__tests__/**/*", "scripts/**/*"], + "exclude": ["__tests__"], "compilerOptions": { "lib": ["ES2019"], "target": "ES2019", diff --git a/packages/remix-serve/tsconfig.json b/packages/remix-serve/tsconfig.json index 0d5c397549..c36ea68888 100644 --- a/packages/remix-serve/tsconfig.json +++ b/packages/remix-serve/tsconfig.json @@ -1,5 +1,5 @@ { - "exclude": ["__tests__/**/*"], + "exclude": ["__tests__"], "compilerOptions": { "lib": ["ES2019"], "target": "ES2019", diff --git a/packages/remix-server-runtime/tsconfig.json b/packages/remix-server-runtime/tsconfig.json index 7ca8c93ad8..356a2f628b 100644 --- a/packages/remix-server-runtime/tsconfig.json +++ b/packages/remix-server-runtime/tsconfig.json @@ -1,5 +1,5 @@ { - "exclude": ["__tests__/**/*", "scripts/**/*"], + "exclude": ["__tests__"], "compilerOptions": { "lib": ["ES2019", "DOM"], "target": "ES2019", From 8acc41b35e6fa26c8c47d951ce8667f1eaf54798 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 27 Sep 2021 14:43:28 -0700 Subject: [PATCH 0079/1690] feat: give cloudflare-workers package some love (#286) feat: add cloudflare kv session storage chore: prepare cloudflare workers template fix: removed console.log that got left behind feat: added devServerBroadcastDelay to config feat: actually utlize devServerPort config option --- packages/remix-dev/__tests__/readConfig-test.ts | 1 + packages/remix-dev/cli/commands.ts | 14 ++++++++------ packages/remix-dev/compiler/plugins/mdx.ts | 2 -- packages/remix-dev/config.ts | 11 +++++++++++ packages/remix-dev/setup.ts | 7 +++++-- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 698e1dc24a..a63d80b7d5 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -25,6 +25,7 @@ describe("readConfig", () => { "appDirectory": Any, "assetsBuildDirectory": Any, "cacheDirectory": Any, + "devServerBroadcastDelay": 0, "devServerPort": 8002, "entryClientFile": "entry.client.jsx", "entryServerFile": "entry.server.jsx", diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index c0008ce47e..85e2ac396f 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -49,13 +49,15 @@ export async function watch( ? remixRootOrConfig : await readConfig(remixRootOrConfig); - let wss = new WebSocket.Server({ port: 3001 }); + let wss = new WebSocket.Server({ port: config.devServerPort }); function broadcast(event: { type: string; [key: string]: any }) { - wss.clients.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify(event)); - } - }); + setTimeout(() => { + wss.clients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(event)); + } + }); + }, config.devServerBroadcastDelay); } function log(_message: string) { diff --git a/packages/remix-dev/compiler/plugins/mdx.ts b/packages/remix-dev/compiler/plugins/mdx.ts index 90142eb8a2..ab39e160f8 100644 --- a/packages/remix-dev/compiler/plugins/mdx.ts +++ b/packages/remix-dev/compiler/plugins/mdx.ts @@ -46,8 +46,6 @@ export function mdxPlugin(config: RemixConfig): esbuild.Plugin { break; } - console.log(rehypePlugins); - let remixExports = ` export const filename = ${JSON.stringify(path.basename(args.path))}; export const headers = typeof attributes !== "undefined" && attributes.headers; diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index b49474e9d7..a233b6bb9e 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -69,6 +69,10 @@ export interface AppConfig { * The port number to use for the dev server. Defaults to 8002. */ devServerPort?: number; + /** + * The delay before the dev server broadcasts a reload event + */ + devServerBroadcastDelay?: number; mdx?: RemixMdxConfig | RemixMdxConfigFunction; } @@ -132,6 +136,11 @@ export interface RemixConfig { */ devServerPort: number; + /** + * The delay before the dev (asset) server broadcasts a reload event. + */ + devServerBroadcastDelay: number; + mdx?: RemixMdxConfig | RemixMdxConfigFunction; } @@ -196,6 +205,7 @@ export async function readConfig( ); let devServerPort = appConfig.devServerPort || 8002; + let devServerBroadcastDelay = appConfig.devServerBroadcastDelay || 0; let publicPath = addTrailingSlash(appConfig.publicPath || "/build/"); @@ -228,6 +238,7 @@ export async function readConfig( entryClientFile, entryServerFile, devServerPort, + devServerBroadcastDelay, assetsBuildDirectory, publicPath, rootDirectory, diff --git a/packages/remix-dev/setup.ts b/packages/remix-dev/setup.ts index 4352e244cd..6fbe8e4a44 100644 --- a/packages/remix-dev/setup.ts +++ b/packages/remix-dev/setup.ts @@ -2,18 +2,21 @@ import * as fse from "fs-extra"; import * as path from "path"; export enum SetupPlatform { + CloudflareWorkers = "cloudflare-workers", Node = "node" } export function isSetupPlatform(platform: any): platform is SetupPlatform { - return platform === SetupPlatform.Node; + return [SetupPlatform.CloudflareWorkers, SetupPlatform.Node].includes( + platform + ); } export async function setupRemix(platform: SetupPlatform): Promise { let remixPkgJsonFile: string; try { remixPkgJsonFile = resolvePackageJsonFile("remix"); - } catch (error) { + } catch (error: any) { if (error.code === "MODULE_NOT_FOUND") { console.error( `Missing the "remix" package. Please run \`npm install remix\` before \`remix setup\`.` From 4a52d8daadca39b9c1f52da508a3f8e79bd8600d Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 28 Sep 2021 14:17:06 -0700 Subject: [PATCH 0080/1690] feat: updated to RR 6.0.0-beta.5 (#290) chore: upgraded react, react-dom and @types chore: pointing yarn.lock to npmjs.org feat: updated to use index router property chore: pinned react-router-dom version chore: added versions for type packages fix: index and caseSensitive undefined when false fix: added cookie signing types to allow x-package usage feat: added index child route detection chore: normalize createRoutePath chore: add test for createRoutePath feat: remove empty string paths from manifest --- .../remix-dev/__tests__/defineRoutes-test.ts | 23 ++- .../remix-dev/__tests__/readConfig-test.ts | 141 ++++++++++++------ .../__tests__/routesConvention-test.ts | 24 +++ packages/remix-dev/compiler/assets.ts | 4 +- packages/remix-dev/config/routes.ts | 21 ++- packages/remix-dev/config/routesConvention.ts | 31 +++- packages/remix-node/cookieSigning.ts | 15 +- packages/remix-node/globals.ts | 15 +- packages/remix-node/sessions/fileStorage.ts | 6 +- .../remix-server-runtime/cookieSigning.ts | 15 ++ packages/remix-server-runtime/cookies.ts | 2 - packages/remix-server-runtime/package.json | 2 +- 12 files changed, 219 insertions(+), 80 deletions(-) create mode 100644 packages/remix-dev/__tests__/routesConvention-test.ts diff --git a/packages/remix-dev/__tests__/defineRoutes-test.ts b/packages/remix-dev/__tests__/defineRoutes-test.ts index 4f176afd47..cb5fa80341 100644 --- a/packages/remix-dev/__tests__/defineRoutes-test.ts +++ b/packages/remix-dev/__tests__/defineRoutes-test.ts @@ -5,7 +5,7 @@ describe("defineRoutes", () => { let routes = defineRoutes(route => { route("/", "routes/home.js"); route("inbox", "routes/inbox.js", () => { - route("/", "routes/inbox/index.js"); + route("/", "routes/inbox/index.js", { index: true }); route(":messageId", "routes/inbox/$messageId.js"); route("archive", "routes/inbox/archive.js"); }); @@ -14,37 +14,42 @@ describe("defineRoutes", () => { expect(routes).toMatchInlineSnapshot(` Object { "routes/home": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/home.js", "id": "routes/home", + "index": undefined, "parentId": undefined, "path": "/", }, "routes/inbox": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/inbox.js", "id": "routes/inbox", + "index": undefined, "parentId": undefined, "path": "inbox", }, "routes/inbox/$messageId": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/inbox/$messageId.js", "id": "routes/inbox/$messageId", + "index": undefined, "parentId": "routes/inbox", "path": ":messageId", }, "routes/inbox/archive": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/inbox/archive.js", "id": "routes/inbox/archive", + "index": undefined, "parentId": "routes/inbox", "path": "archive", }, "routes/inbox/index": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/inbox/index.js", "id": "routes/inbox/index", + "index": true, "parentId": "routes/inbox", "path": "/", }, @@ -64,16 +69,18 @@ describe("defineRoutes", () => { expect(routes).toMatchInlineSnapshot(` Object { "one": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "one.md", "id": "one", + "index": undefined, "parentId": undefined, "path": "one", }, "two": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "two.md", "id": "two", + "index": undefined, "parentId": undefined, "path": "two", }, diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index a63d80b7d5..289d2a7b18 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -33,17 +33,35 @@ describe("readConfig", () => { "publicPath": "/build/", "rootDirectory": Any, "routes": Object { + "pages/child": Object { + "caseSensitive": undefined, + "file": "pages/child.jsx", + "id": "pages/child", + "index": undefined, + "parentId": "pages/test", + "path": ":messageId", + }, "pages/four": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "pages/four.jsx", "id": "pages/four", + "index": undefined, "parentId": "root", "path": "/page/four", }, + "pages/test": Object { + "caseSensitive": undefined, + "file": "pages/test.jsx", + "id": "pages/test", + "index": undefined, + "parentId": "root", + "path": "programatic", + }, "pages/three": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "pages/three.jsx", "id": "pages/three", + "index": undefined, "parentId": "root", "path": "/page/three", }, @@ -53,261 +71,298 @@ describe("readConfig", () => { "path": "", }, "routes/action-catches": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/action-catches.jsx", "id": "routes/action-catches", + "index": undefined, "parentId": "root", "path": "action-catches", }, "routes/action-catches-from-loader": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/action-catches-from-loader.jsx", "id": "routes/action-catches-from-loader", + "index": undefined, "parentId": "root", "path": "action-catches-from-loader", }, "routes/action-catches-from-loader-self-boundary": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/action-catches-from-loader-self-boundary.jsx", "id": "routes/action-catches-from-loader-self-boundary", + "index": undefined, "parentId": "root", "path": "action-catches-from-loader-self-boundary", }, "routes/action-catches-self-boundary": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/action-catches-self-boundary.jsx", "id": "routes/action-catches-self-boundary", + "index": undefined, "parentId": "root", "path": "action-catches-self-boundary", }, "routes/action-errors": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/action-errors.jsx", "id": "routes/action-errors", + "index": undefined, "parentId": "root", "path": "action-errors", }, "routes/action-errors-self-boundary": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/action-errors-self-boundary.jsx", "id": "routes/action-errors-self-boundary", + "index": undefined, "parentId": "root", "path": "action-errors-self-boundary", }, "routes/actions": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/actions.tsx", "id": "routes/actions", + "index": undefined, "parentId": "root", "path": "actions", }, "routes/blog": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/blog.tsx", "id": "routes/blog", + "index": undefined, "parentId": "root", "path": "blog", }, "routes/blog/hello-world": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/blog/hello-world.mdx", "id": "routes/blog/hello-world", + "index": undefined, "parentId": "routes/blog", "path": "hello-world", }, "routes/blog/index": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/blog/index.tsx", "id": "routes/blog/index", + "index": true, "parentId": "routes/blog", - "path": "/", + "path": undefined, }, "routes/blog/second": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/blog/second.md", "id": "routes/blog/second", + "index": undefined, "parentId": "routes/blog", "path": "second", }, "routes/blog/third": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/blog/third.md", "id": "routes/blog/third", + "index": undefined, "parentId": "routes/blog", "path": "third", }, "routes/catchall-nested": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/catchall-nested.jsx", "id": "routes/catchall-nested", + "index": undefined, "parentId": "root", "path": "catchall-nested", }, "routes/catchall-nested-no-layout/$": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/catchall-nested-no-layout/$.jsx", "id": "routes/catchall-nested-no-layout/$", + "index": undefined, "parentId": "root", "path": "catchall-nested-no-layout/*", }, "routes/catchall-nested/$": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/catchall-nested/$.jsx", "id": "routes/catchall-nested/$", + "index": undefined, "parentId": "routes/catchall-nested", "path": "*", }, "routes/catchall.flat.$": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/catchall.flat.$.jsx", "id": "routes/catchall.flat.$", + "index": undefined, "parentId": "root", "path": "catchall/flat/*", }, "routes/empty": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/empty.jsx", "id": "routes/empty", + "index": undefined, "parentId": "root", "path": "empty", }, "routes/fetchers": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/fetchers.tsx", "id": "routes/fetchers", + "index": undefined, "parentId": "root", "path": "fetchers", }, "routes/gists": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/gists.jsx", "id": "routes/gists", + "index": undefined, "parentId": "root", "path": "gists", }, "routes/gists.mine": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/gists.mine.jsx", "id": "routes/gists.mine", + "index": undefined, "parentId": "root", "path": "gists/mine", }, "routes/gists/$username": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/gists/$username.jsx", "id": "routes/gists/$username", + "index": undefined, "parentId": "routes/gists", "path": ":username", }, "routes/gists/index": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/gists/index.jsx", "id": "routes/gists/index", + "index": true, "parentId": "routes/gists", - "path": "/", + "path": undefined, }, "routes/index": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/index.jsx", "id": "routes/index", + "index": true, "parentId": "root", - "path": "/", + "path": undefined, }, "routes/links": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/links.tsx", "id": "routes/links", + "index": undefined, "parentId": "root", "path": "links", }, "routes/loader-errors": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/loader-errors.jsx", "id": "routes/loader-errors", + "index": undefined, "parentId": "root", "path": "loader-errors", }, "routes/loader-errors/nested": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/loader-errors/nested.jsx", "id": "routes/loader-errors/nested", + "index": undefined, "parentId": "routes/loader-errors", "path": "nested", }, "routes/loader-errors/nested-catch": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/loader-errors/nested-catch.jsx", "id": "routes/loader-errors/nested-catch", + "index": undefined, "parentId": "routes/loader-errors", "path": "nested-catch", }, "routes/methods": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/methods.tsx", "id": "routes/methods", + "index": undefined, "parentId": "root", "path": "methods", }, "routes/multiple-set-cookies": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/multiple-set-cookies.tsx", "id": "routes/multiple-set-cookies", + "index": undefined, "parentId": "root", "path": "multiple-set-cookies", }, "routes/nested-forms": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/nested-forms.tsx", "id": "routes/nested-forms", + "index": undefined, "parentId": "root", "path": "nested-forms", }, "routes/nested-forms/nested": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/nested-forms/nested.tsx", "id": "routes/nested-forms/nested", + "index": undefined, "parentId": "routes/nested-forms", "path": "nested", }, "routes/nested-forms/nested/index": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/nested-forms/nested/index.tsx", "id": "routes/nested-forms/nested/index", + "index": true, "parentId": "routes/nested-forms/nested", - "path": "/", + "path": undefined, }, "routes/one": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/one.mdx", "id": "routes/one", + "index": undefined, "parentId": "root", "path": "one", }, "routes/prefs": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/prefs.tsx", "id": "routes/prefs", + "index": undefined, "parentId": "root", "path": "prefs", }, "routes/render-errors": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/render-errors.jsx", "id": "routes/render-errors", + "index": undefined, "parentId": "root", "path": "render-errors", }, "routes/render-errors/nested": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/render-errors/nested.jsx", "id": "routes/render-errors/nested", + "index": undefined, "parentId": "routes/render-errors", "path": "nested", }, "routes/two": Object { - "caseSensitive": false, + "caseSensitive": undefined, "file": "routes/two.md", "id": "routes/two", + "index": undefined, "parentId": "root", "path": "two", }, diff --git a/packages/remix-dev/__tests__/routesConvention-test.ts b/packages/remix-dev/__tests__/routesConvention-test.ts new file mode 100644 index 0000000000..c9c3a2c561 --- /dev/null +++ b/packages/remix-dev/__tests__/routesConvention-test.ts @@ -0,0 +1,24 @@ +import { createRoutePath } from "../config/routesConvention"; + +describe("createRoutePath", () => { + describe("creates proper route paths", () => { + let tests = [ + ["$", "*"], + ["nested/$", "nested/*"], + ["flat.$", "flat/*"], + ["$slug", ":slug"], + ["nested/$slug", "nested/:slug"], + ["flat.$slug", "flat/:slug"], + ["flat.sub", "flat/sub"], + ["nested/index", "nested"], + ["flat.index", "flat"], + ["index", ""] + ]; + + for (let [input, expected] of tests) { + it(`"${input}" -> "${expected}"`, () => { + expect(createRoutePath(input)).toBe(expected); + }); + } + }); +}); diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index 94d433a3f1..687bb1eb5c 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -20,7 +20,8 @@ interface AssetsManifest { [routeId: string]: { id: string; parentId?: string; - path: string; + path?: string; + index?: boolean; caseSensitive?: boolean; module: string; imports?: string[]; @@ -88,6 +89,7 @@ export async function createAssetsManifest( id: route.id, parentId: route.parentId, path: route.path, + index: route.index, caseSensitive: route.caseSensitive, module: resolveUrl(key), imports: resolveImports(output.imports), diff --git a/packages/remix-dev/config/routes.ts b/packages/remix-dev/config/routes.ts index f9cf5d7ea0..1fad6a03a3 100644 --- a/packages/remix-dev/config/routes.ts +++ b/packages/remix-dev/config/routes.ts @@ -8,7 +8,12 @@ export interface ConfigRoute { /** * The path this route uses to match on the URL pathname. */ - path: string; + path?: string; + + /** + * Should be `true` if it is an index route. This disallows child routes. + */ + index?: boolean; /** * Should be `true` if the `path` is case-sensitive. Defaults to `false`. @@ -44,6 +49,11 @@ export interface DefineRouteOptions { * `false`. */ caseSensitive?: boolean; + + /** + * Should be `true` if this is an index route that does not allow child routes. + */ + index?: boolean; } interface DefineRouteChildren { @@ -69,7 +79,7 @@ export interface DefineRouteFunction { /** * The path this route uses to match the URL pathname. */ - path: string, + path: string | undefined, /** * The path to the file that exports the React component rendered by this @@ -128,8 +138,9 @@ export function defineRoutes( } let route: ConfigRoute = { - path: path || "/", - caseSensitive: !!options.caseSensitive, + path: path ? path : undefined, + index: options.index ? true : undefined, + caseSensitive: options.caseSensitive ? true : undefined, id: createRouteId(file), parentId: parentRoutes.length > 0 @@ -158,7 +169,7 @@ export function createRouteId(file: string) { return normalizeSlashes(stripFileExtension(file)); } -function normalizeSlashes(file: string) { +export function normalizeSlashes(file: string) { return file.split(path.win32.sep).join("/"); } diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 2af501eb1c..08ac869faa 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -2,7 +2,7 @@ import * as fs from "fs"; import * as path from "path"; import type { RouteManifest, DefineRouteFunction } from "./routes"; -import { defineRoutes, createRouteId } from "./routes"; +import { defineRoutes, createRouteId, normalizeSlashes } from "./routes"; const routeModuleExts = [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"]; @@ -49,21 +49,38 @@ export function defineConventionalRoutes(appDir: string): RouteManifest { ); for (let routeId of childRouteIds) { - let routePath = createRoutePath( + let routePath: string | undefined = createRoutePath( routeId.slice((parentId || "routes").length + 1) ); - defineRoute(routePath, files[routeId], () => { - defineNestedRoutes(defineRoute, routeId); - }); + if (routeId.endsWith("/index")) { + let invalidChildRoutes = routeIds.filter( + id => findParentRouteId(routeIds, id) === routeId + ); + + if (invalidChildRoutes.length > 0) { + throw new Error( + `Child routes are not allowed in index routes. Please remove child routes of ${routeId}` + ); + } + + defineRoute(routePath, files[routeId], { + index: true + }); + } else { + defineRoute(routePath, files[routeId], () => { + defineNestedRoutes(defineRoute, routeId); + }); + } } } return defineRoutes(defineNestedRoutes); } -function createRoutePath(routeId: string): string { - let path = routeId +// TODO: Cleanup and write some tests for this function +export function createRoutePath(partialRouteId: string): string { + let path = normalizeSlashes(partialRouteId) // routes/$ -> routes/* // routes/nested/$.tsx (with a "routes/nested.tsx" layout) .replace(/^\$$/, "*") diff --git a/packages/remix-node/cookieSigning.ts b/packages/remix-node/cookieSigning.ts index 7aafdc7e82..22a2b19d4b 100644 --- a/packages/remix-node/cookieSigning.ts +++ b/packages/remix-node/cookieSigning.ts @@ -1,12 +1,17 @@ import cookieSignature from "cookie-signature"; -export async function sign(value: string, secret: string): Promise { +import type { + InternalSignFunctionDoNotUseMe, + InternalUnsignFunctionDoNotUseMe +} from "@remix-run/server-runtime/cookieSigning"; + +export const sign: InternalSignFunctionDoNotUseMe = async (value, secret) => { return cookieSignature.sign(value, secret); -} +}; -export async function unsign( +export const unsign: InternalUnsignFunctionDoNotUseMe = async ( signed: string, secret: string -): Promise { +) => { return cookieSignature.unsign(signed, secret); -} +}; diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts index 7111aeb0d1..cd89275e70 100644 --- a/packages/remix-node/globals.ts +++ b/packages/remix-node/globals.ts @@ -1,5 +1,10 @@ +import type { + InternalSignFunctionDoNotUseMe, + InternalUnsignFunctionDoNotUseMe +} from "@remix-run/server-runtime/cookieSigning"; + import { atob, btoa } from "./base64"; -import { sign, unsign } from "./cookieSigning"; +import { sign as remixSign, unsign as remixUnsign } from "./cookieSigning"; import { Headers as NodeHeaders, Request as NodeRequest, @@ -20,8 +25,8 @@ declare global { // TODO: Once node v16 is available on AWS we should remove these globals // and provide the webcrypto API instead. - sign: typeof sign; - unsign: typeof unsign; + sign: InternalSignFunctionDoNotUseMe; + unsign: InternalUnsignFunctionDoNotUseMe; } } } @@ -35,6 +40,6 @@ export function installGlobals() { global.Response = (NodeResponse as unknown) as typeof Response; global.fetch = (nodeFetch as unknown) as typeof fetch; - global.sign = sign; - global.unsign = unsign; + global.sign = remixSign; + global.unsign = remixUnsign; } diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts index 17f3f8c704..0182b1165c 100644 --- a/packages/remix-node/sessions/fileStorage.ts +++ b/packages/remix-node/sessions/fileStorage.ts @@ -51,7 +51,7 @@ export function createFileSessionStorage({ await fsp.mkdir(path.dirname(file), { recursive: true }); await fsp.writeFile(file, content, { encoding: "utf-8", flag: "wx" }); return id; - } catch (error) { + } catch (error: any) { if (error.code !== "EEXIST") throw error; } } @@ -74,7 +74,7 @@ export function createFileSessionStorage({ if (expires) await fsp.unlink(file); return null; - } catch (error) { + } catch (error: any) { if (error.code !== "ENOENT") throw error; return null; } @@ -88,7 +88,7 @@ export function createFileSessionStorage({ async deleteData(id) { try { await fsp.unlink(getFile(dir, id)); - } catch (error) { + } catch (error: any) { if (error.code !== "ENOENT") throw error; } } diff --git a/packages/remix-server-runtime/cookieSigning.ts b/packages/remix-server-runtime/cookieSigning.ts index 3c968b08b6..6a3dae5647 100644 --- a/packages/remix-server-runtime/cookieSigning.ts +++ b/packages/remix-server-runtime/cookieSigning.ts @@ -1,3 +1,18 @@ +export type InternalSignFunctionDoNotUseMe = ( + value: string, + secret: string +) => Promise; + +export type InternalUnsignFunctionDoNotUseMe = ( + cookie: string, + secret: string +) => Promise; + +declare global { + var sign: InternalSignFunctionDoNotUseMe; + var unsign: InternalUnsignFunctionDoNotUseMe; +} + // TODO: Once node v16 is available on AWS we should use the globally provided // webcrypto "crypto" variable and re-enable this code-path in "./cookies.ts" // instead of referencing the sign and unsign globals. diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index a1f2d1f74d..25bb781b5c 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -127,7 +127,6 @@ async function encodeCookieValue( let encoded = encodeData(value); if (secrets.length > 0) { - // @ts-expect-error encoded = await sign(encoded, secrets[0]); } @@ -140,7 +139,6 @@ async function decodeCookieValue( ): Promise { if (secrets.length > 0) { for (let secret of secrets) { - // @ts-expect-error let unsignedValue = await unsign(value, secret); if (unsignedValue !== false) { return decodeData(unsignedValue); diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 2385b540bb..72a57fe84d 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -8,7 +8,7 @@ "cookie": "^0.4.1", "history": "^5.0.0", "jsesc": "^3.0.1", - "react-router-dom": "^6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.5", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3" }, From ccacc55ae2ff459b8793195584b07dc1b4629be3 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 28 Sep 2021 14:36:20 -0700 Subject: [PATCH 0081/1690] add import so global references don't complain (#292) --- packages/remix-server-runtime/cookies.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index 25bb781b5c..a18785c702 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -4,6 +4,7 @@ import { parse, serialize } from "cookie"; // TODO: Once node v16 is available on AWS we should use these instead of the // global `sign` and `unsign` functions. //import { sign, unsign } from "./cookieSigning"; +import "./cookieSigning"; export type { CookieParseOptions, CookieSerializeOptions }; From a936d4a057a0369c58cf4f8c6fc2d7f163004e82 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 28 Sep 2021 14:52:52 -0700 Subject: [PATCH 0082/1690] feat: updated ActionFunction interface (#293) --- packages/remix-server-runtime/routeModules.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index 140fd7d4db..dfb7f879a5 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -16,7 +16,9 @@ export interface RouteModules { export interface ActionFunction { (args: { request: Request; context: AppLoadContext; params: Params }): | Promise - | Response; + | Response + | Promise + | AppData; } /** From a806173017272072aebdb9e138fd07c65d22aa88 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 6 Oct 2021 16:48:44 -0700 Subject: [PATCH 0083/1690] feat: send invalid req methods to catch boundary (#299) feat: added statusText to useCatch --- .../remix-express/__tests__/server-test.ts | 12 +- packages/remix-express/server.ts | 4 +- .../__tests__/server-test.ts | 76 +++++++++++++ packages/remix-server-runtime/errors.ts | 1 + packages/remix-server-runtime/server.ts | 105 +++++++++++++----- 5 files changed, 166 insertions(+), 32 deletions(-) create mode 100644 packages/remix-server-runtime/__tests__/server-test.ts diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index 8a0588e2f7..29536aa4a8 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -1,6 +1,5 @@ import express from "express"; import supertest from "supertest"; -import { Response, Headers } from "@remix-run/node"; import { createRequest } from "node-mocks-http"; import { @@ -56,6 +55,17 @@ describe("express createRequestHandler", () => { expect(res.headers["x-powered-by"]).toBe("Express"); }); + it("handles null body", async () => { + mockedCreateRequestHandler.mockImplementation(() => async () => { + return new Response(null, { status: 200 }); + }); + + let request = supertest(createApp()); + let res = await request.get("/"); + + expect(res.status).toBe(200); + }); + it("handles status codes", async () => { mockedCreateRequestHandler.mockImplementation(() => async () => { return new Response("", { status: 204 }); diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index 2d2e7f919b..1b56d5c420 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -121,7 +121,9 @@ function sendRemixResponse( if (Buffer.isBuffer(response.body)) { res.end(response.body); - } else { + } else if (response.body?.pipe) { response.body.pipe(res); + } else { + res.end(); } } diff --git a/packages/remix-server-runtime/__tests__/server-test.ts b/packages/remix-server-runtime/__tests__/server-test.ts new file mode 100644 index 0000000000..d6277c659a --- /dev/null +++ b/packages/remix-server-runtime/__tests__/server-test.ts @@ -0,0 +1,76 @@ +import { createRequestHandler } from ".."; +import type { ServerBuild } from "../build"; + +describe("server", () => { + let routeId = "root"; + let build: ServerBuild = { + entry: { + module: { + default: async request => { + return new Response(`${request.method}, ${request.url}`); + } + } + }, + routes: { + [routeId]: { + id: routeId, + path: "", + module: { + action: () => "ACTION", + loader: () => "LOADER", + default: () => "COMPONENT" + } + } + }, + assets: { + routes: { + [routeId]: { + hasAction: true, + hasErrorBoundary: false, + hasLoader: true, + id: routeId, + module: routeId, + path: "" + } + } + } + } as unknown as ServerBuild; + + describe("createRequestHandler", () => { + let allowThrough = [ + ["GET", "/"], + ["GET", "/_data=root"], + ["POST", "/"], + ["POST", "/_data=root"], + ["PUT", "/"], + ["PUT", "/_data=root"], + ["DELETE", "/"], + ["DELETE", "/_data=root"], + ["PATCH", "/"], + ["PATCH", "/_data=root"] + ]; + for (let [method, to] of allowThrough) { + it(`allows through ${method} request to ${to}`, async () => { + let handler = createRequestHandler(build, {}); + let response = await handler( + new Request(`http://localhost:3000${to}`, { + method + }) + ); + + expect(await response.text()).toContain(method); + }); + } + + it("strips body for HEAD requests", async () => { + let handler = createRequestHandler(build, {}); + let response = await handler( + new Request("http://localhost:3000/", { + method: "HEAD" + }) + ); + + expect(await response.text()).toBe(""); + }); + }); +}); diff --git a/packages/remix-server-runtime/errors.ts b/packages/remix-server-runtime/errors.ts index 9fece4e8a5..8b042673b8 100644 --- a/packages/remix-server-runtime/errors.ts +++ b/packages/remix-server-runtime/errors.ts @@ -53,6 +53,7 @@ export interface ComponentDidCatchEmulator { export interface ThrownResponse { status: number; + statusText: string; data: T; } diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index 4d721596d3..bafa9967cd 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -37,8 +37,8 @@ export function createRequestHandler( let routes = createRoutes(build.routes); let serverMode = isServerMode(mode) ? mode : ServerMode.Production; - return (request, loadContext = {}) => - isDataRequest(request) + return async (request, loadContext = {}) => { + let response = await (isDataRequest(request) ? handleDataRequest(request, loadContext, build, platform, routes) : handleDocumentRequest( request, @@ -47,7 +47,18 @@ export function createRequestHandler( platform, routes, serverMode - ); + )); + + if (isHeadRequest(request)) { + return new Response(null, { + headers: response.headers, + status: response.status, + statusText: response.statusText + }); + } + + return response; + }; } async function handleDataRequest( @@ -57,6 +68,10 @@ async function handleDataRequest( platform: ServerPlatform, routes: ServerRoute[] ): Promise { + if (!isValidRequestMethod(request)) { + return jsonError(`Invalid request method "${request.method}"`, 405); + } + let url = new URL(request.url); let matches = matchServerRoutes(routes, url.pathname); @@ -147,13 +162,19 @@ async function handleDocumentRequest( ): Promise { let url = new URL(request.url); - let matches = matchServerRoutes(routes, url.pathname); - let isNoMatch = false; + let requestState: "ok" | "no-match" | "invalid-request" = + isValidRequestMethod(request) ? "ok" : "invalid-request"; + let matches = + requestState === "ok" ? matchServerRoutes(routes, url.pathname) : null; if (!matches) { // If we do not match a user-provided-route, fall back to the root - // to allow the CatchBoundary to take over - isNoMatch = true; + // to allow the CatchBoundary to take over while maintining invalid + // request state if already set + if (requestState === "ok") { + requestState = "no-match"; + } + matches = [ { params: {}, @@ -173,11 +194,12 @@ async function handleDocumentRequest( catch: undefined }; - let actionState: "" | "caught" | "error" = ""; + let responseState: "ok" | "caught" | "error" = "ok"; let actionResponse: Response | undefined; let actionRouteId: string | undefined; - if (isNoMatch) { + if (requestState !== "ok") { + responseState = "caught"; componentDidCatchEmulator.trackCatchBoundaries = false; let withBoundaries = getMatchesUpToDeepestBoundary( matches, @@ -188,7 +210,9 @@ async function handleDocumentRequest( ? withBoundaries[withBoundaries.length - 1].route.id : null; componentDidCatchEmulator.catch = { - status: 404, + status: requestState === "no-match" ? 404 : 405, + statusText: + requestState === "no-match" ? "Not Found" : "Method Not Allowed", data: null }; } else if (isActionRequest(request)) { @@ -211,7 +235,7 @@ async function handleDocumentRequest( } } catch (error: any) { let formattedError = (await platform.formatServerError?.(error)) || error; - actionState = "error"; + responseState = "error"; let withBoundaries = getMatchesUpToDeepestBoundary( matches, "ErrorBoundary" @@ -223,7 +247,7 @@ async function handleDocumentRequest( } if (actionResponse && isCatchResponse(actionResponse)) { - actionState = "caught"; + responseState = "caught"; let withBoundaries = getMatchesUpToDeepestBoundary( matches, "CatchBoundary" @@ -233,13 +257,14 @@ async function handleDocumentRequest( withBoundaries[withBoundaries.length - 1].route.id; componentDidCatchEmulator.catch = { status: actionResponse.status, + statusText: actionResponse.statusText, data: await extractData(actionResponse.clone()) }; } // If we did not match a route, there is no need to call any loaders - let matchesToLoad = isNoMatch ? [] : matches; - switch (actionState) { + let matchesToLoad = requestState !== "ok" ? [] : matches; + switch (responseState) { case "caught": matchesToLoad = getMatchesUpToDeepestBoundary( // get rid of the action, we don't want to call it's loader either @@ -266,16 +291,15 @@ async function handleDocumentRequest( // code is a little weird due to the way unhandled promise rejections are // handled in node. We use a .catch() handler on each promise to avoid the // warning, then handle errors manually afterwards. - let routeLoaderPromises: Promise< - Response | Error - >[] = matchesToLoad.map(match => - loadRouteData( - build, - match.route.id, - request.clone(), - loadContext, - match.params - ).catch(error => error) + let routeLoaderPromises: Promise[] = matchesToLoad.map( + match => + loadRouteData( + build, + match.route.id, + request.clone(), + loadContext, + match.params + ).catch(error => error) ); let routeLoaderResults = await Promise.all(routeLoaderPromises); @@ -296,9 +320,9 @@ async function handleDocumentRequest( // We just give up and move on with rendering the error as deeply as we can, // which is the previous iteration of this loop if ( - (actionState === "error" && + (responseState === "error" && (response instanceof Error || isRedirectResponse(response))) || - (actionState === "caught" && isCatchResponse(response)) + (responseState === "caught" && isCatchResponse(response)) ) { break; } @@ -333,6 +357,7 @@ async function handleDocumentRequest( componentDidCatchEmulator.trackCatchBoundaries = false; componentDidCatchEmulator.catch = { status: response.status, + statusText: response.statusText, data: await extractData(response.clone()) }; routeLoaderResults[index] = json(null, { status: response.status }); @@ -349,12 +374,14 @@ async function handleDocumentRequest( ); let statusCode = - actionState === "error" + requestState === "no-match" + ? 404 + : requestState === "invalid-request" + ? 405 + : responseState === "error" ? 500 : notOkResponse ? notOkResponse.status - : isNoMatch - ? 404 : 200; let renderableMatches = getRenderableMatches( @@ -453,7 +480,25 @@ function jsonError(error: string, status = 403): Response { } function isActionRequest(request: Request): boolean { - return request.method.toLowerCase() !== "get"; + let method = request.method.toLowerCase(); + return ( + method === "post" || + method === "put" || + method === "patch" || + method === "delete" + ); +} + +function isValidRequestMethod(request: Request): boolean { + return ( + request.method.toLowerCase() === "get" || + isHeadRequest(request) || + isActionRequest(request) + ); +} + +function isHeadRequest(request: Request): boolean { + return request.method.toLowerCase() === "head"; } function isDataRequest(request: Request): boolean { From fa8ae4c540b0d9a72303411d4f678af534c08d2e Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Thu, 7 Oct 2021 11:52:33 -0400 Subject: [PATCH 0084/1690] chore: update eslint to report misused type imports (#301) --- packages/remix-dev/compiler/assets.ts | 2 +- packages/remix-dev/compiler/loaders.ts | 2 +- packages/remix-dev/compiler/plugins/mdx.ts | 3 +-- packages/remix-dev/modules.ts | 2 -- packages/remix-dev/setup.ts | 2 +- packages/remix-express/__tests__/server-test.ts | 2 +- packages/remix-node/cookieSigning.ts | 1 - packages/remix-node/errors.ts | 1 - packages/remix-node/sessions/fileStorage.ts | 1 - packages/remix-server-runtime/__tests__/data-test.ts | 2 +- packages/remix-server-runtime/server.ts | 4 ++-- 11 files changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index 687bb1eb5c..ffe7c2ccdb 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import * as esbuild from "esbuild"; +import type * as esbuild from "esbuild"; import type { RemixConfig } from "../config"; import invariant from "../invariant"; diff --git a/packages/remix-dev/compiler/loaders.ts b/packages/remix-dev/compiler/loaders.ts index acd97dbff9..c137879f4a 100644 --- a/packages/remix-dev/compiler/loaders.ts +++ b/packages/remix-dev/compiler/loaders.ts @@ -1,5 +1,5 @@ import * as path from "path"; -import * as esbuild from "esbuild"; +import type * as esbuild from "esbuild"; export const loaders: { [ext: string]: esbuild.Loader } = { ".aac": "file", diff --git a/packages/remix-dev/compiler/plugins/mdx.ts b/packages/remix-dev/compiler/plugins/mdx.ts index ab39e160f8..b5220d3bc6 100644 --- a/packages/remix-dev/compiler/plugins/mdx.ts +++ b/packages/remix-dev/compiler/plugins/mdx.ts @@ -1,7 +1,6 @@ import { promises as fsp } from "fs"; import * as path from "path"; - -import * as esbuild from "esbuild"; +import type * as esbuild from "esbuild"; import { remarkMdxFrontmatter } from "remark-mdx-frontmatter"; import type { RemixConfig } from "../../config"; diff --git a/packages/remix-dev/modules.ts b/packages/remix-dev/modules.ts index 7765b5b4ca..de33c7d73d 100644 --- a/packages/remix-dev/modules.ts +++ b/packages/remix-dev/modules.ts @@ -32,7 +32,6 @@ declare module "*.json" { } declare module "*.md" { import type { ComponentType as MdComponentType } from "react"; - export const attributes: any; export const filename: string; const Component: MdComponentType; @@ -40,7 +39,6 @@ declare module "*.md" { } declare module "*.mdx" { import type { ComponentType as MdxComponentType } from "react"; - export const attributes: any; export const filename: string; const Component: MdxComponentType; diff --git a/packages/remix-dev/setup.ts b/packages/remix-dev/setup.ts index 6fbe8e4a44..bc126ee060 100644 --- a/packages/remix-dev/setup.ts +++ b/packages/remix-dev/setup.ts @@ -1,5 +1,5 @@ -import * as fse from "fs-extra"; import * as path from "path"; +import * as fse from "fs-extra"; export enum SetupPlatform { CloudflareWorkers = "cloudflare-workers", diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index 29536aa4a8..2ddede48a2 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -1,6 +1,7 @@ import express from "express"; import supertest from "supertest"; import { createRequest } from "node-mocks-http"; +import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; import { createRemixHeaders, @@ -8,7 +9,6 @@ import { createRequestHandler } from "../server"; -import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; // We don't want to test that the remix server works here (that's what the // puppetteer tests do), we just want to test the express adapter diff --git a/packages/remix-node/cookieSigning.ts b/packages/remix-node/cookieSigning.ts index 22a2b19d4b..1cbb5e5ad9 100644 --- a/packages/remix-node/cookieSigning.ts +++ b/packages/remix-node/cookieSigning.ts @@ -1,5 +1,4 @@ import cookieSignature from "cookie-signature"; - import type { InternalSignFunctionDoNotUseMe, InternalUnsignFunctionDoNotUseMe diff --git a/packages/remix-node/errors.ts b/packages/remix-node/errors.ts index dd9f72688f..e35fb8ff41 100644 --- a/packages/remix-node/errors.ts +++ b/packages/remix-node/errors.ts @@ -1,7 +1,6 @@ import fs from "fs"; import fsp from "fs/promises"; import path from "path"; - import type { NullableMappedPosition } from "source-map"; import { SourceMapConsumer } from "source-map"; diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts index 0182b1165c..22dc047cce 100644 --- a/packages/remix-node/sessions/fileStorage.ts +++ b/packages/remix-node/sessions/fileStorage.ts @@ -1,7 +1,6 @@ import * as crypto from "crypto"; import { promises as fsp } from "fs"; import * as path from "path"; - import type { SessionStorage, SessionIdStorageStrategy diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index a7f1a24dbc..183007b7ce 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -1,4 +1,4 @@ -import { ServerBuild } from "../build"; +import type { ServerBuild } from "../build"; import { createRequestHandler } from "../server"; describe("loaders", () => { diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index bafa9967cd..94c6f78075 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -1,7 +1,7 @@ -import { AppLoadContext, extractData, isCatchResponse } from "./data"; +import type { AppLoadContext} from "./data"; +import { extractData, isCatchResponse } from "./data"; import { loadRouteData, callRouteAction } from "./data"; import type { ComponentDidCatchEmulator } from "./errors"; - import type { ServerBuild } from "./build"; import type { EntryContext } from "./entry"; import { createEntryMatches, createEntryRouteModules } from "./entry"; From bf5266fe56d2b9347ef04e48ca60d64023603d91 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 7 Oct 2021 13:09:25 -0700 Subject: [PATCH 0085/1690] chore: updated to latest react-router beta fix: passed through "index" route prop to embeded ssr manifest --- packages/remix-dev/compiler.ts | 1 + packages/remix-dev/compiler/routes.ts | 5 +++++ packages/remix-dev/config/routesConvention.ts | 5 +++++ packages/remix-server-runtime/package.json | 2 +- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index d0372c2bb9..7f16d8cfba 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -411,6 +411,7 @@ export const routes = { id: ${JSON.stringify(route.id)}, parentId: ${JSON.stringify(route.parentId)}, path: ${JSON.stringify(route.path)}, + index: ${JSON.stringify(route.index)}, caseSensitive: ${JSON.stringify(route.caseSensitive)}, module: route${index} }`; diff --git a/packages/remix-dev/compiler/routes.ts b/packages/remix-dev/compiler/routes.ts index f7be5c8671..9f4d8d806b 100644 --- a/packages/remix-dev/compiler/routes.ts +++ b/packages/remix-dev/compiler/routes.ts @@ -33,6 +33,11 @@ export async function getRouteModuleExportsCached( } } + // Layout routes can't have actions + if (routeId.match(/\/_[\s\w\d_-]+$/) && cached.exports.includes("action")) { + throw new Error(`Actions are not supported in layout routes: ${routeId}`); + } + return cached.exports; } diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 08ac869faa..ed7581f88b 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -53,6 +53,11 @@ export function defineConventionalRoutes(appDir: string): RouteManifest { routeId.slice((parentId || "routes").length + 1) ); + // layout routes + if (routePath.startsWith("_")) { + routePath = undefined; + } + if (routeId.endsWith("/index")) { let invalidChildRoutes = routeIds.filter( id => findParentRouteId(routeIds, id) === routeId diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 72a57fe84d..584a569001 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -8,7 +8,7 @@ "cookie": "^0.4.1", "history": "^5.0.0", "jsesc": "^3.0.1", - "react-router-dom": "6.0.0-beta.5", + "react-router-dom": "6.0.0-beta.6", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3" }, From f9d51a282f7c163ca4d32b5639ca71c33d053567 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 7 Oct 2021 13:18:38 -0700 Subject: [PATCH 0086/1690] fixture route conflicting with jest file names --- .../remix-dev/__tests__/readConfig-test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 289d2a7b18..48c5b097a4 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -70,6 +70,22 @@ describe("readConfig", () => { "id": "root", "path": "", }, + "routes/_layout": Object { + "caseSensitive": undefined, + "file": "routes/_layout.tsx", + "id": "routes/_layout", + "index": undefined, + "parentId": "root", + "path": undefined, + }, + "routes/_layout/with-layout": Object { + "caseSensitive": undefined, + "file": "routes/_layout/with-layout.tsx", + "id": "routes/_layout/with-layout", + "index": undefined, + "parentId": "routes/_layout", + "path": "with-layout", + }, "routes/action-catches": Object { "caseSensitive": undefined, "file": "routes/action-catches.jsx", @@ -190,6 +206,14 @@ describe("readConfig", () => { "parentId": "routes/catchall-nested", "path": "*", }, + "routes/catchall-nested/index": Object { + "caseSensitive": undefined, + "file": "routes/catchall-nested/index.jsx", + "id": "routes/catchall-nested/index", + "index": true, + "parentId": "routes/catchall-nested", + "path": undefined, + }, "routes/catchall.flat.$": Object { "caseSensitive": undefined, "file": "routes/catchall.flat.$.jsx", From ec8b8e7bdb5b3d68c5553b4db8094dcd60cd0979 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 7 Oct 2021 15:52:28 -0600 Subject: [PATCH 0087/1690] Version 0.19.0-pre.0 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 08f2f75b81..2a900efdbc 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.18.2", + "version": "0.19.0-pre.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 3b02850707..d5e4e2b6c8 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.18.2", + "version": "0.19.0-pre.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.18.2" + "@remix-run/node": "0.19.0-pre.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 4f242df1fc..8441e8d2a4 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.18.2", + "version": "0.19.0-pre.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.18.2", + "@remix-run/server-runtime": "0.19.0-pre.0", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 6122e2b38e..3b87a8609f 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.18.2", + "version": "0.19.0-pre.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.18.2", + "@remix-run/express": "0.19.0-pre.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 584a569001..de1a10ec0b 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.18.2", + "version": "0.19.0-pre.0", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From a24c34d10d338fb8dbe1e9ab9593b14350ec2a0b Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 7 Oct 2021 16:38:16 -0600 Subject: [PATCH 0088/1690] Version 0.19.0-pre.1 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 2a900efdbc..841324d863 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.19.0-pre.0", + "version": "0.19.0-pre.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index d5e4e2b6c8..f424584ff6 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.19.0-pre.0", + "version": "0.19.0-pre.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.19.0-pre.0" + "@remix-run/node": "0.19.0-pre.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 8441e8d2a4..c986079be0 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.19.0-pre.0", + "version": "0.19.0-pre.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.19.0-pre.0", + "@remix-run/server-runtime": "0.19.0-pre.1", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 3b87a8609f..14027df138 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.19.0-pre.0", + "version": "0.19.0-pre.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.19.0-pre.0", + "@remix-run/express": "0.19.0-pre.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index de1a10ec0b..dafcb7542c 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.19.0-pre.0", + "version": "0.19.0-pre.1", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From f8e1f137eb43e029cfb3e1c6bf8788dec074ca8b Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Fri, 8 Oct 2021 12:32:41 -0600 Subject: [PATCH 0089/1690] Version 0.19.0 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 841324d863..86e9ba017f 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.19.0-pre.1", + "version": "0.19.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index f424584ff6..9b27b63937 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.19.0-pre.1", + "version": "0.19.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.19.0-pre.1" + "@remix-run/node": "0.19.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index c986079be0..52923c4546 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.19.0-pre.1", + "version": "0.19.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.19.0-pre.1", + "@remix-run/server-runtime": "0.19.0", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 14027df138..b0e7f81a81 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.19.0-pre.1", + "version": "0.19.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.19.0-pre.1", + "@remix-run/express": "0.19.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index dafcb7542c..5b7d18915f 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.19.0-pre.1", + "version": "0.19.0", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 472c36b6942c0760a49331b2a450b8121a4d6579 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Sat, 9 Oct 2021 08:27:24 -0600 Subject: [PATCH 0090/1690] type fixes --- packages/remix-server-runtime/links.ts | 16 +--------------- packages/remix-server-runtime/routeModules.ts | 2 +- packages/remix-server-runtime/routes.ts | 6 +++++- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/remix-server-runtime/links.ts b/packages/remix-server-runtime/links.ts index 5fa7e8fc5e..c085714985 100644 --- a/packages/remix-server-runtime/links.ts +++ b/packages/remix-server-runtime/links.ts @@ -142,20 +142,6 @@ export interface PageLinkDescriptor * The absolute path of the page to prefetch. */ page: string; - - /** - * If `true` when using `transition: "client"`, instructs Remix to prefetch - * the data for the destination page. - */ - data?: boolean; -} - -export interface BlockLinkDescriptor { - blocker: true; - link: HTMLLinkDescriptor; } -export type LinkDescriptor = - | HTMLLinkDescriptor - | BlockLinkDescriptor - | PageLinkDescriptor; +export type LinkDescriptor = HTMLLinkDescriptor | PageLinkDescriptor; diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index dfb7f879a5..481e6fd24c 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -48,7 +48,7 @@ export interface HeadersFunction { * the document on route transitions. */ export interface LinksFunction { - (args: { data: AppData }): LinkDescriptor[]; + (): LinkDescriptor[]; } /** diff --git a/packages/remix-server-runtime/routes.ts b/packages/remix-server-runtime/routes.ts index 405ab87dc8..712388270f 100644 --- a/packages/remix-server-runtime/routes.ts +++ b/packages/remix-server-runtime/routes.ts @@ -6,16 +6,20 @@ export interface RouteManifest { export type ServerRouteManifest = RouteManifest>; +// NOTE: make sure to change the Route in remix-react if you change this interface Route { + index?: boolean; caseSensitive?: boolean; id: string; parentId?: string; - path: string; + path?: string; } +// NOTE: make sure to change the EntryRoute in remix-react if you change this export interface EntryRoute extends Route { hasAction: boolean; hasLoader: boolean; + hasCatchBoundary: boolean; hasErrorBoundary: boolean; imports?: string[]; module: string; From 980c883fe27d86bd319471953fda04ae0fe34d80 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Sat, 9 Oct 2021 08:43:14 -0600 Subject: [PATCH 0091/1690] missed a spot --- packages/remix-server-runtime/index.ts | 1 - packages/remix-server-runtime/magicExports/server.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/remix-server-runtime/index.ts b/packages/remix-server-runtime/index.ts index 49110b70d3..7251840f65 100644 --- a/packages/remix-server-runtime/index.ts +++ b/packages/remix-server-runtime/index.ts @@ -16,7 +16,6 @@ export type { EntryContext } from "./entry"; export type { LinkDescriptor, HTMLLinkDescriptor, - BlockLinkDescriptor, PageLinkDescriptor } from "./links"; diff --git a/packages/remix-server-runtime/magicExports/server.ts b/packages/remix-server-runtime/magicExports/server.ts index 6203b1b923..2ecef943db 100644 --- a/packages/remix-server-runtime/magicExports/server.ts +++ b/packages/remix-server-runtime/magicExports/server.ts @@ -14,7 +14,6 @@ export type { EntryContext, LinkDescriptor, HTMLLinkDescriptor, - BlockLinkDescriptor, PageLinkDescriptor, ErrorBoundaryComponent, ActionFunction, From d4601523a2801117d421976f5ff91083235db07b Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Sat, 9 Oct 2021 09:01:55 -0600 Subject: [PATCH 0092/1690] Version 0.19.1 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 86e9ba017f..f72c40858a 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.19.0", + "version": "0.19.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 9b27b63937..3db3245178 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.19.0", + "version": "0.19.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.19.0" + "@remix-run/node": "0.19.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 52923c4546..43fbf506f1 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.19.0", + "version": "0.19.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.19.0", + "@remix-run/server-runtime": "0.19.1", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index b0e7f81a81..7bbd41de6d 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.19.0", + "version": "0.19.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.19.0", + "@remix-run/express": "0.19.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 5b7d18915f..10533a34b8 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.19.0", + "version": "0.19.1", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 22caec8998b6e454fd59d5d89b1874a75c7e7cce Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Sat, 9 Oct 2021 22:00:56 -0600 Subject: [PATCH 0093/1690] Change layout routes to double __ prefix. Lots of routes (even our apps!) have URLs that start with _, it's pretty common. --- packages/remix-dev/__tests__/readConfig-test.ts | 14 +++++++------- packages/remix-dev/config/routesConvention.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 48c5b097a4..90642be711 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -70,20 +70,20 @@ describe("readConfig", () => { "id": "root", "path": "", }, - "routes/_layout": Object { + "routes/__layout": Object { "caseSensitive": undefined, - "file": "routes/_layout.tsx", - "id": "routes/_layout", + "file": "routes/__layout.tsx", + "id": "routes/__layout", "index": undefined, "parentId": "root", "path": undefined, }, - "routes/_layout/with-layout": Object { + "routes/__layout/with-layout": Object { "caseSensitive": undefined, - "file": "routes/_layout/with-layout.tsx", - "id": "routes/_layout/with-layout", + "file": "routes/__layout/with-layout.tsx", + "id": "routes/__layout/with-layout", "index": undefined, - "parentId": "routes/_layout", + "parentId": "routes/__layout", "path": "with-layout", }, "routes/action-catches": Object { diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index ed7581f88b..84f7bdb684 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -54,7 +54,7 @@ export function defineConventionalRoutes(appDir: string): RouteManifest { ); // layout routes - if (routePath.startsWith("_")) { + if (routePath.startsWith("__")) { routePath = undefined; } From aa5434d5131b5828284220ea4f9ea09d2dfa819f Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Sat, 9 Oct 2021 22:02:39 -0600 Subject: [PATCH 0094/1690] Version 0.19.2 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index f72c40858a..6a42b2a401 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.19.1", + "version": "0.19.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 3db3245178..8238653f7f 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.19.1", + "version": "0.19.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.19.1" + "@remix-run/node": "0.19.2" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 43fbf506f1..f2f887f69b 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.19.1", + "version": "0.19.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.19.1", + "@remix-run/server-runtime": "0.19.2", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 7bbd41de6d..b7c8d8831d 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.19.1", + "version": "0.19.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.19.1", + "@remix-run/express": "0.19.2", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 10533a34b8..be5d0fd4da 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.19.1", + "version": "0.19.2", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 7461a9e5059d1233d43d52512c1faf06f5041ec6 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Tue, 12 Oct 2021 16:53:54 -0400 Subject: [PATCH 0095/1690] fix(dev): make `~/` imports work for markdown files (#317) --- packages/remix-dev/compiler/plugins/mdx.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler/plugins/mdx.ts b/packages/remix-dev/compiler/plugins/mdx.ts index b5220d3bc6..a18b4228db 100644 --- a/packages/remix-dev/compiler/plugins/mdx.ts +++ b/packages/remix-dev/compiler/plugins/mdx.ts @@ -17,7 +17,9 @@ export function mdxPlugin(config: RemixConfig): esbuild.Plugin { build.onResolve({ filter: /\.mdx?$/ }, args => { return { - path: path.resolve(args.resolveDir, args.path), + path: args.path.startsWith("~/") + ? path.resolve(config.appDirectory, args.path.replace(/^~\//, "")) + : path.resolve(args.resolveDir, args.path), namespace: "mdx" }; }); From dd3d76866216dbd4a2740a6ef52b1ebfbf1cbcf8 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 12 Oct 2021 20:56:37 -0700 Subject: [PATCH 0096/1690] rename CLI commands (#315) `remix dev` is now `remix watch` and `remix run` is now `remix dev` --- packages/remix-dev/__tests__/cli-test.ts | 4 ++-- packages/remix-dev/cli.ts | 13 ++++++------- packages/remix-dev/cli/commands.ts | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts index b2715ad6f7..17b6b2ebfd 100644 --- a/packages/remix-dev/__tests__/cli-test.ts +++ b/packages/remix-dev/__tests__/cli-test.ts @@ -25,7 +25,7 @@ describe("remix cli", () => { " Usage $ remix build [remixRoot] - $ remix run [remixRoot] + $ remix dev [remixRoot] $ remix setup [remixPlatform] Options @@ -37,7 +37,7 @@ describe("remix cli", () => { Examples $ remix build my-website - $ remix run my-website + $ remix dev my-website $ remix setup node " diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index d73c130e85..0ae06a1c67 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -6,7 +6,7 @@ import * as commands from "./cli/commands"; const helpText = ` Usage $ remix build [remixRoot] - $ remix run [remixRoot] + $ remix dev [remixRoot] $ remix setup [remixPlatform] Options @@ -18,7 +18,7 @@ Values Examples $ remix build my-website - $ remix run my-website + $ remix dev my-website $ remix setup node `; @@ -44,19 +44,18 @@ switch (cli.input[0]) { case "build": commands.build(cli.input[1], process.env.NODE_ENV); break; - case "dev": // `remix dev` is alias for `remix watch` case "watch": commands.watch(cli.input[1], process.env.NODE_ENV); break; case "setup": commands.setup(cli.input[1]); break; - case "run": + case "dev": if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; - commands.run(cli.input[1], process.env.NODE_ENV); + commands.dev(cli.input[1], process.env.NODE_ENV); break; default: - // `remix ./my-project` is shorthand for `remix run ./my-project` + // `remix ./my-project` is shorthand for `remix dev ./my-project` if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; - commands.run(cli.input[0], process.env.NODE_ENV); + commands.dev(cli.input[0], process.env.NODE_ENV); } diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 85e2ac396f..f89672a165 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -98,7 +98,7 @@ export async function watch( console.log(`💿 Built in ${prettyMs(Date.now() - start)}`); } -export async function run(remixRoot: string, modeArg?: string) { +export async function dev(remixRoot: string, modeArg?: string) { // TODO: Warn about the need to install @remix-run/serve if it isn't there? let { createApp } = require("@remix-run/serve"); From a04f15706896cfa8be3130c2c237b3c2f93f63a1 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 13 Oct 2021 10:02:30 -0700 Subject: [PATCH 0097/1690] fix: remove "*.json" module definition (#314) --- packages/remix-dev/modules.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/remix-dev/modules.ts b/packages/remix-dev/modules.ts index de33c7d73d..a373d27bb5 100644 --- a/packages/remix-dev/modules.ts +++ b/packages/remix-dev/modules.ts @@ -26,10 +26,6 @@ declare module "*.jpg" { const asset: string; export default asset; } -declare module "*.json" { - const asset: string; - export default asset; -} declare module "*.md" { import type { ComponentType as MdComponentType } from "react"; export const attributes: any; From 5f325e323e657041e5f04aa1441588cb8a3ad5e4 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 14 Oct 2021 08:12:42 -0600 Subject: [PATCH 0098/1690] fix: update layout route action check to two __ --- packages/remix-dev/compiler/routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler/routes.ts b/packages/remix-dev/compiler/routes.ts index 9f4d8d806b..87a49c2e6b 100644 --- a/packages/remix-dev/compiler/routes.ts +++ b/packages/remix-dev/compiler/routes.ts @@ -34,7 +34,7 @@ export async function getRouteModuleExportsCached( } // Layout routes can't have actions - if (routeId.match(/\/_[\s\w\d_-]+$/) && cached.exports.includes("action")) { + if (routeId.match(/\/__[\s\w\d_-]+$/) && cached.exports.includes("action")) { throw new Error(`Actions are not supported in layout routes: ${routeId}`); } From dc390338a10eb80b11288ca7080744aaaf6d05ff Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 14 Oct 2021 08:14:56 -0600 Subject: [PATCH 0099/1690] Version 0.19.3 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 6a42b2a401..4cfbcc4edc 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.19.2", + "version": "0.19.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 8238653f7f..054d4d419c 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.19.2", + "version": "0.19.3", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.19.2" + "@remix-run/node": "0.19.3" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index f2f887f69b..a83960169e 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.19.2", + "version": "0.19.3", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.19.2", + "@remix-run/server-runtime": "0.19.3", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index b7c8d8831d..5244b581b0 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.19.2", + "version": "0.19.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.19.2", + "@remix-run/express": "0.19.3", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index be5d0fd4da..78ea5eeb62 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.19.2", + "version": "0.19.3", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From c35bfe7da93b8dbeb205403685b56c85d9c7c39f Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Tue, 19 Oct 2021 18:15:21 -0400 Subject: [PATCH 0100/1690] chore: run prettier on project (#332) * chore: run prettier on project * chore: merge docs prettierrc with project one * chore: remove unused imports Signed-off-by: Logan McAnsh --- .../remix-express/__tests__/server-test.ts | 8 ++++---- packages/remix-express/server.ts | 6 +++--- packages/remix-node/errors.ts | 3 ++- packages/remix-node/globals.ts | 8 ++++---- .../__tests__/data-test.ts | 18 +++++++++--------- packages/remix-server-runtime/data.ts | 4 ++-- packages/remix-server-runtime/routeMatching.ts | 4 ++-- packages/remix-server-runtime/server.ts | 2 +- 8 files changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index 2ddede48a2..ddd855c119 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -9,13 +9,13 @@ import { createRequestHandler } from "../server"; - // We don't want to test that the remix server works here (that's what the // puppetteer tests do), we just want to test the express adapter jest.mock("@remix-run/server-runtime/server"); -let mockedCreateRequestHandler = createRemixRequestHandler as jest.MockedFunction< - typeof createRemixRequestHandler ->; +let mockedCreateRequestHandler = + createRemixRequestHandler as jest.MockedFunction< + typeof createRemixRequestHandler + >; function createApp() { let app = express(); diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index 1b56d5c420..639cc764ac 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -57,10 +57,10 @@ export function createRequestHandler({ ? getLoadContext(req, res) : undefined; - let response = ((await handleRequest( - (request as unknown) as Request, + let response = (await handleRequest( + request as unknown as Request, loadContext - )) as unknown) as NodeResponse; + )) as unknown as NodeResponse; sendRemixResponse(res, response); } catch (error) { diff --git a/packages/remix-node/errors.ts b/packages/remix-node/errors.ts index e35fb8ff41..577427857f 100644 --- a/packages/remix-node/errors.ts +++ b/packages/remix-node/errors.ts @@ -5,7 +5,8 @@ import type { NullableMappedPosition } from "source-map"; import { SourceMapConsumer } from "source-map"; const ROOT = process.cwd() + path.sep; -const SOURCE_PATTERN = /(?\s+at.+)\((?.+):(?\d+):(?\d+)\)/; +const SOURCE_PATTERN = + /(?\s+at.+)\((?.+):(?\d+):(?\d+)\)/; export const UNKNOWN_LOCATION_POSITION = ""; diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts index cd89275e70..c72b48b495 100644 --- a/packages/remix-node/globals.ts +++ b/packages/remix-node/globals.ts @@ -35,10 +35,10 @@ export function installGlobals() { global.atob = atob; global.btoa = btoa; - global.Headers = (NodeHeaders as unknown) as typeof Headers; - global.Request = (NodeRequest as unknown) as typeof Request; - global.Response = (NodeResponse as unknown) as typeof Response; - global.fetch = (nodeFetch as unknown) as typeof fetch; + global.Headers = NodeHeaders as unknown as typeof Headers; + global.Request = NodeRequest as unknown as typeof Request; + global.Response = NodeResponse as unknown as typeof Response; + global.fetch = nodeFetch as unknown as typeof fetch; global.sign = remixSign; global.unsign = remixUnsign; diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index 183007b7ce..c07f3009f0 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -10,7 +10,7 @@ describe("loaders", () => { }; let routeId = "routes/random"; - let build = ({ + let build = { routes: { [routeId]: { id: routeId, @@ -20,7 +20,7 @@ describe("loaders", () => { } } } - } as unknown) as ServerBuild; + } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -47,7 +47,7 @@ describe("loaders", () => { }; let routeId = "routes/random"; - let build = ({ + let build = { routes: { [routeId]: { id: routeId, @@ -57,7 +57,7 @@ describe("loaders", () => { } } } - } as unknown) as ServerBuild; + } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -74,13 +74,13 @@ describe("loaders", () => { expect(await res.headers.get("X-Remix-Catch")).toBeTruthy(); }); - it("removes index from request.url", async () => { + it("removes index from request.url", async () => { let loader = async ({ request }) => { return new URL(request.url).search; }; let routeId = "routes/random"; - let build = ({ + let build = { routes: { [routeId]: { id: routeId, @@ -90,7 +90,7 @@ describe("loaders", () => { } } } - } as unknown) as ServerBuild; + } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -113,7 +113,7 @@ describe("loaders", () => { }; let routeId = "routes/random"; - let build = ({ + let build = { routes: { [routeId]: { id: routeId, @@ -123,7 +123,7 @@ describe("loaders", () => { } } } - } as unknown) as ServerBuild; + } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index 50ef80cf9d..b189397ac5 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -67,9 +67,9 @@ export async function callRouteAction( ); } - let result + let result; try { - result = await routeModule.action({ request, context, params }); + result = await routeModule.action({ request, context, params }); } catch (error) { if (!isResponse(error)) { throw error; diff --git a/packages/remix-server-runtime/routeMatching.ts b/packages/remix-server-runtime/routeMatching.ts index 9e76f653a9..ccfc8238cf 100644 --- a/packages/remix-server-runtime/routeMatching.ts +++ b/packages/remix-server-runtime/routeMatching.ts @@ -13,12 +13,12 @@ export function matchServerRoutes( routes: ServerRoute[], pathname: string ): RouteMatch[] | null { - let matches = matchRoutes((routes as unknown) as RouteObject[], pathname); + let matches = matchRoutes(routes as unknown as RouteObject[], pathname); if (!matches) return null; return matches.map(match => ({ params: match.params, pathname: match.pathname, - route: (match.route as unknown) as ServerRoute + route: match.route as unknown as ServerRoute })); } diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index 94c6f78075..b1ce48ceac 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -1,4 +1,4 @@ -import type { AppLoadContext} from "./data"; +import type { AppLoadContext } from "./data"; import { extractData, isCatchResponse } from "./data"; import { loadRouteData, callRouteAction } from "./data"; import type { ComponentDidCatchEmulator } from "./errors"; From f998fca74aecf43adf0908fd03e6136f59219d9d Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 19 Oct 2021 19:04:53 -0400 Subject: [PATCH 0101/1690] Add support for multiple meta tags with the same name (#322) --- packages/remix-server-runtime/index.ts | 1 + .../remix-server-runtime/magicExports/server.ts | 1 + packages/remix-server-runtime/routeModules.ts | 14 ++++++++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/remix-server-runtime/index.ts b/packages/remix-server-runtime/index.ts index 7251840f65..f0cb22236c 100644 --- a/packages/remix-server-runtime/index.ts +++ b/packages/remix-server-runtime/index.ts @@ -28,6 +28,7 @@ export type { LinksFunction, LoaderFunction, MetaFunction, + MetaDescriptor, RouteComponent, RouteHandle } from "./routeModules"; diff --git a/packages/remix-server-runtime/magicExports/server.ts b/packages/remix-server-runtime/magicExports/server.ts index 2ecef943db..ea06efafda 100644 --- a/packages/remix-server-runtime/magicExports/server.ts +++ b/packages/remix-server-runtime/magicExports/server.ts @@ -20,6 +20,7 @@ export type { HeadersFunction, LinksFunction, LoaderFunction, + MetaDescriptor, MetaFunction, RouteComponent, RouteHandle, diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index 481e6fd24c..63dc89285d 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -73,7 +73,17 @@ export interface MetaFunction { parentsData: RouteData; params: Params; location: Location; - }): { [name: string]: string }; + }): MetaDescriptor; +} + +/** + * A name/content pair used to render `` tags in a meta function for a + * route. The value can be either a string, which will render a single `` + * tag, or an array of strings that will render multiple tags with the same + * `name` attribute. + */ +export interface MetaDescriptor { + [name: string]: string | string[]; } /** @@ -92,7 +102,7 @@ export interface EntryRouteModule { default: RouteComponent; handle?: RouteHandle; links?: LinksFunction; - meta?: MetaFunction | { [name: string]: string }; + meta?: MetaFunction | MetaDescriptor; } export interface ServerRouteModule extends EntryRouteModule { From c0059521846f1ebd68408421af0bde51b0f58dac Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 21 Oct 2021 13:22:54 -0700 Subject: [PATCH 0102/1690] feat: updated to fail build on duplicate paths (#324) fix: updated layout path generation to just strip them --- packages/remix-dev/compiler.ts | 18 ++++++++-- packages/remix-dev/config/routesConvention.ts | 35 +++++++++++++++---- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 7f16d8cfba..583852ea6d 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -115,7 +115,14 @@ export async function watch( let restartBuilders = debounce(async (newConfig?: RemixConfig) => { await disposeBuilders(); - config = newConfig || (await readConfig(config.rootDirectory)); + try { + newConfig = await readConfig(config.rootDirectory); + } catch (error) { + onBuildFailure(error as Error); + return; + } + + config = newConfig; if (onRebuildStart) onRebuildStart(); let builders = await buildEverything(config, options); if (onRebuildFinish) onRebuildFinish(); @@ -167,7 +174,14 @@ export async function watch( }) .on("add", async file => { if (onFileCreated) onFileCreated(file); - let newConfig = await readConfig(config.rootDirectory); + let newConfig: RemixConfig; + try { + newConfig = await readConfig(config.rootDirectory); + } catch (error) { + onBuildFailure(error as Error); + return; + } + if (isEntryPoint(newConfig, file)) { await restartBuilders(newConfig); } else { diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 84f7bdb684..2f9975bb13 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -39,6 +39,8 @@ export function defineConventionalRoutes(appDir: string): RouteManifest { let routeIds = Object.keys(files).sort(byLongestFirst); + let uniqueRoutes = new Map(); + // Then, recurse through all routes using the public defineRoutes() API function defineNestedRoutes( defineRoute: DefineRouteFunction, @@ -53,12 +55,25 @@ export function defineConventionalRoutes(appDir: string): RouteManifest { routeId.slice((parentId || "routes").length + 1) ); - // layout routes - if (routePath.startsWith("__")) { - routePath = undefined; + let isIndexRoute = routeId.endsWith("/index"); + let fullPath = createRoutePath(routeId.slice("routes".length + 1)); + let uniqueRouteId = (fullPath || "") + (isIndexRoute ? "?index" : ""); + + if (typeof uniqueRouteId !== "undefined") { + if (uniqueRoutes.has(uniqueRouteId)) { + throw new Error( + `Path ${JSON.stringify(fullPath)} defined by route ${JSON.stringify( + routeId + )} conflicts with route ${JSON.stringify( + uniqueRoutes.get(uniqueRouteId) + )}` + ); + } else { + uniqueRoutes.set(uniqueRouteId, routeId); + } } - if (routeId.endsWith("/index")) { + if (isIndexRoute) { let invalidChildRoutes = routeIds.filter( id => findParentRouteId(routeIds, id) === routeId ); @@ -84,7 +99,7 @@ export function defineConventionalRoutes(appDir: string): RouteManifest { } // TODO: Cleanup and write some tests for this function -export function createRoutePath(partialRouteId: string): string { +export function createRoutePath(partialRouteId: string): string | undefined { let path = normalizeSlashes(partialRouteId) // routes/$ -> routes/* // routes/nested/$.tsx (with a "routes/nested.tsx" layout) @@ -96,7 +111,15 @@ export function createRoutePath(partialRouteId: string): string { .replace(/\$/g, ":") // routes/not.nested -> routes/not/nested .replace(/\./g, "/"); - return /\b\/?index$/.test(path) ? path.replace(/\/?index$/, "") : path; + path = /\b\/?index$/.test(path) ? path.replace(/\/?index$/, "") : path; + + // remove "__" layout segments + path = path + .split("/") + .filter(segment => !segment.startsWith("__")) + .join("/"); + + return path ? path : undefined; } function findParentRouteId( From 15aa84c3351cac1f90411e92f252df80bf1f7a91 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 21 Oct 2021 14:47:58 -0700 Subject: [PATCH 0103/1690] fix: updated route convention test (#343) also added tests for layout --- packages/remix-dev/__tests__/routesConvention-test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/__tests__/routesConvention-test.ts b/packages/remix-dev/__tests__/routesConvention-test.ts index c9c3a2c561..11bebad147 100644 --- a/packages/remix-dev/__tests__/routesConvention-test.ts +++ b/packages/remix-dev/__tests__/routesConvention-test.ts @@ -12,7 +12,12 @@ describe("createRoutePath", () => { ["flat.sub", "flat/sub"], ["nested/index", "nested"], ["flat.index", "flat"], - ["index", ""] + ["index", undefined], + ["__layout/index", undefined], + ["__layout/test", "test"], + ["__layout.test", "test"], + ["__layout/$slug", ":slug"], + ["nested/__layout/$slug", "nested/:slug"] ]; for (let [input, expected] of tests) { From 43e22e6436e54e410d2d01f409680a15df700253 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 21 Oct 2021 16:31:57 -0700 Subject: [PATCH 0104/1690] feat: add "routes" visualization command to cli (#326) fix: updated fixture pm2 config fix: updated fixture install script to link cli --- packages/remix-dev/cli.ts | 9 +++ packages/remix-dev/cli/commands.ts | 12 ++++ packages/remix-dev/config/format.ts | 98 +++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 packages/remix-dev/config/format.ts diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index 0ae06a1c67..c1c420560e 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -8,10 +8,12 @@ Usage $ remix build [remixRoot] $ remix dev [remixRoot] $ remix setup [remixPlatform] + $ remix routes [remixRoot] Options --help Print this help message and exit --version, -v Print the CLI version and exit + --json Print the routes as JSON Values [remixPlatform] "node" is currently the only platform @@ -20,12 +22,16 @@ Examples $ remix build my-website $ remix dev my-website $ remix setup node + $ remix routes my-website `; const flags: AnyFlags = { version: { type: "boolean", alias: "v" + }, + json: { + type: "boolean" } }; @@ -41,6 +47,9 @@ if (cli.flags.version) { } switch (cli.input[0]) { + case "routes": + commands.routes(cli.input[1], cli.flags.json ? "json" : "jsx"); + break; case "build": commands.build(cli.input[1], process.env.NODE_ENV); break; diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index f89672a165..2a9f5ca282 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -8,6 +8,7 @@ import { BuildMode, isBuildMode } from "../build"; import * as compiler from "../compiler"; import type { RemixConfig } from "../config"; import { readConfig } from "../config"; +import { formatRoutes, RoutesFormat, isRoutesFormat } from "../config/format"; import { setupRemix, isSetupPlatform, SetupPlatform } from "../setup"; export async function setup(platformArg?: string) { @@ -20,6 +21,17 @@ export async function setup(platformArg?: string) { console.log(`Successfully setup Remix for ${platform}.`); } +export async function routes( + remixRoot: string, + formatArg?: string +): Promise { + let config = await readConfig(remixRoot); + + let format = isRoutesFormat(formatArg) ? formatArg : RoutesFormat.jsx; + + console.log(formatRoutes(config.routes, format)); +} + export async function build( remixRoot: string, modeArg?: string diff --git a/packages/remix-dev/config/format.ts b/packages/remix-dev/config/format.ts new file mode 100644 index 0000000000..1317a54aae --- /dev/null +++ b/packages/remix-dev/config/format.ts @@ -0,0 +1,98 @@ +import type { RouteManifest } from "./routes"; + +export enum RoutesFormat { + json = "json", + jsx = "jsx" +} + +export function isRoutesFormat(format: any): format is RoutesFormat { + return format === RoutesFormat.json || format === RoutesFormat.jsx; +} + +export function formatRoutes( + routeManifest: RouteManifest, + format: RoutesFormat +) { + switch (format) { + case RoutesFormat.json: + return formatRoutesAsJson(routeManifest); + case RoutesFormat.jsx: + return formatRoutesAsJsx(routeManifest); + } +} + +type JsonFormattedRoute = { + id: string; + index?: boolean; + path?: string; + caseSensitive?: boolean; + file: string; + children?: JsonFormattedRoute[]; +}; + +export function formatRoutesAsJson(routeManifest: RouteManifest): string { + function handleRoutesRecursive( + parentId?: string + ): JsonFormattedRoute[] | undefined { + let routes = Object.values(routeManifest).filter( + route => route.parentId === parentId + ); + + let children = []; + + for (let route of routes) { + children.push({ + id: route.id, + index: route.index, + path: route.path, + caseSensitive: route.caseSensitive, + file: route.file, + children: handleRoutesRecursive(route.id) + }); + } + + if (children.length > 0) { + return children; + } + return undefined; + } + + return JSON.stringify(handleRoutesRecursive() || null, null, 2); +} + +export function formatRoutesAsJsx(routeManifest: RouteManifest) { + let output = ""; + + function handleRoutesRecursive(parentId?: string, level = 1): boolean { + let routes = Object.values(routeManifest).filter( + route => route.parentId === parentId + ); + + let indent = Array(level * 2) + .fill(" ") + .join(""); + + for (let route of routes) { + output += "\n" + indent; + output += ``; + if (handleRoutesRecursive(route.id, level + 1)) { + output += "\n" + indent; + output += ""; + } else { + output = output.slice(0, -1) + " />"; + } + } + + return routes.length > 0; + } + + handleRoutesRecursive(); + + output += "\n"; + + return output; +} From 161eeb98bf4fd5d0c45d1df651efe54529e4e720 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Fri, 22 Oct 2021 17:53:50 -0400 Subject: [PATCH 0105/1690] chore(dev): exit when using fake built ins (#321) --- packages/remix-dev/compiler.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 583852ea6d..57d0822983 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -259,6 +259,16 @@ async function createBrowserBuild( // on node built-ins in browser bundles. let dependencies = Object.keys(await getAppDependencies(config)); let externals = nodeBuiltins.filter(mod => !dependencies.includes(mod)); + let fakeBuiltins = nodeBuiltins.filter(mod => dependencies.includes(mod)); + + if (fakeBuiltins.length > 0) { + console.error( + `It appears you're using a module that is built in to node, but you installed it as a dependency which could cause problems. Please remove ${fakeBuiltins.join( + ", " + )} before continuing.` + ); + process.exit(1); + } let entryPoints: esbuild.BuildOptions["entryPoints"] = { "entry.client": path.resolve(config.appDirectory, config.entryClientFile) From b533541832d4fa6f2a98a235a747a311397d5023 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 22 Oct 2021 16:30:15 -0700 Subject: [PATCH 0106/1690] feat: upgrade react router (#345) test: update cli snapshot --- packages/remix-dev/__tests__/cli-test.ts | 3 +++ packages/remix-server-runtime/package.json | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts index 17b6b2ebfd..5ca5bc4ba0 100644 --- a/packages/remix-dev/__tests__/cli-test.ts +++ b/packages/remix-dev/__tests__/cli-test.ts @@ -27,10 +27,12 @@ describe("remix cli", () => { $ remix build [remixRoot] $ remix dev [remixRoot] $ remix setup [remixPlatform] + $ remix routes [remixRoot] Options --help Print this help message and exit --version, -v Print the CLI version and exit + --json Print the routes as JSON Values [remixPlatform] \\"node\\" is currently the only platform @@ -39,6 +41,7 @@ describe("remix cli", () => { $ remix build my-website $ remix dev my-website $ remix setup node + $ remix routes my-website " `); diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 78ea5eeb62..a67cab5eea 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -8,13 +8,13 @@ "cookie": "^0.4.1", "history": "^5.0.0", "jsesc": "^3.0.1", - "react-router-dom": "6.0.0-beta.6", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3" }, "peerDependencies": { "react": ">=16.8", - "react-dom": ">=16.8" + "react-dom": ">=16.8", + "react-router-dom": "6.0.0-beta.8" }, "devDependencies": { "@types/jsesc": "^2.5.1", From 1817836006b38181894d0c7d1db2804f86e840ee Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 25 Oct 2021 16:12:15 -0700 Subject: [PATCH 0107/1690] fix: compiler should allow unstable_shouldReload (#347) --- packages/remix-dev/compiler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 57d0822983..be0a52006b 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -458,7 +458,7 @@ const browserSafeRouteExports: { [name: string]: boolean } = { handle: true, links: true, meta: true, - shouldReload: true + unstable_shouldReload: true }; /** From a9c37499cf999870e172c9a15938c5a9dcd6ce02 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 28 Oct 2021 14:26:53 -0700 Subject: [PATCH 0108/1690] feat: add `--sourcemap` option to `remix build` --- packages/remix-dev/cli.ts | 32 +++++++++++++++++------------- packages/remix-dev/cli/commands.ts | 17 ++++++++++++++-- packages/remix-dev/compiler.ts | 9 +++++++-- packages/remix-node/errors.ts | 5 ++++- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index c1c420560e..b4150b983b 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -1,4 +1,3 @@ -import type { AnyFlags } from "meow"; import meow from "meow"; import * as commands from "./cli/commands"; @@ -13,7 +12,9 @@ Usage Options --help Print this help message and exit --version, -v Print the CLI version and exit - --json Print the routes as JSON + + --json Print the routes as JSON (remix routes only) + --sourcemap Generate source maps (remix build only) Values [remixPlatform] "node" is currently the only platform @@ -25,21 +26,22 @@ Examples $ remix routes my-website `; -const flags: AnyFlags = { - version: { - type: "boolean", - alias: "v" - }, - json: { - type: "boolean" - } -}; - const cli = meow(helpText, { autoHelp: true, autoVersion: false, description: false, - flags + flags: { + version: { + type: "boolean", + alias: "v" + }, + json: { + type: "boolean" + }, + sourcemap: { + type: "boolean" + } + } }); if (cli.flags.version) { @@ -51,9 +53,11 @@ switch (cli.input[0]) { commands.routes(cli.input[1], cli.flags.json ? "json" : "jsx"); break; case "build": - commands.build(cli.input[1], process.env.NODE_ENV); + if (!process.env.NODE_ENV) process.env.NODE_ENV = "production"; + commands.build(cli.input[1], process.env.NODE_ENV, cli.flags.sourcemap); break; case "watch": + if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; commands.watch(cli.input[1], process.env.NODE_ENV); break; case "setup": diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 2a9f5ca282..b0e59ec0eb 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -34,15 +34,28 @@ export async function routes( export async function build( remixRoot: string, - modeArg?: string + modeArg?: string, + sourcemap: boolean = false ): Promise { let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Production; console.log(`Building Remix app in ${mode} mode...`); + if (modeArg === BuildMode.Production && sourcemap) { + console.warn( + "\n⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️" + ); + console.warn( + "You have enabled source maps in production. This will make your server side code visible to the public and is highly discouraged! If you insist, please ensure you are using environment variables for secrets and not hard-coding them into your source!" + ); + console.warn( + "⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️\n" + ); + } + let start = Date.now(); let config = await readConfig(remixRoot); - await compiler.build(config, { mode: mode }); + await compiler.build(config, { mode: mode, sourcemap }); console.log(`Built in ${prettyMs(Date.now() - start)}`); } diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index be0a52006b..da4fd0384e 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -25,6 +25,7 @@ const reactShim = path.resolve(__dirname, "compiler/shims/react.ts"); interface BuildConfig { mode: BuildMode; target: BuildTarget; + sourcemap: boolean; } function defaultWarningHandler(message: string, key: string) { @@ -63,6 +64,7 @@ export async function build( { mode = BuildMode.Production, target = BuildTarget.Node14, + sourcemap = false, onWarning = defaultWarningHandler, onBuildFailure = defaultBuildFailureHandler }: BuildOptions = {} @@ -70,6 +72,7 @@ export async function build( await buildEverything(config, { mode, target, + sourcemap, onWarning, onBuildFailure }); @@ -88,6 +91,7 @@ export async function watch( { mode = BuildMode.Development, target = BuildTarget.Node14, + sourcemap = true, onWarning = defaultWarningHandler, onBuildFailure = defaultBuildFailureHandler, onRebuildStart, @@ -100,6 +104,7 @@ export async function watch( let options = { mode, target, + sourcemap, onBuildFailure, onWarning, incremental: true @@ -292,7 +297,7 @@ async function createBrowserBuild( bundle: true, logLevel: "silent", splitting: true, - sourcemap: true, + sourcemap: options.sourcemap, metafile: true, incremental: options.incremental, minify: options.mode === BuildMode.Production, @@ -331,7 +336,7 @@ async function createServerBuild( bundle: true, logLevel: "silent", incremental: options.incremental, - sourcemap: true, + sourcemap: options.sourcemap, // The server build needs to know how to generate asset URLs for imports // of CSS and other files. assetNames: "_assets/[name]-[hash]", diff --git a/packages/remix-node/errors.ts b/packages/remix-node/errors.ts index 577427857f..b91aa759d1 100644 --- a/packages/remix-node/errors.ts +++ b/packages/remix-node/errors.ts @@ -11,7 +11,10 @@ const SOURCE_PATTERN = export const UNKNOWN_LOCATION_POSITION = ""; export async function formatServerError(error: Error): Promise { - error.stack = await formatStackTrace(error); + try { + error.stack = await formatStackTrace(error); + } catch {} + return error; } From d77d60a91fb18b20df02f6b31ed7e03f3a3719ff Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 28 Oct 2021 14:29:34 -0700 Subject: [PATCH 0109/1690] updated test snapshot --- packages/remix-dev/__tests__/cli-test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts index 5ca5bc4ba0..5ef687c1e9 100644 --- a/packages/remix-dev/__tests__/cli-test.ts +++ b/packages/remix-dev/__tests__/cli-test.ts @@ -32,7 +32,9 @@ describe("remix cli", () => { Options --help Print this help message and exit --version, -v Print the CLI version and exit - --json Print the routes as JSON + + --json Print the routes as JSON (remix routes only) + --sourcemap Generate source maps (remix build only) Values [remixPlatform] \\"node\\" is currently the only platform From ec6fcca179c957cd9c19a6b5287b4ba71e1435bd Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 28 Oct 2021 10:00:07 -0700 Subject: [PATCH 0110/1690] fix: redirects are not followed when thrown from an action for client side form submissions --- packages/remix-dev/__tests__/readConfig-test.ts | 8 ++++++++ packages/remix-server-runtime/data.ts | 14 ++++++++++++-- packages/remix-server-runtime/server.ts | 8 +------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 90642be711..4c6dce80f2 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -366,6 +366,14 @@ describe("readConfig", () => { "parentId": "root", "path": "prefs", }, + "routes/redirects/login": Object { + "caseSensitive": undefined, + "file": "routes/redirects/login.jsx", + "id": "routes/redirects/login", + "index": undefined, + "parentId": "root", + "path": "redirects/login", + }, "routes/render-errors": Object { "caseSensitive": undefined, "file": "routes/render-errors.jsx", diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index b189397ac5..7cf2898192 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -36,7 +36,9 @@ export async function loadRouteData( throw error; } - error.headers.set("X-Remix-Catch", "yes"); + if (!isRedirectResponse(error)) { + error.headers.set("X-Remix-Catch", "yes"); + } result = error; } @@ -75,7 +77,9 @@ export async function callRouteAction( throw error; } - error.headers.set("X-Remix-Catch", "yes"); + if (!isRedirectResponse(error)) { + error.headers.set("X-Remix-Catch", "yes"); + } result = error; } @@ -103,6 +107,12 @@ function isResponse(value: any): value is Response { ); } +const redirectStatusCodes = new Set([301, 302, 303, 307, 308]); + +export function isRedirectResponse(response: Response): boolean { + return redirectStatusCodes.has(response.status); +} + export function extractData(response: Response): Promise { let contentType = response.headers.get("Content-Type"); diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index b1ce48ceac..db60ba84f0 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -1,6 +1,6 @@ import type { AppLoadContext } from "./data"; import { extractData, isCatchResponse } from "./data"; -import { loadRouteData, callRouteAction } from "./data"; +import { loadRouteData, callRouteAction, isRedirectResponse } from "./data"; import type { ComponentDidCatchEmulator } from "./errors"; import type { ServerBuild } from "./build"; import type { EntryContext } from "./entry"; @@ -505,12 +505,6 @@ function isDataRequest(request: Request): boolean { return new URL(request.url).searchParams.has("_data"); } -const redirectStatusCodes = new Set([301, 302, 303, 307, 308]); - -function isRedirectResponse(response: Response): boolean { - return redirectStatusCodes.has(response.status); -} - function isIndexRequestUrl(url: URL) { let indexRequest = false; From 5dc8987435947edd1174898fcb5139145a062148 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 15 Oct 2021 21:59:43 -0700 Subject: [PATCH 0111/1690] feat: added entry.server handleDataRequest --- packages/remix-server-runtime/build.ts | 22 ++++++++++++++----- packages/remix-server-runtime/index.ts | 8 ++++++- packages/remix-server-runtime/routeModules.ts | 15 ++++++++++--- packages/remix-server-runtime/server.ts | 11 +++++++++- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/packages/remix-server-runtime/build.ts b/packages/remix-server-runtime/build.ts index afc1f5c073..80f6ed4b02 100644 --- a/packages/remix-server-runtime/build.ts +++ b/packages/remix-server-runtime/build.ts @@ -1,3 +1,4 @@ +import type { DataFunctionArgs } from "./routeModules"; import type { EntryContext, AssetsManifest } from "./entry"; import type { ServerRouteManifest } from "./routes"; @@ -12,15 +13,24 @@ export interface ServerBuild { assets: AssetsManifest; } +export interface HandleDocumentRequestFunction { + ( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + context: EntryContext + ): Promise | Response; +} + +export interface HandleDataRequestFunction { + (response: Response, args: DataFunctionArgs): Promise | Response; +} + /** * A module that serves as the entry point for a Remix app during server * rendering. */ export interface ServerEntryModule { - default( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - context: EntryContext - ): Promise; + default: HandleDocumentRequestFunction; + handleDataRequest?: HandleDataRequestFunction; } diff --git a/packages/remix-server-runtime/index.ts b/packages/remix-server-runtime/index.ts index f0cb22236c..17dc3c98eb 100644 --- a/packages/remix-server-runtime/index.ts +++ b/packages/remix-server-runtime/index.ts @@ -1,4 +1,9 @@ -export type { ServerBuild, ServerEntryModule } from "./build"; +export type { + ServerBuild, + ServerEntryModule, + HandleDataRequestFunction, + HandleDocumentRequestFunction +} from "./build"; export type { CookieParseOptions, @@ -23,6 +28,7 @@ export type { ServerPlatform } from "./platform"; export type { ActionFunction, + DataFunctionArgs, ErrorBoundaryComponent, HeadersFunction, LinksFunction, diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index 63dc89285d..847261ca22 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -1,6 +1,6 @@ import type { Location } from "history"; import type { ComponentType } from "react"; -import type { Params } from "react-router"; // TODO: import/export from react-router-dom +import type { Params } from "react-router-dom"; import type { AppLoadContext, AppData } from "./data"; import type { LinkDescriptor } from "./links"; @@ -10,11 +10,20 @@ export interface RouteModules { [routeId: string]: RouteModule; } +/** + * The arguments passed to ActionFunction and LoaderFunction. + */ +export interface DataFunctionArgs { + request: Request; + context: AppLoadContext; + params: Params; +} + /** * A function that handles data mutations for a route. */ export interface ActionFunction { - (args: { request: Request; context: AppLoadContext; params: Params }): + (args: DataFunctionArgs): | Promise | Response | Promise @@ -55,7 +64,7 @@ export interface LinksFunction { * A function that loads data for a route. */ export interface LoaderFunction { - (args: { request: Request; context: AppLoadContext; params: Params }): + (args: DataFunctionArgs): | Promise | Response | Promise diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index db60ba84f0..d7f8326edd 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -127,7 +127,7 @@ async function handleDataRequest( ); } catch (error: any) { let formattedError = (await platform.formatServerError?.(error)) || error; - return json(await serializeError(formattedError), { + response = json(await serializeError(formattedError), { status: 500, headers: { "X-Remix-Error": "unfortunately, yes" @@ -149,6 +149,15 @@ async function handleDataRequest( }); } + if (build.entry.module.handleDataRequest) { + clonedRequest = stripIndexParam(stripDataParam(request)); + return build.entry.module.handleDataRequest(response, { + request: clonedRequest, + context: loadContext, + params: routeMatch.params + }); + } + return response; } From 4c365bdbc91fc3352b8bcd7ddab24d947bb548dc Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 29 Oct 2021 12:03:11 -0700 Subject: [PATCH 0112/1690] Jacob/ci (#353) fix: fixed rollup config shim copy fix: update CLI to exit with non zero code on failure --- packages/remix-dev/cli.ts | 21 +++++++++++++++------ packages/remix-dev/config.ts | 4 +--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index b4150b983b..bb8a1c33fc 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -48,27 +48,36 @@ if (cli.flags.version) { cli.showVersion(); } +function handleError(error: Error) { + console.error(error.message); + process.exit(1); +} + switch (cli.input[0]) { case "routes": - commands.routes(cli.input[1], cli.flags.json ? "json" : "jsx"); + commands + .routes(cli.input[1], cli.flags.json ? "json" : "jsx") + .catch(handleError); break; case "build": if (!process.env.NODE_ENV) process.env.NODE_ENV = "production"; - commands.build(cli.input[1], process.env.NODE_ENV, cli.flags.sourcemap); + commands + .build(cli.input[1], process.env.NODE_ENV, cli.flags.sourcemap) + .catch(handleError); break; case "watch": if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; - commands.watch(cli.input[1], process.env.NODE_ENV); + commands.watch(cli.input[1], process.env.NODE_ENV).catch(handleError); break; case "setup": - commands.setup(cli.input[1]); + commands.setup(cli.input[1]).catch(handleError); break; case "dev": if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; - commands.dev(cli.input[1], process.env.NODE_ENV); + commands.dev(cli.input[1], process.env.NODE_ENV).catch(handleError); break; default: // `remix ./my-project` is shorthand for `remix dev ./my-project` if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; - commands.dev(cli.input[0], process.env.NODE_ENV); + commands.dev(cli.input[0], process.env.NODE_ENV).catch(handleError); } diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index a233b6bb9e..219a439ad6 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -167,9 +167,7 @@ export async function readConfig( try { appConfig = require(configFile); } catch (error) { - console.error(`Error loading Remix config in ${configFile}`); - console.error(error); - process.exit(); + throw new Error(`Error loading Remix config in ${configFile}`); } let appDirectory = path.resolve( From 998eb79633457eea5c8702c1485b37aae010b9fe Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 29 Oct 2021 12:09:57 -0700 Subject: [PATCH 0113/1690] rebased (#352) --- packages/remix-server-runtime/__tests__/data-test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index c07f3009f0..bc52fa78da 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -19,7 +19,8 @@ describe("loaders", () => { loader } } - } + }, + entry: { module: {} } } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -56,7 +57,8 @@ describe("loaders", () => { loader } } - } + }, + entry: { module: {} } } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -89,7 +91,8 @@ describe("loaders", () => { loader } } - } + }, + entry: { module: {} } } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -122,7 +125,8 @@ describe("loaders", () => { loader } } - } + }, + entry: { module: {} } } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); From 40d977f9f3b128394cda26370430da8fdb227a3d Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 29 Oct 2021 14:46:49 -0700 Subject: [PATCH 0114/1690] Version 0.20.0 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 4cfbcc4edc..3a862eb7a0 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.19.3", + "version": "0.20.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 054d4d419c..611dde81c1 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.19.3", + "version": "0.20.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.19.3" + "@remix-run/node": "0.20.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index a83960169e..11c5ec76b9 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.19.3", + "version": "0.20.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.19.3", + "@remix-run/server-runtime": "0.20.0", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 5244b581b0..13beb80a13 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.19.3", + "version": "0.20.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.19.3", + "@remix-run/express": "0.20.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index a67cab5eea..1fceeb9e4e 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.19.3", + "version": "0.20.0", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From a833251e8c38f8e8652a63d5ee186459d66322e5 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 29 Oct 2021 15:22:37 -0700 Subject: [PATCH 0115/1690] Version 0.20.1 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 3a862eb7a0..a0e96c07d7 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.20.0", + "version": "0.20.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 611dde81c1..9f4801ad5e 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.20.0", + "version": "0.20.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.20.0" + "@remix-run/node": "0.20.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 11c5ec76b9..ad4a62c6f5 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.20.0", + "version": "0.20.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.20.0", + "@remix-run/server-runtime": "0.20.1", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 13beb80a13..54db22af68 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.20.0", + "version": "0.20.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.20.0", + "@remix-run/express": "0.20.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 1fceeb9e4e..5ea40712d1 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.20.0", + "version": "0.20.1", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 761ab2583b59d1479e177faee21d990d1c66fcfb Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 2 Nov 2021 07:41:11 -0700 Subject: [PATCH 0116/1690] feat: remove ?index from query for get submissions (#342) We need `?index` for POST to know which route to post to, but for GET it doesn't matter: we call both the parent and the index loaders anyway. This removes it. --- packages/remix-server-runtime/server.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index d7f8326edd..e97ee81b04 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -232,10 +232,12 @@ async function handleDocumentRequest( actionRouteId = actionMatch.route.id; try { + let clonedRequest = stripIndexParam(stripDataParam(request)); + actionResponse = await callRouteAction( build, actionMatch.route.id, - request.clone(), + clonedRequest, loadContext, actionMatch.params ); @@ -305,7 +307,7 @@ async function handleDocumentRequest( loadRouteData( build, match.route.id, - request.clone(), + stripIndexParam(stripDataParam(request.clone())), loadContext, match.params ).catch(error => error) From d6031597232e291c3a87af5eb65a7e205e58d857 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 8 Nov 2021 18:27:51 -0800 Subject: [PATCH 0117/1690] added sourcemaps back to server builds (#354) --- packages/remix-dev/compiler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index da4fd0384e..a25d6216cc 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -336,7 +336,7 @@ async function createServerBuild( bundle: true, logLevel: "silent", incremental: options.incremental, - sourcemap: options.sourcemap, + sourcemap: true, // The server build needs to know how to generate asset URLs for imports // of CSS and other files. assetNames: "_assets/[name]-[hash]", From dba6b3690b40d90a607c2040f85f1d6ab0f11c70 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 9 Nov 2021 05:28:11 -0800 Subject: [PATCH 0118/1690] feat: add initial formData polyfill for Request (#361) * feat: add initial formData polyfill for Request * updated tsconfig to implemented iterable interface * chore: updated tsconfig to include "DOM.Iterable" --- packages/remix-dev/tsconfig.json | 2 +- packages/remix-express/tsconfig.json | 2 +- .../remix-node/__tests__/form-data-test.ts | 18 +++++++ packages/remix-node/fetch.ts | 17 +++++- packages/remix-node/form-data.ts | 53 +++++++++++++++++++ packages/remix-node/tsconfig.json | 2 +- packages/remix-serve/tsconfig.json | 2 +- packages/remix-server-runtime/tsconfig.json | 2 +- 8 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 packages/remix-node/__tests__/form-data-test.ts create mode 100644 packages/remix-node/form-data.ts diff --git a/packages/remix-dev/tsconfig.json b/packages/remix-dev/tsconfig.json index 7d7236fdf0..9510b82e2c 100644 --- a/packages/remix-dev/tsconfig.json +++ b/packages/remix-dev/tsconfig.json @@ -2,7 +2,7 @@ "include": ["../../types/mdx-js__mdx.d.ts", "**/*"], "exclude": ["__tests__"], "compilerOptions": { - "lib": ["ES2019"], + "lib": ["ES2019", "DOM.Iterable"], "target": "ES2019", "module": "CommonJS", diff --git a/packages/remix-express/tsconfig.json b/packages/remix-express/tsconfig.json index 2e44d701e5..415a17c92f 100644 --- a/packages/remix-express/tsconfig.json +++ b/packages/remix-express/tsconfig.json @@ -1,7 +1,7 @@ { "exclude": ["__tests__"], "compilerOptions": { - "lib": ["ES2019"], + "lib": ["ES2019", "DOM.Iterable"], "target": "ES2019", "moduleResolution": "node", diff --git a/packages/remix-node/__tests__/form-data-test.ts b/packages/remix-node/__tests__/form-data-test.ts new file mode 100644 index 0000000000..38ceced7c9 --- /dev/null +++ b/packages/remix-node/__tests__/form-data-test.ts @@ -0,0 +1,18 @@ +import { RemixFormData as FormData } from "../form-data"; + +describe("FormData", () => { + it("allows for mix of set and append", () => { + let formData = new FormData(); + formData.set("single", "heyo"); + formData.append("multi", "one"); + formData.append("multi", "two"); + + let results = []; + for (let [k, v] of formData) results.push([k, v]); + expect(results).toEqual([ + ["single", "heyo"], + ["multi", "one"], + ["multi", "two"] + ]); + }); +}); diff --git a/packages/remix-node/fetch.ts b/packages/remix-node/fetch.ts index 0a2be2b5a5..aa83e3aa82 100644 --- a/packages/remix-node/fetch.ts +++ b/packages/remix-node/fetch.ts @@ -1,5 +1,7 @@ import type { RequestInfo, RequestInit, Response } from "node-fetch"; -import nodeFetch from "node-fetch"; +import nodeFetch, { Request as NodeRequest } from "node-fetch"; + +import { RemixFormData } from "./form-data"; export type { HeadersInit, @@ -7,7 +9,18 @@ export type { RequestInit, ResponseInit } from "node-fetch"; -export { Headers, Request, Response } from "node-fetch"; +export { Headers, Response } from "node-fetch"; + +export class Request extends NodeRequest { + constructor(input: RequestInfo, init?: RequestInit | undefined) { + super(input, init); + } + + async formData() { + let body = await this.clone().text(); + return new RemixFormData(body); + } +} /** * A `fetch` function for node that matches the web Fetch API. Based on diff --git a/packages/remix-node/form-data.ts b/packages/remix-node/form-data.ts new file mode 100644 index 0000000000..87ba6951f8 --- /dev/null +++ b/packages/remix-node/form-data.ts @@ -0,0 +1,53 @@ +export class RemixFormData implements FormData { + private _params: URLSearchParams; + + constructor(body?: string) { + this._params = new URLSearchParams(body); + } + append(name: string, value: string | Blob, fileName?: string): void { + if (typeof value !== "string") { + throw new Error("formData.append can only accept a string"); + } + this._params.append(name, value); + } + delete(name: string): void { + this._params.delete(name); + } + get(name: string): FormDataEntryValue | null { + return this._params.get(name); + } + getAll(name: string): FormDataEntryValue[] { + return this._params.getAll(name); + } + has(name: string): boolean { + return this._params.has(name); + } + set(name: string, value: string | Blob, fileName?: string): void { + if (typeof value !== "string") { + throw new Error("formData.set can only accept a string"); + } + this._params.set(name, value); + } + forEach( + callbackfn: ( + value: FormDataEntryValue, + key: string, + parent: FormData + ) => void, + thisArg?: any + ): void { + this._params.forEach(callbackfn, thisArg); + } + entries(): IterableIterator<[string, FormDataEntryValue]> { + return this._params.entries(); + } + keys(): IterableIterator { + return this._params.keys(); + } + values(): IterableIterator { + return this._params.values(); + } + *[Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]> { + yield* this._params; + } +} diff --git a/packages/remix-node/tsconfig.json b/packages/remix-node/tsconfig.json index a3d9fe2987..33578ea089 100644 --- a/packages/remix-node/tsconfig.json +++ b/packages/remix-node/tsconfig.json @@ -1,7 +1,7 @@ { "exclude": ["__tests__"], "compilerOptions": { - "lib": ["ES2019"], + "lib": ["ES2019", "DOM.Iterable"], "target": "ES2019", "moduleResolution": "node", diff --git a/packages/remix-serve/tsconfig.json b/packages/remix-serve/tsconfig.json index c36ea68888..2af5b78292 100644 --- a/packages/remix-serve/tsconfig.json +++ b/packages/remix-serve/tsconfig.json @@ -1,7 +1,7 @@ { "exclude": ["__tests__"], "compilerOptions": { - "lib": ["ES2019"], + "lib": ["ES2019", "DOM.Iterable"], "target": "ES2019", "moduleResolution": "node", diff --git a/packages/remix-server-runtime/tsconfig.json b/packages/remix-server-runtime/tsconfig.json index 356a2f628b..a9b170dc26 100644 --- a/packages/remix-server-runtime/tsconfig.json +++ b/packages/remix-server-runtime/tsconfig.json @@ -1,7 +1,7 @@ { "exclude": ["__tests__"], "compilerOptions": { - "lib": ["ES2019", "DOM"], + "lib": ["ES2019", "DOM", "DOM.Iterable"], "target": "ES2019", "moduleResolution": "node", From b3e809a2f875ac63863d647a4707f5c96a18c847 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 9 Nov 2021 05:54:07 -0800 Subject: [PATCH 0119/1690] feat: resource route support (#370) * feat: resource route support * updated snapshots --- .../remix-dev/__tests__/readConfig-test.ts | 32 ++++++ packages/remix-server-runtime/server.ts | 99 +++++++++++++++++-- 2 files changed, 125 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 4c6dce80f2..27f4bf7a19 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -390,6 +390,38 @@ describe("readConfig", () => { "parentId": "routes/render-errors", "path": "nested", }, + "routes/resources/index": Object { + "caseSensitive": undefined, + "file": "routes/resources/index.tsx", + "id": "routes/resources/index", + "index": true, + "parentId": "root", + "path": "resources", + }, + "routes/resources/redirect": Object { + "caseSensitive": undefined, + "file": "routes/resources/redirect.ts", + "id": "routes/resources/redirect", + "index": undefined, + "parentId": "root", + "path": "resources/redirect", + }, + "routes/resources/settings": Object { + "caseSensitive": undefined, + "file": "routes/resources/settings.tsx", + "id": "routes/resources/settings", + "index": undefined, + "parentId": "root", + "path": "resources/settings", + }, + "routes/resources/theme-css": Object { + "caseSensitive": undefined, + "file": "routes/resources/theme-css.ts", + "id": "routes/resources/theme-css", + "index": undefined, + "parentId": "root", + "path": "resources/theme-css", + }, "routes/two": Object { "caseSensitive": undefined, "file": "routes/two.md", diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index e97ee81b04..dade7302eb 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -26,6 +26,28 @@ export interface RequestHandler { (request: Request, loadContext?: AppLoadContext): Promise; } +type RequestType = "data" | "document" | "resource"; + +function getRequestType( + request: Request, + matches: RouteMatch[] | null +): RequestType { + if (isDataRequest(request)) { + return "data"; + } + + if (!matches) { + return "document"; + } + + let match = matches.slice(-1)[0]; + if (!match.route.module.default) { + return "resource"; + } + + return "document"; +} + /** * Creates a function that serves HTTP requests. */ @@ -38,16 +60,46 @@ export function createRequestHandler( let serverMode = isServerMode(mode) ? mode : ServerMode.Production; return async (request, loadContext = {}) => { - let response = await (isDataRequest(request) - ? handleDataRequest(request, loadContext, build, platform, routes) - : handleDocumentRequest( + let url = new URL(request.url); + let matches = matchServerRoutes(routes, url.pathname); + + let requestType = getRequestType(request, matches); + + let response: Response; + + switch (requestType) { + // has _data + case "data": + response = await handleDataRequest( + request, + loadContext, + build, + platform, + matches + ); + break; + // no _data & default export + case "document": + response = await handleDocumentRequest( request, loadContext, build, platform, routes, serverMode - )); + ); + break; + // no _data or default export + case "resource": + response = await handleResourceRequest( + request, + loadContext, + build, + platform, + matches + ); + break; + } if (isHeadRequest(request)) { return new Response(null, { @@ -61,12 +113,48 @@ export function createRequestHandler( }; } +async function handleResourceRequest( + request: Request, + loadContext: AppLoadContext, + build: ServerBuild, + platform: ServerPlatform, + matches: RouteMatch[] | null +): Promise { + let url = new URL(request.url); + + if (!matches) { + return jsonError(`No route matches URL "${url.pathname}"`, 404); + } + + let routeMatch: RouteMatch = matches.slice(-1)[0]; + try { + return isActionRequest(request) + ? await callRouteAction( + build, + routeMatch.route.id, + request, + loadContext, + routeMatch.params + ) + : await loadRouteData( + build, + routeMatch.route.id, + request, + loadContext, + routeMatch.params + ); + } catch (error: any) { + let formattedError = (await platform.formatServerError?.(error)) || error; + throw formattedError; + } +} + async function handleDataRequest( request: Request, loadContext: AppLoadContext, build: ServerBuild, platform: ServerPlatform, - routes: ServerRoute[] + matches: RouteMatch[] | null ): Promise { if (!isValidRequestMethod(request)) { return jsonError(`Invalid request method "${request.method}"`, 405); @@ -74,7 +162,6 @@ async function handleDataRequest( let url = new URL(request.url); - let matches = matchServerRoutes(routes, url.pathname); if (!matches) { return jsonError(`No route matches URL "${url.pathname}"`, 404); } From 076ad05c015b9fbcc6eb00065cdbca7ce4bdb67f Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 10 Nov 2021 13:21:58 -0800 Subject: [PATCH 0120/1690] feat: utilize react-router v6 stable (#363) chore: update history chore: moved history to peer dep for react and server pkgs chore: remove genaric from Location usages --- packages/remix-server-runtime/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 5ea40712d1..1d9e49d5dd 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -6,15 +6,15 @@ "dependencies": { "@types/cookie": "^0.4.0", "cookie": "^0.4.1", - "history": "^5.0.0", "jsesc": "^3.0.1", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3" }, "peerDependencies": { + "history": "^5.1.0", "react": ">=16.8", "react-dom": ">=16.8", - "react-router-dom": "6.0.0-beta.8" + "react-router-dom": "^6.0.2" }, "devDependencies": { "@types/jsesc": "^2.5.1", From 6971c66648ef89a50964f3d1c6b6d07e00809d53 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 16 Nov 2021 15:25:09 -0800 Subject: [PATCH 0121/1690] Version 0.21.0 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index a0e96c07d7..069f29a4c9 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.20.1", + "version": "0.21.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 9f4801ad5e..d8eb189e13 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.20.1", + "version": "0.21.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.20.1" + "@remix-run/node": "0.21.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index ad4a62c6f5..03701245b9 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.20.1", + "version": "0.21.0", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.20.1", + "@remix-run/server-runtime": "0.21.0", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 54db22af68..0bc3379b83 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.20.1", + "version": "0.21.0", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.20.1", + "@remix-run/express": "0.21.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 1d9e49d5dd..081ab680f4 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.20.1", + "version": "0.21.0", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 455668a3267664fc2f47d139bcdbe6df8a2c6e29 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 17 Nov 2021 23:21:27 -0800 Subject: [PATCH 0122/1690] feat: add escape convention `[]` for routes (#392) Co-authored-by: Kent C. Dodds --- .../__tests__/routesConvention-test.ts | 20 +++- packages/remix-dev/config/routesConvention.ts | 100 ++++++++++++++---- 2 files changed, 99 insertions(+), 21 deletions(-) diff --git a/packages/remix-dev/__tests__/routesConvention-test.ts b/packages/remix-dev/__tests__/routesConvention-test.ts index 11bebad147..bf29137997 100644 --- a/packages/remix-dev/__tests__/routesConvention-test.ts +++ b/packages/remix-dev/__tests__/routesConvention-test.ts @@ -3,6 +3,12 @@ import { createRoutePath } from "../config/routesConvention"; describe("createRoutePath", () => { describe("creates proper route paths", () => { let tests = [ + ["routes/$", "routes/*"], + ["routes/sub/$", "routes/sub/*"], + ["routes.sub/$", "routes/sub/*"], + ["routes/$slug", "routes/:slug"], + ["routes/sub/$slug", "routes/sub/:slug"], + ["routes.sub/$slug", "routes/sub/:slug"], ["$", "*"], ["nested/$", "nested/*"], ["flat.$", "flat/*"], @@ -17,7 +23,19 @@ describe("createRoutePath", () => { ["__layout/test", "test"], ["__layout.test", "test"], ["__layout/$slug", ":slug"], - ["nested/__layout/$slug", "nested/:slug"] + ["nested/__layout/$slug", "nested/:slug"], + ["$slug[.]json", ":slug.json"], + ["sub/[sitemap.xml]", "sub/sitemap.xml"], + ["posts/$slug/[image.jpg]", "posts/:slug/image.jpg"], + ["$[$dollabills].[.]lol[/]what/[$].$", ":$dollabills/.lol/what/$/*"], + ["sub.[[]", "sub/["], + ["sub.]", "sub/]"], + ["sub.[[]]", "sub/[]"], + ["sub.[[]", "sub/["], + ["beef]", "beef]"], + ["[index]", "index"], + ["test/inde[x]", "test/index"], + ["[i]ndex/[[].[[]]", "index/[/[]"] ]; for (let [input, expected] of tests) { diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 2f9975bb13..9dc873a725 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -98,28 +98,88 @@ export function defineConventionalRoutes(appDir: string): RouteManifest { return defineRoutes(defineNestedRoutes); } +let escapeStart = "["; +let escapeEnd = "]"; + // TODO: Cleanup and write some tests for this function export function createRoutePath(partialRouteId: string): string | undefined { - let path = normalizeSlashes(partialRouteId) - // routes/$ -> routes/* - // routes/nested/$.tsx (with a "routes/nested.tsx" layout) - .replace(/^\$$/, "*") - // routes/docs.$ -> routes/docs/* - // routes/docs/$ -> routes/docs/* - .replace(/(\/|\.)\$$/, "/*") - // routes/$user -> routes/:user - .replace(/\$/g, ":") - // routes/not.nested -> routes/not/nested - .replace(/\./g, "/"); - path = /\b\/?index$/.test(path) ? path.replace(/\/?index$/, "") : path; - - // remove "__" layout segments - path = path - .split("/") - .filter(segment => !segment.startsWith("__")) - .join("/"); - - return path ? path : undefined; + let result = ""; + let rawSegmentBuffer = ""; + + let inEscapeSequence = 0; + let skipSegment = false; + for (let i = 0; i < partialRouteId.length; i++) { + let char = partialRouteId.charAt(i); + let lastChar = i > 0 ? partialRouteId.charAt(i - 1) : undefined; + let nextChar = + i < partialRouteId.length - 1 ? partialRouteId.charAt(i + 1) : undefined; + + function isNewEscapeSequence() { + return ( + !inEscapeSequence && char === escapeStart && lastChar !== escapeStart + ); + } + + function isCloseEscapeSequence() { + return inEscapeSequence && char === escapeEnd && nextChar !== escapeEnd; + } + + function isStartOfLayoutSegment() { + return char === "_" && nextChar === "_" && !rawSegmentBuffer; + } + + if (skipSegment) { + if (char === "/" || char === "." || char === path.win32.sep) { + skipSegment = false; + } + continue; + } + + if (isNewEscapeSequence()) { + inEscapeSequence++; + continue; + } + + if (isCloseEscapeSequence()) { + inEscapeSequence--; + continue; + } + + if (inEscapeSequence) { + result += char; + continue; + } + + if (char === "/" || char === path.win32.sep || char === ".") { + if (rawSegmentBuffer === "index" && result.endsWith("index")) { + result = result.replace(/\/?index$/, ""); + } else { + result += "/"; + } + rawSegmentBuffer = ""; + continue; + } + + if (isStartOfLayoutSegment()) { + skipSegment = true; + continue; + } + + rawSegmentBuffer += char; + + if (char === "$") { + result += typeof nextChar === "undefined" ? "*" : ":"; + continue; + } + + result += char; + } + + if (rawSegmentBuffer === "index" && result.endsWith("index")) { + result = result.replace(/\/?index$/, ""); + } + + return result || undefined; } function findParentRouteId( From a152d2053bcab91c5b01fc00d8f4902d9a5df1c9 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 18 Nov 2021 11:34:17 -0800 Subject: [PATCH 0123/1690] chore: template cleanup (#397) chore: removed webpack.config.js from cf-workers template chore: quite morgan and only log remix routes, not static files --- packages/remix-serve/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-serve/index.ts b/packages/remix-serve/index.ts index c25c1348f0..9dbbcae7dc 100644 --- a/packages/remix-serve/index.ts +++ b/packages/remix-serve/index.ts @@ -7,9 +7,9 @@ export function createApp(buildPath: string, mode = "production") { let app = express(); app.use(compression()); - app.use(morgan("tiny")); app.use(express.static("public", { immutable: true, maxAge: "1y" })); + app.use(morgan("tiny")); app.all( "*", mode === "production" From de1a3bf4a9518b1e13ea3061a5e14278b664a1c9 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 19 Nov 2021 10:23:44 -0800 Subject: [PATCH 0124/1690] chore: update esbuild to latest (#402) --- packages/remix-dev/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 069f29a4c9..29af8ef719 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -9,7 +9,7 @@ "dependencies": { "cacache": "^15.0.5", "chokidar": "^3.5.1", - "esbuild": "0.11.16", + "esbuild": "0.13.14", "fs-extra": "^10.0.0", "lodash.debounce": "^4.0.8", "meow": "^7.1.1", From 9c1e1d07a9e81ae7343d2f2e8803ede75572dd01 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 19 Nov 2021 11:57:31 -0800 Subject: [PATCH 0125/1690] refactor(types): Rename types for consistency (#378) BREAKING CHANGE: Consumers will need to update their type imports. This renames `MetaDescriptor` to `HtmlMetaDescriptor` for better consistency with `HtmlLinkDescriptor`. `remix-server-runtime` also exposed a type `HTMLLinkDescriptor` with inconsistent casing, so I changed that as well. --- packages/remix-server-runtime/index.ts | 5 +++-- packages/remix-server-runtime/links.ts | 6 +++--- packages/remix-server-runtime/magicExports/server.ts | 3 ++- packages/remix-server-runtime/routeModules.ts | 8 +++++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/remix-server-runtime/index.ts b/packages/remix-server-runtime/index.ts index 17dc3c98eb..fd51daea14 100644 --- a/packages/remix-server-runtime/index.ts +++ b/packages/remix-server-runtime/index.ts @@ -20,7 +20,7 @@ export type { EntryContext } from "./entry"; export type { LinkDescriptor, - HTMLLinkDescriptor, + HtmlLinkDescriptor, PageLinkDescriptor } from "./links"; @@ -31,10 +31,11 @@ export type { DataFunctionArgs, ErrorBoundaryComponent, HeadersFunction, + HtmlMetaDescriptor, LinksFunction, LoaderFunction, - MetaFunction, MetaDescriptor, + MetaFunction, RouteComponent, RouteHandle } from "./routeModules"; diff --git a/packages/remix-server-runtime/links.ts b/packages/remix-server-runtime/links.ts index c085714985..e59854a47c 100644 --- a/packages/remix-server-runtime/links.ts +++ b/packages/remix-server-runtime/links.ts @@ -3,7 +3,7 @@ * * WHATWG Specification: https://html.spec.whatwg.org/multipage/semantics.html#the-link-element */ -export interface HTMLLinkDescriptor { +export interface HtmlLinkDescriptor { /** * Address of the hyperlink */ @@ -127,7 +127,7 @@ export interface HTMLLinkDescriptor { export interface PageLinkDescriptor extends Omit< - HTMLLinkDescriptor, + HtmlLinkDescriptor, | "href" | "rel" | "type" @@ -144,4 +144,4 @@ export interface PageLinkDescriptor page: string; } -export type LinkDescriptor = HTMLLinkDescriptor | PageLinkDescriptor; +export type LinkDescriptor = HtmlLinkDescriptor | PageLinkDescriptor; diff --git a/packages/remix-server-runtime/magicExports/server.ts b/packages/remix-server-runtime/magicExports/server.ts index ea06efafda..101036e58b 100644 --- a/packages/remix-server-runtime/magicExports/server.ts +++ b/packages/remix-server-runtime/magicExports/server.ts @@ -13,7 +13,7 @@ export type { AppData, EntryContext, LinkDescriptor, - HTMLLinkDescriptor, + HtmlLinkDescriptor, PageLinkDescriptor, ErrorBoundaryComponent, ActionFunction, @@ -21,6 +21,7 @@ export type { LinksFunction, LoaderFunction, MetaDescriptor, + HtmlMetaDescriptor, MetaFunction, RouteComponent, RouteHandle, diff --git a/packages/remix-server-runtime/routeModules.ts b/packages/remix-server-runtime/routeModules.ts index 847261ca22..0193058e06 100644 --- a/packages/remix-server-runtime/routeModules.ts +++ b/packages/remix-server-runtime/routeModules.ts @@ -82,7 +82,7 @@ export interface MetaFunction { parentsData: RouteData; params: Params; location: Location; - }): MetaDescriptor; + }): HtmlMetaDescriptor; } /** @@ -91,10 +91,12 @@ export interface MetaFunction { * tag, or an array of strings that will render multiple tags with the same * `name` attribute. */ -export interface MetaDescriptor { +export interface HtmlMetaDescriptor { [name: string]: string | string[]; } +export type MetaDescriptor = HtmlMetaDescriptor; + /** * A React component that is rendered for a route. */ @@ -111,7 +113,7 @@ export interface EntryRouteModule { default: RouteComponent; handle?: RouteHandle; links?: LinksFunction; - meta?: MetaFunction | MetaDescriptor; + meta?: MetaFunction | HtmlMetaDescriptor; } export interface ServerRouteModule extends EntryRouteModule { From 14f000ced41a043b4a3aeb74004c833650cf1d8e Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 19 Nov 2021 13:07:56 -0800 Subject: [PATCH 0126/1690] feat: handle CLI shutdown more gracefully (#400) --- packages/remix-dev/cli/commands.ts | 73 ++++++++++++++++++------------ packages/remix-dev/compiler.ts | 58 ++++++++++++------------ 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index b0e59ec0eb..7fd97dbeaa 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -3,6 +3,7 @@ import * as fse from "fs-extra"; import signalExit from "signal-exit"; import prettyMs from "pretty-ms"; import WebSocket from "ws"; +import type { createApp as createAppType } from "@remix-run/serve"; import { BuildMode, isBuildMode } from "../build"; import * as compiler from "../compiler"; @@ -91,53 +92,65 @@ export async function watch( broadcast({ type: "LOG", message }); } - signalExit( - await compiler.watch(config, { - mode, - onRebuildStart() { - start = Date.now(); - onRebuildStart && onRebuildStart(); - log("Rebuilding..."); - }, - onRebuildFinish() { - log(`Rebuilt in ${prettyMs(Date.now() - start)}`); - broadcast({ type: "RELOAD" }); - }, - onFileCreated(file) { - log(`File created: ${path.relative(process.cwd(), file)}`); - }, - onFileChanged(file) { - log(`File changed: ${path.relative(process.cwd(), file)}`); - }, - onFileDeleted(file) { - log(`File deleted: ${path.relative(process.cwd(), file)}`); - } - }) - ); + let closeWatcher = await compiler.watch(config, { + mode, + onRebuildStart() { + start = Date.now(); + onRebuildStart && onRebuildStart(); + log("Rebuilding..."); + }, + onRebuildFinish() { + log(`Rebuilt in ${prettyMs(Date.now() - start)}`); + broadcast({ type: "RELOAD" }); + }, + onFileCreated(file) { + log(`File created: ${path.relative(process.cwd(), file)}`); + }, + onFileChanged(file) { + log(`File changed: ${path.relative(process.cwd(), file)}`); + }, + onFileDeleted(file) { + log(`File deleted: ${path.relative(process.cwd(), file)}`); + } + }); + console.log(`💿 Built in ${prettyMs(Date.now() - start)}`); + + let resolve: () => void; signalExit(() => { + resolve(); + }); + return new Promise(r => { + resolve = r; + }).then(async () => { + wss.close(); + await closeWatcher(); fse.emptyDirSync(config.assetsBuildDirectory); fse.emptyDirSync(config.serverBuildDirectory); }); - - console.log(`💿 Built in ${prettyMs(Date.now() - start)}`); } export async function dev(remixRoot: string, modeArg?: string) { // TODO: Warn about the need to install @remix-run/serve if it isn't there? - let { createApp } = require("@remix-run/serve"); + let { createApp } = require("@remix-run/serve") as { + createApp: typeof createAppType; + }; let config = await readConfig(remixRoot); let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Development; let port = process.env.PORT || 3000; - createApp(config.serverBuildDirectory, mode).listen(port, () => { + let app = createApp(config.serverBuildDirectory, mode).listen(port, () => { console.log(`Remix App Server started at http://localhost:${port}`); }); - watch(config, mode, () => { - purgeAppRequireCache(config.serverBuildDirectory); - }); + try { + await watch(config, mode, () => { + purgeAppRequireCache(config.serverBuildDirectory); + }); + } finally { + app.close(); + } } function purgeAppRequireCache(buildPath: string) { diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index a25d6216cc..94405abda7 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -100,7 +100,7 @@ export async function watch( onFileChanged, onFileDeleted }: WatchOptions = {} -): Promise<() => void> { +): Promise<() => Promise> { let options = { mode, target, @@ -111,15 +111,15 @@ export async function watch( }; let [browserBuild, serverBuild] = await buildEverything(config, options); - async function disposeBuilders() { - await Promise.all([ - browserBuild?.rebuild?.dispose(), - serverBuild?.rebuild?.dispose() - ]); + function disposeBuilders() { + browserBuild?.rebuild?.dispose(); + serverBuild?.rebuild?.dispose(); + browserBuild = undefined; + serverBuild = undefined; } let restartBuilders = debounce(async (newConfig?: RemixConfig) => { - await disposeBuilders(); + disposeBuilders(); try { newConfig = await readConfig(config.rootDirectory); } catch (error) { @@ -138,8 +138,8 @@ export async function watch( let rebuildEverything = debounce(async () => { if (onRebuildStart) onRebuildStart(); - if (!browserBuild || !serverBuild) { - await disposeBuilders(); + if (!browserBuild?.rebuild || !serverBuild?.rebuild) { + disposeBuilders(); try { [browserBuild, serverBuild] = await buildEverything(config, options); @@ -153,11 +153,12 @@ export async function watch( await Promise.all([ // If we get here and can't call rebuild something went wrong and we // should probably blow as it's not really recoverable. - browserBuild.rebuild!().then(build => - generateManifests(config, build.metafile!) - ), - serverBuild.rebuild!() + browserBuild + .rebuild() + .then(build => generateManifests(config, build.metafile!)), + serverBuild.rebuild() ]).catch(err => { + disposeBuilders(); onBuildFailure(err); }); if (onRebuildFinish) onRebuildFinish(); @@ -203,8 +204,8 @@ export async function watch( }); return async () => { - await watcher.close(); - await disposeBuilders(); + await watcher.close().catch(() => {}); + disposeBuilders(); }; } @@ -238,19 +239,21 @@ async function buildEverything( // builds serially so we can inline the asset manifest into the server build // in a single JavaScript file. - let browserBuildPromise = createBrowserBuild(config, options); - let serverBuildPromise = createServerBuild(config, options); + try { + let browserBuildPromise = createBrowserBuild(config, options); + let serverBuildPromise = createServerBuild(config, options); - return Promise.all([ - browserBuildPromise.then(async build => { - await generateManifests(config, build.metafile!); - return build; - }), - serverBuildPromise - ]).catch(err => { - options.onBuildFailure(err); + return await Promise.all([ + browserBuildPromise.then(async build => { + await generateManifests(config, build.metafile!); + return build; + }), + serverBuildPromise + ]); + } catch (err) { + options.onBuildFailure(err as Error); return [undefined, undefined]; - }); + } } async function createBrowserBuild( @@ -267,12 +270,11 @@ async function createBrowserBuild( let fakeBuiltins = nodeBuiltins.filter(mod => dependencies.includes(mod)); if (fakeBuiltins.length > 0) { - console.error( + throw new Error( `It appears you're using a module that is built in to node, but you installed it as a dependency which could cause problems. Please remove ${fakeBuiltins.join( ", " )} before continuing.` ); - process.exit(1); } let entryPoints: esbuild.BuildOptions["entryPoints"] = { From 8cd9a7f9a5e774ba5ab149d22380ea0c258ad864 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 19 Nov 2021 13:35:45 -0800 Subject: [PATCH 0127/1690] Version 1.0.0-rc.1 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 29af8ef719..4d191537b1 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.21.0", + "version": "1.0.0-rc.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index d8eb189e13..908138902e 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.21.0", + "version": "1.0.0-rc.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "0.21.0" + "@remix-run/node": "1.0.0-rc.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 03701245b9..19fe9a76ea 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.21.0", + "version": "1.0.0-rc.1", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "0.21.0", + "@remix-run/server-runtime": "1.0.0-rc.1", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 0bc3379b83..15f1d2fea7 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.21.0", + "version": "1.0.0-rc.1", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.21.0", + "@remix-run/express": "1.0.0-rc.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 081ab680f4..792ed45ff9 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.21.0", + "version": "1.0.0-rc.1", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 6189cef46ca2938a55b1595ae794cb09c7da058a Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 19 Nov 2021 15:39:28 -0800 Subject: [PATCH 0128/1690] chore: re-export server handler types (#412) --- packages/remix-server-runtime/magicExports/server.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/remix-server-runtime/magicExports/server.ts b/packages/remix-server-runtime/magicExports/server.ts index 101036e58b..37d570c18e 100644 --- a/packages/remix-server-runtime/magicExports/server.ts +++ b/packages/remix-server-runtime/magicExports/server.ts @@ -4,6 +4,8 @@ export type { ServerBuild, ServerEntryModule, + HandleDataRequestFunction, + HandleDocumentRequestFunction, CookieParseOptions, CookieSerializeOptions, CookieSignatureOptions, From d4d2a5229b2c574bb5e44e984e3719394cba160c Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 19 Nov 2021 15:42:36 -0800 Subject: [PATCH 0129/1690] fix: await handlers otherwise we never hit the catch in time to handle error boundaries (#413) --- packages/remix-server-runtime/server.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index dade7302eb..b8b03be166 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -518,9 +518,9 @@ async function handleDocumentRequest( serverHandoffString: createServerHandoffString(serverHandoff) }; - let response: Response | Promise; + let response: Response; try { - response = serverEntryModule.default( + response = await serverEntryModule.default( request, statusCode, headers, @@ -545,7 +545,7 @@ async function handleDocumentRequest( entryContext.serverHandoffString = createServerHandoffString(serverHandoff); try { - response = serverEntryModule.default( + response = await serverEntryModule.default( request, statusCode, headers, From efb7838dfb97ce746c5a482944cd206572d9664b Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 19 Nov 2021 16:02:03 -0800 Subject: [PATCH 0130/1690] Version 1.0.0-rc.2 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 4d191537b1..f337a9f84d 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.0.0-rc.1", + "version": "1.0.0-rc.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 908138902e..b0a72e87f8 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.0.0-rc.1", + "version": "1.0.0-rc.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "1.0.0-rc.1" + "@remix-run/node": "1.0.0-rc.2" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 19fe9a76ea..71cd1acb97 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.0.0-rc.1", + "version": "1.0.0-rc.2", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "1.0.0-rc.1", + "@remix-run/server-runtime": "1.0.0-rc.2", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 15f1d2fea7..6ae5c4a087 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.0.0-rc.1", + "version": "1.0.0-rc.2", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.0.0-rc.1", + "@remix-run/express": "1.0.0-rc.2", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 792ed45ff9..1c25f9cf0d 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.0.0-rc.1", + "version": "1.0.0-rc.2", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 7c300ff2642cb25f02101038179e48be598c9caf Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Sat, 20 Nov 2021 17:27:26 -0800 Subject: [PATCH 0131/1690] fix: wait for initial build to start server (#417) fix: show a nicer message if @remix-run/serve is missing in dev command --- packages/remix-dev/cli/commands.ts | 40 ++++++++++++++++++++++-------- packages/remix-dev/compiler.ts | 16 +++++++++++- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 7fd97dbeaa..d008f5fb9f 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -3,6 +3,7 @@ import * as fse from "fs-extra"; import signalExit from "signal-exit"; import prettyMs from "pretty-ms"; import WebSocket from "ws"; +import type { Server } from "http"; import type { createApp as createAppType } from "@remix-run/serve"; import { BuildMode, isBuildMode } from "../build"; @@ -61,11 +62,17 @@ export async function build( console.log(`Built in ${prettyMs(Date.now() - start)}`); } +type WatchCallbacks = { + onRebuildStart?(): void; + onInitialBuild?(): void; +}; + export async function watch( remixRootOrConfig: string | RemixConfig, modeArg?: string, - onRebuildStart?: () => void + callbacks?: WatchCallbacks ): Promise { + let { onInitialBuild, onRebuildStart } = callbacks || {}; let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Development; console.log(`Watching Remix app in ${mode} mode...`); @@ -94,6 +101,7 @@ export async function watch( let closeWatcher = await compiler.watch(config, { mode, + onInitialBuild, onRebuildStart() { start = Date.now(); onRebuildStart && onRebuildStart(); @@ -132,24 +140,36 @@ export async function watch( export async function dev(remixRoot: string, modeArg?: string) { // TODO: Warn about the need to install @remix-run/serve if it isn't there? - let { createApp } = require("@remix-run/serve") as { - createApp: typeof createAppType; - }; + let createApp: typeof createAppType; + try { + let serve = require("@remix-run/serve"); + createApp = serve.createApp; + } catch (err) { + throw new Error( + "Could not locate @remix-run/serve. Please verify you have it installed to use the dev command." + ); + } let config = await readConfig(remixRoot); let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Development; let port = process.env.PORT || 3000; - let app = createApp(config.serverBuildDirectory, mode).listen(port, () => { - console.log(`Remix App Server started at http://localhost:${port}`); - }); + let app = createApp(config.serverBuildDirectory, mode); + let server: Server | null = null; try { - await watch(config, mode, () => { - purgeAppRequireCache(config.serverBuildDirectory); + await watch(config, mode, { + onRebuildStart: () => { + purgeAppRequireCache(config.serverBuildDirectory); + }, + onInitialBuild: () => { + server = app.listen(port, () => { + console.log(`Remix App Server started at http://localhost:${port}`); + }); + } }); } finally { - app.close(); + server!?.close(); } } diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 94405abda7..fede87c86b 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -84,6 +84,7 @@ interface WatchOptions extends BuildOptions { onFileCreated?(file: string): void; onFileChanged?(file: string): void; onFileDeleted?(file: string): void; + onInitialBuild?(): void; } export async function watch( @@ -98,7 +99,8 @@ export async function watch( onRebuildFinish, onFileCreated, onFileChanged, - onFileDeleted + onFileDeleted, + onInitialBuild }: WatchOptions = {} ): Promise<() => Promise> { let options = { @@ -111,6 +113,11 @@ export async function watch( }; let [browserBuild, serverBuild] = await buildEverything(config, options); + let initialBuildComplete = !!browserBuild && !!serverBuild; + if (initialBuildComplete) { + onInitialBuild?.(); + } + function disposeBuilders() { browserBuild?.rebuild?.dispose(); serverBuild?.rebuild?.dispose(); @@ -143,6 +150,13 @@ export async function watch( try { [browserBuild, serverBuild] = await buildEverything(config, options); + + if (!initialBuildComplete) { + initialBuildComplete = !!browserBuild && !!serverBuild; + if (initialBuildComplete) { + onInitialBuild?.(); + } + } if (onRebuildFinish) onRebuildFinish(); } catch (err: any) { onBuildFailure(err); From 9a758664ca815817b299a3a84ea2f721617ab2a5 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 22 Nov 2021 07:32:56 -0800 Subject: [PATCH 0132/1690] chore: proofreading comments --- packages/remix-dev/invariant.ts | 2 +- packages/remix-server-runtime/cookies.ts | 2 -- packages/remix-server-runtime/sessions.ts | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/remix-dev/invariant.ts b/packages/remix-dev/invariant.ts index d75cc80110..690ae3ffe4 100644 --- a/packages/remix-dev/invariant.ts +++ b/packages/remix-dev/invariant.ts @@ -11,7 +11,7 @@ export default function invariant( export default function invariant(value: any, message?: string) { if (value === false || value === null || typeof value === "undefined") { console.error( - "The following error is a bug in Remix, please file an issue! https://remix.run/dashboard/support" + "The following error is a bug in Remix; please open an issue! https://github.com/remix-run/remix/issues/new" ); throw new Error(message); } diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index a18785c702..a2e8c05406 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -40,8 +40,6 @@ export interface Cookie { /** * True if this cookie uses one or more secrets for verification. - * - * See https://remix.run/dashboard/docs/cookies#signing-cookies */ readonly isSigned: boolean; diff --git a/packages/remix-server-runtime/sessions.ts b/packages/remix-server-runtime/sessions.ts index b03596ab87..4ac16b5ee4 100644 --- a/packages/remix-server-runtime/sessions.ts +++ b/packages/remix-server-runtime/sessions.ts @@ -244,7 +244,7 @@ export function warnOnceAboutSigningSessionCookie(cookie: Cookie) { cookie.isSigned, `The "${cookie.name}" cookie is not signed, but session cookies should be ` + `signed to prevent tampering on the client before they are sent back to the ` + - `server. See https://remix.run/dashboard/docs/cookies#signing-cookies ` + - `for more information.` + `server.` /* TODO: Update link with new docs. See https://remix.run/cookies#signing-cookies ` + + `for more information.` */ ); } From 9fbe1c8f6ff8be332db06d5c7633a8492310501c Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 22 Nov 2021 09:15:47 -0800 Subject: [PATCH 0133/1690] fix: force production env when serve CLI is ran (#426) --- packages/remix-serve/cli.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/remix-serve/cli.ts b/packages/remix-serve/cli.ts index 563af9a282..de1e622f27 100644 --- a/packages/remix-serve/cli.ts +++ b/packages/remix-serve/cli.ts @@ -2,6 +2,7 @@ import path from "path"; import { createApp } from "./index"; +process.env.NODE_ENV = "production"; let port = process.env.PORT || 3000; let buildPathArg = process.argv[2]; From 7dd906ce188b0dfc8ff46805522163bdc38f7056 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 22 Nov 2021 09:16:24 -0800 Subject: [PATCH 0134/1690] fix: reload require cache on every request in dev (#431) --- packages/remix-dev/cli/commands.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index d008f5fb9f..5521ed568b 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -4,6 +4,7 @@ import signalExit from "signal-exit"; import prettyMs from "pretty-ms"; import WebSocket from "ws"; import type { Server } from "http"; +import type * as Express from "express"; import type { createApp as createAppType } from "@remix-run/serve"; import { BuildMode, isBuildMode } from "../build"; @@ -141,9 +142,11 @@ export async function watch( export async function dev(remixRoot: string, modeArg?: string) { // TODO: Warn about the need to install @remix-run/serve if it isn't there? let createApp: typeof createAppType; + let express: typeof Express; try { let serve = require("@remix-run/serve"); createApp = serve.createApp; + express = require("express"); } catch (err) { throw new Error( "Could not locate @remix-run/serve. Please verify you have it installed to use the dev command." @@ -154,14 +157,17 @@ export async function dev(remixRoot: string, modeArg?: string) { let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Development; let port = process.env.PORT || 3000; - let app = createApp(config.serverBuildDirectory, mode); + let app = express(); + app.use((_, __, next) => { + purgeAppRequireCache(config.serverBuildDirectory); + next(); + }); + app.use(createApp(config.serverBuildDirectory, mode)); + let server: Server | null = null; try { await watch(config, mode, { - onRebuildStart: () => { - purgeAppRequireCache(config.serverBuildDirectory); - }, onInitialBuild: () => { server = app.listen(port, () => { console.log(`Remix App Server started at http://localhost:${port}`); From 672bb4e154c2a9c30ad09d27dba2033973fec8f3 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 22 Nov 2021 09:17:15 -0800 Subject: [PATCH 0135/1690] feat: re-export react-router-dom utilities (#424) chore: remove explicit history dep --- packages/remix-server-runtime/package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 1c25f9cf0d..944270c64c 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -7,14 +7,13 @@ "@types/cookie": "^0.4.0", "cookie": "^0.4.1", "jsesc": "^3.0.1", + "react-router-dom": "^6.0.2", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3" }, "peerDependencies": { - "history": "^5.1.0", "react": ">=16.8", - "react-dom": ">=16.8", - "react-router-dom": "^6.0.2" + "react-dom": ">=16.8" }, "devDependencies": { "@types/jsesc": "^2.5.1", From 62e0c03cfd7ca8747764b4605a314ec70c9632f3 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 22 Nov 2021 10:08:17 -0800 Subject: [PATCH 0136/1690] Version 1.0.0-rc.3 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index f337a9f84d..3197e9fb4b 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.0.0-rc.2", + "version": "1.0.0-rc.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index b0a72e87f8..da5686e2e6 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.0.0-rc.2", + "version": "1.0.0-rc.3", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "1.0.0-rc.2" + "@remix-run/node": "1.0.0-rc.3" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 71cd1acb97..afde1c5548 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.0.0-rc.2", + "version": "1.0.0-rc.3", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "1.0.0-rc.2", + "@remix-run/server-runtime": "1.0.0-rc.3", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 6ae5c4a087..ee79d8f27e 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.0.0-rc.2", + "version": "1.0.0-rc.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.0.0-rc.2", + "@remix-run/express": "1.0.0-rc.3", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 944270c64c..ac1cac45a7 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.0.0-rc.2", + "version": "1.0.0-rc.3", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From b71a03d7283af595a785446d39436beeec20a132 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 22 Nov 2021 10:53:40 -0800 Subject: [PATCH 0137/1690] Version 1.0.0-rc.4 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 3197e9fb4b..6d324f873e 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.0.0-rc.3", + "version": "1.0.0-rc.4", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index da5686e2e6..af61ffb2e9 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.0.0-rc.3", + "version": "1.0.0-rc.4", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "1.0.0-rc.3" + "@remix-run/node": "1.0.0-rc.4" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index afde1c5548..1a6f0ea071 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.0.0-rc.3", + "version": "1.0.0-rc.4", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "1.0.0-rc.3", + "@remix-run/server-runtime": "1.0.0-rc.4", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index ee79d8f27e..ae5c185871 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.0.0-rc.3", + "version": "1.0.0-rc.4", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.0.0-rc.3", + "@remix-run/express": "1.0.0-rc.4", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index ac1cac45a7..fae73c535e 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.0.0-rc.3", + "version": "1.0.0-rc.4", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From f685bf3ec8937b8bdc6ba762e13749665a6cf4b9 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 22 Nov 2021 12:41:12 -0800 Subject: [PATCH 0138/1690] Version 1.0.3 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 6d324f873e..19258e577c 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.0.0-rc.4", + "version": "1.0.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index af61ffb2e9..68559f0aa2 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.0.0-rc.4", + "version": "1.0.3", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "1.0.0-rc.4" + "@remix-run/node": "1.0.3" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 1a6f0ea071..0494ffccd9 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.0.0-rc.4", + "version": "1.0.3", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "1.0.0-rc.4", + "@remix-run/server-runtime": "1.0.3", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index ae5c185871..f15882367a 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.0.0-rc.4", + "version": "1.0.3", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.0.0-rc.4", + "@remix-run/express": "1.0.3", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index fae73c535e..91c3ecdfac 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.0.0-rc.4", + "version": "1.0.3", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 1f47a5ed41e7bfa8cda27354c621f47317a9c1b6 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 22 Nov 2021 19:24:50 -0800 Subject: [PATCH 0139/1690] Version 1.0.4 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 19258e577c..21a2cda9cd 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.0.3", + "version": "1.0.4", "repository": "https://github.com/remix-run/packages", "bin": { "remix": "cli.js" diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 68559f0aa2..90e2bc4fbb 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.0.3", + "version": "1.0.4", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/node": "1.0.3" + "@remix-run/node": "1.0.4" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 0494ffccd9..e2d1b87c9d 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,10 +1,10 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.0.3", + "version": "1.0.4", "repository": "https://github.com/remix-run/packages", "dependencies": { - "@remix-run/server-runtime": "1.0.3", + "@remix-run/server-runtime": "1.0.4", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index f15882367a..59eb8c706b 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,13 +1,13 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.0.3", + "version": "1.0.4", "repository": "https://github.com/remix-run/packages", "bin": { "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.0.3", + "@remix-run/express": "1.0.4", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 91c3ecdfac..2abaada7dc 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.0.3", + "version": "1.0.4", "repository": "https://github.com/remix-run/packages", "dependencies": { "@types/cookie": "^0.4.0", From 742e5242318ded274e1d6cc0e5b177e09ce1026f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Tue, 23 Nov 2021 05:32:12 +0100 Subject: [PATCH 0140/1690] chore: fix `repository` field in `package.json` (#423) Co-authored-by: Ryan Florence --- packages/remix-dev/package.json | 9 ++++++++- packages/remix-express/package.json | 9 ++++++++- packages/remix-node/package.json | 9 ++++++++- packages/remix-serve/package.json | 9 ++++++++- packages/remix-server-runtime/package.json | 9 ++++++++- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 21a2cda9cd..275fde5e0e 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -2,7 +2,14 @@ "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", "version": "1.0.4", - "repository": "https://github.com/remix-run/packages", + "repository": { + "type": "git", + "url": "https://github.com/remix-run/remix", + "directory": "packages/remix-dev" + }, + "bugs": { + "url": "https://github.com/remix-run/remix/issues" + }, "bin": { "remix": "cli.js" }, diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 90e2bc4fbb..e4bedd9434 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -2,7 +2,14 @@ "name": "@remix-run/express", "description": "Express server request handler for Remix", "version": "1.0.4", - "repository": "https://github.com/remix-run/packages", + "repository": { + "type": "git", + "url": "https://github.com/remix-run/remix", + "directory": "packages/remix-express" + }, + "bugs": { + "url": "https://github.com/remix-run/remix/issues" + }, "dependencies": { "@remix-run/node": "1.0.4" }, diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index e2d1b87c9d..914626435a 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -2,7 +2,14 @@ "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", "version": "1.0.4", - "repository": "https://github.com/remix-run/packages", + "repository": { + "type": "git", + "url": "https://github.com/remix-run/remix", + "directory": "packages/remix-node" + }, + "bugs": { + "url": "https://github.com/remix-run/remix/issues" + }, "dependencies": { "@remix-run/server-runtime": "1.0.4", "@types/node-fetch": "^2.5.12", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 59eb8c706b..54b0910f67 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -2,7 +2,14 @@ "name": "@remix-run/serve", "description": "Production application server for Remix", "version": "1.0.4", - "repository": "https://github.com/remix-run/packages", + "repository": { + "type": "git", + "url": "https://github.com/remix-run/remix", + "directory": "packages/remix-serve" + }, + "bugs": { + "url": "https://github.com/remix-run/remix/issues" + }, "bin": { "remix-serve": "cli.js" }, diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 2abaada7dc..b9e697ca6b 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -2,7 +2,14 @@ "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", "version": "1.0.4", - "repository": "https://github.com/remix-run/packages", + "repository": { + "type": "git", + "url": "https://github.com/remix-run/remix", + "directory": "packages/remix-server-runtime" + }, + "bugs": { + "url": "https://github.com/remix-run/remix/issues" + }, "dependencies": { "@types/cookie": "^0.4.0", "cookie": "^0.4.1", From 6277616d64f61929b2a36bfa49d0a437275abdde Mon Sep 17 00:00:00 2001 From: Matt Sutkowski Date: Mon, 22 Nov 2021 20:49:42 -0800 Subject: [PATCH 0141/1690] chore: Link to github issues for invariant error (#441) * Link to github issues for invariant error * Add self to contributors Co-authored-by: Ryan Florence --- packages/remix-server-runtime/invariant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-server-runtime/invariant.ts b/packages/remix-server-runtime/invariant.ts index 6f91537bf4..123cc25cb4 100644 --- a/packages/remix-server-runtime/invariant.ts +++ b/packages/remix-server-runtime/invariant.ts @@ -9,7 +9,7 @@ export default function invariant( export default function invariant(value: any, message?: string) { if (value === false || value === null || typeof value === "undefined") { console.error( - "The following error is a bug in Remix, please file an issue! https://remix.run/dashbaord/support" + "The following error is a bug in Remix; please open an issue! https://github.com/remix-run/remix/issues/new" ); throw new Error(message); } From b02b9c7b0229b7641f13d35ffc7a9b61248052dc Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 22 Nov 2021 21:26:18 -0800 Subject: [PATCH 0142/1690] Add READMEs for all @remix-run/* packages --- packages/remix-dev/README.md | 13 +++++++++++++ packages/remix-express/README.md | 13 +++++++++++++ packages/remix-node/README.md | 13 +++++++++++++ packages/remix-serve/README.md | 13 +++++++++++++ packages/remix-server-runtime/README.md | 13 +++++++++++++ 5 files changed, 65 insertions(+) create mode 100644 packages/remix-dev/README.md create mode 100644 packages/remix-express/README.md create mode 100644 packages/remix-node/README.md create mode 100644 packages/remix-serve/README.md create mode 100644 packages/remix-server-runtime/README.md diff --git a/packages/remix-dev/README.md b/packages/remix-dev/README.md new file mode 100644 index 0000000000..5c278710d2 --- /dev/null +++ b/packages/remix-dev/README.md @@ -0,0 +1,13 @@ +# Welcome to Remix! + +[Remix](https://remix.run) is a web framework that helps you build better websites with React. + +To get started, open a new shell and run: + +```sh +$ npx create-remix@latest +``` + +Then follow the prompts you see in your terminal. + +For more information about Remix, [visit remix.run](https://remix.run)! diff --git a/packages/remix-express/README.md b/packages/remix-express/README.md new file mode 100644 index 0000000000..5c278710d2 --- /dev/null +++ b/packages/remix-express/README.md @@ -0,0 +1,13 @@ +# Welcome to Remix! + +[Remix](https://remix.run) is a web framework that helps you build better websites with React. + +To get started, open a new shell and run: + +```sh +$ npx create-remix@latest +``` + +Then follow the prompts you see in your terminal. + +For more information about Remix, [visit remix.run](https://remix.run)! diff --git a/packages/remix-node/README.md b/packages/remix-node/README.md new file mode 100644 index 0000000000..5c278710d2 --- /dev/null +++ b/packages/remix-node/README.md @@ -0,0 +1,13 @@ +# Welcome to Remix! + +[Remix](https://remix.run) is a web framework that helps you build better websites with React. + +To get started, open a new shell and run: + +```sh +$ npx create-remix@latest +``` + +Then follow the prompts you see in your terminal. + +For more information about Remix, [visit remix.run](https://remix.run)! diff --git a/packages/remix-serve/README.md b/packages/remix-serve/README.md new file mode 100644 index 0000000000..5c278710d2 --- /dev/null +++ b/packages/remix-serve/README.md @@ -0,0 +1,13 @@ +# Welcome to Remix! + +[Remix](https://remix.run) is a web framework that helps you build better websites with React. + +To get started, open a new shell and run: + +```sh +$ npx create-remix@latest +``` + +Then follow the prompts you see in your terminal. + +For more information about Remix, [visit remix.run](https://remix.run)! diff --git a/packages/remix-server-runtime/README.md b/packages/remix-server-runtime/README.md new file mode 100644 index 0000000000..5c278710d2 --- /dev/null +++ b/packages/remix-server-runtime/README.md @@ -0,0 +1,13 @@ +# Welcome to Remix! + +[Remix](https://remix.run) is a web framework that helps you build better websites with React. + +To get started, open a new shell and run: + +```sh +$ npx create-remix@latest +``` + +Then follow the prompts you see in your terminal. + +For more information about Remix, [visit remix.run](https://remix.run)! From 27d339473c02ac18542e62eba1ff5bb3f82d2bcb Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Wed, 24 Nov 2021 13:15:16 -0500 Subject: [PATCH 0143/1690] Version 1.0.5 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 275fde5e0e..e3aef255de 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.0.4", + "version": "1.0.5", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index e4bedd9434..6e9b2bedfa 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.0.4", + "version": "1.0.5", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", @@ -11,7 +11,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.0.4" + "@remix-run/node": "1.0.5" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 914626435a..c5a6fa3b91 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.0.4", + "version": "1.0.5", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", @@ -11,7 +11,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.0.4", + "@remix-run/server-runtime": "1.0.5", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 54b0910f67..6dece725d2 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.0.4", + "version": "1.0.5", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", @@ -14,7 +14,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.0.4", + "@remix-run/express": "1.0.5", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index b9e697ca6b..04b24d5727 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.0.4", + "version": "1.0.5", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", From aeeea64322d9bcb278d3b27de855a502373d7735 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Fri, 26 Nov 2021 12:31:38 -0500 Subject: [PATCH 0144/1690] Version 1.0.6 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index e3aef255de..c1128cd1c6 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.0.5", + "version": "1.0.6", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 6e9b2bedfa..186e3a3afc 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.0.5", + "version": "1.0.6", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", @@ -11,7 +11,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.0.5" + "@remix-run/node": "1.0.6" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index c5a6fa3b91..7cd5ad8acc 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.0.5", + "version": "1.0.6", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", @@ -11,7 +11,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.0.5", + "@remix-run/server-runtime": "1.0.6", "@types/node-fetch": "^2.5.12", "cookie-signature": "^1.1.0", "node-fetch": "^2.6.1", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 6dece725d2..305a4aaf92 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.0.5", + "version": "1.0.6", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", @@ -14,7 +14,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.0.5", + "@remix-run/express": "1.0.6", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 04b24d5727..aaf620a314 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.0.5", + "version": "1.0.6", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", From 80cfdbaeed0a5022401fe5e3badd2d9ec85a705e Mon Sep 17 00:00:00 2001 From: Ahmed ElDessouki Date: Mon, 29 Nov 2021 19:32:33 +0300 Subject: [PATCH 0145/1690] docs: updating link with new docs (#575) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michaël De Boey --- packages/remix-server-runtime/sessions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix-server-runtime/sessions.ts b/packages/remix-server-runtime/sessions.ts index 4ac16b5ee4..21c25fb222 100644 --- a/packages/remix-server-runtime/sessions.ts +++ b/packages/remix-server-runtime/sessions.ts @@ -244,7 +244,7 @@ export function warnOnceAboutSigningSessionCookie(cookie: Cookie) { cookie.isSigned, `The "${cookie.name}" cookie is not signed, but session cookies should be ` + `signed to prevent tampering on the client before they are sent back to the ` + - `server.` /* TODO: Update link with new docs. See https://remix.run/cookies#signing-cookies ` + - `for more information.` */ + `server. See https://remix.run/docs/en/v1/api/remix#signing-cookies ` + + `for more information.` ); } From 0c82e2121a20f6c1ef0f8fabf2154ae75211eaa9 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 29 Nov 2021 09:19:44 -0800 Subject: [PATCH 0146/1690] fix: null for body to conform to standards (#743) fix: pass full request to handleDataRequest, not stripped --- packages/remix-server-runtime/responses.ts | 2 +- packages/remix-server-runtime/server.ts | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/remix-server-runtime/responses.ts b/packages/remix-server-runtime/responses.ts index ca20c40492..8f8ef0cddb 100644 --- a/packages/remix-server-runtime/responses.ts +++ b/packages/remix-server-runtime/responses.ts @@ -36,7 +36,7 @@ export function redirect( let headers = new Headers(responseInit.headers); headers.set("Location", url); - return new Response("", { + return new Response(null, { ...responseInit, headers }); diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index b8b03be166..385e1f57b9 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -193,22 +193,20 @@ async function handleDataRequest( routeMatch = match; } - let clonedRequest = stripIndexParam(stripDataParam(request)); - let response: Response; try { response = isActionRequest(request) ? await callRouteAction( build, routeMatch.route.id, - clonedRequest, + stripIndexParam(stripDataParam(request.clone())), loadContext, routeMatch.params ) : await loadRouteData( build, routeMatch.route.id, - clonedRequest, + stripIndexParam(stripDataParam(request.clone())), loadContext, routeMatch.params ); @@ -230,16 +228,15 @@ async function handleDataRequest( headers.set("X-Remix-Redirect", headers.get("Location")!); headers.delete("Location"); - return new Response("", { + return new Response(null, { status: 204, headers }); } if (build.entry.module.handleDataRequest) { - clonedRequest = stripIndexParam(stripDataParam(request)); return build.entry.module.handleDataRequest(response, { - request: clonedRequest, + request: request.clone(), context: loadContext, params: routeMatch.params }); @@ -319,12 +316,10 @@ async function handleDocumentRequest( actionRouteId = actionMatch.route.id; try { - let clonedRequest = stripIndexParam(stripDataParam(request)); - actionResponse = await callRouteAction( build, actionMatch.route.id, - clonedRequest, + stripIndexParam(stripDataParam(request.clone())), loadContext, actionMatch.params ); @@ -501,7 +496,7 @@ async function handleDocumentRequest( let actionData = actionResponse && actionRouteId ? { - [actionRouteId]: await createActionData(actionResponse) + [actionRouteId]: await createActionData(actionResponse.clone()) } : undefined; let routeModules = createEntryRouteModules(build.routes); From 4ef688e3b30e2dc13ed4e8d236410d80e0899b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Mon, 29 Nov 2021 21:25:42 +0100 Subject: [PATCH 0147/1690] chore: add MIT license (#432) Co-authored-by: Kent C. Dodds --- packages/remix-dev/package.json | 1 + packages/remix-express/package.json | 1 + packages/remix-node/package.json | 1 + packages/remix-serve/package.json | 1 + packages/remix-server-runtime/package.json | 1 + 5 files changed, 5 insertions(+) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index c1128cd1c6..37106e93dc 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -2,6 +2,7 @@ "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", "version": "1.0.6", + "license": "MIT", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 186e3a3afc..882a70ffa4 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -2,6 +2,7 @@ "name": "@remix-run/express", "description": "Express server request handler for Remix", "version": "1.0.6", + "license": "MIT", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 7cd5ad8acc..0eaed4b4df 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -2,6 +2,7 @@ "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", "version": "1.0.6", + "license": "MIT", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 305a4aaf92..757ec77a8d 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -2,6 +2,7 @@ "name": "@remix-run/serve", "description": "Production application server for Remix", "version": "1.0.6", + "license": "MIT", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index aaf620a314..0eb6827fc3 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -2,6 +2,7 @@ "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", "version": "1.0.6", + "license": "MIT", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", From b7b9f9fb48de956205f8190d4e6c131ef653f6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Tue, 30 Nov 2021 19:02:43 +0100 Subject: [PATCH 0148/1690] chore: fix ESLint errors & warnings (#611) --- packages/remix-dev/config/routesConvention.ts | 2 +- packages/remix-node/fetch.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 9dc873a725..09abce8a76 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -2,7 +2,7 @@ import * as fs from "fs"; import * as path from "path"; import type { RouteManifest, DefineRouteFunction } from "./routes"; -import { defineRoutes, createRouteId, normalizeSlashes } from "./routes"; +import { defineRoutes, createRouteId } from "./routes"; const routeModuleExts = [".js", ".jsx", ".ts", ".tsx", ".md", ".mdx"]; diff --git a/packages/remix-node/fetch.ts b/packages/remix-node/fetch.ts index aa83e3aa82..e247e7b85d 100644 --- a/packages/remix-node/fetch.ts +++ b/packages/remix-node/fetch.ts @@ -12,10 +12,6 @@ export type { export { Headers, Response } from "node-fetch"; export class Request extends NodeRequest { - constructor(input: RequestInfo, init?: RequestInit | undefined) { - super(input, init); - } - async formData() { let body = await this.clone().text(); return new RemixFormData(body); From cf2c72a1ce9d654b3a0f5a437151f3090bee68a9 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Tue, 30 Nov 2021 12:32:00 -0700 Subject: [PATCH 0149/1690] chore: let -> const (for external-facing code) (#809) --- packages/remix-dev/config/routesConvention.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 09abce8a76..15e4fda5b7 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -209,3 +209,8 @@ function visitFiles( } } } + +/* +eslint + no-loop-func: "off", +*/ From 482844adb6c9aec74e769130fca880f36037c037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Xalambr=C3=AD?= Date: Thu, 2 Dec 2021 11:03:06 -0500 Subject: [PATCH 0150/1690] feat: Support generic in json (aka typed json) (#439) * Support generic in json (aka typed json) * Sign CLA Co-authored-by: Ryan Florence --- packages/remix-server-runtime/responses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-server-runtime/responses.ts b/packages/remix-server-runtime/responses.ts index 8f8ef0cddb..89892a2e15 100644 --- a/packages/remix-server-runtime/responses.ts +++ b/packages/remix-server-runtime/responses.ts @@ -1,7 +1,7 @@ /** * A JSON response. Converts `data` to JSON and sets the `Content-Type` header. */ -export function json(data: any, init: number | ResponseInit = {}): Response { +export function json(data: Data, init: number | ResponseInit = {}): Response { let responseInit: any = init; if (typeof init === "number") { responseInit = { status: init }; From fb5b480e3cab3debb4c8c9065177f04b979fbf98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Thu, 2 Dec 2021 19:09:13 +0100 Subject: [PATCH 0151/1690] fix: use `null` body instead of empty string (#847) --- packages/remix-express/__tests__/server-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index ddd855c119..ced520cb4a 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -68,7 +68,7 @@ describe("express createRequestHandler", () => { it("handles status codes", async () => { mockedCreateRequestHandler.mockImplementation(() => async () => { - return new Response("", { status: 204 }); + return new Response(null, { status: 204 }); }); let request = supertest(createApp()); @@ -92,7 +92,7 @@ describe("express createRequestHandler", () => { "Set-Cookie", "third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax" ); - return new Response("", { headers }); + return new Response(null, { headers }); }); let request = supertest(createApp()); From 10107d234fb9b15c79f2c299b3a4fe4ea2df5953 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 3 Dec 2021 17:27:31 -0800 Subject: [PATCH 0152/1690] feat: add support for multipart / file uploads (#383) feat: enable multipart form data in node package feat: added global FormData, Blob and File polyfill feat: utlize abort controller to close connection early feat: add file and memory upload storage handlers chore: import jest globals from @remix-run/node chore: consume AbortController from remix-run/node chore: move and rename internal form data parser func chore: update unique file path to be timestamp chore: updated max size message Co-authored-by: Kent C. Dodds --- .../remix-express/__tests__/server-test.ts | 5 +- packages/remix-express/package.json | 3 +- packages/remix-express/server.ts | 26 ++- .../remix-node/__tests__/form-data-test.ts | 18 -- .../remix-node/__tests__/formData-test.ts | 30 +++ .../__tests__/parseMultipartFormData-test.ts | 35 ++++ packages/remix-node/fetch.ts | 123 ++++++++++-- packages/remix-node/form-data.ts | 53 ----- packages/remix-node/formData.ts | 125 ++++++++++++ packages/remix-node/globals.ts | 10 + packages/remix-node/index.ts | 12 ++ packages/remix-node/magicExports/platform.ts | 7 +- packages/remix-node/package.json | 7 + packages/remix-node/parseMultipartFormData.ts | 100 +++++++++ .../remix-node/upload/fileUploadHandler.ts | 190 ++++++++++++++++++ .../remix-node/upload/memoryUploadHandler.ts | 83 ++++++++ packages/remix-node/upload/meter.ts | 28 +++ 17 files changed, 761 insertions(+), 94 deletions(-) delete mode 100644 packages/remix-node/__tests__/form-data-test.ts create mode 100644 packages/remix-node/__tests__/formData-test.ts create mode 100644 packages/remix-node/__tests__/parseMultipartFormData-test.ts delete mode 100644 packages/remix-node/form-data.ts create mode 100644 packages/remix-node/formData.ts create mode 100644 packages/remix-node/parseMultipartFormData.ts create mode 100644 packages/remix-node/upload/fileUploadHandler.ts create mode 100644 packages/remix-node/upload/memoryUploadHandler.ts create mode 100644 packages/remix-node/upload/meter.ts diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index ced520cb4a..75ccadce03 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -211,7 +211,8 @@ describe("express createRemixRequest", () => { }); expect(createRemixRequest(expressRequest)).toMatchInlineSnapshot(` - Request { + RemixRequest { + "abortController": undefined, "agent": undefined, "compress": true, "counter": 0, @@ -250,7 +251,7 @@ describe("express createRemixRequest", () => { "slashes": true, }, "redirect": "follow", - "signal": null, + "signal": undefined, }, } `); diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 882a70ffa4..4f9e251d34 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -12,7 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.0.6" + "@remix-run/node": "1.0.6", + "@remix-run/server-runtime": "1.0.6" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index 639cc764ac..55ee522126 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -1,4 +1,3 @@ -import { PassThrough } from "stream"; import type * as express from "express"; import type { AppLoadContext, @@ -11,6 +10,8 @@ import type { Response as NodeResponse } from "@remix-run/node"; import { + // This has been added as a global in node 15+ + AbortController, Headers as NodeHeaders, Request as NodeRequest, formatServerError @@ -51,7 +52,8 @@ export function createRequestHandler({ next: express.NextFunction ) => { try { - let request = createRemixRequest(req); + let abortController = new AbortController(); + let request = createRemixRequest(req, abortController); let loadContext = typeof getLoadContext === "function" ? getLoadContext(req, res) @@ -62,7 +64,7 @@ export function createRequestHandler({ loadContext )) as unknown as NodeResponse; - sendRemixResponse(res, response); + sendRemixResponse(res, response, abortController); } catch (error) { // Express doesn't support async functions, so we have to pass along the // error manually using next(). @@ -91,17 +93,22 @@ export function createRemixHeaders( return headers; } -export function createRemixRequest(req: express.Request): NodeRequest { +export function createRemixRequest( + req: express.Request, + abortController?: AbortController +): NodeRequest { let origin = `${req.protocol}://${req.get("host")}`; let url = new URL(req.url, origin); let init: NodeRequestInit = { method: req.method, - headers: createRemixHeaders(req.headers) + headers: createRemixHeaders(req.headers), + signal: abortController?.signal, + abortController }; if (req.method !== "GET" && req.method !== "HEAD") { - init.body = req.pipe(new PassThrough({ highWaterMark: 16384 })); + init.body = req; //req.pipe(new PassThrough({ highWaterMark: 16384 })); } return new NodeRequest(url.toString(), init); @@ -109,7 +116,8 @@ export function createRemixRequest(req: express.Request): NodeRequest { function sendRemixResponse( res: express.Response, - response: NodeResponse + response: NodeResponse, + abortController: AbortController ): void { res.status(response.status); @@ -119,6 +127,10 @@ function sendRemixResponse( } } + if (abortController.signal.aborted) { + res.set("Connection", "close"); + } + if (Buffer.isBuffer(response.body)) { res.end(response.body); } else if (response.body?.pipe) { diff --git a/packages/remix-node/__tests__/form-data-test.ts b/packages/remix-node/__tests__/form-data-test.ts deleted file mode 100644 index 38ceced7c9..0000000000 --- a/packages/remix-node/__tests__/form-data-test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { RemixFormData as FormData } from "../form-data"; - -describe("FormData", () => { - it("allows for mix of set and append", () => { - let formData = new FormData(); - formData.set("single", "heyo"); - formData.append("multi", "one"); - formData.append("multi", "two"); - - let results = []; - for (let [k, v] of formData) results.push([k, v]); - expect(results).toEqual([ - ["single", "heyo"], - ["multi", "one"], - ["multi", "two"] - ]); - }); -}); diff --git a/packages/remix-node/__tests__/formData-test.ts b/packages/remix-node/__tests__/formData-test.ts new file mode 100644 index 0000000000..6a4df1837a --- /dev/null +++ b/packages/remix-node/__tests__/formData-test.ts @@ -0,0 +1,30 @@ +import { Blob, File } from "@web-std/file"; + +import { FormData as NodeFormData } from "../formData"; + +describe("FormData", () => { + it("allows for mix of set and append", () => { + let formData = new NodeFormData(); + formData.set("single", "heyo"); + formData.append("multi", "one"); + formData.append("multi", "two"); + + let results = []; + for (let [k, v] of formData) results.push([k, v]); + expect(results).toEqual([ + ["single", "heyo"], + ["multi", "one"], + ["multi", "two"] + ]); + }); + + it("allows for mix of set and append with blobs and files", () => { + let formData = new NodeFormData(); + formData.set("single", new Blob([])); + formData.append("multi", new Blob([])); + formData.append("multi", new File([], "test.txt")); + + expect(formData.getAll("single")).toHaveLength(1); + expect(formData.getAll("multi")).toHaveLength(2); + }); +}); diff --git a/packages/remix-node/__tests__/parseMultipartFormData-test.ts b/packages/remix-node/__tests__/parseMultipartFormData-test.ts new file mode 100644 index 0000000000..2dd3847047 --- /dev/null +++ b/packages/remix-node/__tests__/parseMultipartFormData-test.ts @@ -0,0 +1,35 @@ +import { Blob, File } from "@web-std/file"; + +import { Request as NodeRequest } from "../fetch"; +import { FormData as NodeFormData } from "../formData"; +import { internalParseFormData } from "../parseMultipartFormData"; +import { createMemoryUploadHandler } from "../upload/memoryUploadHandler"; + +describe("internalParseFormData", () => { + it("plays nice with node-fetch", async () => { + let formData = new NodeFormData(); + formData.set("a", "value"); + formData.set("blob", new Blob(["blob"]), "blob.txt"); + formData.set("file", new File(["file"], "file.txt")); + + let req = new NodeRequest("https://test.com", { + method: "post", + body: formData as any + }); + + let uploadHandler = createMemoryUploadHandler({}); + let parsedFormData = await internalParseFormData( + req.headers.get("Content-Type"), + req.body as any, + undefined, + uploadHandler + ); + + expect(parsedFormData.get("a")).toBe("value"); + let blob = parsedFormData.get("blob") as Blob; + expect(await blob.text()).toBe("blob"); + let file = parsedFormData.get("file") as File; + expect(file.name).toBe("file.txt"); + expect(await file.text()).toBe("file"); + }); +}); diff --git a/packages/remix-node/fetch.ts b/packages/remix-node/fetch.ts index e247e7b85d..d0219f442b 100644 --- a/packages/remix-node/fetch.ts +++ b/packages/remix-node/fetch.ts @@ -1,23 +1,113 @@ +import type { Readable } from "stream"; +import { PassThrough } from "stream"; +import type AbortController from "abort-controller"; +import FormStream from "form-data"; import type { RequestInfo, RequestInit, Response } from "node-fetch"; import nodeFetch, { Request as NodeRequest } from "node-fetch"; -import { RemixFormData } from "./form-data"; +import { FormData as NodeFormData, isFile } from "./formData"; +import type { UploadHandler } from "./formData"; +import { internalParseFormData } from "./parseMultipartFormData"; -export type { - HeadersInit, - RequestInfo, - RequestInit, - ResponseInit -} from "node-fetch"; +export type { HeadersInit, RequestInfo, ResponseInit } from "node-fetch"; export { Headers, Response } from "node-fetch"; -export class Request extends NodeRequest { - async formData() { - let body = await this.clone().text(); - return new RemixFormData(body); +function formDataToStream(formData: NodeFormData): FormStream { + let formStream = new FormStream(); + + function toNodeStream(input: any) { + // The input is either a Node stream or a web stream, if it has + // a `on` method it's a node stream so we can just return it + if (typeof input?.on === "function") { + return input; + } + + let passthrough = new PassThrough(); + let stream = input as ReadableStream; + let reader = stream.getReader(); + reader + .read() + .then(async ({ done, value }) => { + while (!done) { + passthrough.push(value); + ({ done, value } = await reader.read()); + } + passthrough.push(null); + }) + .catch(error => { + passthrough.emit("error", error); + }); + + return passthrough; + } + + for (let [key, value] of formData.entries()) { + if (typeof value === "string") { + formStream.append(key, value); + } else if (isFile(value)) { + let stream = toNodeStream(value.stream()); + formStream.append(key, stream, { + filename: value.name, + contentType: value.type, + knownLength: value.size + }); + } else { + let file = value as File; + let stream = toNodeStream(file.stream()); + formStream.append(key, stream, { + filename: "unknown" + }); + } + } + + return formStream; +} + +interface RemixRequestInit extends RequestInit { + abortController?: AbortController; +} + +class RemixRequest extends NodeRequest { + private abortController?: AbortController; + + constructor(input: RequestInfo, init?: RemixRequestInit | undefined) { + if (init?.body instanceof NodeFormData) { + init = { + ...init, + body: formDataToStream(init.body) + }; + } + + super(input, init); + + let anyInput = input as any; + let anyInit = init as any; + + this.abortController = + anyInput?.abortController || anyInit?.abortController; + } + + async formData(uploadHandler?: UploadHandler): Promise { + let contentType = this.headers.get("Content-Type"); + if (contentType) { + return await internalParseFormData( + contentType, + this.body as Readable, + this.abortController, + uploadHandler + ); + } + + throw new Error("Invalid MIME type"); + } + + clone() { + return new RemixRequest(super.clone()); } } +export { RemixRequest as Request, RemixRequestInit as RequestInit }; + /** * A `fetch` function for node that matches the web Fetch API. Based on * `node-fetch`. @@ -29,8 +119,17 @@ export function fetch( input: RequestInfo, init?: RequestInit ): Promise { + init = { compress: false, ...init }; + + if (init?.body instanceof NodeFormData) { + init = { + ...init, + body: formDataToStream(init.body) + }; + } + // Default to { compress: false } so responses can be proxied through more // easily in loaders. Otherwise the response stream encoding will not match // the Content-Encoding response header. - return nodeFetch(input, { compress: false, ...init }); + return nodeFetch(input, init); } diff --git a/packages/remix-node/form-data.ts b/packages/remix-node/form-data.ts deleted file mode 100644 index 87ba6951f8..0000000000 --- a/packages/remix-node/form-data.ts +++ /dev/null @@ -1,53 +0,0 @@ -export class RemixFormData implements FormData { - private _params: URLSearchParams; - - constructor(body?: string) { - this._params = new URLSearchParams(body); - } - append(name: string, value: string | Blob, fileName?: string): void { - if (typeof value !== "string") { - throw new Error("formData.append can only accept a string"); - } - this._params.append(name, value); - } - delete(name: string): void { - this._params.delete(name); - } - get(name: string): FormDataEntryValue | null { - return this._params.get(name); - } - getAll(name: string): FormDataEntryValue[] { - return this._params.getAll(name); - } - has(name: string): boolean { - return this._params.has(name); - } - set(name: string, value: string | Blob, fileName?: string): void { - if (typeof value !== "string") { - throw new Error("formData.set can only accept a string"); - } - this._params.set(name, value); - } - forEach( - callbackfn: ( - value: FormDataEntryValue, - key: string, - parent: FormData - ) => void, - thisArg?: any - ): void { - this._params.forEach(callbackfn, thisArg); - } - entries(): IterableIterator<[string, FormDataEntryValue]> { - return this._params.entries(); - } - keys(): IterableIterator { - return this._params.keys(); - } - values(): IterableIterator { - return this._params.values(); - } - *[Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]> { - yield* this._params; - } -} diff --git a/packages/remix-node/formData.ts b/packages/remix-node/formData.ts new file mode 100644 index 0000000000..35a166fa9a --- /dev/null +++ b/packages/remix-node/formData.ts @@ -0,0 +1,125 @@ +import type { Readable } from "stream"; + +export type UploadHandlerArgs = { + name: string; + stream: Readable; + filename: string; + encoding: string; + mimetype: string; +}; + +export type UploadHandler = ( + args: UploadHandlerArgs +) => Promise; + +function isBlob(value: any): value is Blob { + return ( + typeof value === "object" && + (typeof value.arrayBuffer === "function" || + typeof value.size === "number" || + typeof value.slice === "function" || + typeof value.stream === "function" || + typeof value.text === "function" || + typeof value.type === "string") + ); +} + +export function isFile(blob: Blob): blob is File { + let file = blob as File; + return typeof file.name === "string"; +} + +class NodeFormData implements FormData { + private _fields: Record; + + constructor(form?: any) { + if (typeof form !== "undefined") { + throw new Error("Form data on the server is not supported."); + } + this._fields = {}; + } + + append(name: string, value: string | Blob, fileName?: string): void { + if (typeof value !== "string" && !isBlob(value)) { + throw new Error("formData.append can only accept a string or Blob"); + } + + this._fields[name] = this._fields[name] || []; + if (typeof value === "string" || isFile(value)) { + this._fields[name].push(value); + } else { + this._fields[name].push(new File([value], fileName || "unknown")); + } + } + + delete(name: string): void { + delete this._fields[name]; + } + + get(name: string): FormDataEntryValue | null { + let arr = this._fields[name]; + return (arr && arr.slice(-1)[0]) || null; + } + + getAll(name: string): FormDataEntryValue[] { + let arr = this._fields[name]; + return arr || []; + } + + has(name: string): boolean { + return name in this._fields; + } + + set(name: string, value: string | Blob, fileName?: string): void { + if (typeof value !== "string" && !isBlob(value)) { + throw new Error("formData.set can only accept a string or Blob"); + } + + if (typeof value === "string" || isFile(value)) { + this._fields[name] = [value]; + } else { + this._fields[name] = [new File([value], fileName || "unknown")]; + } + } + + forEach( + callbackfn: ( + value: FormDataEntryValue, + key: string, + parent: FormData + ) => void, + thisArg?: any + ): void { + Object.entries(this._fields).forEach(([name, values]) => { + values.forEach(value => callbackfn(value, name, thisArg), thisArg); + }); + } + + entries(): IterableIterator<[string, FormDataEntryValue]> { + return Object.entries(this._fields) + .reduce((entries, [name, values]) => { + values.forEach(value => entries.push([name, value])); + return entries; + }, [] as [string, FormDataEntryValue][]) + .values(); + } + + keys(): IterableIterator { + return Object.keys(this._fields).values(); + } + + values(): IterableIterator { + return Object.entries(this._fields) + .reduce((results, [name, values]) => { + values.forEach(value => results.push(value)); + return results; + }, [] as FormDataEntryValue[]) + .values(); + } + + *[Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]> { + yield* this.entries(); + } +} + +export { NodeFormData as FormData }; diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts index c72b48b495..abf8f9cdf7 100644 --- a/packages/remix-node/globals.ts +++ b/packages/remix-node/globals.ts @@ -2,6 +2,7 @@ import type { InternalSignFunctionDoNotUseMe, InternalUnsignFunctionDoNotUseMe } from "@remix-run/server-runtime/cookieSigning"; +import { Blob as NodeBlob, File as NodeFile } from "@web-std/file"; import { atob, btoa } from "./base64"; import { sign as remixSign, unsign as remixUnsign } from "./cookieSigning"; @@ -11,6 +12,7 @@ import { Response as NodeResponse, fetch as nodeFetch } from "./fetch"; +import { FormData as NodeFormData } from "./formData"; declare global { namespace NodeJS { @@ -18,10 +20,14 @@ declare global { atob: typeof atob; btoa: typeof btoa; + Blob: typeof Blob; + File: typeof File; + Headers: typeof Headers; Request: typeof Request; Response: typeof Response; fetch: typeof fetch; + FormData: typeof FormData; // TODO: Once node v16 is available on AWS we should remove these globals // and provide the webcrypto API instead. @@ -35,10 +41,14 @@ export function installGlobals() { global.atob = atob; global.btoa = btoa; + global.Blob = NodeBlob as unknown as typeof Blob; + global.File = NodeFile as unknown as typeof File; + global.Headers = NodeHeaders as unknown as typeof Headers; global.Request = NodeRequest as unknown as typeof Request; global.Response = NodeResponse as unknown as typeof Response; global.fetch = nodeFetch as unknown as typeof fetch; + global.FormData = NodeFormData as unknown as typeof FormData; global.sign = remixSign; global.unsign = remixUnsign; diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index 3acd439515..39702da746 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -1,3 +1,5 @@ +export { AbortController } from "abort-controller"; + export { formatServerError } from "./errors"; export type { @@ -8,6 +10,16 @@ export type { } from "./fetch"; export { Headers, Request, Response, fetch } from "./fetch"; +export { FormData } from "./formData"; + export { installGlobals } from "./globals"; +export { parseMultipartFormData } from "./parseMultipartFormData"; + export { createFileSessionStorage } from "./sessions/fileStorage"; + +export { + createFileUploadHandler, + NodeOnDiskFile +} from "./upload/fileUploadHandler"; +export { createMemoryUploadHandler } from "./upload/memoryUploadHandler"; diff --git a/packages/remix-node/magicExports/platform.ts b/packages/remix-node/magicExports/platform.ts index 253f4412fe..0b196604ea 100644 --- a/packages/remix-node/magicExports/platform.ts +++ b/packages/remix-node/magicExports/platform.ts @@ -1,4 +1,9 @@ // This file lists all exports from this package that are available to `import // "remix"`. -export { createFileSessionStorage } from "@remix-run/node"; +export { + createFileSessionStorage, + createFileUploadHandler, + createMemoryUploadHandler, + parseMultipartFormData +} from "@remix-run/node"; diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 0eaed4b4df..d90aa32e37 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -13,12 +13,19 @@ }, "dependencies": { "@remix-run/server-runtime": "1.0.6", + "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", + "@web-std/file": "^3.0.0", + "abort-controller": "^3.0.0", + "blob-stream": "^0.1.3", + "busboy": "^0.3.1", "cookie-signature": "^1.1.0", + "form-data": "^4.0.0", "node-fetch": "^2.6.1", "source-map": "^0.7.3" }, "devDependencies": { + "@types/blob-stream": "^0.1.30", "@types/cookie-signature": "^1.0.3" }, "sideEffects": false diff --git a/packages/remix-node/parseMultipartFormData.ts b/packages/remix-node/parseMultipartFormData.ts new file mode 100644 index 0000000000..889e915386 --- /dev/null +++ b/packages/remix-node/parseMultipartFormData.ts @@ -0,0 +1,100 @@ +import type { Readable } from "stream"; +import Busboy from "busboy"; + +import type { Request as NodeRequest } from "./fetch"; +import type { UploadHandler } from "./formData"; +import { FormData as NodeFormData } from "./formData"; + +export function parseMultipartFormData( + request: Request, + uploadHandler: UploadHandler +) { + return (request as unknown as NodeRequest).formData(uploadHandler); +} + +export async function internalParseFormData( + contentType: string, + stream: Readable, + abortController?: AbortController, + uploadHandler?: UploadHandler +) { + let formData = new NodeFormData(); + let fileWorkQueue: Promise[] = []; + + await new Promise(async (resolve, reject) => { + let busboy = new Busboy({ + highWaterMark: 2 * 1024 * 1024, + headers: { + "content-type": contentType + } + }); + + let aborted = false; + function abort(error?: Error) { + if (aborted) return; + aborted = true; + + stream.unpipe(); + stream.removeAllListeners(); + busboy.removeAllListeners(); + + abortController?.abort(); + reject(error || new Error("failed to parse form data")); + } + + busboy.on("field", (name, value) => { + formData.append(name, value); + }); + + busboy.on("file", (name, filestream, filename, encoding, mimetype) => { + if (uploadHandler) { + fileWorkQueue.push( + (async () => { + try { + let value = await uploadHandler({ + name, + stream: filestream, + filename, + encoding, + mimetype + }); + + if (typeof value !== "undefined") { + formData.append(name, value); + } + } catch (error: any) { + // Emit error to busboy to bail early if possible + busboy.emit("error", error); + // It's possible that the handler is doing stuff and fails + // *after* busboy has finished. Rethrow the error for surfacing + // in the Promise.all(fileWorkQueue) below. + throw error; + } finally { + filestream.resume(); + } + })() + ); + } else { + filestream.resume(); + } + + if (!uploadHandler) { + console.warn( + `Tried to parse multipart file upload for field "${name}" but no uploadHandler was provided.` + + " Read more here: https://remix.run/api/remix#parseMultipartFormData-node" + ); + } + }); + + stream.on("error", abort); + stream.on("aborted", abort); + busboy.on("error", abort); + busboy.on("finish", resolve); + + stream.pipe(busboy); + }); + + await Promise.all(fileWorkQueue); + + return formData; +} diff --git a/packages/remix-node/upload/fileUploadHandler.ts b/packages/remix-node/upload/fileUploadHandler.ts new file mode 100644 index 0000000000..5dc5ddf7eb --- /dev/null +++ b/packages/remix-node/upload/fileUploadHandler.ts @@ -0,0 +1,190 @@ +import { randomBytes } from "crypto"; +import { createReadStream, createWriteStream } from "fs"; +import { rm, mkdir, readFile, stat } from "fs/promises"; +import { tmpdir } from "os"; +import { basename, dirname, extname, resolve as resolvePath } from "path"; + +import { Meter } from "./meter"; +import type { UploadHandler } from "../formData"; + +export type FileUploadHandlerFilterArgs = { + filename: string; + encoding: string; + mimetype: string; +}; + +export type FileUploadHandlerPathResolverArgs = { + filename: string; + encoding: string; + mimetype: string; +}; + +/** + * Chooses the path of the file to be uploaded. If a string is not + * returned the file will not be written. + */ +export type FileUploadHandlerPathResolver = ( + args: FileUploadHandlerPathResolverArgs +) => string | undefined; + +export type FileUploadHandlerOptions = { + /** + * Avoid file conflicts by appending a count on the end of the filename + * if it already exists on disk. Defaults to `true`. + */ + avoidFileConflicts?: boolean; + /** + * The directory to write the upload. + */ + directory?: string | FileUploadHandlerPathResolver; + /** + * The name of the file in the directory. Can be a relative path, the directory + * structure will be created if it does not exist. + */ + file?: FileUploadHandlerPathResolver; + /** + * The maximum upload size allowed. If the size is exceeded an error will be thrown. + * Defaults to 3000000B (3MB). + */ + maxFileSize?: number; + /** + * + * @param filename + * @param mimetype + * @param encoding + */ + filter?(args: FileUploadHandlerFilterArgs): boolean | Promise; +}; + +let defaultFilePathResolver: FileUploadHandlerPathResolver = ({ filename }) => { + let ext = filename ? extname(filename) : ""; + return "upload_" + randomBytes(4).readUInt32LE(0) + ext; +}; + +async function uniqueFile(filepath: string) { + let ext = extname(filepath); + let uniqueFilepath = filepath; + + for ( + let i = 1; + await stat(uniqueFilepath) + .then(() => true) + .catch(() => false); + i++ + ) { + uniqueFilepath = + (ext ? filepath.slice(0, -ext.length) : filepath) + + `-${new Date().getTime()}${ext}`; + } + + return uniqueFilepath; +} + +export function createFileUploadHandler({ + directory = tmpdir(), + avoidFileConflicts = true, + file = defaultFilePathResolver, + filter, + maxFileSize = 3000000 +}: FileUploadHandlerOptions): UploadHandler { + return async ({ name, stream, filename, encoding, mimetype }) => { + if (filter && !(await filter({ filename, encoding, mimetype }))) { + stream.resume(); + return; + } + + let dir = + typeof directory === "string" + ? directory + : directory({ filename, encoding, mimetype }); + + if (!dir) { + stream.resume(); + return; + } + + let filedir = resolvePath(dir); + let path = + typeof file === "string" ? file : file({ filename, encoding, mimetype }); + + if (!path) { + stream.resume(); + return; + } + + let filepath = resolvePath(filedir, path); + + if (avoidFileConflicts) { + filepath = await uniqueFile(filepath); + } + + await mkdir(dirname(filepath), { recursive: true }).catch(() => {}); + + let meter = new Meter(name, maxFileSize); + await new Promise((resolve, reject) => { + let writeFileStream = createWriteStream(filepath); + + let aborted = false; + async function abort(error: Error) { + if (aborted) return; + aborted = true; + + stream.unpipe(); + meter.unpipe(); + stream.removeAllListeners(); + meter.removeAllListeners(); + writeFileStream.removeAllListeners(); + + await rm(filepath, { force: true }).catch(() => {}); + + reject(error); + } + + stream.on("error", abort); + meter.on("error", abort); + writeFileStream.on("error", abort); + writeFileStream.on("finish", resolve); + + stream.pipe(meter).pipe(writeFileStream); + }); + + return new NodeOnDiskFile(filepath, meter.bytes, mimetype); + }; +} + +export class NodeOnDiskFile implements File { + name: string; + lastModified: number = 0; + webkitRelativePath: string = ""; + + constructor( + private filepath: string, + public size: number, + public type: string + ) { + this.name = basename(filepath); + } + + async arrayBuffer(): Promise { + let stream = createReadStream(this.filepath); + + return new Promise((resolve, reject) => { + const buf: any[] = []; + stream.on("data", chunk => buf.push(chunk)); + stream.on("end", () => resolve(Buffer.concat(buf))); + stream.on("error", err => reject(err)); + }); + } + + slice(start?: any, end?: any, contentType?: any): Blob { + throw new Error("Method not implemented."); + } + stream(): ReadableStream; + stream(): NodeJS.ReadableStream; + stream(): ReadableStream | NodeJS.ReadableStream { + return createReadStream(this.filepath); + } + text(): Promise { + return readFile(this.filepath, "utf-8"); + } +} diff --git a/packages/remix-node/upload/memoryUploadHandler.ts b/packages/remix-node/upload/memoryUploadHandler.ts new file mode 100644 index 0000000000..572cb592e6 --- /dev/null +++ b/packages/remix-node/upload/memoryUploadHandler.ts @@ -0,0 +1,83 @@ +import type { TransformCallback } from "stream"; +import { Transform } from "stream"; +import { File as BufferFile } from "@web-std/file"; + +import { Meter } from "./meter"; +import type { UploadHandler } from "../formData"; + +export type MemoryUploadHandlerFilterArgs = { + filename: string; + encoding: string; + mimetype: string; +}; + +export type MemoryUploadHandlerOptions = { + /** + * The maximum upload size allowed. If the size is exceeded an error will be thrown. + * Defaults to 3000000B (3MB). + */ + maxFileSize?: number; + /** + * + * @param filename + * @param mimetype + * @param encoding + */ + filter?(args: MemoryUploadHandlerFilterArgs): boolean | Promise; +}; + +export function createMemoryUploadHandler({ + filter, + maxFileSize = 3000000 +}: MemoryUploadHandlerOptions): UploadHandler { + return async ({ name, stream, filename, encoding, mimetype }) => { + if (filter && !(await filter({ filename, encoding, mimetype }))) { + stream.resume(); + return; + } + + let bufferStream = new BufferStream(); + await new Promise((resolve, reject) => { + let meter = new Meter(name, maxFileSize); + + let aborted = false; + async function abort(error: Error) { + if (aborted) return; + aborted = true; + + stream.unpipe(); + meter.unpipe(); + stream.removeAllListeners(); + meter.removeAllListeners(); + bufferStream.removeAllListeners(); + + reject(error); + } + + stream.on("error", abort); + meter.on("error", abort); + bufferStream.on("error", abort); + bufferStream.on("finish", resolve); + + stream.pipe(meter).pipe(bufferStream); + }); + + return new BufferFile(bufferStream.data, filename, { + type: mimetype + }); + }; +} + +class BufferStream extends Transform { + public data: any[]; + + constructor() { + super(); + this.data = []; + } + + _transform(chunk: any, _: BufferEncoding, callback: TransformCallback) { + this.data.push(chunk); + callback(); + } +} diff --git a/packages/remix-node/upload/meter.ts b/packages/remix-node/upload/meter.ts new file mode 100644 index 0000000000..01e64b9641 --- /dev/null +++ b/packages/remix-node/upload/meter.ts @@ -0,0 +1,28 @@ +import type { TransformCallback } from "stream"; +import { Transform } from "stream"; + +export class Meter extends Transform { + public bytes: number; + + constructor(public field: string, public maxBytes: number | undefined) { + super(); + this.bytes = 0; + } + + _transform(chunk: any, _: BufferEncoding, callback: TransformCallback) { + this.bytes += chunk.length; + this.push(chunk); + + if (typeof this.maxBytes === "number" && this.bytes > this.maxBytes) { + return callback(new MeterError(this.field, this.maxBytes)); + } + + callback(); + } +} + +export class MeterError extends Error { + constructor(public field: string, public maxBytes: number) { + super(`Field "${field}" exceeded upload size of ${maxBytes} bytes.`); + } +} From dbc779e08547a577a53e8477021b436b6f0d40cd Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 8 Dec 2021 10:22:45 -0800 Subject: [PATCH 0153/1690] wip: re-work server runtime (#864) chore: renamed componentDidCatchEmulator to appState chore: added a bunch of tests feat: strip error messages when not in development mode goals: - simplify shared server runtime - simplify loop that tracks boundaries - simplify how actions / loaders are called - make sure all boundary cases are surfaced properly addresses https://github.com/remix-run/remix/issues/599 (todo: add testcase) --- .../__tests__/server-test.ts | 1608 +++++++++++++++++ .../remix-server-runtime/__tests__/utils.ts | 89 + packages/remix-server-runtime/data.ts | 117 +- packages/remix-server-runtime/entry.ts | 4 +- packages/remix-server-runtime/errors.ts | 2 +- packages/remix-server-runtime/responses.ts | 46 +- packages/remix-server-runtime/routeData.ts | 19 - packages/remix-server-runtime/server.ts | 853 ++++----- 8 files changed, 2237 insertions(+), 501 deletions(-) diff --git a/packages/remix-server-runtime/__tests__/server-test.ts b/packages/remix-server-runtime/__tests__/server-test.ts index d6277c659a..6d727f5121 100644 --- a/packages/remix-server-runtime/__tests__/server-test.ts +++ b/packages/remix-server-runtime/__tests__/server-test.ts @@ -1,7 +1,26 @@ import { createRequestHandler } from ".."; +import { ServerMode } from "../mode"; import type { ServerBuild } from "../build"; +import { mockServerBuild } from "./utils"; + +function spyConsole() { + // https://github.com/facebook/react/issues/7047 + let spy: any = {}; + + beforeAll(() => { + spy.console = jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterAll(() => { + spy.console.mockRestore(); + }); + + return spy; +} describe("server", () => { + let spy = spyConsole(); + let routeId = "root"; let build: ServerBuild = { entry: { @@ -74,3 +93,1592 @@ describe("server", () => { }); }); }); + +describe("shared server runtime", () => { + const spy = spyConsole(); + + beforeEach(() => { + spy.console.mockClear(); + }); + + let baseUrl = "http://test.com"; + + describe("resource routes", () => { + test("calls resource route loader", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let resourceLoader = jest.fn(() => { + return "resource"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/resource": { + loader: resourceLoader, + path: "resource" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/resource`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(200); + expect(await result.json()).toBe("resource"); + expect(rootLoader.mock.calls.length).toBe(0); + expect(resourceLoader.mock.calls.length).toBe(1); + }); + + test("calls sub resource route loader", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let resourceLoader = jest.fn(() => { + return "resource"; + }); + let subResourceLoader = jest.fn(() => { + return "sub"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/resource": { + loader: resourceLoader, + path: "resource" + }, + "routes/resource.sub": { + loader: subResourceLoader, + path: "resource/sub" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/resource/sub`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(200); + expect(await result.json()).toBe("sub"); + expect(rootLoader.mock.calls.length).toBe(0); + expect(resourceLoader.mock.calls.length).toBe(0); + expect(subResourceLoader.mock.calls.length).toBe(1); + }); + + test("resource route loader allows thrown responses", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let resourceLoader = jest.fn(() => { + throw new Response("resource"); + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/resource": { + loader: resourceLoader, + path: "resource" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/resource`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(200); + expect(await result.text()).toBe("resource"); + expect(rootLoader.mock.calls.length).toBe(0); + expect(resourceLoader.mock.calls.length).toBe(1); + }); + + test("resource route loader responds with generic error when thrown", async () => { + let error = new Error("should be logged when resource loader throws"); + let loader = jest.fn(() => { + throw error; + }); + let build = mockServerBuild({ + "routes/resource": { + loader, + path: "resource" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/resource`, { method: "get" }); + + let result = await handler(request); + expect(await result.text()).toBe("Unexpected Server Error"); + }); + + test("resource route loader responds with detailed error when thrown in development", async () => { + let error = new Error("should be logged when resource loader throws"); + let loader = jest.fn(() => { + throw error; + }); + let build = mockServerBuild({ + "routes/resource": { + loader, + path: "resource" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Development); + + let request = new Request(`${baseUrl}/resource`, { method: "get" }); + + let result = await handler(request); + expect((await result.text()).includes(error.message)).toBe(true); + expect(spy.console.mock.calls.length).toBe(1); + }); + + test("calls resource route action", async () => { + let rootAction = jest.fn(() => { + return "root"; + }); + let resourceAction = jest.fn(() => { + return "resource"; + }); + let build = mockServerBuild({ + root: { + default: {}, + action: rootAction + }, + "routes/resource": { + action: resourceAction, + path: "resource" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/resource`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(200); + expect(await result.json()).toBe("resource"); + expect(rootAction.mock.calls.length).toBe(0); + expect(resourceAction.mock.calls.length).toBe(1); + }); + + test("calls sub resource route action", async () => { + let rootAction = jest.fn(() => { + return "root"; + }); + let resourceAction = jest.fn(() => { + return "resource"; + }); + let subResourceAction = jest.fn(() => { + return "sub"; + }); + let build = mockServerBuild({ + root: { + default: {}, + action: rootAction + }, + "routes/resource": { + action: resourceAction, + path: "resource" + }, + "routes/resource.sub": { + action: subResourceAction, + path: "resource/sub" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/resource/sub`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(200); + expect(await result.json()).toBe("sub"); + expect(rootAction.mock.calls.length).toBe(0); + expect(resourceAction.mock.calls.length).toBe(0); + expect(subResourceAction.mock.calls.length).toBe(1); + }); + + test("resource route action allows thrown responses", async () => { + let rootAction = jest.fn(() => { + return "root"; + }); + let resourceAction = jest.fn(() => { + throw new Response("resource"); + }); + let build = mockServerBuild({ + root: { + default: {}, + action: rootAction + }, + "routes/resource": { + action: resourceAction, + path: "resource" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/resource`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(200); + expect(await result.text()).toBe("resource"); + expect(rootAction.mock.calls.length).toBe(0); + expect(resourceAction.mock.calls.length).toBe(1); + }); + + test("resource route action responds with generic error when thrown", async () => { + let error = new Error("should be logged when resource loader throws"); + let action = jest.fn(() => { + throw error; + }); + let build = mockServerBuild({ + "routes/resource": { + action, + path: "resource" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/resource`, { method: "post" }); + + let result = await handler(request); + expect(await result.text()).toBe("Unexpected Server Error"); + }); + + test("resource route action responds with detailed error when thrown in development", async () => { + let message = "should be logged when resource loader throws"; + let action = jest.fn(() => { + throw new Error(message); + }); + let build = mockServerBuild({ + "routes/resource": { + action, + path: "resource" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Development); + + let request = new Request(`${baseUrl}/resource`, { method: "post" }); + + let result = await handler(request); + expect((await result.text()).includes(message)).toBe(true); + expect(spy.console.mock.calls.length).toBe(1); + }); + }); + + describe("data requests", () => { + test("data request that does not match loader surfaces error for boundary", async () => { + let build = mockServerBuild({ + root: { + default: {} + }, + "routes/index": { + parentId: "root", + index: true + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/?_data=routes/index`, { + method: "get" + }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(result.headers.get("X-Remix-Error")).toBe("yes"); + expect((await result.json()).message).toBeTruthy(); + }); + + test("data request calls loader", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexLoader = jest.fn(() => { + return "index"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/index": { + parentId: "root", + loader: indexLoader, + index: true + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/?_data=routes/index`, { + method: "get" + }); + + let result = await handler(request); + expect(result.status).toBe(200); + expect(await result.json()).toBe("index"); + expect(rootLoader.mock.calls.length).toBe(0); + expect(indexLoader.mock.calls.length).toBe(1); + }); + + test("data request calls loader and responds with generic message and error header", async () => { + let rootLoader = jest.fn(() => { + throw new Error("test"); + }); + let testAction = jest.fn(() => { + return "root"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/test": { + parentId: "root", + action: testAction, + path: "test" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/test?_data=root`, { + method: "get" + }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect((await result.json()).message).toBe("Unexpected Server Error"); + expect(result.headers.get("X-Remix-Error")).toBe("yes"); + expect(rootLoader.mock.calls.length).toBe(1); + expect(testAction.mock.calls.length).toBe(0); + }); + + test("data request calls loader and responds with detailed info and error header in development mode", async () => { + let message = + "data request loader error logged to console once in dev mode"; + let rootLoader = jest.fn(() => { + throw new Error(message); + }); + let testAction = jest.fn(() => { + return "root"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/test": { + parentId: "root", + action: testAction, + path: "test" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Development); + + let request = new Request(`${baseUrl}/test?_data=root`, { + method: "get" + }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect((await result.json()).message).toBe(message); + expect(result.headers.get("X-Remix-Error")).toBe("yes"); + expect(rootLoader.mock.calls.length).toBe(1); + expect(testAction.mock.calls.length).toBe(0); + expect(spy.console.mock.calls.length).toBe(1); + }); + + test("data request calls loader and responds with catch header", async () => { + let rootLoader = jest.fn(() => { + throw new Response("test", { status: 400 }); + }); + let testAction = jest.fn(() => { + return "root"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/test": { + parentId: "root", + action: testAction, + path: "test" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/test?_data=root`, { + method: "get" + }); + + let result = await handler(request); + expect(result.status).toBe(400); + expect(await result.text()).toBe("test"); + expect(result.headers.get("X-Remix-Catch")).toBe("yes"); + expect(rootLoader.mock.calls.length).toBe(1); + expect(testAction.mock.calls.length).toBe(0); + }); + + test("data request that does not match action surfaces error for boundary", async () => { + let build = mockServerBuild({ + root: { + default: {} + }, + "routes/index": { + parentId: "root", + index: true + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/?index&_data=routes/index`, { + method: "post" + }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(result.headers.get("X-Remix-Error")).toBe("yes"); + expect((await result.json()).message).toBeTruthy(); + }); + + test("data request calls action", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let testAction = jest.fn(() => { + return "test"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/test": { + parentId: "root", + action: testAction, + path: "test" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/test?_data=routes/test`, { + method: "post" + }); + + let result = await handler(request); + expect(result.status).toBe(200); + expect(await result.json()).toBe("test"); + expect(rootLoader.mock.calls.length).toBe(0); + expect(testAction.mock.calls.length).toBe(1); + }); + + test("data request calls action and responds with generic message and error header", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let testAction = jest.fn(() => { + throw new Error("test"); + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/test": { + parentId: "root", + action: testAction, + path: "test" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/test?_data=routes/test`, { + method: "post" + }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect((await result.json()).message).toBe("Unexpected Server Error"); + expect(result.headers.get("X-Remix-Error")).toBe("yes"); + expect(rootLoader.mock.calls.length).toBe(0); + expect(testAction.mock.calls.length).toBe(1); + }); + + test("data request calls action and responds with detailed info and error header in development mode", async () => { + let message = + "data request action error logged to console once in dev mode"; + let rootLoader = jest.fn(() => { + return "root"; + }); + let testAction = jest.fn(() => { + throw new Error(message); + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/test": { + parentId: "root", + action: testAction, + path: "test" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Development); + + let request = new Request(`${baseUrl}/test?_data=routes/test`, { + method: "post" + }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect((await result.json()).message).toBe(message); + expect(result.headers.get("X-Remix-Error")).toBe("yes"); + expect(rootLoader.mock.calls.length).toBe(0); + expect(testAction.mock.calls.length).toBe(1); + expect(spy.console.mock.calls.length).toBe(1); + }); + + test("data request calls action and responds with catch header", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let testAction = jest.fn(() => { + throw new Response("test", { status: 400 }); + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/test": { + parentId: "root", + action: testAction, + path: "test" + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/test?_data=routes/test`, { + method: "post" + }); + + let result = await handler(request); + expect(result.status).toBe(400); + expect(await result.text()).toBe("test"); + expect(result.headers.get("X-Remix-Catch")).toBe("yes"); + expect(rootLoader.mock.calls.length).toBe(0); + expect(testAction.mock.calls.length).toBe(1); + }); + + test("data request calls layout action", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let rootAction = jest.fn(() => { + return "root"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + action: rootAction + }, + "routes/index": { + parentId: "root", + index: true + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/?_data=root`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(200); + expect(await result.json()).toBe("root"); + expect(rootLoader.mock.calls.length).toBe(0); + expect(rootAction.mock.calls.length).toBe(1); + }); + + test("data request calls index action", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexAction = jest.fn(() => { + return "index"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + }, + "routes/index": { + parentId: "root", + action: indexAction, + index: true + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/?index&_data=routes/index`, { + method: "post" + }); + + let result = await handler(request); + expect(result.status).toBe(200); + expect(await result.json()).toBe("index"); + expect(rootLoader.mock.calls.length).toBe(0); + expect(indexAction.mock.calls.length).toBe(1); + }); + }); + + describe("document requests", () => { + test("not found document request for no matches and no CatchBoundary", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(404); + expect(rootLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.catch).toBeTruthy(); + expect(entryContext.appState.catch!.status).toBe(404); + expect(entryContext.appState.catchBoundaryRouteId).toBe(null); + }); + + test("sets root as catch boundary for not found document request", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + CatchBoundary: {} + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(404); + expect(rootLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.catch).toBeTruthy(); + expect(entryContext.appState.catch!.status).toBe(404); + expect(entryContext.appState.catchBoundaryRouteId).toBe("root"); + expect(entryContext.routeData).toEqual({}); + }); + + test("thrown loader responses bubble up", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexLoader = jest.fn(() => { + throw new Response(null, { status: 400 }); + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + CatchBoundary: {} + }, + "routes/index": { + parentId: "root", + index: true, + default: {}, + loader: indexLoader + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(400); + expect(rootLoader.mock.calls.length).toBe(1); + expect(indexLoader.mock.calls.length).toBe(1); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.catch).toBeTruthy(); + expect(entryContext.appState.catch!.status).toBe(400); + expect(entryContext.appState.catchBoundaryRouteId).toBe("root"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("thrown loader responses catch deep", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexLoader = jest.fn(() => { + throw new Response(null, { status: 400 }); + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + CatchBoundary: {} + }, + "routes/index": { + parentId: "root", + index: true, + default: {}, + loader: indexLoader, + CatchBoundary: {} + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(400); + expect(rootLoader.mock.calls.length).toBe(1); + expect(indexLoader.mock.calls.length).toBe(1); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.catch).toBeTruthy(); + expect(entryContext.appState.catch!.status).toBe(400); + expect(entryContext.appState.catchBoundaryRouteId).toBe("routes/index"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("thrown action responses bubble up", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let testAction = jest.fn(() => { + throw new Response(null, { status: 400 }); + }); + let testLoader = jest.fn(() => { + return "test"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + CatchBoundary: {} + }, + "routes/test": { + parentId: "root", + path: "test", + default: {}, + loader: testLoader, + action: testAction + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/test`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(400); + expect(testAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(testLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.catch).toBeTruthy(); + expect(entryContext.appState.catch!.status).toBe(400); + expect(entryContext.appState.catchBoundaryRouteId).toBe("root"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("thrown action responses bubble up for index routes", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexAction = jest.fn(() => { + throw new Response(null, { status: 400 }); + }); + let indexLoader = jest.fn(() => { + return "index"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + CatchBoundary: {} + }, + "routes/index": { + parentId: "root", + index: true, + default: {}, + loader: indexLoader, + action: indexAction + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/?index`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(400); + expect(indexAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(indexLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.catch).toBeTruthy(); + expect(entryContext.appState.catch!.status).toBe(400); + expect(entryContext.appState.catchBoundaryRouteId).toBe("root"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("thrown action responses catch deep", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let testAction = jest.fn(() => { + throw new Response(null, { status: 400 }); + }); + let testLoader = jest.fn(() => { + return "test"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + CatchBoundary: {} + }, + "routes/test": { + parentId: "root", + path: "test", + default: {}, + loader: testLoader, + action: testAction, + CatchBoundary: {} + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/test`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(400); + expect(testAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(testLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.catch).toBeTruthy(); + expect(entryContext.appState.catch!.status).toBe(400); + expect(entryContext.appState.catchBoundaryRouteId).toBe("routes/test"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("thrown action responses catch deep for index routes", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexAction = jest.fn(() => { + throw new Response(null, { status: 400 }); + }); + let indexLoader = jest.fn(() => { + return "index"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + CatchBoundary: {} + }, + "routes/index": { + parentId: "root", + index: true, + default: {}, + loader: indexLoader, + action: indexAction, + CatchBoundary: {} + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/?index`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(400); + expect(indexAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(indexLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.catch).toBeTruthy(); + expect(entryContext.appState.catch!.status).toBe(400); + expect(entryContext.appState.catchBoundaryRouteId).toBe("routes/index"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("thrown loader response after thrown action response bubble up action throw to deepest loader boundary", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let layoutLoader = jest.fn(() => { + throw new Response("layout", { status: 401 }); + }); + let testAction = jest.fn(() => { + throw new Response("action", { status: 400 }); + }); + let testLoader = jest.fn(() => { + return "test"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + CatchBoundary: {} + }, + "routes/__layout": { + parentId: "root", + default: {}, + loader: layoutLoader, + CatchBoundary: {} + }, + "routes/__layout/test": { + parentId: "routes/__layout", + path: "test", + default: {}, + loader: testLoader, + action: testAction + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/test`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(400); + expect(testAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(testLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.catch).toBeTruthy(); + expect(entryContext.appState.catch.data).toBe("action"); + expect(entryContext.appState.catchBoundaryRouteId).toBe( + "routes/__layout" + ); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("thrown loader response after thrown index action response bubble up action throw to deepest loader boundary", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let layoutLoader = jest.fn(() => { + throw new Response("layout", { status: 401 }); + }); + let indexAction = jest.fn(() => { + throw new Response("action", { status: 400 }); + }); + let indexLoader = jest.fn(() => { + return "index"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + CatchBoundary: {} + }, + "routes/__layout": { + parentId: "root", + default: {}, + loader: layoutLoader, + CatchBoundary: {} + }, + "routes/__layout/index": { + parentId: "routes/__layout", + index: true, + default: {}, + loader: indexLoader, + action: indexAction + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/?index`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(400); + expect(indexAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(indexLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.catch).toBeTruthy(); + expect(entryContext.appState.catch.data).toBe("action"); + expect(entryContext.appState.catchBoundaryRouteId).toBe( + "routes/__layout" + ); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("loader errors bubble up", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexLoader = jest.fn(() => { + throw new Error("index"); + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + ErrorBoundary: {} + }, + "routes/index": { + parentId: "root", + index: true, + default: {}, + loader: indexLoader + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(rootLoader.mock.calls.length).toBe(1); + expect(indexLoader.mock.calls.length).toBe(1); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.error).toBeTruthy(); + expect(entryContext.appState.error.message).toBe("index"); + expect(entryContext.appState.loaderBoundaryRouteId).toBe("root"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("loader errors catch deep", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexLoader = jest.fn(() => { + throw new Error("index"); + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + ErrorBoundary: {} + }, + "routes/index": { + parentId: "root", + index: true, + default: {}, + loader: indexLoader, + ErrorBoundary: {} + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(rootLoader.mock.calls.length).toBe(1); + expect(indexLoader.mock.calls.length).toBe(1); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.error).toBeTruthy(); + expect(entryContext.appState.error.message).toBe("index"); + expect(entryContext.appState.loaderBoundaryRouteId).toBe("routes/index"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("action errors bubble up", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let testAction = jest.fn(() => { + throw new Error("test"); + }); + let testLoader = jest.fn(() => { + return "test"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + ErrorBoundary: {} + }, + "routes/test": { + parentId: "root", + path: "test", + default: {}, + loader: testLoader, + action: testAction + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/test`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(testAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(testLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.error).toBeTruthy(); + expect(entryContext.appState.error.message).toBe("test"); + expect(entryContext.appState.loaderBoundaryRouteId).toBe("root"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("action errors bubble up for index routes", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexAction = jest.fn(() => { + throw new Error("index"); + }); + let indexLoader = jest.fn(() => { + return "index"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + ErrorBoundary: {} + }, + "routes/index": { + parentId: "root", + index: true, + default: {}, + loader: indexLoader, + action: indexAction + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/?index`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(indexAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(indexLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.error).toBeTruthy(); + expect(entryContext.appState.error.message).toBe("index"); + expect(entryContext.appState.loaderBoundaryRouteId).toBe("root"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("action errors catch deep", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let testAction = jest.fn(() => { + throw new Error("test"); + }); + let testLoader = jest.fn(() => { + return "test"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + ErrorBoundary: {} + }, + "routes/test": { + parentId: "root", + path: "test", + default: {}, + loader: testLoader, + action: testAction, + ErrorBoundary: {} + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/test`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(testAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(testLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.error).toBeTruthy(); + expect(entryContext.appState.error.message).toBe("test"); + expect(entryContext.appState.loaderBoundaryRouteId).toBe("routes/test"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("action errors catch deep for index routes", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexAction = jest.fn(() => { + throw new Error("index"); + }); + let indexLoader = jest.fn(() => { + return "index"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + ErrorBoundary: {} + }, + "routes/index": { + parentId: "root", + index: true, + default: {}, + loader: indexLoader, + action: indexAction, + ErrorBoundary: {} + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/?index`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(indexAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(indexLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.error).toBeTruthy(); + expect(entryContext.appState.error.message).toBe("index"); + expect(entryContext.appState.loaderBoundaryRouteId).toBe("routes/index"); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("loader errors after action error bubble up action error to deepest loader boundary", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let layoutLoader = jest.fn(() => { + throw new Error("layout"); + }); + let testAction = jest.fn(() => { + throw new Error("action"); + }); + let testLoader = jest.fn(() => { + return "test"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + ErrorBoundary: {} + }, + "routes/__layout": { + parentId: "root", + default: {}, + loader: layoutLoader, + ErrorBoundary: {} + }, + "routes/__layout/test": { + parentId: "routes/__layout", + path: "test", + default: {}, + loader: testLoader, + action: testAction + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/test`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(testAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(testLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.error).toBeTruthy(); + expect(entryContext.appState.error.message).toBe("action"); + expect(entryContext.appState.loaderBoundaryRouteId).toBe( + "routes/__layout" + ); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("loader errors after index action error bubble up action error to deepest loader boundary", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let layoutLoader = jest.fn(() => { + throw new Error("layout"); + }); + let indexAction = jest.fn(() => { + throw new Error("action"); + }); + let indexLoader = jest.fn(() => { + return "index"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + ErrorBoundary: {} + }, + "routes/__layout": { + parentId: "root", + default: {}, + loader: layoutLoader, + ErrorBoundary: {} + }, + "routes/__layout/index": { + parentId: "routes/__layout", + index: true, + default: {}, + loader: indexLoader, + action: indexAction + } + }); + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/?index`, { method: "post" }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(indexAction.mock.calls.length).toBe(1); + expect(rootLoader.mock.calls.length).toBe(1); + expect(indexLoader.mock.calls.length).toBe(0); + expect(build.entry.module.default.mock.calls.length).toBe(1); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(1); + let entryContext = calls[0][3]; + expect(entryContext.appState.error).toBeTruthy(); + expect(entryContext.appState.error.message).toBe("action"); + expect(entryContext.appState.loaderBoundaryRouteId).toBe( + "routes/__layout" + ); + expect(entryContext.routeData).toEqual({ + root: "root" + }); + }); + + test("calls handleDocumentRequest again with new error when handleDocumentRequest throws", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexLoader = jest.fn(() => { + return "index"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + ErrorBoundary: {} + }, + "routes/index": { + parentId: "root", + default: {}, + loader: indexLoader + } + }); + let calledBefore = false; + let ogHandleDocumentRequest = build.entry.module.default; + build.entry.module.default = jest.fn(function () { + if (!calledBefore) { + throw new Error("thrown"); + } + calledBefore = true; + return ogHandleDocumentRequest.call(null, arguments); + }) as any; + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(rootLoader.mock.calls.length).toBe(0); + expect(indexLoader.mock.calls.length).toBe(0); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(2); + let entryContext = calls[1][3]; + expect(entryContext.appState.error).toBeTruthy(); + expect(entryContext.appState.error.message).toBe("thrown"); + expect(entryContext.appState.trackBoundaries).toBe(false); + expect(entryContext.routeData).toEqual({}); + }); + + test("returns generic message if handleDocumentRequest throws a second time", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexLoader = jest.fn(() => { + return "index"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + ErrorBoundary: {} + }, + "routes/index": { + parentId: "root", + default: {}, + loader: indexLoader + } + }); + let lastThrownError; + build.entry.module.default = jest.fn(function () { + lastThrownError = new Error("rofl"); + throw lastThrownError; + }) as any; + let handler = createRequestHandler(build, {}, ServerMode.Test); + + let request = new Request(`${baseUrl}/`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect(await result.text()).toBe("Unexpected Server Error"); + expect(rootLoader.mock.calls.length).toBe(0); + expect(indexLoader.mock.calls.length).toBe(0); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(2); + }); + + test("returns more detailed message if handleDocumentRequest throws a second time in development mode", async () => { + let rootLoader = jest.fn(() => { + return "root"; + }); + let indexLoader = jest.fn(() => { + return "index"; + }); + let build = mockServerBuild({ + root: { + default: {}, + loader: rootLoader, + ErrorBoundary: {} + }, + "routes/index": { + parentId: "root", + default: {}, + loader: indexLoader + } + }); + let errorMessage = + "thrown from handleDocumentRequest and expected to be logged in console only once"; + let lastThrownError; + build.entry.module.default = jest.fn(function () { + lastThrownError = new Error(errorMessage); + throw lastThrownError; + }) as any; + let handler = createRequestHandler(build, {}, ServerMode.Development); + + let request = new Request(`${baseUrl}/`, { method: "get" }); + + let result = await handler(request); + expect(result.status).toBe(500); + expect((await result.text()).includes(errorMessage)).toBe(true); + expect(rootLoader.mock.calls.length).toBe(0); + expect(indexLoader.mock.calls.length).toBe(0); + + let calls = build.entry.module.default.mock.calls; + expect(calls.length).toBe(2); + expect(spy.console.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/packages/remix-server-runtime/__tests__/utils.ts b/packages/remix-server-runtime/__tests__/utils.ts index 06ad0653dd..90d2d15c85 100644 --- a/packages/remix-server-runtime/__tests__/utils.ts +++ b/packages/remix-server-runtime/__tests__/utils.ts @@ -1,5 +1,94 @@ import prettier from "prettier"; +import type { + ActionFunction, + HandleDataRequestFunction, + HandleDocumentRequestFunction, + HeadersFunction, + LoaderFunction +} from "../"; +import type { EntryRoute, ServerRoute, ServerRouteManifest } from "../routes"; + +export function mockServerBuild( + routes: Record< + string, + { + parentId?: string; + index?: true; + path?: string; + default?: any; + CatchBoundary?: any; + ErrorBoundary?: any; + action?: ActionFunction; + headers?: HeadersFunction; + loader?: LoaderFunction; + } + > +) { + return { + assets: { + entry: { + imports: [""], + module: "" + }, + routes: Object.entries(routes).reduce((p, [id, config]) => { + let route: EntryRoute = { + hasAction: !!config.action, + hasCatchBoundary: !!config.CatchBoundary, + hasErrorBoundary: !!config.ErrorBoundary, + hasLoader: !!config.loader, + id, + module: "", + index: config.index, + path: config.path, + parentId: config.parentId + }; + return { + ...p, + [id]: route + }; + }, {}), + url: "", + version: "" + }, + entry: { + module: { + default: jest.fn( + async (request, responseStatusCode, responseHeaders, entryContext) => + new Response(null, { + status: responseStatusCode, + headers: responseHeaders + }) + ), + handleDataRequest: jest.fn(async response => response) + } + }, + routes: Object.entries(routes).reduce( + (p, [id, config]) => { + let route: Omit = { + id, + index: config.index, + path: config.path, + parentId: config.parentId, + module: { + default: config.default, + CatchBoundary: config.CatchBoundary, + ErrorBoundary: config.ErrorBoundary, + action: config.action, + headers: config.headers, + loader: config.loader + } + }; + return { + ...p, + [id]: route + }; + }, + {} + ) + }; +} + export function prettyHtml(source: string): string { return prettier.format(source, { parser: "html" }); } diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index 7cf2898192..254c6a80d3 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -1,7 +1,6 @@ -import type { Params } from "react-router"; - -import type { ServerBuild } from "./build"; -import { json } from "./responses"; +import type { RouteMatch } from "./routeMatching"; +import type { ServerRoute } from "./routes"; +import { json, isResponse, isRedirectResponse } from "./responses"; /** * An object of arbitrary for route loaders and actions provided by the @@ -14,24 +13,33 @@ export type AppLoadContext = any; */ export type AppData = any; -export async function loadRouteData( - build: ServerBuild, - routeId: string, - request: Request, - context: AppLoadContext, - params: Params -): Promise { - let routeModule = build.routes[routeId].module; - - if (!routeModule.loader) { - return Promise.resolve(json(null)); +export async function callRouteAction({ + loadContext, + match, + request +}: { + loadContext: unknown; + match: RouteMatch; + request: Request; +}) { + let action = match.route.module.action; + + if (!action) { + throw new Error( + `You made a ${request.method} request to ${request.url} but did not provide ` + + `an \`action\` for route "${match.route.id}", so there is no way to handle the ` + + `request.` + ); } let result; - try { - result = await routeModule.loader({ request, context, params }); - } catch (error) { + result = await action({ + request: stripDataParam(stripIndexParam(request.clone())), + context: loadContext, + params: match.params + }); + } catch (error: unknown) { if (!isResponse(error)) { throw error; } @@ -44,35 +52,41 @@ export async function loadRouteData( if (result === undefined) { throw new Error( - `You defined a loader for route "${routeId}" but didn't return ` + - `anything from your \`loader\` function. Please return a value or \`null\`.` + `You defined an action for route "${match.route.id}" but didn't return ` + + `anything from your \`action\` function. Please return a value or \`null\`.` ); } return isResponse(result) ? result : json(result); } -export async function callRouteAction( - build: ServerBuild, - routeId: string, - request: Request, - context: AppLoadContext, - params: Params -): Promise { - let routeModule = build.routes[routeId].module; - - if (!routeModule.action) { +export async function callRouteLoader({ + loadContext, + match, + request +}: { + request: Request; + match: RouteMatch; + loadContext: unknown; +}) { + let loader = match.route.module.loader; + + if (!loader) { throw new Error( `You made a ${request.method} request to ${request.url} but did not provide ` + - `an \`action\` for route "${routeId}", so there is no way to handle the ` + + `a \`loader\` for route "${match.route.id}", so there is no way to handle the ` + `request.` ); } let result; try { - result = await routeModule.action({ request, context, params }); - } catch (error) { + result = await loader({ + request: stripDataParam(stripIndexParam(request.clone())), + context: loadContext, + params: match.params + }); + } catch (error: unknown) { if (!isResponse(error)) { throw error; } @@ -85,7 +99,7 @@ export async function callRouteAction( if (result === undefined) { throw new Error( - `You defined an action for route "${routeId}" but didn't return ` + + `You defined an action for route "${match.route.id}" but didn't return ` + `anything from your \`action\` function. Please return a value or \`null\`.` ); } @@ -93,27 +107,30 @@ export async function callRouteAction( return isResponse(result) ? result : json(result); } -export function isCatchResponse(value: any) { - return isResponse(value) && value.headers.get("X-Remix-Catch") != null; -} +function stripIndexParam(request: Request) { + let url = new URL(request.url); + let indexValues = url.searchParams.getAll("index"); + url.searchParams.delete("index"); + let indexValuesToKeep = []; + for (let indexValue of indexValues) { + if (indexValue) { + indexValuesToKeep.push(indexValue); + } + } + for (let toKeep of indexValuesToKeep) { + url.searchParams.append("index", toKeep); + } -function isResponse(value: any): value is Response { - return ( - value != null && - typeof value.status === "number" && - typeof value.statusText === "string" && - typeof value.headers === "object" && - typeof value.body !== "undefined" - ); + return new Request(url.toString(), request); } -const redirectStatusCodes = new Set([301, 302, 303, 307, 308]); - -export function isRedirectResponse(response: Response): boolean { - return redirectStatusCodes.has(response.status); +function stripDataParam(request: Request) { + let url = new URL(request.url); + url.searchParams.delete("_data"); + return new Request(url.toString(), request); } -export function extractData(response: Response): Promise { +export function extractData(response: Response): Promise { let contentType = response.headers.get("Content-Type"); if (contentType && /\bapplication\/json\b/.test(contentType)) { diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index 25d0be08ed..c14c740d90 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -1,4 +1,4 @@ -import type { ComponentDidCatchEmulator } from "./errors"; +import type { AppState } from "./errors"; import type { RouteManifest, ServerRouteManifest, @@ -10,7 +10,7 @@ import type { RouteMatch } from "./routeMatching"; import type { RouteModules, EntryRouteModule } from "./routeModules"; export interface EntryContext { - componentDidCatchEmulator: ComponentDidCatchEmulator; + appState: AppState; manifest: AssetsManifest; matches: RouteMatch[]; routeData: RouteData; diff --git a/packages/remix-server-runtime/errors.ts b/packages/remix-server-runtime/errors.ts index 8b042673b8..7dad52c404 100644 --- a/packages/remix-server-runtime/errors.ts +++ b/packages/remix-server-runtime/errors.ts @@ -40,7 +40,7 @@ * line. */ -export interface ComponentDidCatchEmulator { +export interface AppState { error?: SerializedError; catch?: ThrownResponse; catchBoundaryRouteId: string | null; diff --git a/packages/remix-server-runtime/responses.ts b/packages/remix-server-runtime/responses.ts index 89892a2e15..1285f404a9 100644 --- a/packages/remix-server-runtime/responses.ts +++ b/packages/remix-server-runtime/responses.ts @@ -1,7 +1,10 @@ /** * A JSON response. Converts `data` to JSON and sets the `Content-Type` header. */ -export function json(data: Data, init: number | ResponseInit = {}): Response { +export function json( + data: Data, + init: number | ResponseInit = {} +): Response { let responseInit: any = init; if (typeof init === "number") { responseInit = { status: init }; @@ -26,9 +29,9 @@ export function redirect( url: string, init: number | ResponseInit = 302 ): Response { - let responseInit: any = init; - if (typeof init === "number") { - responseInit = { status: init }; + let responseInit = init; + if (typeof responseInit === "number") { + responseInit = { status: responseInit }; } else if (typeof responseInit.status === "undefined") { responseInit.status = 302; } @@ -41,3 +44,38 @@ export function redirect( headers }); } + +export function isResponse(value: any): value is Response { + return ( + value != null && + typeof value.status === "number" && + typeof value.statusText === "string" && + typeof value.headers === "object" && + typeof value.body !== "undefined" + ); +} + +const redirectStatusCodes = new Set([301, 302, 303, 307, 308]); +export function isRedirectResponse(response: Response): boolean { + return redirectStatusCodes.has(response.status); +} + +export function isCatchResponse(response: Response) { + return response.headers.get("X-Remix-Catch") != null; +} + +export function extractData(response: Response): Promise { + let contentType = response.headers.get("Content-Type"); + + if (contentType && /\bapplication\/json\b/.test(contentType)) { + return response.json(); + } + + // What other data types do we need to handle here? What other kinds of + // responses are people going to be returning from their loaders? + // - application/x-www-form-urlencoded ? + // - multipart/form-data ? + // - binary (audio/video) ? + + return response.text(); +} diff --git a/packages/remix-server-runtime/routeData.ts b/packages/remix-server-runtime/routeData.ts index d6da0dba2a..a147435e7f 100644 --- a/packages/remix-server-runtime/routeData.ts +++ b/packages/remix-server-runtime/routeData.ts @@ -1,24 +1,5 @@ import type { AppData } from "./data"; -import { extractData } from "./data"; -import type { ServerRoute } from "./routes"; -import type { RouteMatch } from "./routeMatching"; export interface RouteData { [routeId: string]: AppData; } - -export async function createRouteData( - matches: RouteMatch[], - responses: Response[] -): Promise { - let data = await Promise.all(responses.map(extractData)); - - return matches.reduce((memo, match, index) => { - memo[match.route.id] = data[index]; - return memo; - }, {} as RouteData); -} - -export async function createActionData(response: Response): Promise { - return extractData(response); -} diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index 385e1f57b9..a546cfcac4 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -1,8 +1,8 @@ import type { AppLoadContext } from "./data"; -import { extractData, isCatchResponse } from "./data"; -import { loadRouteData, callRouteAction, isRedirectResponse } from "./data"; -import type { ComponentDidCatchEmulator } from "./errors"; -import type { ServerBuild } from "./build"; +import { extractData } from "./data"; +import { callRouteAction, callRouteLoader } from "./data"; +import type { AppState } from "./errors"; +import type { HandleDataRequestFunction, ServerBuild } from "./build"; import type { EntryContext } from "./entry"; import { createEntryMatches, createEntryRouteModules } from "./entry"; import { serializeError } from "./errors"; @@ -13,8 +13,7 @@ import { matchServerRoutes } from "./routeMatching"; import { ServerMode, isServerMode } from "./mode"; import type { ServerRoute } from "./routes"; import { createRoutes } from "./routes"; -import { createActionData, createRouteData } from "./routeData"; -import { json } from "./responses"; +import { json, isRedirectResponse, isCatchResponse } from "./responses"; import { createServerHandoffString } from "./serverHandoff"; /** @@ -26,28 +25,6 @@ export interface RequestHandler { (request: Request, loadContext?: AppLoadContext): Promise; } -type RequestType = "data" | "document" | "resource"; - -function getRequestType( - request: Request, - matches: RouteMatch[] | null -): RequestType { - if (isDataRequest(request)) { - return "data"; - } - - if (!matches) { - return "document"; - } - - let match = matches.slice(-1)[0]; - if (!match.route.module.default) { - return "resource"; - } - - return "document"; -} - /** * Creates a function that serves HTTP requests. */ @@ -59,49 +36,43 @@ export function createRequestHandler( let routes = createRoutes(build.routes); let serverMode = isServerMode(mode) ? mode : ServerMode.Production; - return async (request, loadContext = {}) => { + return async function requestHandler(request, loadContext) { let url = new URL(request.url); let matches = matchServerRoutes(routes, url.pathname); - - let requestType = getRequestType(request, matches); + let requestType = getRequestType(url, matches); let response: Response; - switch (requestType) { - // has _data case "data": - response = await handleDataRequest( + response = await handleDataRequest({ request, loadContext, - build, - platform, - matches - ); + matches: matches!, + handleDataRequest: build.entry.module.handleDataRequest, + serverMode + }); break; - // no _data & default export case "document": - response = await handleDocumentRequest( - request, - loadContext, + response = await renderDocumentRequest({ build, - platform, + loadContext, + matches, + request, routes, serverMode - ); + }); break; - // no _data or default export case "resource": - response = await handleResourceRequest( + response = await handleResourceRequest({ request, loadContext, - build, - platform, - matches - ); + matches: matches!, + serverMode + }); break; } - if (isHeadRequest(request)) { + if (request.method.toLowerCase() === "head") { return new Response(null, { headers: response.headers, status: response.status, @@ -112,172 +83,118 @@ export function createRequestHandler( return response; }; } +async function handleDataRequest({ + handleDataRequest, + loadContext, + matches, + request, + serverMode +}: { + handleDataRequest?: HandleDataRequestFunction; + loadContext: unknown; + matches: RouteMatch[]; + request: Request; + serverMode: ServerMode; +}): Promise { + if (!isValidRequestMethod(request)) { + return errorBoundaryError( + new Error(`Invalid request method "${request.method}"`), + 405 + ); + } -async function handleResourceRequest( - request: Request, - loadContext: AppLoadContext, - build: ServerBuild, - platform: ServerPlatform, - matches: RouteMatch[] | null -): Promise { let url = new URL(request.url); if (!matches) { - return jsonError(`No route matches URL "${url.pathname}"`, 404); + return errorBoundaryError( + new Error(`No route matches URL "${url.pathname}"`), + 404 + ); } - let routeMatch: RouteMatch = matches.slice(-1)[0]; + let response: Response; + let match: RouteMatch; try { - return isActionRequest(request) - ? await callRouteAction( - build, - routeMatch.route.id, - request, - loadContext, - routeMatch.params - ) - : await loadRouteData( - build, - routeMatch.route.id, - request, - loadContext, - routeMatch.params - ); - } catch (error: any) { - let formattedError = (await platform.formatServerError?.(error)) || error; - throw formattedError; - } -} + if (isActionRequest(request)) { + match = getActionRequestMatch(url, matches); -async function handleDataRequest( - request: Request, - loadContext: AppLoadContext, - build: ServerBuild, - platform: ServerPlatform, - matches: RouteMatch[] | null -): Promise { - if (!isValidRequestMethod(request)) { - return jsonError(`Invalid request method "${request.method}"`, 405); - } + response = await callRouteAction({ + loadContext, + match, + request: request + }); + } else { + let routeId = url.searchParams.get("_data"); + if (!routeId) { + return errorBoundaryError(new Error(`Missing route id in ?_data`), 403); + } - let url = new URL(request.url); + let tempMatch = matches.find(match => match.route.id === routeId); + if (!tempMatch) { + return errorBoundaryError( + new Error(`Route "${routeId}" does not match URL "${url.pathname}"`), + 403 + ); + } + match = tempMatch; - if (!matches) { - return jsonError(`No route matches URL "${url.pathname}"`, 404); - } + response = await callRouteLoader({ loadContext, match, request }); + } - let routeMatch: RouteMatch; - if (isActionRequest(request)) { - routeMatch = matches[matches.length - 1]; + if (isRedirectResponse(response)) { + // We don't have any way to prevent a fetch request from following + // redirects. So we use the `X-Remix-Redirect` header to indicate the + // next URL, and then "follow" the redirect manually on the client. + let headers = new Headers(response.headers); + headers.set("X-Remix-Redirect", headers.get("Location")!); + headers.delete("Location"); - if ( - !isIndexRequestUrl(url) && - matches[matches.length - 1].route.id.endsWith("/index") - ) { - routeMatch = matches[matches.length - 2]; - } - } else { - let routeId = url.searchParams.get("_data"); - if (!routeId) { - return jsonError(`Missing route id in ?_data`, 403); + return new Response(null, { + status: 204, + headers + }); } - let match = matches.find(match => match.route.id === routeId); - if (!match) { - return jsonError( - `Route "${routeId}" does not match URL "${url.pathname}"`, - 403 - ); + if (handleDataRequest) { + response = await handleDataRequest(response.clone(), { + context: loadContext, + params: match.params, + request: request.clone() + }); } - routeMatch = match; - } - - let response: Response; - try { - response = isActionRequest(request) - ? await callRouteAction( - build, - routeMatch.route.id, - stripIndexParam(stripDataParam(request.clone())), - loadContext, - routeMatch.params - ) - : await loadRouteData( - build, - routeMatch.route.id, - stripIndexParam(stripDataParam(request.clone())), - loadContext, - routeMatch.params - ); - } catch (error: any) { - let formattedError = (await platform.formatServerError?.(error)) || error; - response = json(await serializeError(formattedError), { - status: 500, - headers: { - "X-Remix-Error": "unfortunately, yes" - } - }); - } + return response; + } catch (error: unknown) { + if (serverMode !== ServerMode.Test) { + console.error(error); + } - if (isRedirectResponse(response)) { - // We don't have any way to prevent a fetch request from following - // redirects. So we use the `X-Remix-Redirect` header to indicate the - // next URL, and then "follow" the redirect manually on the client. - let headers = new Headers(response.headers); - headers.set("X-Remix-Redirect", headers.get("Location")!); - headers.delete("Location"); - - return new Response(null, { - status: 204, - headers - }); - } + if (serverMode === ServerMode.Development) { + return errorBoundaryError(error as Error, 500); + } - if (build.entry.module.handleDataRequest) { - return build.entry.module.handleDataRequest(response, { - request: request.clone(), - context: loadContext, - params: routeMatch.params - }); + return errorBoundaryError(new Error("Unexpected Server Error"), 500); } - - return response; } -async function handleDocumentRequest( - request: Request, - loadContext: AppLoadContext, - build: ServerBuild, - platform: ServerPlatform, - routes: ServerRoute[], - serverMode: ServerMode -): Promise { +async function renderDocumentRequest({ + build, + loadContext, + matches, + request, + routes, + serverMode +}: { + build: ServerBuild; + loadContext: unknown; + matches: RouteMatch[] | null; + request: Request; + routes: ServerRoute[]; + serverMode?: ServerMode; +}): Promise { let url = new URL(request.url); - let requestState: "ok" | "no-match" | "invalid-request" = - isValidRequestMethod(request) ? "ok" : "invalid-request"; - let matches = - requestState === "ok" ? matchServerRoutes(routes, url.pathname) : null; - - if (!matches) { - // If we do not match a user-provided-route, fall back to the root - // to allow the CatchBoundary to take over while maintining invalid - // request state if already set - if (requestState === "ok") { - requestState = "no-match"; - } - - matches = [ - { - params: {}, - pathname: "", - route: routes[0] - } - ]; - } - - let componentDidCatchEmulator: ComponentDidCatchEmulator = { + let appState: AppState = { trackBoundaries: true, trackCatchBoundaries: true, catchBoundaryRouteId: null, @@ -287,225 +204,255 @@ async function handleDocumentRequest( catch: undefined }; - let responseState: "ok" | "caught" | "error" = "ok"; + if (!isValidRequestMethod(request)) { + matches = null; + appState.trackCatchBoundaries = false; + appState.catch = { + data: null, + status: 405, + statusText: "Method Not Allowed" + }; + } else if (!matches) { + appState.trackCatchBoundaries = false; + appState.catch = { + data: null, + status: 404, + statusText: "Not Found" + }; + } + + let actionStatus: { status: number; statusText: string } | undefined; + let actionData: Record | undefined; + let actionMatch: RouteMatch | undefined; let actionResponse: Response | undefined; - let actionRouteId: string | undefined; - if (requestState !== "ok") { - responseState = "caught"; - componentDidCatchEmulator.trackCatchBoundaries = false; - let withBoundaries = getMatchesUpToDeepestBoundary( - matches, - "CatchBoundary" - ); - componentDidCatchEmulator.catchBoundaryRouteId = - withBoundaries.length > 0 - ? withBoundaries[withBoundaries.length - 1].route.id - : null; - componentDidCatchEmulator.catch = { - status: requestState === "no-match" ? 404 : 405, - statusText: - requestState === "no-match" ? "Not Found" : "Method Not Allowed", - data: null - }; - } else if (isActionRequest(request)) { - let actionMatch = matches[matches.length - 1]; - if (!isIndexRequestUrl(url) && actionMatch.route.id.endsWith("/index")) { - actionMatch = matches[matches.length - 2]; - } - actionRouteId = actionMatch.route.id; + if (matches && isActionRequest(request)) { + actionMatch = getActionRequestMatch(url, matches); try { - actionResponse = await callRouteAction( - build, - actionMatch.route.id, - stripIndexParam(stripDataParam(request.clone())), + actionResponse = await callRouteAction({ loadContext, - actionMatch.params - ); + match: actionMatch, + request: request + }); + if (isRedirectResponse(actionResponse)) { return actionResponse; } + + actionStatus = { + status: actionResponse.status, + statusText: actionResponse.statusText + }; + + if (isCatchResponse(actionResponse)) { + appState.catchBoundaryRouteId = getDeepestRouteIdWithBoundary( + matches, + "CatchBoundary" + ); + appState.trackCatchBoundaries = false; + appState.catch = { + ...actionStatus, + data: await extractData(actionResponse) + }; + } else { + actionData = { + [actionMatch.route.id]: await extractData(actionResponse) + }; + } } catch (error: any) { - let formattedError = (await platform.formatServerError?.(error)) || error; - responseState = "error"; - let withBoundaries = getMatchesUpToDeepestBoundary( + appState.loaderBoundaryRouteId = getDeepestRouteIdWithBoundary( matches, "ErrorBoundary" ); - componentDidCatchEmulator.loaderBoundaryRouteId = - withBoundaries[withBoundaries.length - 1].route.id; - componentDidCatchEmulator.error = await serializeError(formattedError); + appState.trackBoundaries = false; + appState.error = await serializeError(error); + + if (serverMode !== ServerMode.Test) { + console.error( + `There was an error running the action for route ${actionMatch.route.id}` + ); + } } } - if (actionResponse && isCatchResponse(actionResponse)) { - responseState = "caught"; - let withBoundaries = getMatchesUpToDeepestBoundary( - matches, + let routeModules = createEntryRouteModules(build.routes); + + let matchesToLoad = matches || []; + if (appState.catch) { + matchesToLoad = getMatchesUpToDeepestBoundary( + // get rid of the action, we don't want to call it's loader either + // because we'll be rendering the catch boundary, if you can get access + // to the loader data in the catch boundary then how the heck is it + // supposed to deal with thrown responses? + matchesToLoad.slice(0, -1), "CatchBoundary" ); - componentDidCatchEmulator.trackCatchBoundaries = false; - componentDidCatchEmulator.catchBoundaryRouteId = - withBoundaries[withBoundaries.length - 1].route.id; - componentDidCatchEmulator.catch = { - status: actionResponse.status, - statusText: actionResponse.statusText, - data: await extractData(actionResponse.clone()) - }; - } - - // If we did not match a route, there is no need to call any loaders - let matchesToLoad = requestState !== "ok" ? [] : matches; - switch (responseState) { - case "caught": - matchesToLoad = getMatchesUpToDeepestBoundary( - // get rid of the action, we don't want to call it's loader either - // because we'll be rendering the catch boundary, if you can get access - // to the loader data in the catch boundary then how the heck is it - // supposed to deal with thrown responses? - matches.slice(0, -1), - "CatchBoundary" - ); - break; - case "error": - matchesToLoad = getMatchesUpToDeepestBoundary( - // get rid of the action, we don't want to call it's loader either - // because we'll be rendering the error boundary, if you can get access - // to the loader data in the error boundary then how the heck is it - // supposed to deal with errors in the loader, too? - matches.slice(0, -1), - "ErrorBoundary" - ); - break; + } else if (appState.error) { + matchesToLoad = getMatchesUpToDeepestBoundary( + // get rid of the action, we don't want to call it's loader either + // because we'll be rendering the error boundary, if you can get access + // to the loader data in the error boundary then how the heck is it + // supposed to deal with errors in the loader, too? + matchesToLoad.slice(0, -1), + "ErrorBoundary" + ); } - // Run all data loaders in parallel. Await them in series below. Note: This - // code is a little weird due to the way unhandled promise rejections are - // handled in node. We use a .catch() handler on each promise to avoid the - // warning, then handle errors manually afterwards. - let routeLoaderPromises: Promise[] = matchesToLoad.map( - match => - loadRouteData( - build, - match.route.id, - stripIndexParam(stripDataParam(request.clone())), - loadContext, - match.params - ).catch(error => error) + let routeLoaderResults = await Promise.allSettled( + matchesToLoad.map(match => + match.route.module.loader + ? callRouteLoader({ + loadContext, + match, + request + }) + : Promise.resolve(undefined) + ) ); - let routeLoaderResults = await Promise.all(routeLoaderPromises); - for (let [index, response] of routeLoaderResults.entries()) { - let route = matches[index].route; - let routeModule = build.routes[route.id].module; - - // Rare case where an action throws an error, and then when we try to render - // the action's page to tell the user about the the error, a loader above - // the action route *also* threw an error or tried to redirect! - // - // Instead of rendering the loader error or redirecting like usual, we - // ignore the loader error or redirect because the action error was first - // and is higher priority to surface. Perhaps the action error is the - // reason the loader blows up now! It happened first and is more important - // to address. - // - // We just give up and move on with rendering the error as deeply as we can, - // which is the previous iteration of this loop - if ( - (responseState === "error" && - (response instanceof Error || isRedirectResponse(response))) || - (responseState === "caught" && isCatchResponse(response)) - ) { + // Store the state of the action. We will use this to determine later + // what catch or error boundary should be rendered under cases where + // actions don't throw but loaders do, actions throw and parent loaders + // also throw, etc. + let actionCatch = appState.catch; + let actionError = appState.error; + let actionCatchBoundaryRouteId = appState.catchBoundaryRouteId; + let actionLoaderBoundaryRouteId = appState.loaderBoundaryRouteId; + // Reset the app error and catch state to propogate the loader states + // from the results into the app state. + appState.catch = undefined; + appState.error = undefined; + + let headerMatches: RouteMatch[] = []; + let routeLoaderResponses: Response[] = []; + let loaderStatusCodes: number[] = []; + let routeData: Record = {}; + for (let index = 0; index < matchesToLoad.length; index++) { + let match = matchesToLoad[index]; + let result = routeLoaderResults[index]; + + let error = result.status === "rejected" ? result.reason : undefined; + let response = result.status === "fulfilled" ? result.value : undefined; + let isRedirect = response ? isRedirectResponse(response) : false; + let isCatch = response ? isCatchResponse(response) : false; + + // If a parent loader has already caught or error'd, bail because + // we don't need any more child data. + if (appState.catch || appState.error) { break; } - if (componentDidCatchEmulator.catch || componentDidCatchEmulator.error) { - continue; + // If there is a response and it's a redirect, do it unless there + // is an action error or catch state, those action boundary states + // take precedence over loader sates, this means if a loader redirects + // after an action catches or errors we won't follow it, and instead + // render the boundary caused by the action. + if (!actionCatch && !actionError && response && isRedirect) { + return response; } - if (routeModule.CatchBoundary) { - componentDidCatchEmulator.catchBoundaryRouteId = route.id; + // Track the boundary ID's for the loaders + if (match.route.module.CatchBoundary) { + appState.catchBoundaryRouteId = match.route.id; } - - if (routeModule.ErrorBoundary) { - componentDidCatchEmulator.loaderBoundaryRouteId = route.id; + if (match.route.module.ErrorBoundary) { + appState.loaderBoundaryRouteId = match.route.id; } - if (response instanceof Error) { + if (error) { + loaderStatusCodes.push(500); + appState.trackBoundaries = false; + appState.error = await serializeError(error); + if (serverMode !== ServerMode.Test) { console.error( - `There was an error running the data loader for route ${route.id}` + `There was an error running the data loader for route ${match.route.id}` ); } - - let formattedError = - (await platform.formatServerError?.(response)) || response; - - componentDidCatchEmulator.error = await serializeError(formattedError); - routeLoaderResults[index] = json(null, { status: 500 }); - } else if (isRedirectResponse(response)) { - return response; - } else if (isCatchResponse(response)) { - componentDidCatchEmulator.trackCatchBoundaries = false; - componentDidCatchEmulator.catch = { - status: response.status, - statusText: response.statusText, - data: await extractData(response.clone()) - }; - routeLoaderResults[index] = json(null, { status: response.status }); + break; + } else if (response) { + headerMatches.push(match); + routeLoaderResponses.push(response); + loaderStatusCodes.push(response.status); + + if (isCatch) { + // If it's a catch response, store it in app state, and bail + appState.trackCatchBoundaries = false; + appState.catch = { + data: await extractData(response), + status: response.status, + statusText: response.statusText + }; + break; + } else { + // Extract and store the loader data + routeData[match.route.id] = await extractData(response); + } } } - // We already filtered out all Errors, so these are all Responses. - let routeLoaderResponses: Response[] = routeLoaderResults as Response[]; + // If there was not a loader catch or error state triggered reset the + // boundaries as they are probably deeper in the tree if the action + // initially triggered a boundary as that match would not exist in the + // matches to load. + if (!appState.catch) { + appState.catchBoundaryRouteId = actionCatchBoundaryRouteId; + } + if (!appState.error) { + appState.loaderBoundaryRouteId = actionLoaderBoundaryRouteId; + } + // If there was an action error or catch, we will reset the state to the + // initial values, otherwise we will use whatever came out of the loaders. + appState.catch = actionCatch || appState.catch; + appState.error = actionError || appState.error; + + let renderableMatches = getRenderableMatches(matches, appState); + if (!renderableMatches) { + renderableMatches = []; + + let root = routes[0]; + if (root && root.module.CatchBoundary) { + appState.catchBoundaryRouteId = "root"; + renderableMatches.push({ + params: {}, + pathname: "", + route: routes[0] + }); + } + } // Handle responses with a non-200 status code. The first loader with a // non-200 status code determines the status code for the whole response. - let notOkResponse = [actionResponse, ...routeLoaderResponses].find( - response => response && response.status !== 200 - ); - - let statusCode = - requestState === "no-match" - ? 404 - : requestState === "invalid-request" - ? 405 - : responseState === "error" - ? 500 - : notOkResponse - ? notOkResponse.status - : 200; - - let renderableMatches = getRenderableMatches( - matches, - componentDidCatchEmulator - ); - let serverEntryModule = build.entry.module; - let headers = getDocumentHeaders( + let notOkResponse = + actionStatus && actionStatus.status !== 200 + ? actionStatus.status + : loaderStatusCodes.find(status => status !== 200); + + let responseStatusCode = appState.error + ? 500 + : typeof notOkResponse === "number" + ? notOkResponse + : appState.catch + ? appState.catch.status + : 200; + + let responseHeaders = getDocumentHeaders( build, renderableMatches, routeLoaderResponses, actionResponse ); + let entryMatches = createEntryMatches(renderableMatches, build.assets.routes); - let routeData = await createRouteData( - renderableMatches, - routeLoaderResponses - ); - let actionData = - actionResponse && actionRouteId - ? { - [actionRouteId]: await createActionData(actionResponse.clone()) - } - : undefined; - let routeModules = createEntryRouteModules(build.routes); + let serverHandoff = { + actionData, + appState: appState, matches: entryMatches, - componentDidCatchEmulator, - routeData, - actionData + routeData }; + let entryContext: EntryContext = { ...serverHandoff, manifest: build.assets, @@ -513,21 +460,16 @@ async function handleDocumentRequest( serverHandoffString: createServerHandoffString(serverHandoff) }; - let response: Response; + let handleDocumentRequest = build.entry.module.default; try { - response = await serverEntryModule.default( - request, - statusCode, - headers, + return await handleDocumentRequest( + request.clone(), + responseStatusCode, + responseHeaders, entryContext ); } catch (error: any) { - let formattedError = (await platform.formatServerError?.(error)) || error; - if (serverMode !== ServerMode.Test) { - console.error(formattedError); - } - - statusCode = 500; + responseStatusCode = 500; // Go again, this time with the componentDidCatch emulation. As it rendered // last time we mutated `componentDidCatch.routeId` for the last rendered @@ -535,41 +477,99 @@ async function handleDocumentRequest( // hacky but that's how hooks work). This tells the emulator to stop // tracking the `routeId` as we render because we already have an error to // render. - componentDidCatchEmulator.trackBoundaries = false; - componentDidCatchEmulator.error = await serializeError(formattedError); + appState.trackBoundaries = false; + appState.error = await serializeError(error); entryContext.serverHandoffString = createServerHandoffString(serverHandoff); try { - response = await serverEntryModule.default( - request, - statusCode, - headers, + return await handleDocumentRequest( + request.clone(), + responseStatusCode, + responseHeaders, entryContext ); } catch (error: any) { - let formattedError = (await platform.formatServerError?.(error)) || error; if (serverMode !== ServerMode.Test) { - console.error(formattedError); + console.error(error); + } + + let message = "Unexpected Server Error"; + + if (serverMode === ServerMode.Development) { + message += `\n\n${String(error)}`; } // Good grief folks, get your act together 😂! - response = new Response( - `Unexpected Server Error\n\n${formattedError.message}`, - { - status: 500, - headers: { - "Content-Type": "text/plain" - } + return new Response(message, { + status: 500, + headers: { + "Content-Type": "text/plain" } - ); + }); } } +} + +async function handleResourceRequest({ + loadContext, + matches, + request, + serverMode +}: { + request: Request; + loadContext: unknown; + matches: RouteMatch[]; + serverMode: ServerMode; +}): Promise { + let match = matches.slice(-1)[0]; - return response; + try { + if (isActionRequest(request)) { + return await callRouteAction({ match, loadContext, request }); + } else { + return await callRouteLoader({ match, loadContext, request }); + } + } catch (error: any) { + if (serverMode !== ServerMode.Test) { + console.error(error); + } + + let message = "Unexpected Server Error"; + + if (serverMode === ServerMode.Development) { + message += `\n\n${String(error)}`; + } + + // Good grief folks, get your act together 😂! + return new Response(message, { + status: 500, + headers: { + "Content-Type": "text/plain" + } + }); + } } -function jsonError(error: string, status = 403): Response { - return json({ error }, { status }); +type RequestType = "data" | "document" | "resource"; + +function getRequestType( + url: URL, + matches: RouteMatch[] | null +): RequestType { + if (url.searchParams.has("_data")) { + return "data"; + } + + if (!matches) { + return "document"; + } + + let match = matches.slice(-1)[0]; + if (!match.route.module.default) { + return "resource"; + } + + return "document"; } function isActionRequest(request: Request): boolean { @@ -582,6 +582,10 @@ function isActionRequest(request: Request): boolean { ); } +function isHeadRequest(request: Request): boolean { + return request.method.toLowerCase() === "head"; +} + function isValidRequestMethod(request: Request): boolean { return ( request.method.toLowerCase() === "get" || @@ -590,12 +594,13 @@ function isValidRequestMethod(request: Request): boolean { ); } -function isHeadRequest(request: Request): boolean { - return request.method.toLowerCase() === "head"; -} - -function isDataRequest(request: Request): boolean { - return new URL(request.url).searchParams.has("_data"); +async function errorBoundaryError(error: Error, status: number) { + return json(await serializeError(error), { + status, + headers: { + "X-Remix-Error": "yes" + } + }); } function isIndexRequestUrl(url: URL) { @@ -610,30 +615,24 @@ function isIndexRequestUrl(url: URL) { return indexRequest; } -function stripIndexParam(request: Request) { - let url = new URL(request.url); - let indexValues = url.searchParams.getAll("index"); - url.searchParams.delete("index"); - let indexValuesToKeep = []; - for (let indexValue of indexValues) { - if (indexValue) { - indexValuesToKeep.push(indexValue); - } - } - for (let toKeep of indexValuesToKeep) { - url.searchParams.append("index", toKeep); +function getActionRequestMatch(url: URL, matches: RouteMatch[]) { + let match = matches.slice(-1)[0]; + + if (!isIndexRequestUrl(url) && match.route.id.endsWith("/index")) { + return matches.slice(-2)[0]; } - return new Request(url.toString(), request); + return match; } -function stripDataParam(request: Request) { - let url = new URL(request.url); - url.searchParams.delete("_data"); - return new Request(url.toString(), request); +function getDeepestRouteIdWithBoundary( + matches: RouteMatch[], + key: "CatchBoundary" | "ErrorBoundary" +) { + let matched = getMatchesUpToDeepestBoundary(matches, key).slice(-1)[0]; + return matched ? matched.route.id : null; } -// TODO: update to use key for lookup function getMatchesUpToDeepestBoundary( matches: RouteMatch[], key: "CatchBoundary" | "ErrorBoundary" @@ -657,11 +656,15 @@ function getMatchesUpToDeepestBoundary( // This prevents `` from rendering anything below where the error threw // TODO: maybe do this in function getRenderableMatches( - matches: RouteMatch[], - componentDidCatchEmulator: ComponentDidCatchEmulator + matches: RouteMatch[] | null, + appState: AppState ) { + if (!matches) { + return null; + } + // no error, no worries - if (!componentDidCatchEmulator.catch && !componentDidCatchEmulator.error) { + if (!appState.catch && !appState.error) { return matches; } @@ -670,9 +673,9 @@ function getRenderableMatches( matches.forEach((match, index) => { let id = match.route.id; if ( - componentDidCatchEmulator.renderBoundaryRouteId === id || - componentDidCatchEmulator.loaderBoundaryRouteId === id || - componentDidCatchEmulator.catchBoundaryRouteId === id + appState.renderBoundaryRouteId === id || + appState.loaderBoundaryRouteId === id || + appState.catchBoundaryRouteId === id ) { lastRenderableIndex = index; } From 29421df8935c1c7509b405385876d71f01f23ecf Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Wed, 8 Dec 2021 18:17:30 -0800 Subject: [PATCH 0154/1690] Version 0.0.0-experimental-b697c4f3 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 37106e93dc..891327c09e 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.0.6", + "version": "0.0.0-experimental-b697c4f3", "license": "MIT", "repository": { "type": "git", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 4f9e251d34..e3fbde7d3e 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.0.6", + "version": "0.0.0-experimental-b697c4f3", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.0.6", + "@remix-run/node": "0.0.0-experimental-b697c4f3", "@remix-run/server-runtime": "1.0.6" }, "peerDependencies": { diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index d90aa32e37..c82e6de725 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.0.6", + "version": "0.0.0-experimental-b697c4f3", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.0.6", + "@remix-run/server-runtime": "0.0.0-experimental-b697c4f3", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 757ec77a8d..0f94f0fe28 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.0.6", + "version": "0.0.0-experimental-b697c4f3", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.0.6", + "@remix-run/express": "0.0.0-experimental-b697c4f3", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 0eb6827fc3..1c57b687e2 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.0.6", + "version": "0.0.0-experimental-b697c4f3", "license": "MIT", "repository": { "type": "git", From 15bc97e4f38bedff365bcd2d13e2c139e5bfd2a0 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 9 Dec 2021 15:27:38 -0800 Subject: [PATCH 0155/1690] feat: Introduce `@remix-run/eslint-config` package (#357) to the trepidation of the founders: remix-eslint. --- packages/remix-server-runtime/__tests__/server-test.ts | 2 -- packages/remix-server-runtime/__tests__/utils.ts | 8 +------- packages/remix-server-runtime/server.ts | 3 +-- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/remix-server-runtime/__tests__/server-test.ts b/packages/remix-server-runtime/__tests__/server-test.ts index 6d727f5121..b3f479be61 100644 --- a/packages/remix-server-runtime/__tests__/server-test.ts +++ b/packages/remix-server-runtime/__tests__/server-test.ts @@ -19,8 +19,6 @@ function spyConsole() { } describe("server", () => { - let spy = spyConsole(); - let routeId = "root"; let build: ServerBuild = { entry: { diff --git a/packages/remix-server-runtime/__tests__/utils.ts b/packages/remix-server-runtime/__tests__/utils.ts index 90d2d15c85..e5a6ddbc8b 100644 --- a/packages/remix-server-runtime/__tests__/utils.ts +++ b/packages/remix-server-runtime/__tests__/utils.ts @@ -1,12 +1,6 @@ import prettier from "prettier"; -import type { - ActionFunction, - HandleDataRequestFunction, - HandleDocumentRequestFunction, - HeadersFunction, - LoaderFunction -} from "../"; +import type { ActionFunction, HeadersFunction, LoaderFunction } from "../"; import type { EntryRoute, ServerRoute, ServerRouteManifest } from "../routes"; export function mockServerBuild( diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index a546cfcac4..ea60feeb5c 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -1,6 +1,5 @@ import type { AppLoadContext } from "./data"; -import { extractData } from "./data"; -import { callRouteAction, callRouteLoader } from "./data"; +import { callRouteAction, callRouteLoader, extractData } from "./data"; import type { AppState } from "./errors"; import type { HandleDataRequestFunction, ServerBuild } from "./build"; import type { EntryContext } from "./entry"; From c51aebae1bbf4cc0d335d8a2f9558f6e91a715ba Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 10 Dec 2021 10:07:18 -0800 Subject: [PATCH 0156/1690] revert: experimantal version accidentally merged (#981) --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 4 ++-- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 891327c09e..37106e93dc 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "0.0.0-experimental-b697c4f3", + "version": "1.0.6", "license": "MIT", "repository": { "type": "git", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index e3fbde7d3e..4f9e251d34 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "0.0.0-experimental-b697c4f3", + "version": "1.0.6", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "0.0.0-experimental-b697c4f3", + "@remix-run/node": "1.0.6", "@remix-run/server-runtime": "1.0.6" }, "peerDependencies": { diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index c82e6de725..d90aa32e37 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "0.0.0-experimental-b697c4f3", + "version": "1.0.6", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "0.0.0-experimental-b697c4f3", + "@remix-run/server-runtime": "1.0.6", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 0f94f0fe28..757ec77a8d 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "0.0.0-experimental-b697c4f3", + "version": "1.0.6", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "0.0.0-experimental-b697c4f3", + "@remix-run/express": "1.0.6", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 1c57b687e2..0eb6827fc3 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "0.0.0-experimental-b697c4f3", + "version": "1.0.6", "license": "MIT", "repository": { "type": "git", From 66b007a25950e0c10a5be807f6d55b0843ae8513 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Fri, 10 Dec 2021 13:45:27 -0500 Subject: [PATCH 0157/1690] chore: move Outlet context to React Router (#974) * chore: moves Outlet's context to React Router * chore(deps): bump react-router-dom to latest * chore: remove outlet context test --- packages/remix-server-runtime/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 0eb6827fc3..c3c91aa42e 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -15,7 +15,7 @@ "@types/cookie": "^0.4.0", "cookie": "^0.4.1", "jsesc": "^3.0.1", - "react-router-dom": "^6.0.2", + "react-router-dom": "^6.1.0", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3" }, From 00ebbc7543031cfdcf392bd1d753d992394eee21 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 10 Dec 2021 12:07:42 -0800 Subject: [PATCH 0158/1690] feat: allow ESM output of server bundle (#976) chore: updated package.json from "browser" to "module" field --- .../remix-dev/__tests__/readConfig-test.ts | 1 + packages/remix-dev/compiler.ts | 2 +- packages/remix-dev/config.ts | 24 +++++++++++++++++-- packages/remix-server-runtime/package.json | 2 ++ 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 27f4bf7a19..b31af42d29 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -433,6 +433,7 @@ describe("readConfig", () => { }, "serverBuildDirectory": Any, "serverMode": "production", + "serverModuleFormat": "cjs", } ` ); diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index fede87c86b..f085773180 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -345,7 +345,7 @@ async function createServerBuild( }, outfile: path.resolve(config.serverBuildDirectory, "index.js"), platform: "node", - format: "cjs", + format: config.serverModuleFormat, target: options.target, inject: [reactShim], loader: loaders, diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 219a439ad6..34f7249f06 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -70,11 +70,19 @@ export interface AppConfig { */ devServerPort?: number; /** - * The delay before the dev server broadcasts a reload event + * The delay before the dev server broadcasts a reload event. */ devServerBroadcastDelay?: number; + /** + * Additional MDX remark / rehype plugins. + */ mdx?: RemixMdxConfig | RemixMdxConfigFunction; + + /** + * The output format of the server build. Defaults to "cjs". + */ + serverModuleFormat: "esm" | "cjs"; } /** @@ -141,7 +149,15 @@ export interface RemixConfig { */ devServerBroadcastDelay: number; + /** + * Additional MDX remark / rehype plugins. + */ mdx?: RemixMdxConfig | RemixMdxConfigFunction; + + /** + * The output format of the server build. Defaults to "cjs". + */ + serverModuleFormat: "esm" | "cjs"; } /** @@ -170,6 +186,9 @@ export async function readConfig( throw new Error(`Error loading Remix config in ${configFile}`); } + let serverModuleFormat = appConfig.serverModuleFormat || "cjs"; + let mdx = appConfig.mdx; + let appDirectory = path.resolve( rootDirectory, appConfig.appDirectory || "app" @@ -243,7 +262,8 @@ export async function readConfig( routes, serverBuildDirectory, serverMode, - mdx: appConfig.mdx + serverModuleFormat, + mdx }; } diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index c3c91aa42e..f1d7433a13 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -3,6 +3,8 @@ "description": "Server runtime for Remix", "version": "1.0.6", "license": "MIT", + "main": "./index.js", + "module": "./esm/index.js", "repository": { "type": "git", "url": "https://github.com/remix-run/remix", From 932c2ab51879d3fabcc53949821bc5c505bf515b Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Sat, 11 Dec 2021 15:56:03 -0800 Subject: [PATCH 0159/1690] fix(dev): Ignore dotfiles in routes directory (#988) * fix(dev): ignore dotfiles in routes directory * chore: add dotfile to gists app --- packages/remix-dev/config/routesConvention.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 15e4fda5b7..9dddcf0524 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -30,11 +30,17 @@ export function defineConventionalRoutes(appDir: string): RouteManifest { if (isRouteModuleFile(file)) { files[routeId] = path.join("routes", file); - } else { - throw new Error( - `Invalid route module file: ${path.join(appDir, "routes", file)}` - ); + return; + } + + // https://github.com/remix-run/remix/issues/391 + if (path.basename(file).startsWith(".")) { + return; } + + throw new Error( + `Invalid route module file: ${path.join(appDir, "routes", file)}` + ); }); let routeIds = Object.keys(files).sort(byLongestFirst); From 4ff355e06b210fd49276b130f09b9c97bef68dfe Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Sat, 11 Dec 2021 16:00:05 -0800 Subject: [PATCH 0160/1690] chore: update React Router to v6.1.1 (#987) --- packages/remix-server-runtime/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index f1d7433a13..2e007276b3 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -17,7 +17,7 @@ "@types/cookie": "^0.4.0", "cookie": "^0.4.1", "jsesc": "^3.0.1", - "react-router-dom": "^6.1.0", + "react-router-dom": "^6.1.1", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3" }, From 290f4a2434e1d5855cbbcc29d8690a9e7f993f10 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 13 Dec 2021 10:58:55 -0800 Subject: [PATCH 0161/1690] feat: define process.env.NODE_ENV for server build (#983) feat: added typedefs for process.env.NODE_ENV This will help avoid descrepancies at runtime between it being defined for the browser and a different runtime value for the server. This has been a source of issues for many regarding surfacing `` in production. --- packages/remix-dev/compiler.ts | 3 +++ packages/remix-node/globals.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index f085773180..d8f853091b 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -357,6 +357,9 @@ async function createServerBuild( // of CSS and other files. assetNames: "_assets/[name]-[hash]", publicPath: config.publicPath, + define: { + "process.env.NODE_ENV": JSON.stringify(options.mode) + }, plugins: [ mdxPlugin(config), serverRouteModulesPlugin(config), diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts index abf8f9cdf7..168bf02f7a 100644 --- a/packages/remix-node/globals.ts +++ b/packages/remix-node/globals.ts @@ -16,6 +16,10 @@ import { FormData as NodeFormData } from "./formData"; declare global { namespace NodeJS { + interface ProcessEnv { + NODE_ENV: "development" | "production" | "test"; + } + interface Global { atob: typeof atob; btoa: typeof btoa; From 4ffa9e3facd17100d371bf9d86ab6d16c10896f3 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 13 Dec 2021 14:24:41 -0800 Subject: [PATCH 0162/1690] chore: typos --- packages/remix-dev/__tests__/readConfig-test.ts | 2 +- packages/remix-dev/compiler.ts | 2 +- packages/remix-server-runtime/__tests__/cookies-test.ts | 2 +- packages/remix-server-runtime/platform.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index b31af42d29..36173e6d22 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -55,7 +55,7 @@ describe("readConfig", () => { "id": "pages/test", "index": undefined, "parentId": "root", - "path": "programatic", + "path": "programmatic", }, "pages/three": Object { "caseSensitive": undefined, diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index d8f853091b..e50d97363a 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -51,7 +51,7 @@ function defaultBuildFailureHandler(failure: Error | esbuild.BuildFailure) { } } - console.error(failure?.message || "An unknown build error occured"); + console.error(failure?.message || "An unknown build error occurred"); } interface BuildOptions extends Partial { diff --git a/packages/remix-server-runtime/__tests__/cookies-test.ts b/packages/remix-server-runtime/__tests__/cookies-test.ts index a5de0661c5..db47a95392 100644 --- a/packages/remix-server-runtime/__tests__/cookies-test.ts +++ b/packages/remix-server-runtime/__tests__/cookies-test.ts @@ -79,7 +79,7 @@ describe("cookies", () => { `); }); - it("failes to parses signed object values with invalid signature", async () => { + it("fails to parse signed object values with invalid signature", async () => { let cookie = createCookie("my-cookie", { secrets: ["secret1"] }); diff --git a/packages/remix-server-runtime/platform.ts b/packages/remix-server-runtime/platform.ts index e0da0cc21f..9862bb57e9 100644 --- a/packages/remix-server-runtime/platform.ts +++ b/packages/remix-server-runtime/platform.ts @@ -2,7 +2,7 @@ * This also probably warrants some explanation. * * The whole point here is to abstract out the server functionality that is required - * by the server runtime but is dependant on the platform runtime. + * by the server runtime but is dependent on the platform runtime. * * An example of this is error beautification as it depends on loading sourcemaps from * the file system in node, while functions hosted on cloudflare workers will not need From 0c8a4c442bb84fe33659b256094fd446070d748f Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Wed, 15 Dec 2021 09:16:19 -0800 Subject: [PATCH 0163/1690] feat(dev): `ignoredRouteFiles` config option (#989) Added default setting in all template configs to ignore dotfiles --- packages/remix-dev/config.ts | 12 +++++++++++- packages/remix-dev/config/routesConvention.ts | 19 ++++++++++++------- packages/remix-dev/package.json | 1 + 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 34f7249f06..7fde8053b0 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -83,6 +83,13 @@ export interface AppConfig { * The output format of the server build. Defaults to "cjs". */ serverModuleFormat: "esm" | "cjs"; + + /** + * 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[]; } /** @@ -235,7 +242,10 @@ export async function readConfig( root: { path: "", id: "root", file: rootRouteFile } }; if (fs.existsSync(path.resolve(appDirectory, "routes"))) { - let conventionalRoutes = defineConventionalRoutes(appDirectory); + let conventionalRoutes = defineConventionalRoutes( + appDirectory, + appConfig.ignoredRouteFiles + ); for (let key of Object.keys(conventionalRoutes)) { let route = conventionalRoutes[key]; routes[route.id] = { ...route, parentId: route.parentId || "root" }; diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 9dddcf0524..b096a4093f 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -1,5 +1,6 @@ import * as fs from "fs"; import * as path from "path"; +import minimatch from "minimatch"; import type { RouteManifest, DefineRouteFunction } from "./routes"; import { defineRoutes, createRouteId } from "./routes"; @@ -21,20 +22,24 @@ export function isRouteModuleFile(filename: string): boolean { * For example, a file named `app/routes/gists/$username.tsx` creates a route * with a path of `gists/:username`. */ -export function defineConventionalRoutes(appDir: string): RouteManifest { +export function defineConventionalRoutes( + appDir: string, + ignoredFilePatterns?: string[] +): RouteManifest { let files: { [routeId: string]: string } = {}; // First, find all route modules in app/routes visitFiles(path.join(appDir, "routes"), file => { - let routeId = createRouteId(path.join("routes", file)); - - if (isRouteModuleFile(file)) { - files[routeId] = path.join("routes", file); + if ( + ignoredFilePatterns && + ignoredFilePatterns.some(pattern => minimatch(file, pattern)) + ) { return; } - // https://github.com/remix-run/remix/issues/391 - if (path.basename(file).startsWith(".")) { + if (isRouteModuleFile(file)) { + let routeId = createRouteId(path.join("routes", file)); + files[routeId] = path.join("routes", file); return; } diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 37106e93dc..83c1c9300c 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -21,6 +21,7 @@ "fs-extra": "^10.0.0", "lodash.debounce": "^4.0.8", "meow": "^7.1.1", + "minimatch": "^3.0.4", "pretty-ms": "^7.0.1", "read-package-json-fast": "^2.0.2", "remark-frontmatter": "^4.0.0", From 75dcb16d8df7a6dfbf179956ebff733134584f37 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 15 Dec 2021 13:51:05 -0800 Subject: [PATCH 0164/1690] feat: initial cloudflare-pages package (#985) feat: added esm build for cloudflare packages --- packages/remix-dev/setup.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/remix-dev/setup.ts b/packages/remix-dev/setup.ts index bc126ee060..3ab2a719c1 100644 --- a/packages/remix-dev/setup.ts +++ b/packages/remix-dev/setup.ts @@ -2,14 +2,17 @@ import * as path from "path"; import * as fse from "fs-extra"; export enum SetupPlatform { + CloudflarePages = "cloudflare-pages", CloudflareWorkers = "cloudflare-workers", Node = "node" } export function isSetupPlatform(platform: any): platform is SetupPlatform { - return [SetupPlatform.CloudflareWorkers, SetupPlatform.Node].includes( - platform - ); + return [ + SetupPlatform.CloudflarePages, + SetupPlatform.CloudflareWorkers, + SetupPlatform.Node + ].includes(platform); } export async function setupRemix(platform: SetupPlatform): Promise { From bb857eabfc69c7a405824607d1f322f4f0946309 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Thu, 16 Dec 2021 10:53:39 -0500 Subject: [PATCH 0165/1690] fix: magic exports breaking deployments (#1026) This bundles the magic "remix" package into the server build, instead of being required at runtime. Apps can import everything with `import { json } from "remix"` even when `json` is completely different in Node.js vs. Cloudflare Workers vs. Deno, etc. This works because `remix setup` (ab)uses node_module conventions by copying files from the environment package (`"@remix-run/node"`) to the `"remix"` package. Some deployment targets (particularly Architect) run into issues when Remix messes around with the node_modules directory. This ensures that the server code doesn't actually depend on the `"remix"` package at run time anymore by moving it to a build-time thing. We love the DX of the `"remix"` package but may one day recommend apps just import from the various packages in the future like `"@remix-run/node"` and `"@remix-run/react"` instead of the magic exports in `"remix"`. In fact, you can do that already today if the magic exports give you the heebie jeebies. Co-authored-by: Ryan Florence --- packages/remix-dev/compiler.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index e50d97363a..d16437b03b 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -369,7 +369,7 @@ async function createServerBuild( // browser build and it's not there yet. if (id === "./assets.json" && importer === "") return true; - // Mark all bare imports as external. They will be require()'d at + // Mark all bare imports as external. They will be require()'d (or imported if esm) at // runtime from node_modules. if (isBareModuleId(id)) { let packageName = getNpmPackageName(id); @@ -387,9 +387,14 @@ async function createServerBuild( ); } - // allow importing css files for bundling / hashing from node_modules. + // include .css files from node_modules in the build + // so we can get a hashed file name to put into the HTML if (id.endsWith(".css")) return false; + // include "remix" in the build so the server runtime (node) doesn't have to try + // to find the magic exports + if (packageName === "remix") return false; + return true; } From 273f82ad61af325cf0cad5b0c32873aa1c2ea9e8 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Thu, 16 Dec 2021 11:48:01 -0500 Subject: [PATCH 0166/1690] fix(dev/compiler): server build source maps (#1076) This sets the server publicPath to "./" instead of incorrectly using the browser's public path. Here's the diff of the server build for the gists app https://www.diffchecker.com/UNCvWDLA --- packages/remix-dev/compiler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index d16437b03b..113d3adaf0 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -356,7 +356,7 @@ async function createServerBuild( // The server build needs to know how to generate asset URLs for imports // of CSS and other files. assetNames: "_assets/[name]-[hash]", - publicPath: config.publicPath, + publicPath: "./", define: { "process.env.NODE_ENV": JSON.stringify(options.mode) }, From 2576833bfb549f13efe4ada43ad4161b3d613c9c Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 16 Dec 2021 11:24:29 -0800 Subject: [PATCH 0167/1690] fix: account for string and buffer bodies (#1070) --- packages/remix-node/parseMultipartFormData.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/remix-node/parseMultipartFormData.ts b/packages/remix-node/parseMultipartFormData.ts index 889e915386..c73968688d 100644 --- a/packages/remix-node/parseMultipartFormData.ts +++ b/packages/remix-node/parseMultipartFormData.ts @@ -1,4 +1,4 @@ -import type { Readable } from "stream"; +import { Readable } from "stream"; import Busboy from "busboy"; import type { Request as NodeRequest } from "./fetch"; @@ -14,13 +14,20 @@ export function parseMultipartFormData( export async function internalParseFormData( contentType: string, - stream: Readable, + body: string | Buffer | Readable, abortController?: AbortController, uploadHandler?: UploadHandler ) { let formData = new NodeFormData(); let fileWorkQueue: Promise[] = []; + let stream: Readable; + if (typeof body === "string" || Buffer.isBuffer(body)) { + stream = Readable.from(body.toString()); + } else { + stream = body; + } + await new Promise(async (resolve, reject) => { let busboy = new Busboy({ highWaterMark: 2 * 1024 * 1024, From c40d6b79e377b57f3286638c1bcdc6bb1a2eb961 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 16 Dec 2021 12:09:44 -0800 Subject: [PATCH 0168/1690] feat: added server platform config (#1084) --- packages/remix-dev/__tests__/readConfig-test.ts | 1 + packages/remix-dev/compiler.ts | 2 +- packages/remix-dev/config.ts | 14 +++++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 36173e6d22..dad28fe421 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -434,6 +434,7 @@ describe("readConfig", () => { "serverBuildDirectory": Any, "serverMode": "production", "serverModuleFormat": "cjs", + "serverPlatform": "node", } ` ); diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 113d3adaf0..197d112b81 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -344,7 +344,7 @@ async function createServerBuild( resolveDir: config.serverBuildDirectory }, outfile: path.resolve(config.serverBuildDirectory, "index.js"), - platform: "node", + platform: config.serverPlatform, format: config.serverModuleFormat, target: options.target, inject: [reactShim], diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 7fde8053b0..7334303715 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -82,7 +82,12 @@ export interface AppConfig { /** * The output format of the server build. Defaults to "cjs". */ - serverModuleFormat: "esm" | "cjs"; + serverModuleFormat?: "esm" | "cjs"; + + /** + * The platform the server build is targeting. Defaults to "node". + */ + serverPlatform?: "node" | "neutral"; /** * A list of filenames or a glob patterns to match files in the `app/routes` @@ -165,6 +170,11 @@ export interface RemixConfig { * The output format of the server build. Defaults to "cjs". */ serverModuleFormat: "esm" | "cjs"; + + /** + * The platform the server build is targeting. Defaults to "node". + */ + serverPlatform: "node" | "neutral"; } /** @@ -194,6 +204,7 @@ export async function readConfig( } let serverModuleFormat = appConfig.serverModuleFormat || "cjs"; + let serverPlatform = appConfig.serverPlatform || "node"; let mdx = appConfig.mdx; let appDirectory = path.resolve( @@ -273,6 +284,7 @@ export async function readConfig( serverBuildDirectory, serverMode, serverModuleFormat, + serverPlatform, mdx }; } From 4624c88d7ca30c790ff0a84acb67ec04d98e2422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Thu, 16 Dec 2021 21:14:56 +0100 Subject: [PATCH 0169/1690] refactor: use optional chaining (#1040) --- packages/remix-dev/cli/commands.ts | 2 +- packages/remix-node/formData.ts | 2 +- packages/remix-server-runtime/server.ts | 2 +- packages/remix-server-runtime/sessions.ts | 2 +- packages/remix-server-runtime/sessions/cookieStorage.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 5521ed568b..b258dcb33e 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -105,7 +105,7 @@ export async function watch( onInitialBuild, onRebuildStart() { start = Date.now(); - onRebuildStart && onRebuildStart(); + onRebuildStart?.(); log("Rebuilding..."); }, onRebuildFinish() { diff --git a/packages/remix-node/formData.ts b/packages/remix-node/formData.ts index 35a166fa9a..116a69491b 100644 --- a/packages/remix-node/formData.ts +++ b/packages/remix-node/formData.ts @@ -58,7 +58,7 @@ class NodeFormData implements FormData { get(name: string): FormDataEntryValue | null { let arr = this._fields[name]; - return (arr && arr.slice(-1)[0]) || null; + return arr?.slice(-1)[0] || null; } getAll(name: string): FormDataEntryValue[] { diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index ea60feeb5c..087ca3ea35 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -411,7 +411,7 @@ async function renderDocumentRequest({ renderableMatches = []; let root = routes[0]; - if (root && root.module.CatchBoundary) { + if (root?.module.CatchBoundary) { appState.catchBoundaryRouteId = "root"; renderableMatches.push({ params: {}, diff --git a/packages/remix-server-runtime/sessions.ts b/packages/remix-server-runtime/sessions.ts index 21c25fb222..d5764cca12 100644 --- a/packages/remix-server-runtime/sessions.ts +++ b/packages/remix-server-runtime/sessions.ts @@ -208,7 +208,7 @@ export function createSessionStorage({ }: SessionIdStorageStrategy): SessionStorage { let cookie = isCookie(cookieArg) ? cookieArg - : createCookie((cookieArg && cookieArg.name) || "__session", cookieArg); + : createCookie(cookieArg?.name || "__session", cookieArg); warnOnceAboutSigningSessionCookie(cookie); diff --git a/packages/remix-server-runtime/sessions/cookieStorage.ts b/packages/remix-server-runtime/sessions/cookieStorage.ts index 44f63dba10..45e62a0acb 100644 --- a/packages/remix-server-runtime/sessions/cookieStorage.ts +++ b/packages/remix-server-runtime/sessions/cookieStorage.ts @@ -24,7 +24,7 @@ export function createCookieSessionStorage({ }: CookieSessionStorageOptions = {}): SessionStorage { let cookie = isCookie(cookieArg) ? cookieArg - : createCookie((cookieArg && cookieArg.name) || "__session", cookieArg); + : createCookie(cookieArg?.name || "__session", cookieArg); warnOnceAboutSigningSessionCookie(cookie); From b6e49ecce0753f0e17f340e8d12abdf8d3c7d683 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Thu, 16 Dec 2021 16:00:34 -0500 Subject: [PATCH 0170/1690] fix(dev/compiler): use inline sourcemaps for server build (#1087) Signed-off-by: Logan McAnsh (cherry picked from commit 1d3f18402a6fd89be883dc7eb7292feaf07e9582) (cherry picked from commit d0b5edab5a73a8c49bbcbc9634676a5adc092ae2) --- packages/remix-dev/compiler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 197d112b81..0dbf053fda 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -352,11 +352,11 @@ async function createServerBuild( bundle: true, logLevel: "silent", incremental: options.incremental, - sourcemap: true, + sourcemap: options.sourcemap ? "inline" : false, // The server build needs to know how to generate asset URLs for imports // of CSS and other files. assetNames: "_assets/[name]-[hash]", - publicPath: "./", + publicPath: config.publicPath, define: { "process.env.NODE_ENV": JSON.stringify(options.mode) }, From 6203ae2281236461111a74dc91ea3ba39008436b Mon Sep 17 00:00:00 2001 From: Wei Zhu Date: Fri, 17 Dec 2021 05:56:56 +0800 Subject: [PATCH 0171/1690] fix(remix-serve): ensure react and react-dom match (#926) Since react is loaded (in react-router-dom) before the NODE_ENV is set, but react-dom is loaded after NODE_ENV is set, so react is resolved to the development version, but react-dom is resolved to the production version. fix #632 --- packages/remix-serve/cli.ts | 2 +- packages/remix-serve/env.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 packages/remix-serve/env.ts diff --git a/packages/remix-serve/cli.ts b/packages/remix-serve/cli.ts index de1e622f27..4cae3a74ca 100644 --- a/packages/remix-serve/cli.ts +++ b/packages/remix-serve/cli.ts @@ -1,8 +1,8 @@ +import "./env"; import path from "path"; import { createApp } from "./index"; -process.env.NODE_ENV = "production"; let port = process.env.PORT || 3000; let buildPathArg = process.argv[2]; diff --git a/packages/remix-serve/env.ts b/packages/remix-serve/env.ts new file mode 100644 index 0000000000..f0bae35bdc --- /dev/null +++ b/packages/remix-serve/env.ts @@ -0,0 +1 @@ +process.env.NODE_ENV = "production"; From 2f15ad8e98b838ea2afe2a87bcf956305a284698 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Thu, 16 Dec 2021 14:02:47 -0800 Subject: [PATCH 0172/1690] feat: mainfield config based on serverModuleFormat (#1071) --- packages/remix-dev/compiler.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 0dbf053fda..a55926b1ef 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -346,6 +346,10 @@ async function createServerBuild( outfile: path.resolve(config.serverBuildDirectory, "index.js"), platform: config.serverPlatform, format: config.serverModuleFormat, + mainFields: + config.serverModuleFormat === "esm" + ? ["module", "main"] + : ["main", "module"], target: options.target, inject: [reactShim], loader: loaders, From df01559d2956a1ba909f8f75fa0beedf6c9e9717 Mon Sep 17 00:00:00 2001 From: Donavon West Date: Thu, 16 Dec 2021 23:08:11 -0500 Subject: [PATCH 0173/1690] refactor: use url.href vs url.toString() (#1097) --- packages/remix-express/server.ts | 2 +- packages/remix-server-runtime/data.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index 55ee522126..e0523d1397 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -111,7 +111,7 @@ export function createRemixRequest( init.body = req; //req.pipe(new PassThrough({ highWaterMark: 16384 })); } - return new NodeRequest(url.toString(), init); + return new NodeRequest(url.href, init); } function sendRemixResponse( diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index 254c6a80d3..93a1edb196 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -121,13 +121,13 @@ function stripIndexParam(request: Request) { url.searchParams.append("index", toKeep); } - return new Request(url.toString(), request); + return new Request(url.href, request); } function stripDataParam(request: Request) { let url = new URL(request.url); url.searchParams.delete("_data"); - return new Request(url.toString(), request); + return new Request(url.href, request); } export function extractData(response: Response): Promise { From fce12c692f9654d5839b76337ee184443367e523 Mon Sep 17 00:00:00 2001 From: Wei Zhu Date: Fri, 17 Dec 2021 12:27:08 +0800 Subject: [PATCH 0174/1690] fix: dev process can't exit after using prisma (#905) --- packages/remix-dev/cli/commands.ts | 4 ++-- packages/remix-dev/package.json | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index b258dcb33e..c49a7cd008 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -1,6 +1,6 @@ import * as path from "path"; import * as fse from "fs-extra"; -import signalExit from "signal-exit"; +import exitHook from "exit-hook"; import prettyMs from "pretty-ms"; import WebSocket from "ws"; import type { Server } from "http"; @@ -126,7 +126,7 @@ export async function watch( console.log(`💿 Built in ${prettyMs(Date.now() - start)}`); let resolve: () => void; - signalExit(() => { + exitHook(() => { resolve(); }); return new Promise(r => { diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 83c1c9300c..059228c0ef 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -18,6 +18,7 @@ "cacache": "^15.0.5", "chokidar": "^3.5.1", "esbuild": "0.13.14", + "exit-hook": "2.2.1", "fs-extra": "^10.0.0", "lodash.debounce": "^4.0.8", "meow": "^7.1.1", @@ -26,14 +27,12 @@ "read-package-json-fast": "^2.0.2", "remark-frontmatter": "^4.0.0", "remark-mdx-frontmatter": "^1.0.1", - "signal-exit": "^3.0.3", "ws": "^7.4.5", "xdm": "^2.0.0" }, "devDependencies": { "@types/cacache": "^15.0.0", "@types/lodash.debounce": "^4.0.6", - "@types/signal-exit": "^3.0.0", "@types/ws": "^7.4.1", "semver": "^7.3.4" } From 4b16ecd77bddbb70ed4a208569ce0d061d37a99a Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 16 Dec 2021 20:33:09 -0800 Subject: [PATCH 0175/1690] chore: fix formatting issues --- packages/remix-dev/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 7334303715..9dca8038ca 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -174,7 +174,7 @@ export interface RemixConfig { /** * The platform the server build is targeting. Defaults to "node". */ - serverPlatform: "node" | "neutral"; + serverPlatform: "node" | "neutral"; } /** From 757be89d7c02a481e7a39f928b77d3cdc913bc21 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 17 Dec 2021 11:02:54 -0800 Subject: [PATCH 0176/1690] chore: marked upload handler exports as unstable (#1116) --- packages/remix-node/index.ts | 6 +++--- packages/remix-node/magicExports/platform.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index 39702da746..e6df2457b1 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -14,12 +14,12 @@ export { FormData } from "./formData"; export { installGlobals } from "./globals"; -export { parseMultipartFormData } from "./parseMultipartFormData"; +export { parseMultipartFormData as unstable_parseMultipartFormData } from "./parseMultipartFormData"; export { createFileSessionStorage } from "./sessions/fileStorage"; export { - createFileUploadHandler, + createFileUploadHandler as unstable_createFileUploadHandler, NodeOnDiskFile } from "./upload/fileUploadHandler"; -export { createMemoryUploadHandler } from "./upload/memoryUploadHandler"; +export { createMemoryUploadHandler as unstable_createMemoryUploadHandler } from "./upload/memoryUploadHandler"; diff --git a/packages/remix-node/magicExports/platform.ts b/packages/remix-node/magicExports/platform.ts index 0b196604ea..719c256e48 100644 --- a/packages/remix-node/magicExports/platform.ts +++ b/packages/remix-node/magicExports/platform.ts @@ -3,7 +3,7 @@ export { createFileSessionStorage, - createFileUploadHandler, - createMemoryUploadHandler, - parseMultipartFormData + unstable_createFileUploadHandler, + unstable_createMemoryUploadHandler, + unstable_parseMultipartFormData } from "@remix-run/node"; From 273f50be18ef5cc119182f12b97a19ad10fe9c4a Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 17 Dec 2021 12:15:04 -0800 Subject: [PATCH 0177/1690] fix: request clone + tests (#1120) --- .../remix-express/__tests__/server-test.ts | 2 +- packages/remix-node/__tests__/fetch-test.ts | 110 ++++++++++++++++++ packages/remix-node/fetch.ts | 14 +-- 3 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 packages/remix-node/__tests__/fetch-test.ts diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index 75ccadce03..4053393c3c 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -211,7 +211,7 @@ describe("express createRemixRequest", () => { }); expect(createRemixRequest(expressRequest)).toMatchInlineSnapshot(` - RemixRequest { + NodeRequest { "abortController": undefined, "agent": undefined, "compress": true, diff --git a/packages/remix-node/__tests__/fetch-test.ts b/packages/remix-node/__tests__/fetch-test.ts new file mode 100644 index 0000000000..0297b08933 --- /dev/null +++ b/packages/remix-node/__tests__/fetch-test.ts @@ -0,0 +1,110 @@ +import { PassThrough } from "stream"; +import { Request } from "../fetch"; +import { createMemoryUploadHandler } from "../upload/memoryUploadHandler"; + +let test = { + source: [ + [ + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + 'Content-Disposition: form-data; name="file_name_0"', + "", + "super alpha file", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + 'Content-Disposition: form-data; name="file_name_1"', + "", + "super beta file", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + 'Content-Disposition: form-data; name="upload_file_0"; filename="1k_a.dat"', + "Content-Type: application/octet-stream", + "", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + 'Content-Disposition: form-data; name="upload_file_1"; filename="1k_b.dat"', + "Content-Type: application/octet-stream", + "", + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" + ].join("\r\n") + ], + boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", + expected: [ + [ + "field", + "file_name_0", + "super alpha file", + false, + false, + "7bit", + "text/plain" + ], + [ + "field", + "file_name_1", + "super beta file", + false, + false, + "7bit", + "text/plain" + ], + [ + "file", + "upload_file_0", + 1023, + 0, + "1k_a.dat", + "7bit", + "application/octet-stream" + ], + [ + "file", + "upload_file_1", + 1023, + 0, + "1k_b.dat", + "7bit", + "application/octet-stream" + ] + ], + what: "Fields and files" +}; + +describe("Request", () => { + let uploadHandler = createMemoryUploadHandler({}); + + it("clones", async () => { + let body = new PassThrough(); + test.source.forEach(chunk => body.write(chunk)); + + let req = new Request("http://test.com", { + method: "post", + body, + headers: { + "Content-Type": "multipart/form-data; boundary=" + test.boundary + } + }); + + let cloned = req.clone(); + expect(Object.getPrototypeOf(req)).toBe(Object.getPrototypeOf(cloned)); + + let formData = await req.formData(uploadHandler); + let clonedFormData = await cloned.formData(uploadHandler); + + expect(formData.get("file_name_0")).toBe("super alpha file"); + expect(clonedFormData.get("file_name_0")).toBe("super alpha file"); + expect(formData.get("file_name_1")).toBe("super beta file"); + expect(clonedFormData.get("file_name_1")).toBe("super beta file"); + let file = formData.get("upload_file_0") as File; + expect(file.name).toBe("1k_a.dat"); + expect(file.size).toBe(1023); + file = clonedFormData.get("upload_file_0") as File; + expect(file.name).toBe("1k_a.dat"); + expect(file.size).toBe(1023); + + file = formData.get("upload_file_1") as File; + expect(file.name).toBe("1k_b.dat"); + expect(file.size).toBe(1023); + file = clonedFormData.get("upload_file_1") as File; + expect(file.name).toBe("1k_b.dat"); + expect(file.size).toBe(1023); + }); +}); diff --git a/packages/remix-node/fetch.ts b/packages/remix-node/fetch.ts index d0219f442b..0ca71d1ea8 100644 --- a/packages/remix-node/fetch.ts +++ b/packages/remix-node/fetch.ts @@ -3,7 +3,7 @@ import { PassThrough } from "stream"; import type AbortController from "abort-controller"; import FormStream from "form-data"; import type { RequestInfo, RequestInit, Response } from "node-fetch"; -import nodeFetch, { Request as NodeRequest } from "node-fetch"; +import nodeFetch, { Request as BaseNodeRequest } from "node-fetch"; import { FormData as NodeFormData, isFile } from "./formData"; import type { UploadHandler } from "./formData"; @@ -63,14 +63,14 @@ function formDataToStream(formData: NodeFormData): FormStream { return formStream; } -interface RemixRequestInit extends RequestInit { +interface NodeRequestInit extends RequestInit { abortController?: AbortController; } -class RemixRequest extends NodeRequest { +class NodeRequest extends BaseNodeRequest { private abortController?: AbortController; - constructor(input: RequestInfo, init?: RemixRequestInit | undefined) { + constructor(input: RequestInfo, init?: NodeRequestInit | undefined) { if (init?.body instanceof NodeFormData) { init = { ...init, @@ -101,12 +101,12 @@ class RemixRequest extends NodeRequest { throw new Error("Invalid MIME type"); } - clone() { - return new RemixRequest(super.clone()); + clone(): NodeRequest { + return new NodeRequest(this); } } -export { RemixRequest as Request, RemixRequestInit as RequestInit }; +export { NodeRequest as Request, NodeRequestInit as RequestInit }; /** * A `fetch` function for node that matches the web Fetch API. Based on From 323dd63c88587b595c22ed00a3648fc44b472bb9 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 17 Dec 2021 12:49:59 -0800 Subject: [PATCH 0178/1690] chore: Bump react-router-dom dependency (#1122) * Bump react-router-dom dependency * Fix some types based on changes in history --- packages/remix-server-runtime/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 2e007276b3..a59205c946 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -17,7 +17,7 @@ "@types/cookie": "^0.4.0", "cookie": "^0.4.1", "jsesc": "^3.0.1", - "react-router-dom": "^6.1.1", + "react-router-dom": "^6.2.1", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3" }, From 8edf0ecb32cf7fcfa6a1070427689acb29dc19db Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 17 Dec 2021 15:24:29 -0800 Subject: [PATCH 0179/1690] fix: re-enable highwatermark for express (#1125) --- packages/remix-express/server.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index e0523d1397..e1022a1cfb 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -1,3 +1,4 @@ +import { PassThrough } from "stream"; import type * as express from "express"; import type { AppLoadContext, @@ -108,7 +109,7 @@ export function createRemixRequest( }; if (req.method !== "GET" && req.method !== "HEAD") { - init.body = req; //req.pipe(new PassThrough({ highWaterMark: 16384 })); + init.body = req.pipe(new PassThrough({ highWaterMark: 16384 })); } return new NodeRequest(url.href, init); From b2cfef4a026b1fe67b0753ebde37006e69edea17 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 17 Dec 2021 15:39:29 -0800 Subject: [PATCH 0180/1690] chore: formatting --- packages/remix-node/__tests__/fetch-test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/remix-node/__tests__/fetch-test.ts b/packages/remix-node/__tests__/fetch-test.ts index 0297b08933..3062e3e6d5 100644 --- a/packages/remix-node/__tests__/fetch-test.ts +++ b/packages/remix-node/__tests__/fetch-test.ts @@ -1,4 +1,5 @@ import { PassThrough } from "stream"; + import { Request } from "../fetch"; import { createMemoryUploadHandler } from "../upload/memoryUploadHandler"; From 2f92ce1da3b962cc6d6032f9a3c14db61618cbbb Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 17 Dec 2021 15:40:16 -0800 Subject: [PATCH 0181/1690] Version 1.1.0 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 059228c0ef..8ac1b5a167 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.0.6", + "version": "1.1.0", "license": "MIT", "repository": { "type": "git", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 4f9e251d34..6abb7168fd 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.0.6", + "version": "1.1.0", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.0.6", - "@remix-run/server-runtime": "1.0.6" + "@remix-run/node": "1.1.0", + "@remix-run/server-runtime": "1.1.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index d90aa32e37..0ec160fc88 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.0.6", + "version": "1.1.0", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.0.6", + "@remix-run/server-runtime": "1.1.0", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 757ec77a8d..54f2adf10e 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.0.6", + "version": "1.1.0", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.0.6", + "@remix-run/express": "1.1.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index a59205c946..038554db4a 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.0.6", + "version": "1.1.0", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From a3c806f29539000cf4e2a69c79b30a941644637d Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 17 Dec 2021 16:26:55 -0800 Subject: [PATCH 0182/1690] Update code comments --- packages/remix-dev/compiler.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index a55926b1ef..a9005652f8 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -373,8 +373,8 @@ async function createServerBuild( // browser build and it's not there yet. if (id === "./assets.json" && importer === "") return true; - // Mark all bare imports as external. They will be require()'d (or imported if esm) at - // runtime from node_modules. + // Mark all bare imports as external. They will be require()'d (or + // imported if ESM) at runtime from node_modules. if (isBareModuleId(id)) { let packageName = getNpmPackageName(id); if ( @@ -391,12 +391,14 @@ async function createServerBuild( ); } - // include .css files from node_modules in the build - // so we can get a hashed file name to put into the HTML + // Include .css files from node_modules in the build so we can get a + // hashed file name to put into the HTML. if (id.endsWith(".css")) return false; - // include "remix" in the build so the server runtime (node) doesn't have to try - // to find the magic exports + // Include "remix" in the build so the server runtime (node) doesn't + // have to try to find the magic exports at runtime. This essentially + // translates all `import x from "remix"` statements into `import x + // from "@remix-run/x"` in the build. if (packageName === "remix") return false; return true; From bd2b20d2c7b3e275f3797e59e8a43b4c2aca051c Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 4 Jan 2022 16:52:04 -0800 Subject: [PATCH 0183/1690] fix: sibling __layout routes conflict (#1347) --- packages/remix-dev/__tests__/readConfig-test.ts | 16 ++++++++++++++++ packages/remix-dev/config/routesConvention.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index dad28fe421..0081ebb8a3 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -86,6 +86,22 @@ describe("readConfig", () => { "parentId": "routes/__layout", "path": "with-layout", }, + "routes/__layout2": Object { + "caseSensitive": undefined, + "file": "routes/__layout2.tsx", + "id": "routes/__layout2", + "index": undefined, + "parentId": "root", + "path": undefined, + }, + "routes/__layout2/with-layout2": Object { + "caseSensitive": undefined, + "file": "routes/__layout2/with-layout2.tsx", + "id": "routes/__layout2/with-layout2", + "index": undefined, + "parentId": "routes/__layout2", + "path": "with-layout2", + }, "routes/action-catches": Object { "caseSensitive": undefined, "file": "routes/action-catches.jsx", diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index b096a4093f..2ff0143af1 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -70,7 +70,7 @@ export function defineConventionalRoutes( let fullPath = createRoutePath(routeId.slice("routes".length + 1)); let uniqueRouteId = (fullPath || "") + (isIndexRoute ? "?index" : ""); - if (typeof uniqueRouteId !== "undefined") { + if (uniqueRouteId) { if (uniqueRoutes.has(uniqueRouteId)) { throw new Error( `Path ${JSON.stringify(fullPath)} defined by route ${JSON.stringify( From 849b597cedc07a255cb869e30e284a10390194e5 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Wed, 5 Jan 2022 16:16:52 -0500 Subject: [PATCH 0184/1690] chore(remix-server): update Remix App Server log to use local ip (#1328) Signed-off-by: Logan McAnsh --- packages/remix-dev/cli/commands.ts | 11 ++++++++++- packages/remix-serve/cli.ts | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index c49a7cd008..77ec24161a 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -1,4 +1,5 @@ import * as path from "path"; +import os from "os"; import * as fse from "fs-extra"; import exitHook from "exit-hook"; import prettyMs from "pretty-ms"; @@ -169,8 +170,16 @@ export async function dev(remixRoot: string, modeArg?: string) { try { await watch(config, mode, { onInitialBuild: () => { + let address = Object.values(os.networkInterfaces()) + .flat() + .find(ip => ip?.family == "IPv4" && !ip.internal)?.address; + + if (!address) { + throw new Error("Could not find an IPv4 address."); + } + server = app.listen(port, () => { - console.log(`Remix App Server started at http://localhost:${port}`); + console.log(`Remix App Server started at http://${address}:${port}`); }); } }); diff --git a/packages/remix-serve/cli.ts b/packages/remix-serve/cli.ts index 4cae3a74ca..fbf07c1a1b 100644 --- a/packages/remix-serve/cli.ts +++ b/packages/remix-serve/cli.ts @@ -1,5 +1,6 @@ import "./env"; import path from "path"; +import os from "os"; import { createApp } from "./index"; @@ -15,5 +16,13 @@ if (!buildPathArg) { let buildPath = path.resolve(process.cwd(), buildPathArg); createApp(buildPath).listen(port, () => { - console.log(`Remix App Server started at http://localhost:${port}`); + let address = Object.values(os.networkInterfaces()) + .flat() + .find(ip => ip?.family == "IPv4" && !ip.internal)?.address; + + if (!address) { + throw new Error("Could not find an IPv4 address."); + } + + console.log(`Remix App Server started at http://${address}:${port}`); }); From 685d49984994012525320ebe16049d43b4a04bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Campa=C3=B1a?= Date: Tue, 18 Jan 2022 04:48:22 +0900 Subject: [PATCH 0185/1690] Fixes error message when `loader` returns undefined (#1530) --- .../__tests__/data-test.ts | 59 +++++++++++++++++++ packages/remix-server-runtime/data.ts | 4 +- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index bc52fa78da..e79611f249 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -1,5 +1,8 @@ import type { ServerBuild } from "../build"; import { createRequestHandler } from "../server"; +import { callRouteAction, callRouteLoader } from "../data"; +import type { RouteMatch } from "../routeMatching"; +import type { ServerRoute } from "../routes"; describe("loaders", () => { // so that HTML/Fetch requests are the same, and so redirects don't hang on to @@ -143,4 +146,60 @@ describe("loaders", () => { let res = await handler(request); expect(await res.json()).toMatchInlineSnapshot(`"?foo=bar&index=test"`); }); + + it("throws the right error message when `loader` returns undefined", async () => { + let loader = async () => {}; + + let routeId = "routes/random"; + + let request = new Request("http://example.com/random?_data=routes/random"); + + let match = { + params: {}, + pathname: "random", + route: { + id: routeId, + module: { + loader + } + } + } as unknown as RouteMatch; + + try { + await callRouteLoader({ request, match, loadContext: {} }); + } catch (error) { + expect(error.message).toMatchInlineSnapshot( + '"You defined a loader for route \\"routes/random\\" but didn\'t return anything from your `loader` function. Please return a value or `null`."' + ); + } + }); +}); + +describe("actions", () => { + it("throws the right error message when `action` returns undefined", async () => { + let action = async () => {}; + + let routeId = "routes/random"; + + let request = new Request("http://example.com/random?_data=routes/random"); + + let match = { + params: {}, + pathname: "random", + route: { + id: routeId, + module: { + action + } + } + } as unknown as RouteMatch; + + try { + await callRouteAction({ request, match, loadContext: {} }); + } catch (error) { + expect(error.message).toMatchInlineSnapshot( + '"You defined an action for route \\"routes/random\\" but didn\'t return anything from your `action` function. Please return a value or `null`."' + ); + } + }); }); diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index 93a1edb196..973e1574fd 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -99,8 +99,8 @@ export async function callRouteLoader({ if (result === undefined) { throw new Error( - `You defined an action for route "${match.route.id}" but didn't return ` + - `anything from your \`action\` function. Please return a value or \`null\`.` + `You defined a loader for route "${match.route.id}" but didn't return ` + + `anything from your \`loader\` function. Please return a value or \`null\`.` ); } From b123e93501a5aa807940b1d222d073d1b75e0b31 Mon Sep 17 00:00:00 2001 From: chancestrickland Date: Thu, 20 Jan 2022 05:53:27 -0800 Subject: [PATCH 0186/1690] Version 1.1.2 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 8ac1b5a167..a688adcfd4 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.1.0", + "version": "1.1.2", "license": "MIT", "repository": { "type": "git", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 6abb7168fd..fdb0ab0169 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.1.0", + "version": "1.1.2", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.1.0", - "@remix-run/server-runtime": "1.1.0" + "@remix-run/node": "1.1.2", + "@remix-run/server-runtime": "1.1.2" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 0ec160fc88..8f21e8e214 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.1.0", + "version": "1.1.2", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.1.0", + "@remix-run/server-runtime": "1.1.2", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 54f2adf10e..fc80fc275f 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.1.0", + "version": "1.1.2", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.1.0", + "@remix-run/express": "1.1.2", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 038554db4a..16c9d5564a 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.1.0", + "version": "1.1.2", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From 02a89438190446baedeca18162a09c33c0733d44 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 20 Jan 2022 08:10:47 -0700 Subject: [PATCH 0187/1690] Version 1.1.3 --- packages/remix-dev/package.json | 2 +- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index a688adcfd4..de3b1e1f91 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.1.2", + "version": "1.1.3", "license": "MIT", "repository": { "type": "git", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index fdb0ab0169..1634451536 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.1.2", + "version": "1.1.3", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.1.2", - "@remix-run/server-runtime": "1.1.2" + "@remix-run/node": "1.1.3", + "@remix-run/server-runtime": "1.1.3" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 8f21e8e214..26be5f4588 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.1.2", + "version": "1.1.3", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.1.2", + "@remix-run/server-runtime": "1.1.3", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index fc80fc275f..f94eb5d84a 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.1.2", + "version": "1.1.3", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.1.2", + "@remix-run/express": "1.1.3", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 16c9d5564a..f48ccb0401 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.1.2", + "version": "1.1.3", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From bb0e8a0db1f5b93189432d442fb638071d230d1b Mon Sep 17 00:00:00 2001 From: mbarto Date: Thu, 20 Jan 2022 17:33:09 +0100 Subject: [PATCH 0188/1690] fix(vercel): allow for streams to be returned as the response body (#1470) closes #1244 also adds a stream test to the express adapter Co-authored-by: Logan McAnsh --- packages/remix-express/__tests__/server-test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index 4053393c3c..5b532ad4ba 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -2,6 +2,8 @@ import express from "express"; import supertest from "supertest"; import { createRequest } from "node-mocks-http"; import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; +import { Response as NodeResponse } from "@remix-run/node"; +import { Readable } from "stream"; import { createRemixHeaders, @@ -66,6 +68,21 @@ describe("express createRequestHandler", () => { expect(res.status).toBe(200); }); + // https://github.com/node-fetch/node-fetch/blob/4ae35388b078bddda238277142bf091898ce6fda/test/response.js#L142-L148 + it("handles body as stream", async () => { + mockedCreateRequestHandler.mockImplementation(() => async () => { + const stream = Readable.from("hello world"); + return new NodeResponse(stream, { status: 200 }) as unknown as Response; + }); + + let request = supertest(createApp()); + // note: vercel's createServerWithHelpers requires a x-now-bridge-request-id + let res = await request.get("/").set({ "x-now-bridge-request-id": "2" }); + + expect(res.status).toBe(200); + expect(res.text).toBe("hello world"); + }); + it("handles status codes", async () => { mockedCreateRequestHandler.mockImplementation(() => async () => { return new Response(null, { status: 204 }); From 52a7b667c5e36cc98aa1a4fcc2ca115c95d728ca Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Thu, 20 Jan 2022 15:50:04 -0700 Subject: [PATCH 0189/1690] example: newsletter signup form From fd9848e981161415dc8bbe6fc56e835f79e654ad Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 21 Jan 2022 19:03:18 -0800 Subject: [PATCH 0190/1690] feat: Allow for custom server to participate in bundling (#1504) feat: updated templates to make use of new compiler defaults feat: added node polyfills for "neutral" server platforms that are not deno fix(create-remix): make sure app has required properties before merging fix(create-remix): add missing dependencies ci: remove some junk from deployment scripts chore(create-remix): workers requires a build command feat: remove extra build step from CF template fix: made getLoadContext optional for arc handler Co-authored-by: Logan McAnsh --- .../remix-dev/__tests__/readConfig-test.ts | 7 +- packages/remix-dev/cli/commands.ts | 10 +- packages/remix-dev/compiler.ts | 452 ++++++------------ .../plugins/browserRouteModulesPlugin.ts | 81 ++++ .../compiler/plugins/emptyModulesPlugin.ts | 41 ++ .../compiler/plugins/serverAssetsPlugin.ts | 40 ++ .../plugins/serverBareModulesPlugin.ts | 97 ++++ .../plugins/serverEntryModulesPlugin.ts | 66 +++ .../plugins/serverRouteModulesPlugin.ts | 49 ++ packages/remix-dev/compiler/virtualModules.ts | 10 + packages/remix-dev/config.ts | 134 +++++- packages/remix-dev/package.json | 2 + packages/remix-dev/server-build.ts | 5 + packages/remix-dev/tsconfig.json | 2 +- 14 files changed, 658 insertions(+), 338 deletions(-) create mode 100644 packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts create mode 100644 packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts create mode 100644 packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts create mode 100644 packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts create mode 100644 packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts create mode 100644 packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts create mode 100644 packages/remix-dev/compiler/virtualModules.ts create mode 100644 packages/remix-dev/server-build.ts diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 0081ebb8a3..e5647bf0a8 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -17,7 +17,7 @@ describe("readConfig", () => { rootDirectory: expect.any(String), appDirectory: expect.any(String), cacheDirectory: expect.any(String), - serverBuildDirectory: expect.any(String), + serverBuildPath: expect.any(String), assetsBuildDirectory: expect.any(String) }, ` @@ -447,7 +447,10 @@ describe("readConfig", () => { "path": "two", }, }, - "serverBuildDirectory": Any, + "serverBuildPath": Any, + "serverBuildTarget": undefined, + "serverBuildTargetEntryModule": "export * from \\"@remix-run/dev/server-build\\";", + "serverEntryPoint": "./server.js", "serverMode": "production", "serverModuleFormat": "cjs", "serverPlatform": "node", diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 77ec24161a..dd28b825ee 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -136,7 +136,7 @@ export async function watch( wss.close(); await closeWatcher(); fse.emptyDirSync(config.assetsBuildDirectory); - fse.emptyDirSync(config.serverBuildDirectory); + fse.rmSync(config.serverBuildPath); }); } @@ -158,12 +158,16 @@ export async function dev(remixRoot: string, modeArg?: string) { let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Development; let port = process.env.PORT || 3000; + if (config.serverEntryPoint) { + throw new Error("remix dev is not supported for custom servers."); + } + let app = express(); app.use((_, __, next) => { - purgeAppRequireCache(config.serverBuildDirectory); + purgeAppRequireCache(config.serverBuildPath); next(); }); - app.use(createApp(config.serverBuildDirectory, mode)); + app.use(createApp(config.serverBuildPath, mode)); let server: Server | null = null; diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index a9005652f8..feb1481c79 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -1,21 +1,28 @@ -import { promises as fsp } from "fs"; import * as path from "path"; import { builtinModules as nodeBuiltins } from "module"; import * as esbuild from "esbuild"; +import * as fse from "fs-extra"; import debounce from "lodash.debounce"; import chokidar from "chokidar"; import { BuildMode, BuildTarget } from "./build"; import type { RemixConfig } from "./config"; import { readConfig } from "./config"; -import invariant from "./invariant"; import { warnOnce } from "./warnings"; import { createAssetsManifest } from "./compiler/assets"; import { getAppDependencies } from "./compiler/dependencies"; -import { loaders, getLoaderForFile } from "./compiler/loaders"; +import { loaders } from "./compiler/loaders"; +import { browserRouteModulesPlugin } from "./compiler/plugins/browserRouteModulesPlugin"; +import { emptyModulesPlugin } from "./compiler/plugins/emptyModulesPlugin"; import { mdxPlugin } from "./compiler/plugins/mdx"; -import { getRouteModuleExportsCached } from "./compiler/routes"; +import { serverAssetsPlugin } from "./compiler/plugins/serverAssetsPlugin"; +import type { BrowserManifestPromiseRef } from "./compiler/plugins/serverAssetsPlugin"; +import { serverBareModulesPlugin } from "./compiler/plugins/serverBareModulesPlugin"; +import { serverEntryModulesPlugin } from "./compiler/plugins/serverEntryModulesPlugin"; +import { serverRouteModulesPlugin } from "./compiler/plugins/serverRouteModulesPlugin"; import { writeFileSafe } from "./compiler/utils/fs"; +import type { AssetsManifest } from "@remix-run/server-runtime/entry"; +import { NodeModulesPolyfillPlugin } from "@esbuild-plugins/node-modules-polyfill"; // When we build Remix, this shim file is copied directly into the output // directory in the same place relative to this file. It is eventually injected @@ -69,7 +76,9 @@ export async function build( onBuildFailure = defaultBuildFailureHandler }: BuildOptions = {} ): Promise { - await buildEverything(config, { + let ref: BrowserManifestPromiseRef = {}; + + await buildEverything(config, ref, { mode, target, sourcemap, @@ -111,7 +120,12 @@ export async function watch( onWarning, incremental: true }; - let [browserBuild, serverBuild] = await buildEverything(config, options); + let browserManifestPromiseRef: BrowserManifestPromiseRef = {}; + let [browserBuild, serverBuild] = await buildEverything( + config, + browserManifestPromiseRef, + options + ); let initialBuildComplete = !!browserBuild && !!serverBuild; if (initialBuildComplete) { @@ -136,7 +150,11 @@ export async function watch( config = newConfig; if (onRebuildStart) onRebuildStart(); - let builders = await buildEverything(config, options); + let builders = await buildEverything( + config, + browserManifestPromiseRef, + options + ); if (onRebuildFinish) onRebuildFinish(); browserBuild = builders[0]; serverBuild = builders[1]; @@ -149,7 +167,11 @@ export async function watch( disposeBuilders(); try { - [browserBuild, serverBuild] = await buildEverything(config, options); + [browserBuild, serverBuild] = await buildEverything( + config, + browserManifestPromiseRef, + options + ); if (!initialBuildComplete) { initialBuildComplete = !!browserBuild && !!serverBuild; @@ -164,13 +186,18 @@ export async function watch( return; } + // If we get here and can't call rebuild something went wrong and we + // should probably blow as it's not really recoverable. + let browserBuildPromise = browserBuild + .rebuild() + .then(build => generateManifests(config, build.metafile!)); + // Do not await the client build, instead assign the promise to a ref + // so the server build can await it to gain access to the client manifest. + browserManifestPromiseRef.current = browserBuildPromise; + await Promise.all([ - // If we get here and can't call rebuild something went wrong and we - // should probably blow as it's not really recoverable. - browserBuild - .rebuild() - .then(build => generateManifests(config, build.metafile!)), - serverBuild.rebuild() + browserBuildPromise, + serverBuild.rebuild().then(writeServerBuildResult(config)) ]).catch(err => { disposeBuilders(); onBuildFailure(err); @@ -243,6 +270,7 @@ function isEntryPoint(config: RemixConfig, file: string) { async function buildEverything( config: RemixConfig, + browserManifestPromiseRef: BrowserManifestPromiseRef, options: Required & { incremental?: boolean } ): Promise<(esbuild.BuildResult | undefined)[]> { // TODO: @@ -255,13 +283,20 @@ async function buildEverything( try { let browserBuildPromise = createBrowserBuild(config, options); - let serverBuildPromise = createServerBuild(config, options); + let manifestPromise = browserBuildPromise.then(build => { + return generateManifests(config, build.metafile!); + }); + // Do not await the client build, instead assign the promise to a ref + // so the server build can await it to gain access to the client manifest. + browserManifestPromiseRef.current = manifestPromise; + let serverBuildPromise = createServerBuild( + config, + options, + browserManifestPromiseRef + ); return await Promise.all([ - browserBuildPromise.then(async build => { - await generateManifests(config, build.metafile!); - return build; - }), + manifestPromise.then(() => browserBuildPromise), serverBuildPromise ]); } catch (err) { @@ -316,6 +351,7 @@ async function createBrowserBuild( sourcemap: options.sourcemap, metafile: true, incremental: options.incremental, + treeShaking: true, minify: options.mode === BuildMode.Production, entryNames: "[dir]/[name]-[hash]", chunkNames: "_shared/[name]-[hash]", @@ -334,319 +370,107 @@ async function createBrowserBuild( async function createServerBuild( config: RemixConfig, - options: Required & { incremental?: boolean } + options: Required & { incremental?: boolean }, + browserManifestPromiseRef: BrowserManifestPromiseRef ): Promise { - let dependencies = Object.keys(await getAppDependencies(config)); - - return esbuild.build({ - stdin: { - contents: getServerEntryPointModule(config, options), - resolveDir: config.serverBuildDirectory - }, - outfile: path.resolve(config.serverBuildDirectory, "index.js"), - platform: config.serverPlatform, - format: config.serverModuleFormat, - mainFields: - config.serverModuleFormat === "esm" - ? ["module", "main"] - : ["main", "module"], - target: options.target, - inject: [reactShim], - loader: loaders, - bundle: true, - logLevel: "silent", - incremental: options.incremental, - sourcemap: options.sourcemap ? "inline" : false, - // The server build needs to know how to generate asset URLs for imports - // of CSS and other files. - assetNames: "_assets/[name]-[hash]", - publicPath: config.publicPath, - define: { - "process.env.NODE_ENV": JSON.stringify(options.mode) - }, - plugins: [ - mdxPlugin(config), - serverRouteModulesPlugin(config), - emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), - manualExternalsPlugin((id, importer) => { - // assets.json is external because this build runs in parallel with the - // browser build and it's not there yet. - if (id === "./assets.json" && importer === "") return true; - - // Mark all bare imports as external. They will be require()'d (or - // imported if ESM) at runtime from node_modules. - if (isBareModuleId(id)) { - let packageName = getNpmPackageName(id); - if ( - !/\bnode_modules\b/.test(importer) && - !nodeBuiltins.includes(packageName) && - !dependencies.includes(packageName) - ) { - options.onWarning( - `The path "${id}" is imported in ` + - `${path.relative(process.cwd(), importer)} but ` + - `${packageName} is not listed in your package.json dependencies. ` + - `Did you forget to install it?`, - packageName - ); - } - - // Include .css files from node_modules in the build so we can get a - // hashed file name to put into the HTML. - if (id.endsWith(".css")) return false; - - // Include "remix" in the build so the server runtime (node) doesn't - // have to try to find the magic exports at runtime. This essentially - // translates all `import x from "remix"` statements into `import x - // from "@remix-run/x"` in the build. - if (packageName === "remix") return false; - - return true; - } - - return false; - }) - ] - }); -} + let dependencies = await getAppDependencies(config); + + let stdin: esbuild.StdinOptions | undefined; + let entryPoints: string[] | undefined; + + if (config.serverEntryPoint) { + entryPoints = [config.serverEntryPoint]; + } else { + stdin = { + contents: config.serverBuildTargetEntryModule, + loader: "ts", + resolveDir: config.rootDirectory + }; + } -function isBareModuleId(id: string): boolean { - return !id.startsWith(".") && !id.startsWith("~") && !path.isAbsolute(id); -} + let plugins: esbuild.Plugin[] = []; + if (config.serverPlatform !== "node") { + plugins.push(NodeModulesPolyfillPlugin()); + } -function getNpmPackageName(id: string): string { - let split = id.split("/"); - let packageName = split[0]; - if (packageName.startsWith("@")) packageName += `/${split[1]}`; - return packageName; + plugins.push( + mdxPlugin(config), + emptyModulesPlugin(config, /\.client\.[tj]sx?$/), + serverRouteModulesPlugin(config), + serverEntryModulesPlugin(config), + serverAssetsPlugin(browserManifestPromiseRef), + serverBareModulesPlugin(config, dependencies) + ); + + return esbuild + .build({ + absWorkingDir: config.rootDirectory, + stdin, + entryPoints, + outfile: config.serverBuildPath, + write: false, + platform: config.serverPlatform, + format: config.serverModuleFormat, + treeShaking: true, + minify: + options.mode === BuildMode.Production && + !!config.serverBuildTarget && + ["cloudflare-workers", "cloudflare-pages"].includes( + config.serverBuildTarget + ), + mainFields: + config.serverModuleFormat === "esm" + ? ["module", "main"] + : ["main", "module"], + target: options.target, + inject: [reactShim], + loader: loaders, + bundle: true, + logLevel: "silent", + incremental: options.incremental, + sourcemap: options.sourcemap ? "inline" : false, + // The server build needs to know how to generate asset URLs for imports + // of CSS and other files. + assetNames: "_assets/[name]-[hash]", + publicPath: config.publicPath, + define: { + "process.env.NODE_ENV": JSON.stringify(options.mode) + }, + plugins + }) + .then(writeServerBuildResult(config)); } async function generateManifests( config: RemixConfig, metafile: esbuild.Metafile -): Promise { +): Promise { let assetsManifest = await createAssetsManifest(config, metafile); let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; assetsManifest.url = config.publicPath + filename; - return Promise.all([ - writeFileSafe( - path.join(config.assetsBuildDirectory, filename), - `window.__remixManifest=${JSON.stringify(assetsManifest)};` - ), - writeFileSafe( - path.join(config.serverBuildDirectory, "assets.json"), - JSON.stringify(assetsManifest, null, 2) - ) - ]); -} - -function getServerEntryPointModule( - config: RemixConfig, - options: BuildOptions -): string { - switch (options.target) { - case BuildTarget.Node14: - return ` -import * as entryServer from ${JSON.stringify( - path.resolve(config.appDirectory, config.entryServerFile) - )}; -${Object.keys(config.routes) - .map((key, index) => { - let route = config.routes[key]; - return `import * as route${index} from ${JSON.stringify( - path.resolve(config.appDirectory, route.file) - )};`; - }) - .join("\n")} -export { default as assets } from "./assets.json"; -export const entry = { module: entryServer }; -export const routes = { - ${Object.keys(config.routes) - .map((key, index) => { - let route = config.routes[key]; - return `${JSON.stringify(key)}: { - id: ${JSON.stringify(route.id)}, - parentId: ${JSON.stringify(route.parentId)}, - path: ${JSON.stringify(route.path)}, - index: ${JSON.stringify(route.index)}, - caseSensitive: ${JSON.stringify(route.caseSensitive)}, - module: route${index} - }`; - }) - .join(",\n ")} -};`; - default: - throw new Error( - `Cannot generate server entry point module for target: ${options.target}` - ); - } -} - -type Route = RemixConfig["routes"][string]; - -const browserSafeRouteExports: { [name: string]: boolean } = { - CatchBoundary: true, - ErrorBoundary: true, - default: true, - handle: true, - links: true, - meta: true, - unstable_shouldReload: true -}; - -/** - * This plugin loads route modules for the browser build, using module shims - * that re-export only the route module exports that are safe for the browser. - */ -function browserRouteModulesPlugin( - config: RemixConfig, - suffixMatcher: RegExp -): esbuild.Plugin { - return { - name: "browser-route-modules", - async setup(build) { - let routesByFile: Map = Object.keys(config.routes).reduce( - (map, key) => { - let route = config.routes[key]; - map.set(path.resolve(config.appDirectory, route.file), route); - return map; - }, - new Map() - ); - - build.onResolve({ filter: suffixMatcher }, args => { - return { path: args.path, namespace: "browser-route-module" }; - }); - - build.onLoad( - { filter: suffixMatcher, namespace: "browser-route-module" }, - async args => { - let file = args.path.replace(suffixMatcher, ""); - let route = routesByFile.get(file); - invariant(route, `Cannot get route by path: ${args.path}`); - - let exports; - try { - exports = ( - await getRouteModuleExportsCached(config, route.id) - ).filter(ex => !!browserSafeRouteExports[ex]); - } catch (error: any) { - return { - errors: [ - { - text: error.message, - pluginName: "browser-route-module" - } - ] - }; - } - let spec = exports.length > 0 ? `{ ${exports.join(", ")} }` : "*"; - let contents = `export ${spec} from ${JSON.stringify(file)};`; - - return { - contents, - resolveDir: path.dirname(file), - loader: "js" - }; - } - ); - } - }; -} - -/** - * This plugin substitutes an empty module for any modules in the `app` - * directory that match the given `filter`. - */ -function emptyModulesPlugin( - config: RemixConfig, - filter: RegExp -): esbuild.Plugin { - return { - name: "empty-modules", - setup(build) { - build.onResolve({ filter }, args => { - let resolved = path.resolve(args.resolveDir, args.path); - if ( - // Limit this behavior to modules found in only the `app` directory. - // This allows node_modules to use the `.server.js` and `.client.js` - // naming conventions with different semantics. - resolved.startsWith(config.appDirectory) - ) { - return { path: args.path, namespace: "empty-module" }; - } - }); + await writeFileSafe( + path.join(config.assetsBuildDirectory, filename), + `window.__remixManifest=${JSON.stringify(assetsManifest)};` + ); - build.onLoad({ filter: /.*/, namespace: "empty-module" }, () => { - return { - // Use an empty CommonJS module here instead of ESM to avoid "No - // matching export" errors in esbuild for stuff that is imported - // from this file. - contents: "module.exports = {};", - loader: "js" - }; - }); - } - }; + return assetsManifest as AssetsManifest; } -/** - * This plugin loads route modules for the server build. - */ -function serverRouteModulesPlugin(config: RemixConfig): esbuild.Plugin { - return { - name: "server-route-modules", - setup(build) { - let routeFiles = new Set( - Object.keys(config.routes).map(key => - path.resolve(config.appDirectory, config.routes[key].file) - ) - ); - - build.onResolve({ filter: /.*/ }, args => { - if (routeFiles.has(args.path)) { - return { path: args.path, namespace: "route-module" }; - } - }); +function writeServerBuildResult(config: RemixConfig) { + return async (buildResult: esbuild.BuildResult) => { + await fse.ensureDir(path.dirname(config.serverBuildPath)); - build.onLoad({ filter: /.*/, namespace: "route-module" }, async args => { - let file = args.path; - let contents = await fsp.readFile(file, "utf-8"); - - // Default to `export {}` if the file is empty so esbuild interprets - // this file as ESM instead of CommonJS with `default: {}`. This helps - // in development when creating new files. - // See https://github.com/evanw/esbuild/issues/1043 - if (!/\S/.test(contents)) { - return { contents: "export {}", loader: "js" }; - } - - return { - contents, - resolveDir: path.dirname(file), - loader: getLoaderForFile(file) - }; - }); + // manually write files to exclude assets from server build + for (let file of buildResult.outputFiles!) { + if (file.path !== config.serverBuildPath) { + continue; + } + await fse.writeFile(file.path, file.contents); + break; } - }; -} -/** - * This plugin marks paths external using a callback function. - */ -function manualExternalsPlugin( - isExternal: (id: string, importer: string) => boolean -): esbuild.Plugin { - return { - name: "manual-externals", - setup(build) { - build.onResolve({ filter: /.*/ }, args => { - if (isExternal(args.path, args.importer)) { - return { path: args.path, external: true }; - } - }); - } + return buildResult; }; } diff --git a/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts new file mode 100644 index 0000000000..aca6a527e8 --- /dev/null +++ b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts @@ -0,0 +1,81 @@ +import * as path from "path"; + +import type esbuild from "esbuild"; + +import { RemixConfig } from "../../config"; +import { getRouteModuleExportsCached } from "../routes"; +import invariant from "../../invariant"; + +type Route = RemixConfig["routes"][string]; + +const browserSafeRouteExports: { [name: string]: boolean } = { + CatchBoundary: true, + ErrorBoundary: true, + default: true, + handle: true, + links: true, + meta: true, + unstable_shouldReload: true +}; + +/** + * This plugin loads route modules for the browser build, using module shims + * that re-export only the route module exports that are safe for the browser. + */ +export function browserRouteModulesPlugin( + config: RemixConfig, + suffixMatcher: RegExp +): esbuild.Plugin { + return { + name: "browser-route-modules", + async setup(build) { + let routesByFile: Map = Object.keys(config.routes).reduce( + (map, key) => { + let route = config.routes[key]; + map.set(path.resolve(config.appDirectory, route.file), route); + return map; + }, + new Map() + ); + + build.onResolve({ filter: suffixMatcher }, args => { + return { path: args.path, namespace: "browser-route-module" }; + }); + + build.onLoad( + { filter: suffixMatcher, namespace: "browser-route-module" }, + async args => { + let theExports; + let file = args.path.replace(suffixMatcher, ""); + let route = routesByFile.get(file); + + try { + invariant(route, `Cannot get route by path: ${args.path}`); + + theExports = ( + await getRouteModuleExportsCached(config, route.id) + ).filter(ex => !!browserSafeRouteExports[ex]); + } catch (error: any) { + return { + errors: [ + { + text: error.message, + pluginName: "browser-route-module" + } + ] + }; + } + let spec = + theExports.length > 0 ? `{ ${theExports.join(", ")} }` : "*"; + let contents = `export ${spec} from ${JSON.stringify(file)};`; + + return { + contents, + resolveDir: path.dirname(file), + loader: "js", + }; + } + ); + } + }; +} diff --git a/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts b/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts new file mode 100644 index 0000000000..9ece7f92be --- /dev/null +++ b/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts @@ -0,0 +1,41 @@ +import * as path from "path"; + +import type esbuild from "esbuild"; + +import { RemixConfig } from "../../config"; + +/** + * This plugin substitutes an empty module for any modules in the `app` + * directory that match the given `filter`. + */ +export function emptyModulesPlugin( + config: RemixConfig, + filter: RegExp +): esbuild.Plugin { + return { + name: "empty-modules", + setup(build) { + build.onResolve({ filter }, args => { + let resolved = path.resolve(args.resolveDir, args.path); + if ( + // Limit this behavior to modules found in only the `app` directory. + // This allows node_modules to use the `.server.js` and `.client.js` + // naming conventions with different semantics. + resolved.startsWith(config.appDirectory) + ) { + return { path: args.path, namespace: "empty-module" }; + } + }); + + build.onLoad({ filter: /.*/, namespace: "empty-module" }, () => { + return { + // Use an empty CommonJS module here instead of ESM to avoid "No + // matching export" errors in esbuild for stuff that is imported + // from this file. + contents: "module.exports = {};", + loader: "js" + }; + }); + } + }; +} diff --git a/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts b/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts new file mode 100644 index 0000000000..13a5e9c815 --- /dev/null +++ b/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts @@ -0,0 +1,40 @@ +import type { Plugin } from "esbuild"; +import jsesc from "jsesc"; +import invariant from "../../invariant"; +import virtualModules from "../virtualModules"; +import type { serverEntryModulesPlugin } from "./serverEntryModulesPlugin"; + +export type BrowserManifestPromiseRef = { current?: Promise }; + +/** + * Creates a virtual module of the asset manifest for consumption. + * See {@link serverEntryModulesPlugin} for consumption. + */ +export function serverAssetsPlugin( + browserManifestPromiseRef: BrowserManifestPromiseRef, + filter: RegExp = virtualModules.assetsManifestVirtualModule.filter +): Plugin { + return { + name: "server-assets", + setup(build) { + build.onResolve({ filter }, ({ path }) => { + return { + path, + namespace: "assets" + }; + }); + build.onLoad({ filter }, async () => { + invariant( + browserManifestPromiseRef.current, + "Missing browser manifest assets ref in server build." + ); + let manifest = await browserManifestPromiseRef.current; + + return { + contents: `export default ${jsesc(manifest, { es6: true })};`, + loader: "js" + }; + }); + } + }; +} diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts new file mode 100644 index 0000000000..e3eab0789b --- /dev/null +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -0,0 +1,97 @@ +import { builtinModules } from "module"; +import { isAbsolute, relative } from "path"; + +import type { Plugin } from "esbuild"; + +import { RemixConfig } from "../../config"; +import virtualModules from "../virtualModules"; + +/** + * A plugin responsible for resolving bare module ids based on server target. + * This includes externalizing for node based plaforms, and bundling for single file + * environments such as cloudflare. + */ +export function serverBareModulesPlugin( + remixConfig: RemixConfig, + dependencies: Record, + onWarning?: (warning: string, key: string) => void +): Plugin { + return { + name: "server-bare-modules", + setup(build) { + build.onResolve({ filter: /.*/ }, ({ importer, path }) => { + // If it's not a bare module ID, bundle it. + if (!isBareModuleId(path)) { + return undefined; + } + + // To prevent `import xxx from "remix"` from ending up in the bundle + // we "bundle" remix but the other modules where the code lives. + if (path === "remix") { + return undefined; + } + + // These are our virutal modules, always bundle the because there is no + // "real" file on disk to externalize. + if ( + path === virtualModules.serverBuildVirutalModule.path || + path === virtualModules.assetsManifestVirtualModule.path + ) { + return undefined; + } + + // Always bundle CSS files so we get immutable fingerprinted asset URLs. + if (path.endsWith(".css")) { + return undefined; + } + + let packageName = getNpmPackageName(path); + + // Warn if we can't find an import for a package. + if ( + onWarning && + !isNodeBuiltIn(packageName) && + !/\bnode_modules\b/.test(importer) && + !builtinModules.includes(packageName) && + !dependencies[packageName] + ) { + onWarning( + `The path "${path}" is imported in ` + + `${relative(process.cwd(), importer)} but ` + + `${packageName} is not listed in your package.json dependencies. ` + + `Did you forget to install it?`, + packageName + ); + } + + switch (remixConfig.serverBuildTarget) { + // Always bundle everything for cloudflare. + case "cloudflare-pages": + case "cloudflare-workers": + return undefined; + } + + // Externalize everything else if we've gotten here. + return { + path, + external: true + }; + }); + } + }; +} + +function isNodeBuiltIn(packageName: string) { + return builtinModules.includes(packageName); +} + +function getNpmPackageName(id: string): string { + let split = id.split("/"); + let packageName = split[0]; + if (packageName.startsWith("@")) packageName += `/${split[1]}`; + return packageName; +} + +function isBareModuleId(id: string): boolean { + return !id.startsWith(".") && !id.startsWith("~") && !isAbsolute(id); +} diff --git a/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts new file mode 100644 index 0000000000..ba37110b06 --- /dev/null +++ b/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts @@ -0,0 +1,66 @@ +import * as path from "path"; +import type { Plugin } from "esbuild"; + +import { RemixConfig } from "../../config"; +import virtualModules from "../virtualModules"; + +/** + * Creates a virtual module called `@remix-run/dev/server-build` that exports the + * compiled server build for consumption in remix request handlers. This allows + * for you to consume the build in a custom server entry that is also fed through + * the compiler. + */ +export function serverEntryModulesPlugin( + remixConfig: RemixConfig, + filter: RegExp = virtualModules.serverBuildVirutalModule.filter +): Plugin { + return { + name: "server-entry", + setup(build) { + build.onResolve({ filter }, ({ path }) => { + return { + path, + namespace: "server-entry" + }; + }); + + build.onLoad({ filter }, async () => { + return { + resolveDir: remixConfig.appDirectory, + loader: "js", + contents: ` +import * as entryServer from ${JSON.stringify( + path.resolve(remixConfig.appDirectory, remixConfig.entryServerFile) + )}; +${Object.keys(remixConfig.routes) + .map((key, index) => { + let route = remixConfig.routes[key]; + return `import * as route${index} from ${JSON.stringify( + path.resolve(remixConfig.appDirectory, route.file) + )};`; + }) + .join("\n")} + export { default as assets } from ${JSON.stringify( + virtualModules.assetsManifestVirtualModule.path + )}; + export const entry = { module: entryServer }; + export const routes = { + ${Object.keys(remixConfig.routes) + .map((key, index) => { + let route = remixConfig.routes[key]; + return `${JSON.stringify(key)}: { + id: ${JSON.stringify(route.id)}, + parentId: ${JSON.stringify(route.parentId)}, + path: ${JSON.stringify(route.path)}, + index: ${JSON.stringify(route.index)}, + caseSensitive: ${JSON.stringify(route.caseSensitive)}, + module: route${index} + }`; + }) + .join(",\n ")} + };` + }; + }); + } + }; +} diff --git a/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts new file mode 100644 index 0000000000..ec70432317 --- /dev/null +++ b/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts @@ -0,0 +1,49 @@ +import * as path from "path"; +import * as fsp from "fs/promises"; + +import type esbuild from "esbuild"; + +import type { RemixConfig } from "../../config"; +import { getLoaderForFile } from "../loaders"; + +/** + * This plugin loads route modules for the server build and prevents errors + * while adding new files in development mode. + */ +export function serverRouteModulesPlugin(config: RemixConfig): esbuild.Plugin { + return { + name: "server-route-modules", + setup(build) { + let routeFiles = new Set( + Object.keys(config.routes).map(key => + path.resolve(config.appDirectory, config.routes[key].file) + ) + ); + + build.onResolve({ filter: /.*/ }, args => { + if (routeFiles.has(args.path)) { + return { path: args.path, namespace: "route" }; + } + }); + + build.onLoad({ filter: /.*/, namespace: "route" }, async args => { + let file = args.path; + let contents = await fsp.readFile(file, "utf-8"); + + // Default to `export {}` if the file is empty so esbuild interprets + // this file as ESM instead of CommonJS with `default: {}`. This helps + // in development when creating new files. + // See https://github.com/evanw/esbuild/issues/1043 + if (!/\S/.test(contents)) { + return { contents: "export {}", loader: "js" }; + } + + return { + contents, + resolveDir: path.dirname(file), + loader: getLoaderForFile(file) + }; + }); + } + }; +} diff --git a/packages/remix-dev/compiler/virtualModules.ts b/packages/remix-dev/compiler/virtualModules.ts new file mode 100644 index 0000000000..11c62564cf --- /dev/null +++ b/packages/remix-dev/compiler/virtualModules.ts @@ -0,0 +1,10 @@ +export default { + serverBuildVirutalModule: { + path: "@remix-run/dev/server-build", + filter: /^@remix-run\/dev\/server-build$/ + }, + assetsManifestVirtualModule: { + path: "@remix-run/dev/assets-manifest", + filter: /^@remix-run\/dev\/assets-manifest$/ + } +}; diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 9dca8038ca..e0d04a84b0 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -1,11 +1,13 @@ -import * as fs from "fs"; import * as path from "path"; +import * as fse from "fs-extra"; import type { RouteManifest, DefineRoutesFunction } from "./config/routes"; import { defineRoutes } from "./config/routes"; import { defineConventionalRoutes } from "./config/routesConvention"; import { ServerMode, isValidServerMode } from "./config/serverModes"; +import virtualModules from "./compiler/virtualModules"; + export interface RemixMdxConfig { rehypePlugins?: any[]; remarkPlugins?: any[]; @@ -15,6 +17,17 @@ export type RemixMdxConfigFunction = ( filename: string ) => Promise | RemixMdxConfig | undefined; +export type ServerBuildTarget = + | "node-cjs" + | "arc" + | "netlify" + | "vercel" + | "cloudflare-pages" + | "cloudflare-workers"; + +export type ServerModuleFormat = "esm" | "cjs"; +export type ServerPlatform = "node" | "neutral"; + /** * The user-provided config in `remix.config.js`. */ @@ -42,9 +55,16 @@ export interface AppConfig { /** * 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. This file should end in a `.js`. Defaults + * are based on {@link ServerConfig.serverBuildTarget}. + */ + serverBuildPath?: string; + /** * The path to the browser build, relative to `remix.config.js`. Defaults to * "public/build". @@ -55,7 +75,7 @@ export interface AppConfig { * The path to the browser build, relative to remix.config.js. Defaults to * "public/build". * - * @deprecated Use `assetsBuildDirectory` instead + * @deprecated Use `{@link ServerConfig.assetsBuildDirectory}` instead */ browserBuildDirectory?: string; @@ -81,13 +101,25 @@ export interface AppConfig { /** * The output format of the server build. Defaults to "cjs". + * * @deprecated Use {@link ServerConfig.serverBuildTarget} instead. */ - serverModuleFormat?: "esm" | "cjs"; + serverModuleFormat?: ServerModuleFormat; /** * The platform the server build is targeting. Defaults to "node". + * @deprecated Use {@link ServerConfig.serverBuildTarget} instead. + */ + serverPlatform?: ServerPlatform; + + /** + * The target of the server build. Defaults to "node-cjs". */ - serverPlatform?: "node" | "neutral"; + serverBuildTarget?: ServerBuildTarget; + + /** + * A server entrypoint relative to the root directory that becomes your server's main module. + */ + server?: string; /** * A list of filenames or a glob patterns to match files in the `app/routes` @@ -132,9 +164,10 @@ export interface RemixConfig { routes: RouteManifest; /** - * The absolute path to the server build directory. + * The path to the server build file. This file should end in a `.js`. Defaults + * are based on {@link ServerConfig.serverBuildTarget}. */ - serverBuildDirectory: string; + serverBuildPath: string; /** * The absolute path to the assets build directory. @@ -169,12 +202,27 @@ export interface RemixConfig { /** * The output format of the server build. Defaults to "cjs". */ - serverModuleFormat: "esm" | "cjs"; + serverModuleFormat: ServerModuleFormat; /** * The platform the server build is targeting. Defaults to "node". */ - serverPlatform: "node" | "neutral"; + serverPlatform: ServerPlatform; + + /** + * The target of the server build. + */ + serverBuildTarget?: ServerBuildTarget; + + /** + * The default entry module for the server build if a {@see RemixConfig.customServer} is not provided. + */ + serverBuildTargetEntryModule: string; + + /** + * A server entrypoint relative to the root directory that becomes your server's main module. + */ + serverEntryPoint?: string; } /** @@ -203,8 +251,20 @@ export async function readConfig( throw new Error(`Error loading Remix config in ${configFile}`); } - let serverModuleFormat = appConfig.serverModuleFormat || "cjs"; - let serverPlatform = appConfig.serverPlatform || "node"; + let customServerEntryPoint = appConfig.server; + let serverBuildTarget: ServerBuildTarget | undefined = + appConfig.serverBuildTarget; + let serverModuleFormat: ServerModuleFormat = + appConfig.serverModuleFormat || "cjs"; + let serverPlatform: ServerPlatform = appConfig.serverPlatform || "node"; + switch (appConfig.serverBuildTarget) { + case "cloudflare-pages": + case "cloudflare-workers": + serverModuleFormat = "esm"; + serverPlatform = "neutral"; + break; + } + let mdx = appConfig.mdx; let appDirectory = path.resolve( @@ -227,10 +287,34 @@ export async function readConfig( throw new Error(`Missing "entry.server" file in ${appDirectory}`); } - let serverBuildDirectory = path.resolve( - rootDirectory, - appConfig.serverBuildDirectory || "build" - ); + 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/server/index.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 = path.resolve( rootDirectory, @@ -242,7 +326,14 @@ export async function readConfig( let devServerPort = appConfig.devServerPort || 8002; let devServerBroadcastDelay = appConfig.devServerBroadcastDelay || 0; - let publicPath = addTrailingSlash(appConfig.publicPath || "/build/"); + let defaultPublicPath = "/build/"; + switch (serverBuildTarget) { + case "arc": + defaultPublicPath = "/_static/build/"; + break; + } + + let publicPath = addTrailingSlash(appConfig.publicPath || defaultPublicPath); let rootRouteFile = findEntry(appDirectory, "root"); if (!rootRouteFile) { @@ -252,7 +343,7 @@ export async function readConfig( let routes: RouteManifest = { root: { path: "", id: "root", file: rootRouteFile } }; - if (fs.existsSync(path.resolve(appDirectory, "routes"))) { + if (fse.existsSync(path.resolve(appDirectory, "routes"))) { let conventionalRoutes = defineConventionalRoutes( appDirectory, appConfig.ignoredRouteFiles @@ -270,6 +361,10 @@ export async function readConfig( } } + let serverBuildTargetEntryModule = `export * from ${JSON.stringify( + virtualModules.serverBuildVirutalModule.path + )};`; + return { appDirectory, cacheDirectory, @@ -281,10 +376,13 @@ export async function readConfig( publicPath, rootDirectory, routes, - serverBuildDirectory, + serverBuildPath, serverMode, serverModuleFormat, serverPlatform, + serverBuildTarget, + serverBuildTargetEntryModule, + serverEntryPoint: customServerEntryPoint, mdx }; } @@ -298,7 +396,7 @@ const entryExts = [".js", ".jsx", ".ts", ".tsx"]; function findEntry(dir: string, basename: string): string | undefined { for (let ext of entryExts) { let file = path.resolve(dir, basename + ext); - if (fs.existsSync(file)) return path.relative(dir, file); + if (fse.existsSync(file)) return path.relative(dir, file); } return undefined; diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index de3b1e1f91..6969f5ab0f 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -15,6 +15,8 @@ "remix": "cli.js" }, "dependencies": { + "@esbuild-plugins/node-modules-polyfill": "^0.1.4", + "@remix-run/server-runtime": "1.1.3", "cacache": "^15.0.5", "chokidar": "^3.5.1", "esbuild": "0.13.14", diff --git a/packages/remix-dev/server-build.ts b/packages/remix-dev/server-build.ts new file mode 100644 index 0000000000..0e9c2d097a --- /dev/null +++ b/packages/remix-dev/server-build.ts @@ -0,0 +1,5 @@ +import type { ServerBuild } from "@remix-run/server-runtime"; + +export const assets: ServerBuild["assets"] = undefined!; +export const entry: ServerBuild["entry"] = undefined!; +export const routes: ServerBuild["routes"] = undefined!; diff --git a/packages/remix-dev/tsconfig.json b/packages/remix-dev/tsconfig.json index 9510b82e2c..bc81683011 100644 --- a/packages/remix-dev/tsconfig.json +++ b/packages/remix-dev/tsconfig.json @@ -1,6 +1,6 @@ { "include": ["../../types/mdx-js__mdx.d.ts", "**/*"], - "exclude": ["__tests__"], + "exclude": ["__tests__", "./compiler/shims/*"], "compilerOptions": { "lib": ["ES2019", "DOM.Iterable"], "target": "ES2019", From 6ccf22a3809e48da11cbbb1d03ee961b9373fbd9 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Sat, 22 Jan 2022 03:05:19 +0000 Subject: [PATCH 0191/1690] chore: format formatted fd9848e981161415dc8bbe6fc56e835f79e654ad --- packages/remix-dev/compiler.ts | 4 ++-- .../remix-dev/compiler/plugins/browserRouteModulesPlugin.ts | 5 ++--- packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts | 3 +-- packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts | 1 + .../remix-dev/compiler/plugins/serverBareModulesPlugin.ts | 3 +-- .../remix-dev/compiler/plugins/serverEntryModulesPlugin.ts | 2 +- .../remix-dev/compiler/plugins/serverRouteModulesPlugin.ts | 1 - packages/remix-dev/config.ts | 1 - 8 files changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index feb1481c79..b0de7e5900 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -4,6 +4,8 @@ import * as esbuild from "esbuild"; import * as fse from "fs-extra"; import debounce from "lodash.debounce"; import chokidar from "chokidar"; +import type { AssetsManifest } from "@remix-run/server-runtime/entry"; +import { NodeModulesPolyfillPlugin } from "@esbuild-plugins/node-modules-polyfill"; import { BuildMode, BuildTarget } from "./build"; import type { RemixConfig } from "./config"; @@ -21,8 +23,6 @@ import { serverBareModulesPlugin } from "./compiler/plugins/serverBareModulesPlu import { serverEntryModulesPlugin } from "./compiler/plugins/serverEntryModulesPlugin"; import { serverRouteModulesPlugin } from "./compiler/plugins/serverRouteModulesPlugin"; import { writeFileSafe } from "./compiler/utils/fs"; -import type { AssetsManifest } from "@remix-run/server-runtime/entry"; -import { NodeModulesPolyfillPlugin } from "@esbuild-plugins/node-modules-polyfill"; // When we build Remix, this shim file is copied directly into the output // directory in the same place relative to this file. It is eventually injected diff --git a/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts index aca6a527e8..5ac8a69a9b 100644 --- a/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts @@ -1,8 +1,7 @@ import * as path from "path"; - import type esbuild from "esbuild"; -import { RemixConfig } from "../../config"; +import type { RemixConfig } from "../../config"; import { getRouteModuleExportsCached } from "../routes"; import invariant from "../../invariant"; @@ -72,7 +71,7 @@ export function browserRouteModulesPlugin( return { contents, resolveDir: path.dirname(file), - loader: "js", + loader: "js" }; } ); diff --git a/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts b/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts index 9ece7f92be..54f512038c 100644 --- a/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts @@ -1,8 +1,7 @@ import * as path from "path"; - import type esbuild from "esbuild"; -import { RemixConfig } from "../../config"; +import type { RemixConfig } from "../../config"; /** * This plugin substitutes an empty module for any modules in the `app` diff --git a/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts b/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts index 13a5e9c815..b82a4dbfc0 100644 --- a/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts @@ -1,5 +1,6 @@ import type { Plugin } from "esbuild"; import jsesc from "jsesc"; + import invariant from "../../invariant"; import virtualModules from "../virtualModules"; import type { serverEntryModulesPlugin } from "./serverEntryModulesPlugin"; diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts index e3eab0789b..6134bf4d13 100644 --- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -1,9 +1,8 @@ import { builtinModules } from "module"; import { isAbsolute, relative } from "path"; - import type { Plugin } from "esbuild"; -import { RemixConfig } from "../../config"; +import type { RemixConfig } from "../../config"; import virtualModules from "../virtualModules"; /** diff --git a/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts index ba37110b06..c0b4fa5f6b 100644 --- a/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts @@ -1,7 +1,7 @@ import * as path from "path"; import type { Plugin } from "esbuild"; -import { RemixConfig } from "../../config"; +import type { RemixConfig } from "../../config"; import virtualModules from "../virtualModules"; /** diff --git a/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts index ec70432317..567c2a0aa5 100644 --- a/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts @@ -1,6 +1,5 @@ import * as path from "path"; import * as fsp from "fs/promises"; - import type esbuild from "esbuild"; import type { RemixConfig } from "../../config"; diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index e0d04a84b0..f8beeb497e 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -5,7 +5,6 @@ import type { RouteManifest, DefineRoutesFunction } from "./config/routes"; import { defineRoutes } from "./config/routes"; import { defineConventionalRoutes } from "./config/routesConvention"; import { ServerMode, isValidServerMode } from "./config/serverModes"; - import virtualModules from "./compiler/virtualModules"; export interface RemixMdxConfig { From 8f4fdaf06b7c64ecc822e73e17ba2b896adaa9a0 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 24 Jan 2022 09:58:00 -0800 Subject: [PATCH 0192/1690] fix: watch remix.config.js server file (#1605) --- packages/remix-dev/compiler.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index b0de7e5900..ec27796ef9 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -205,8 +205,13 @@ export async function watch( if (onRebuildFinish) onRebuildFinish(); }, 100); + let toWatch = [config.appDirectory]; + if (config.serverEntryPoint) { + toWatch.push(config.serverEntryPoint); + } + let watcher = chokidar - .watch(config.appDirectory, { + .watch(toWatch, { persistent: true, ignoreInitial: true, awaitWriteFinish: { From 6826037bf26aeedd0ebe00ea77f81082625720c7 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 26 Jan 2022 09:50:02 -0800 Subject: [PATCH 0193/1690] chore: add useful error message if virtual module is used directly (#1656) --- packages/remix-dev/server-build.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/remix-dev/server-build.ts b/packages/remix-dev/server-build.ts index 0e9c2d097a..458eb43d52 100644 --- a/packages/remix-dev/server-build.ts +++ b/packages/remix-dev/server-build.ts @@ -1,5 +1,11 @@ import type { ServerBuild } from "@remix-run/server-runtime"; +throw new Error( + "@remix-run/dev/server-build is not meant to be used directly from node_modules." + + " It is exists to provide type definitions for a virtual module provided" + + " the Remix compiler at build time." +); + export const assets: ServerBuild["assets"] = undefined!; export const entry: ServerBuild["entry"] = undefined!; export const routes: ServerBuild["routes"] = undefined!; From 1a9bff1261dd3320e6cd0e291e6001d74f314236 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Thu, 27 Jan 2022 14:55:44 -0500 Subject: [PATCH 0194/1690] feat(dev): get available port if not available (#871) --- packages/remix-dev/cli/commands.ts | 5 ++++- packages/remix-dev/config.ts | 3 ++- packages/remix-dev/package.json | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index dd28b825ee..42ab8f6803 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -7,6 +7,7 @@ import WebSocket from "ws"; import type { Server } from "http"; import type * as Express from "express"; import type { createApp as createAppType } from "@remix-run/serve"; +import getPort from "get-port"; import { BuildMode, isBuildMode } from "../build"; import * as compiler from "../compiler"; @@ -156,7 +157,9 @@ export async function dev(remixRoot: string, modeArg?: string) { let config = await readConfig(remixRoot); let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Development; - let port = process.env.PORT || 3000; + let port = await getPort({ + port: process.env.PORT ? Number(process.env.PORT) : 3000 + }); if (config.serverEntryPoint) { throw new Error("remix dev is not supported for custom servers."); diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index f8beeb497e..6d1b48369a 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -1,5 +1,6 @@ import * as path from "path"; import * as fse from "fs-extra"; +import getPort from "get-port"; import type { RouteManifest, DefineRoutesFunction } from "./config/routes"; import { defineRoutes } from "./config/routes"; @@ -322,7 +323,7 @@ export async function readConfig( path.join("public", "build") ); - let devServerPort = appConfig.devServerPort || 8002; + let devServerPort = await getPort({ port: appConfig.devServerPort || 8002 }); let devServerBroadcastDelay = appConfig.devServerBroadcastDelay || 0; let defaultPublicPath = "/build/"; diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 6969f5ab0f..e818619ae4 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -22,6 +22,7 @@ "esbuild": "0.13.14", "exit-hook": "2.2.1", "fs-extra": "^10.0.0", + "get-port": "^5.1.1", "lodash.debounce": "^4.0.8", "meow": "^7.1.1", "minimatch": "^3.0.4", From 74c14b65314ca260f4740c1ce1704d4ecd764976 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Thu, 27 Jan 2022 13:06:40 -0700 Subject: [PATCH 0195/1690] feat(LiveReload): automatically determines the port to use and DCE (#1352) --- packages/remix-dev/compiler.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index ec27796ef9..9a02093d1b 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -363,7 +363,10 @@ async function createBrowserBuild( assetNames: "_assets/[name]-[hash]", publicPath: config.publicPath, define: { - "process.env.NODE_ENV": JSON.stringify(options.mode) + "process.env.NODE_ENV": JSON.stringify(options.mode), + "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( + config.devServerPort + ) }, plugins: [ mdxPlugin(config), From 604c92c53b7efec190f75819a6947f37e45a4371 Mon Sep 17 00:00:00 2001 From: Sebastian Davids Date: Sun, 30 Jan 2022 04:56:56 +0100 Subject: [PATCH 0196/1690] feat: Remove x-powered-by header (#1710) (#1712) Signed-off-by: Sebastian Davids --- packages/remix-serve/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/remix-serve/index.ts b/packages/remix-serve/index.ts index 9dbbcae7dc..4e017fd0f9 100644 --- a/packages/remix-serve/index.ts +++ b/packages/remix-serve/index.ts @@ -6,6 +6,8 @@ import { createRequestHandler } from "@remix-run/express"; export function createApp(buildPath: string, mode = "production") { let app = express(); + app.disable("x-powered-by"); + app.use(compression()); app.use(express.static("public", { immutable: true, maxAge: "1y" })); From a3da597cd0e368d0f9d06cc559535654f6dcbeb8 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 31 Jan 2022 11:55:11 -0800 Subject: [PATCH 0197/1690] fix: avoid `export *` in magic files for shaking (#1698) fix: add mainFields to client build to resolve ESM modules fix: default remix module to a useful error message --- packages/remix-dev/compiler.ts | 1 + .../plugins/browserRouteModulesPlugin.ts | 5 ++- packages/remix-dev/setup.ts | 45 ++++++++++++++++--- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 9a02093d1b..d57a0cc444 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -356,6 +356,7 @@ async function createBrowserBuild( sourcemap: options.sourcemap, metafile: true, incremental: options.incremental, + mainFields: ["browser", "module", "main"], treeShaking: true, minify: options.mode === BuildMode.Production, entryNames: "[dir]/[name]-[hash]", diff --git a/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts index 5ac8a69a9b..a5a456776e 100644 --- a/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts @@ -38,7 +38,10 @@ export function browserRouteModulesPlugin( ); build.onResolve({ filter: suffixMatcher }, args => { - return { path: args.path, namespace: "browser-route-module" }; + return { + path: args.path, + namespace: "browser-route-module" + }; }); build.onLoad( diff --git a/packages/remix-dev/setup.ts b/packages/remix-dev/setup.ts index 3ab2a719c1..ee3663d1c1 100644 --- a/packages/remix-dev/setup.ts +++ b/packages/remix-dev/setup.ts @@ -58,11 +58,46 @@ export async function setupRemix(platform: SetupPlatform): Promise { let serverExportsDir = path.resolve(serverPkgJsonFile, "..", "magicExports"); let clientExportsDir = path.resolve(clientPkgJsonFile, "..", "magicExports"); - await Promise.all([ - fse.copy(platformExportsDir, remixPkgDir), - fse.copy(serverExportsDir, remixPkgDir), - fse.copy(clientExportsDir, remixPkgDir) - ]); + let magicTypes = await combineFilesInDirs( + [platformExportsDir, serverExportsDir, clientExportsDir], + ".d.ts" + ); + + let magicCJS = await combineFilesInDirs( + [platformExportsDir, serverExportsDir, clientExportsDir], + ".js" + ); + + let magicESM = await combineFilesInDirs( + [ + path.join(platformExportsDir, "esm"), + path.join(serverExportsDir, "esm"), + path.join(clientExportsDir, "esm") + ], + ".js" + ); + + await fse.writeFile(path.join(remixPkgDir, "index.js"), magicCJS); + await fse.writeFile(path.join(remixPkgDir, "index.d.ts"), magicTypes); + await fse.writeFile(path.join(remixPkgDir, "esm/index.js"), magicESM); +} + +async function combineFilesInDirs( + dirs: string[], + ext: string +): Promise { + let combined = ""; + for (let dir of dirs) { + let files = await fse.readdir(dir); + for (let file of files) { + if (!file.endsWith(ext)) { + continue; + } + let contents = await fse.readFile(path.join(dir, file), "utf8"); + combined += contents + "\n"; + } + } + return combined; } function resolvePackageJsonFile(packageName: string): string { From df383ef43d690aa88d39425cacb3dbf4d8884102 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 31 Jan 2022 12:07:55 -0800 Subject: [PATCH 0198/1690] fix: define REMIX_DEV_SERVER_WS_PORT for server (#1745) --- packages/remix-dev/compiler.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index d57a0cc444..dafabf791e 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -443,7 +443,10 @@ async function createServerBuild( assetNames: "_assets/[name]-[hash]", publicPath: config.publicPath, define: { - "process.env.NODE_ENV": JSON.stringify(options.mode) + "process.env.NODE_ENV": JSON.stringify(options.mode), + "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( + config.devServerPort + ) }, plugins }) From 89207f5b222794eb288fb7748b15e46e922cab57 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 31 Jan 2022 12:32:13 -0800 Subject: [PATCH 0199/1690] feat: add deno template (#1667) --- packages/remix-dev/__tests__/cli-test.ts | 4 ++-- packages/remix-dev/cli.ts | 4 ++-- .../compiler/plugins/serverBareModulesPlugin.ts | 9 +++++++++ packages/remix-dev/config.ts | 4 +++- packages/remix-dev/setup.ts | 6 ++++-- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts index 5ef687c1e9..a825e4e9de 100644 --- a/packages/remix-dev/__tests__/cli-test.ts +++ b/packages/remix-dev/__tests__/cli-test.ts @@ -34,10 +34,10 @@ describe("remix cli", () => { --version, -v Print the CLI version and exit --json Print the routes as JSON (remix routes only) - --sourcemap Generate source maps (remix build only) + --sourcemap Generate source maps (remix build only) Values - [remixPlatform] \\"node\\" is currently the only platform + [remixPlatform] Can be one of: node, cloudflare-pages, cloudflare-workers, or deno Examples $ remix build my-website diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index bb8a1c33fc..6913320ea2 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -14,10 +14,10 @@ Options --version, -v Print the CLI version and exit --json Print the routes as JSON (remix routes only) - --sourcemap Generate source maps (remix build only) + --sourcemap Generate source maps (remix build only) Values - [remixPlatform] "node" is currently the only platform + [remixPlatform] Can be one of: node, cloudflare-pages, cloudflare-workers, or deno Examples $ remix build my-website diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts index 6134bf4d13..00c22f23b7 100644 --- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -68,6 +68,15 @@ export function serverBareModulesPlugin( case "cloudflare-pages": case "cloudflare-workers": return undefined; + // Map node externals to deno std libs and bundle everything else. + case "deno": + if (isNodeBuiltIn(packageName)) { + return { + path: `https://deno.land/std/node/${packageName}/mod.ts`, + external: true + }; + } + return undefined; } // Externalize everything else if we've gotten here. diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 6d1b48369a..c9d4b12b2c 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -23,7 +23,8 @@ export type ServerBuildTarget = | "netlify" | "vercel" | "cloudflare-pages" - | "cloudflare-workers"; + | "cloudflare-workers" + | "deno"; export type ServerModuleFormat = "esm" | "cjs"; export type ServerPlatform = "node" | "neutral"; @@ -260,6 +261,7 @@ export async function readConfig( switch (appConfig.serverBuildTarget) { case "cloudflare-pages": case "cloudflare-workers": + case "deno": serverModuleFormat = "esm"; serverPlatform = "neutral"; break; diff --git a/packages/remix-dev/setup.ts b/packages/remix-dev/setup.ts index ee3663d1c1..ce281c3368 100644 --- a/packages/remix-dev/setup.ts +++ b/packages/remix-dev/setup.ts @@ -4,14 +4,16 @@ import * as fse from "fs-extra"; export enum SetupPlatform { CloudflarePages = "cloudflare-pages", CloudflareWorkers = "cloudflare-workers", - Node = "node" + Node = "node", + Deno = "deno" } export function isSetupPlatform(platform: any): platform is SetupPlatform { return [ SetupPlatform.CloudflarePages, SetupPlatform.CloudflareWorkers, - SetupPlatform.Node + SetupPlatform.Node, + SetupPlatform.Deno ].includes(platform); } From 413e7a4624a0d047aae694402242eed35003665f Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Mon, 31 Jan 2022 15:42:29 -0500 Subject: [PATCH 0200/1690] fix: allow remix dev and remix-serve to work offline (#1743) --- packages/remix-dev/cli/commands.ts | 2 +- packages/remix-serve/cli.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 42ab8f6803..cc508af6e4 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -182,7 +182,7 @@ export async function dev(remixRoot: string, modeArg?: string) { .find(ip => ip?.family == "IPv4" && !ip.internal)?.address; if (!address) { - throw new Error("Could not find an IPv4 address."); + address = "localhost"; } server = app.listen(port, () => { diff --git a/packages/remix-serve/cli.ts b/packages/remix-serve/cli.ts index fbf07c1a1b..1a1e33511a 100644 --- a/packages/remix-serve/cli.ts +++ b/packages/remix-serve/cli.ts @@ -21,7 +21,7 @@ createApp(buildPath).listen(port, () => { .find(ip => ip?.family == "IPv4" && !ip.internal)?.address; if (!address) { - throw new Error("Could not find an IPv4 address."); + address = "localhost"; } console.log(`Remix App Server started at http://${address}:${port}`); From 435ff126eac648724efb9a41d0b0746160cc0fb8 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 1 Feb 2022 08:18:10 -0800 Subject: [PATCH 0201/1690] chore: Fix tests (#1686) * chore: Fix tests * Sign the CLA * Ignore examples dir in jest module loader * Fix JSON formatting --- packages/remix-express/__tests__/server-test.ts | 2 +- packages/remix-express/__tests__/setup.ts | 2 ++ packages/remix-node/__tests__/setup.ts | 2 ++ packages/remix-server-runtime/__tests__/setup.ts | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 packages/remix-express/__tests__/setup.ts create mode 100644 packages/remix-node/__tests__/setup.ts create mode 100644 packages/remix-server-runtime/__tests__/setup.ts diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index 5b532ad4ba..de9667973d 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -13,7 +13,7 @@ import { // We don't want to test that the remix server works here (that's what the // puppetteer tests do), we just want to test the express adapter -jest.mock("@remix-run/server-runtime/server"); +jest.mock("@remix-run/server-runtime"); let mockedCreateRequestHandler = createRemixRequestHandler as jest.MockedFunction< typeof createRemixRequestHandler diff --git a/packages/remix-express/__tests__/setup.ts b/packages/remix-express/__tests__/setup.ts new file mode 100644 index 0000000000..917305ac93 --- /dev/null +++ b/packages/remix-express/__tests__/setup.ts @@ -0,0 +1,2 @@ +import { installGlobals } from "@remix-run/node"; +installGlobals(); diff --git a/packages/remix-node/__tests__/setup.ts b/packages/remix-node/__tests__/setup.ts new file mode 100644 index 0000000000..917305ac93 --- /dev/null +++ b/packages/remix-node/__tests__/setup.ts @@ -0,0 +1,2 @@ +import { installGlobals } from "@remix-run/node"; +installGlobals(); diff --git a/packages/remix-server-runtime/__tests__/setup.ts b/packages/remix-server-runtime/__tests__/setup.ts new file mode 100644 index 0000000000..917305ac93 --- /dev/null +++ b/packages/remix-server-runtime/__tests__/setup.ts @@ -0,0 +1,2 @@ +import { installGlobals } from "@remix-run/node"; +installGlobals(); From f4186dddd1e48e8e72ea4ed074b95a751cabebf8 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 1 Feb 2022 09:15:24 -0800 Subject: [PATCH 0202/1690] Export AppConfig from remix-run/dev (#1763) Also, remove all deep imports from remix-run/dev. --- packages/remix-dev/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/remix-dev/index.ts b/packages/remix-dev/index.ts index 0959592840..4d401d5c46 100644 --- a/packages/remix-dev/index.ts +++ b/packages/remix-dev/index.ts @@ -1 +1,3 @@ import "./modules"; + +export type { AppConfig } from "./config"; From 8d0379f9fa0816482f8455b258c76678ccacd9fb Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 1 Feb 2022 14:31:04 -0700 Subject: [PATCH 0203/1690] chore(tests): add integration fixture tests (#1765) --- integration/action-test.tsx | 83 ++++++++ integration/compiler-test.tsx | 34 ++++ integration/global-setup.ts | 12 ++ integration/headers-test.tsx | 123 ++++++++++++ integration/helpers/create-fixture.tsx | 252 +++++++++++++++++++++++++ integration/loader-test.tsx | 48 +++++ packages/remix-dev/cli/commands.ts | 7 +- packages/remix-dev/log.ts | 5 + 8 files changed, 561 insertions(+), 3 deletions(-) create mode 100644 integration/action-test.tsx create mode 100644 integration/compiler-test.tsx create mode 100644 integration/global-setup.ts create mode 100644 integration/headers-test.tsx create mode 100644 integration/helpers/create-fixture.tsx create mode 100644 integration/loader-test.tsx create mode 100644 packages/remix-dev/log.ts diff --git a/integration/action-test.tsx b/integration/action-test.tsx new file mode 100644 index 0000000000..1d9dc15079 --- /dev/null +++ b/integration/action-test.tsx @@ -0,0 +1,83 @@ +import { + createFixture, + createAppFixture, + selectHtml +} from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; + +describe("action + useActionData", () => { + describe("with x-www-form-urlencoded", () => { + let fixture: Fixture; + let app: AppFixture; + + const FIELD_NAME = "message"; + const WAITING_VALUE = "Waiting..."; + const SUBMITTED_VALUE = "Submission"; + + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/routes/urlencoded.jsx": ` + import { Form, useActionData } from "remix"; + + export let action = async ({ request }) => { + let formData = await request.formData(); + return formData.get("${FIELD_NAME}"); + }; + + export default function Actions() { + let data = useActionData() + + return ( +
+

+ {data ? {data} : "${WAITING_VALUE}"} +

+

+ + +

+
+ ); + } + ` + } + }); + + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); + + it("returns undefined for action data on GET", async () => { + let res = await fixture.requestDocument("/urlencoded"); + let html = selectHtml(await res.text(), "#text"); + expect(html).toMatch(WAITING_VALUE); + }); + + it("returns data from the action after POST", async () => { + const FIELD_VALUE = "cheeseburger"; + + let params = new URLSearchParams(); + params.append(FIELD_NAME, FIELD_VALUE); + + let res = await fixture.postDocument("/urlencoded", params); + + let html = selectHtml(await res.text(), "#text"); + expect(html).toMatch(FIELD_VALUE); + }); + + it("returns data after a form submission with JavaScript", async () => { + await app.goto(`/urlencoded`); + let html = await app.getHtml("#text"); + expect(html).toMatch(WAITING_VALUE); + + await app.page.click("button[type=submit]"); + await app.page.waitForSelector("#action-text"); + html = await app.getHtml("#text"); + expect(html).toMatch(SUBMITTED_VALUE); + }); + }); +}); diff --git a/integration/compiler-test.tsx b/integration/compiler-test.tsx new file mode 100644 index 0000000000..af40074e11 --- /dev/null +++ b/integration/compiler-test.tsx @@ -0,0 +1,34 @@ +import { createFixture, createAppFixture } from "./helpers/create-fixture"; + +describe("compiler", () => { + it("removes server code with `*.server` files", async () => { + let fixture = await createFixture({ + files: { + "app/fake.server.js": ` + import fs from "fs"; + export default fs; + `, + + "app/routes/index.jsx": ` + import fs from "~/fake.server.js"; + + export default function Index() { + return
{Object.keys(fs).length}
+ } + ` + } + }); + + let app = await createAppFixture(fixture); + + let res = await app.goto("/"); + expect(res.status()).toBe(200); // server rendered fine + + // rendered the page instead of the error boundary + expect(await app.getHtml("#index")).toMatchInlineSnapshot( + `"
0
"` + ); + + await app.close(); + }); +}); diff --git a/integration/global-setup.ts b/integration/global-setup.ts new file mode 100644 index 0000000000..dabc232228 --- /dev/null +++ b/integration/global-setup.ts @@ -0,0 +1,12 @@ +import fs from "fs/promises"; +import path from "path"; + +export const TMP_DIR = path.join(process.cwd(), ".tmp"); + +export default async function setup() { + await fs.rm(TMP_DIR, { + force: true, + recursive: true + }); + await fs.mkdir(TMP_DIR); +} diff --git a/integration/headers-test.tsx b/integration/headers-test.tsx new file mode 100644 index 0000000000..504583f3d1 --- /dev/null +++ b/integration/headers-test.tsx @@ -0,0 +1,123 @@ +import { createFixture } from "./helpers/create-fixture"; +import type { Fixture } from "./helpers/create-fixture"; + +describe("headers export", () => { + const ROOT_HEADER_KEY = "X-Test"; + const ROOT_HEADER_VALUE = "SUCCESS"; + const ACTION_HKEY = "X-Test-Action"; + const ACTION_HVALUE = "SUCCESS"; + + let fixture: Fixture; + + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/root.jsx": ` + import { Outlet } from "remix"; + + export function loader() { + return null + } + + export default function Index() { + return + } + `, + + "app/routes/index.jsx": ` + import { json } from "remix"; + + export function loader() { + return json(null, { + headers: { + "${ROOT_HEADER_KEY}": "${ROOT_HEADER_VALUE}" + } + }) + } + + export function headers({ loaderHeaders }) { + return { + "${ROOT_HEADER_KEY}": loaderHeaders.get("${ROOT_HEADER_KEY}") + } + } + + export default function Index() { + return
Heyo!
+ } + `, + + "app/routes/action.jsx": ` + import { json } from "remix"; + + export function action() { + return json(null, { + headers: { + "${ACTION_HKEY}": "${ACTION_HVALUE}" + } + }) + } + + export function headers({ actionHeaders }) { + return { + "${ACTION_HKEY}": actionHeaders.get("${ACTION_HKEY}") + } + } + + export default function Action() { return
} + ` + } + }); + }); + + it("can use `action` headers", async () => { + let response = await fixture.postDocument("/action", new URLSearchParams()); + expect(response.headers.get(ACTION_HKEY)).toBe(ACTION_HVALUE); + }); + + it("can use the loader headers when all routes have loaders", async () => { + let response = await fixture.requestDocument("/"); + expect(response.headers.get(ROOT_HEADER_KEY)).toBe(ROOT_HEADER_VALUE); + }); + + // FIXME: this test is busted + // it("can use the loader headers when parents don't have loaders", async () => { + // const HEADER_KEY = "X-Test"; + // const HEADER_VALUE = "SUCCESS"; + + // let fixture = await createFixture({ + // files: { + // "app/root.jsx": ` + // import { Outlet } from "remix"; + + // export default function Index() { + // return + // } + // `, + + // "app/routes/index.jsx": ` + // import { json } from "remix"; + + // export function loader() { + // return json(null, { + // headers: { + // "${HEADER_KEY}": "${HEADER_VALUE}" + // } + // }) + // } + + // export function headers({ loaderHeaders }) { + // return { + // "${HEADER_KEY}": loaderHeaders.get("${HEADER_KEY}") + // } + // } + + // export default function Index() { + // return
Heyo!
+ // } + // ` + // } + // }); + // let response = await fixture.requestDocument("/"); + // expect(response.headers.get(HEADER_KEY)).toBe(HEADER_VALUE); + // }); +}); diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx new file mode 100644 index 0000000000..75a8ed46e7 --- /dev/null +++ b/integration/helpers/create-fixture.tsx @@ -0,0 +1,252 @@ +import path from "path"; +import fs from "fs/promises"; +import fse from "fs-extra"; +import cp from "child_process"; +import puppeteer from "puppeteer"; +import type { Page } from "puppeteer"; +import express from "express"; +import cheerio from "cheerio"; +import prettier from "prettier"; +import getPort from "get-port"; + +import { createRequestHandler } from "../../packages/remix-server-runtime"; +import { formatServerError } from "../../packages/remix-node"; +import { createApp } from "../../packages/create-remix"; +import { createRequestHandler as createExpressHandler } from "../../packages/remix-express"; +import type { ServerBuild } from "../../packages/remix-server-runtime"; +import type { CreateAppArgs } from "../../packages/create-remix"; +import { TMP_DIR } from "../global-setup"; + +const REMIX_SOURCE_BUILD_DIR = path.join(process.cwd(), "build"); + +interface FixtureInit { + files: { [filename: string]: string }; + server?: CreateAppArgs["server"]; +} + +export type Fixture = Awaited>; +export type AppFixture = Awaited>; + +export async function createFixture(init: FixtureInit) { + let projectDir = await createFixtureProject(init); + let app: ServerBuild = await import(path.resolve(projectDir, "build")); + let platform = { formatServerError }; + let handler = createRequestHandler(app, platform); + + let requestDocument = async (href: string, init?: RequestInit) => { + let url = new URL(href, "test://test"); + let request = new Request(url.toString(), init); + return handler(request); + }; + + let requestData = async ( + href: string, + routeId: string, + init?: RequestInit + ) => { + let url = new URL(href, "test://test"); + url.searchParams.set("_data", routeId); + let request = new Request(url.toString(), init); + return handler(request); + }; + + let postDocument = async (href: string, data: URLSearchParams | FormData) => { + return requestDocument(href, { + method: "POST", + body: data, + headers: { + "Content-Type": + data instanceof URLSearchParams + ? "application/x-www-form-urlencoded" + : "multipart/form-data" + } + }); + }; + + return { + projectDir, + build: app, + requestDocument, + requestData, + postDocument + }; +} + +export async function createAppFixture(fixture: Fixture) { + let startAppServer = async (): Promise<{ + port: number; + stop: () => Promise; + }> => { + return new Promise(async accept => { + let port = await getPort(); + let app = express(); + app.use(express.static(path.join(fixture.projectDir, "public"))); + app.all( + "*", + createExpressHandler({ build: fixture.build, mode: "production" }) + ); + + let server = app.listen(port); + + let stop = (): Promise => { + return new Promise(res => { + server.close(() => res()); + }); + }; + + accept({ stop, port }); + }); + }; + + let launchPuppeteer = async () => { + let browser = await puppeteer.launch(); + let page = await browser.newPage(); + return { browser, page }; + }; + + let start = async () => { + let [{ stop, port }, { browser, page }] = await Promise.all([ + startAppServer(), + launchPuppeteer() + ]); + + let serverUrl = `http://localhost:${port}`; + + return { + /** + * The puppeteer "page" instance. You will probably need to interact with + * this quite a bit in your tests, but our hope is that we can identify + * the most common things we do in our tests and make them helpers on the + * FixtureApp interface instead. Maybe one day we'll want to use cypress, + * would be nice to have an abstraction around our headless browser. + * + * For example, our `fixture.goto` wraps the normal `page.goto` but waits + * for hydration. As a rule of thumb, if you do the same handful of + * operations on the `page` three or more times, it's probably a good + * candidate to be a helper on `FixtureApp`. + * + * @see https://pptr.dev/#?product=Puppeteer&version=v13.1.3&show=api-class-page + */ + page, + + /** + * The puppeteer browser instance, seems unlikely we'll need it in tests, + * but maybe we will, so here it is. + */ + browser, + + /** + * Closes the puppeteer browser and fixture app, **you need to call this + * at the end of a test** or `afterAll` if the fixture is initialized in a + * `beforeAll` block. Also make sure to `await app.close()` or else you'll + * have memory leaks. + */ + close: async () => { + return Promise.all([browser.close(), stop()]); + }, + + /** + * Visits the href with a document request. + * + * @param href The href you want to visit + * @param waitForHydration Will wait for the network to be idle, so + * everything should be loaded and ready to go + */ + goto: async (href: string, waitForHydration?: true) => { + return page.goto(`${serverUrl}${href}`, { + waitUntil: waitForHydration ? "networkidle0" : undefined + }); + }, + + /** + * Get HTML from the page. Useful for asserting something rendered that + * you expected. + * + * @param selector CSS Selector for the element's HTML you want + */ + getHtml: (selector?: string) => getHtml(page, selector), + + /** + * Keeps the fixture running for as many seconds as you want so you can go + * poke around in the browser to see what's up. + * + * @param seconds How long you want the app to stay open + */ + poke: async (seconds: number = 10) => { + let ms = seconds * 1000; + jest.setTimeout(ms); + console.log(`🙈 Poke around for ${seconds} seconds 👉 ${serverUrl}`); + return new Promise(res => setTimeout(res, ms)); + } + }; + }; + + return start(); +} + +//////////////////////////////////////////////////////////////////////////////// +export async function createFixtureProject(init: FixtureInit): Promise { + let projectDir = path.join(TMP_DIR, Math.random().toString(32).slice(2)); + + await createApp({ + install: false, + lang: "js", + server: init.server || "remix", + projectDir, + quiet: true + }); + await Promise.all([ + writeTestFiles(init, projectDir), + installRemix(projectDir) + ]); + build(projectDir); + + return projectDir; +} + +function build(projectDir: string) { + // TODO: log errors (like syntax errors in the fixture file strings) + cp.spawnSync("node", ["node_modules/@remix-run/dev/cli.js", "setup"], { + cwd: projectDir + }); + cp.spawnSync("node", ["node_modules/@remix-run/dev/cli.js", "build"], { + cwd: projectDir + }); +} + +async function installRemix(projectDir: string) { + let buildDir = path.resolve(REMIX_SOURCE_BUILD_DIR, "node_modules"); + let installDir = path.resolve(projectDir, "node_modules"); + + // Install all remix packages + await fse.ensureDir(installDir); + await fse.copy(buildDir, installDir); +} + +function writeTestFiles(init: FixtureInit, dir: string) { + return Promise.all( + Object.keys(init.files).map(async filename => { + let filePath = path.join(dir, filename); + await fs.writeFile(filePath, init.files[filename]); + }) + ); +} + +export async function getHtml(page: Page, selector?: string) { + let html = await page.content(); + return prettyHtml(selector ? selectHtml(html, selector) : html).trim(); +} + +export function selectHtml(source: string, selector: string) { + let el = cheerio(selector, source); + + if (!el.length) { + throw new Error(`No element matches selector "${selector}"`); + } + + return cheerio.html(el); +} + +export function prettyHtml(source: string): string { + return prettier.format(source, { parser: "html" }); +} diff --git a/integration/loader-test.tsx b/integration/loader-test.tsx new file mode 100644 index 0000000000..10551ec799 --- /dev/null +++ b/integration/loader-test.tsx @@ -0,0 +1,48 @@ +import { createFixture } from "./helpers/create-fixture"; + +describe("loader export", () => { + it("returns responses for a specific route", async () => { + const ROOT_DATA = "ROOT_DATA"; + const INDEX_DATA = "INDEX_DATA"; + + let fixture = await createFixture({ + files: { + "app/root.jsx": ` + import { Outlet } from "remix"; + + export function loader() { + return "${ROOT_DATA}" + } + + export default function Index() { + return + } + `, + + "app/routes/index.jsx": ` + import { json } from "remix"; + + export function loader() { + return "${INDEX_DATA}" + } + + export default function Index() { + return
+ } + ` + } + }); + + let [root, index] = await Promise.all([ + fixture.requestData("/", "root"), + fixture.requestData("/", "routes/index") + ]); + + expect(root.headers.get("Content-Type")).toBe( + "application/json; charset=utf-8" + ); + + expect(await root.json()).toBe(ROOT_DATA); + expect(await index.json()).toBe(INDEX_DATA); + }); +}); diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index cc508af6e4..37e03e031f 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -15,6 +15,7 @@ import type { RemixConfig } from "../config"; import { readConfig } from "../config"; import { formatRoutes, RoutesFormat, isRoutesFormat } from "../config/format"; import { setupRemix, isSetupPlatform, SetupPlatform } from "../setup"; +import { log } from "../log"; export async function setup(platformArg?: string) { let platform = isSetupPlatform(platformArg) @@ -23,7 +24,7 @@ export async function setup(platformArg?: string) { await setupRemix(platform); - console.log(`Successfully setup Remix for ${platform}.`); + log(`Successfully setup Remix for ${platform}.`); } export async function routes( @@ -44,7 +45,7 @@ export async function build( ): Promise { let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Production; - console.log(`Building Remix app in ${mode} mode...`); + log(`Building Remix app in ${mode} mode...`); if (modeArg === BuildMode.Production && sourcemap) { console.warn( @@ -62,7 +63,7 @@ export async function build( let config = await readConfig(remixRoot); await compiler.build(config, { mode: mode, sourcemap }); - console.log(`Built in ${prettyMs(Date.now() - start)}`); + log(`Built in ${prettyMs(Date.now() - start)}`); } type WatchCallbacks = { diff --git a/packages/remix-dev/log.ts b/packages/remix-dev/log.ts new file mode 100644 index 0000000000..6e9b9ad04a --- /dev/null +++ b/packages/remix-dev/log.ts @@ -0,0 +1,5 @@ +export function log(...args: any) { + if (process.env.NODE_ENV !== "test") { + console.log(...args); + } +} From 4c98b239423dc4d28051af3e45c9746dca8886ef Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 1 Feb 2022 20:07:20 -0700 Subject: [PATCH 0204/1690] chore(tests): migrate catch boundary tests to new integration tests (#1769) * chore: remove old tutorial fixture * chore(tests): migrated catch boundary tests --- integration/catch-boundary-test.ts | 217 +++++++++++++++++++++++++ integration/global-setup.ts | 3 + integration/helpers/create-fixture.tsx | 88 +++++++++- 3 files changed, 307 insertions(+), 1 deletion(-) create mode 100644 integration/catch-boundary-test.ts diff --git a/integration/catch-boundary-test.ts b/integration/catch-boundary-test.ts new file mode 100644 index 0000000000..436197b984 --- /dev/null +++ b/integration/catch-boundary-test.ts @@ -0,0 +1,217 @@ +import { createAppFixture, createFixture } from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; + +describe("CatchBoundary", () => { + let fixture: Fixture; + let app: AppFixture; + + const ROOT_BOUNDARY_TEXT = "ROOT_TEXT"; + const OWN_BOUNDARY_TEXT = "OWN_BOUNDARY_TEXT"; + + const HAS_BOUNDARY_LOADER = "/yes/loader"; + const HAS_BOUNDARY_ACTION = "/yes/action"; + const NO_BOUNDARY_ACTION = "/no/action"; + const NO_BOUNDARY_LOADER = "/no/loader"; + + const NOT_FOUND_HREF = "/not/found"; + + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/root.jsx": ` + import { Outlet, Scripts } from "remix"; + export default function Root() { + return ( + + + + + + + + ) + } + export function CatchBoundary() { + return ( + + + +
${ROOT_BOUNDARY_TEXT}
+ + + + ) + } + `, + + "app/routes/index.jsx": ` + import { Link, Form } from "remix"; + export default function() { + return ( +
+ ${NOT_FOUND_HREF} + +
+
+ ) + } + `, + + [`app/routes${HAS_BOUNDARY_ACTION}.jsx`]: ` + import { Form } from "remix"; + export async function action() { + throw new Response("", { status: 401 }) + } + export function CatchBoundary() { + return

${OWN_BOUNDARY_TEXT}

+ } + export default function Index() { + return ( +
+ +
+ ); + } + `, + + [`app/routes${NO_BOUNDARY_ACTION}.jsx`]: ` + import { Form } from "remix"; + export function action() { + throw new Response("", { status: 401 }) + } + export default function Index() { + return ( +
+ +
+ ) + } + `, + + [`app/routes${HAS_BOUNDARY_LOADER}.jsx`]: ` + export function loader() { + throw new Response("", { status: 401 }) + } + export function CatchBoundary() { + return
${OWN_BOUNDARY_TEXT}
+ } + export default function Index() { + return
+ } + `, + + [`app/routes${NO_BOUNDARY_LOADER}.jsx`]: ` + export function loader() { + throw new Response("", { status: 401 }) + } + export default function Index() { + return
+ } + ` + } + }); + + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); + + test("non-matching urls on document requests", async () => { + let res = await fixture.requestDocument(NOT_FOUND_HREF); + expect(res.status).toBe(404); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); + + test("non-matching urls on client transitions", async () => { + await app.goto("/"); + await app.clickLink(NOT_FOUND_HREF, { wait: false }); + expect(await app.getHtml()).toMatch(ROOT_BOUNDARY_TEXT); + }); + + test("invalid request methods", async () => { + let res = await fixture.requestDocument("/", { + method: "OPTIONS" + }); + expect(res.status).toBe(405); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); + + test("own boundary, action, document request", async () => { + let params = new URLSearchParams(); + let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); + + // FIXME: this is broken, the request returns but the page doesn't update + test.skip("own boundary, action, client transition from other route", async () => { + await app.goto("/"); + await app.clickSubmitButton(HAS_BOUNDARY_ACTION); + expect(await app.getHtml()).toMatch(OWN_BOUNDARY_TEXT); + }); + + test("own boundary, action, client transition from itself", async () => { + await app.goto(HAS_BOUNDARY_ACTION); + await app.clickSubmitButton(HAS_BOUNDARY_ACTION); + expect(await app.getHtml()).toMatch(OWN_BOUNDARY_TEXT); + }); + + it("bubbles to parent in action document requests", async () => { + let params = new URLSearchParams(); + let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); + + it("bubbles to parent in action script transitions from other routes", async () => { + await app.goto("/"); + await app.clickSubmitButton(NO_BOUNDARY_ACTION); + expect(await app.getHtml()).toMatch(ROOT_BOUNDARY_TEXT); + }); + + it("bubbles to parent in action script transitions from self", async () => { + await app.goto(NO_BOUNDARY_ACTION); + await app.clickSubmitButton(NO_BOUNDARY_ACTION); + expect(await app.getHtml()).toMatch(ROOT_BOUNDARY_TEXT); + }); + + test("own boundary, loader, document request", async () => { + let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); + + test("own boundary, loader, client transition", async () => { + await app.goto("/"); + await app.clickLink(HAS_BOUNDARY_LOADER); + expect(await app.getHtml()).toMatch(OWN_BOUNDARY_TEXT); + }); + + it("bubbles to parent in loader document requests", async () => { + let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); + + it("bubbles to parent in action script transitions from other routes", async () => { + await app.goto("/"); + await app.clickLink(NO_BOUNDARY_LOADER); + expect(await app.getHtml()).toMatch(ROOT_BOUNDARY_TEXT); + }); +}); diff --git a/integration/global-setup.ts b/integration/global-setup.ts index dabc232228..8122431cf3 100644 --- a/integration/global-setup.ts +++ b/integration/global-setup.ts @@ -3,6 +3,9 @@ import path from "path"; export const TMP_DIR = path.join(process.cwd(), ".tmp"); +// TODO: get rid of React Router `console.warn` when no routes match when testing +console.warn = () => {}; + export default async function setup() { await fs.rm(TMP_DIR, { force: true, diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index 75a8ed46e7..3cc1849f55 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -158,6 +158,48 @@ export async function createAppFixture(fixture: Fixture) { }); }, + /** + * Finds a link on the page with a matching href, clicks it, and waits for + * the network to be idle before contininuing. + * + * @param href The href of the link you want to click + * @param options `{ wait }` waits for the network to be idle before moving on + */ + clickLink: async ( + href: string, + options: { wait: boolean } = { wait: true } + ) => { + let selector = `a[href="${href}"]`; + let el = await page.$(selector); + if (options.wait) { + await doAndWait(page, () => el.click(), 200, 2000); + } else { + await el.click(); + } + }, + + /** + * Finds the first submit button with `formAction` that matches the + * `action` supplied, clicks it, and optionally waits for the network to + * be idle before contininuing. + * + * @param formAction The formAction of the button you want to click + * @param options `{ wait }` waits for the network to be idle before moving on + */ + clickSubmitButton: async ( + formAction: string, + options: { wait: boolean } = { wait: true } + ) => { + let selector = `button[formaction="${formAction}"]`; + let el = await page.$(selector); + if (!el) throw new Error(`Can't find button: ${selector}`); + if (options.wait) { + await doAndWait(page, () => el.click(), 200, 2000); + } else { + await el.click(); + } + }, + /** * Get HTML from the page. Useful for asserting something rendered that * you expected. @@ -172,10 +214,11 @@ export async function createAppFixture(fixture: Fixture) { * * @param seconds How long you want the app to stay open */ - poke: async (seconds: number = 10) => { + poke: async (seconds: number = 10, href: string = "/") => { let ms = seconds * 1000; jest.setTimeout(ms); console.log(`🙈 Poke around for ${seconds} seconds 👉 ${serverUrl}`); + cp.exec(`open ${serverUrl}${href}`); return new Promise(res => setTimeout(res, ms)); } }; @@ -227,6 +270,7 @@ function writeTestFiles(init: FixtureInit, dir: string) { return Promise.all( Object.keys(init.files).map(async filename => { let filePath = path.join(dir, filename); + await fse.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, init.files[filename]); }) ); @@ -250,3 +294,45 @@ export function selectHtml(source: string, selector: string) { export function prettyHtml(source: string): string { return prettier.format(source, { parser: "html" }); } + +// Taken from https://github.com/puppeteer/puppeteer/issues/5328#issuecomment-986175620 +// Seems to work? +async function doAndWait( + page: puppeteer.Page, + fun: () => Promise, + pollTime: number = 1000, + timeout: number = 10000 +) { + let waiting: puppeteer.HTTPRequest[] = []; + + await page.setRequestInterception(true); + let onRequest = (interceptedRequest: puppeteer.HTTPRequest) => { + interceptedRequest.continue(); + waiting.push(interceptedRequest); + }; + page.on("request", onRequest); + + await fun(); + + let pollEvent: NodeJS.Timer; + let timeoutEvent: NodeJS.Timer; + return new Promise((res, rej) => { + let clear = () => { + clearInterval(pollEvent); + clearTimeout(timeoutEvent); + page.off("request", onRequest); + return page.setRequestInterception(false); + }; + timeoutEvent = setTimeout(() => { + console.warn("Warning, wait for the address below to time out:"); + console.warn(waiting.map(a => a.url()).join("\n")); + return clear().then(() => res(null)); + }, timeout); + pollEvent = setInterval(() => { + if (waiting.length == 0) { + return clear().then(() => res(null)); + } + waiting = waiting.filter(a => a.response() == null); + }, pollTime); + }); +} From 8e0ff207c9746ba789b299e10b84391e32e5a8a6 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 1 Feb 2022 20:18:17 -0700 Subject: [PATCH 0205/1690] chore: add `js` template tag for integration tests install this for VSCode and get syntax highlighting in the fixture tests: https://marketplace.visualstudio.com/items?itemName=icanhasjonas.vscode-js-template-literal --- integration/action-test.tsx | 5 +- integration/catch-boundary-test.ts | 14 ++-- integration/headers-test.tsx | 88 +++++++++++++------------- integration/helpers/create-fixture.tsx | 2 + integration/loader-test.tsx | 6 +- 5 files changed, 59 insertions(+), 56 deletions(-) diff --git a/integration/action-test.tsx b/integration/action-test.tsx index 1d9dc15079..358a186cdd 100644 --- a/integration/action-test.tsx +++ b/integration/action-test.tsx @@ -1,7 +1,8 @@ import { createFixture, createAppFixture, - selectHtml + selectHtml, + js } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; @@ -17,7 +18,7 @@ describe("action + useActionData", () => { beforeAll(async () => { fixture = await createFixture({ files: { - "app/routes/urlencoded.jsx": ` + "app/routes/urlencoded.jsx": js` import { Form, useActionData } from "remix"; export let action = async ({ request }) => { diff --git a/integration/catch-boundary-test.ts b/integration/catch-boundary-test.ts index 436197b984..80dce4b882 100644 --- a/integration/catch-boundary-test.ts +++ b/integration/catch-boundary-test.ts @@ -1,4 +1,4 @@ -import { createAppFixture, createFixture } from "./helpers/create-fixture"; +import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; describe("CatchBoundary", () => { @@ -18,7 +18,7 @@ describe("CatchBoundary", () => { beforeAll(async () => { fixture = await createFixture({ files: { - "app/root.jsx": ` + "app/root.jsx": js` import { Outlet, Scripts } from "remix"; export default function Root() { return ( @@ -44,7 +44,7 @@ describe("CatchBoundary", () => { } `, - "app/routes/index.jsx": ` + "app/routes/index.jsx": js` import { Link, Form } from "remix"; export default function() { return ( @@ -67,7 +67,7 @@ describe("CatchBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_ACTION}.jsx`]: ` + [`app/routes${HAS_BOUNDARY_ACTION}.jsx`]: js` import { Form } from "remix"; export async function action() { throw new Response("", { status: 401 }) @@ -86,7 +86,7 @@ describe("CatchBoundary", () => { } `, - [`app/routes${NO_BOUNDARY_ACTION}.jsx`]: ` + [`app/routes${NO_BOUNDARY_ACTION}.jsx`]: js` import { Form } from "remix"; export function action() { throw new Response("", { status: 401 }) @@ -102,7 +102,7 @@ describe("CatchBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_LOADER}.jsx`]: ` + [`app/routes${HAS_BOUNDARY_LOADER}.jsx`]: js` export function loader() { throw new Response("", { status: 401 }) } @@ -114,7 +114,7 @@ describe("CatchBoundary", () => { } `, - [`app/routes${NO_BOUNDARY_LOADER}.jsx`]: ` + [`app/routes${NO_BOUNDARY_LOADER}.jsx`]: js` export function loader() { throw new Response("", { status: 401 }) } diff --git a/integration/headers-test.tsx b/integration/headers-test.tsx index 504583f3d1..061ab6cf4f 100644 --- a/integration/headers-test.tsx +++ b/integration/headers-test.tsx @@ -1,4 +1,4 @@ -import { createFixture } from "./helpers/create-fixture"; +import { createFixture, js } from "./helpers/create-fixture"; import type { Fixture } from "./helpers/create-fixture"; describe("headers export", () => { @@ -12,7 +12,7 @@ describe("headers export", () => { beforeAll(async () => { fixture = await createFixture({ files: { - "app/root.jsx": ` + "app/root.jsx": js` import { Outlet } from "remix"; export function loader() { @@ -24,7 +24,7 @@ describe("headers export", () => { } `, - "app/routes/index.jsx": ` + "app/routes/index.jsx": js` import { json } from "remix"; export function loader() { @@ -46,7 +46,7 @@ describe("headers export", () => { } `, - "app/routes/action.jsx": ` + "app/routes/action.jsx": js` import { json } from "remix"; export function action() { @@ -80,44 +80,44 @@ describe("headers export", () => { }); // FIXME: this test is busted - // it("can use the loader headers when parents don't have loaders", async () => { - // const HEADER_KEY = "X-Test"; - // const HEADER_VALUE = "SUCCESS"; - - // let fixture = await createFixture({ - // files: { - // "app/root.jsx": ` - // import { Outlet } from "remix"; - - // export default function Index() { - // return - // } - // `, - - // "app/routes/index.jsx": ` - // import { json } from "remix"; - - // export function loader() { - // return json(null, { - // headers: { - // "${HEADER_KEY}": "${HEADER_VALUE}" - // } - // }) - // } - - // export function headers({ loaderHeaders }) { - // return { - // "${HEADER_KEY}": loaderHeaders.get("${HEADER_KEY}") - // } - // } - - // export default function Index() { - // return
Heyo!
- // } - // ` - // } - // }); - // let response = await fixture.requestDocument("/"); - // expect(response.headers.get(HEADER_KEY)).toBe(HEADER_VALUE); - // }); + it.skip("can use the loader headers when parents don't have loaders", async () => { + const HEADER_KEY = "X-Test"; + const HEADER_VALUE = "SUCCESS"; + + let fixture = await createFixture({ + files: { + "app/root.jsx": js` + import { Outlet } from "remix"; + + export default function Index() { + return + } + `, + + "app/routes/index.jsx": js` + import { json } from "remix"; + + export function loader() { + return json(null, { + headers: { + "${HEADER_KEY}": "${HEADER_VALUE}" + } + }) + } + + export function headers({ loaderHeaders }) { + return { + "${HEADER_KEY}": loaderHeaders.get("${HEADER_KEY}") + } + } + + export default function Index() { + return
Heyo!
+ } + ` + } + }); + let response = await fixture.requestDocument("/"); + expect(response.headers.get(HEADER_KEY)).toBe(HEADER_VALUE); + }); }); diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index 3cc1849f55..48086eaf5d 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -27,6 +27,8 @@ interface FixtureInit { export type Fixture = Awaited>; export type AppFixture = Awaited>; +export let js = String.raw; + export async function createFixture(init: FixtureInit) { let projectDir = await createFixtureProject(init); let app: ServerBuild = await import(path.resolve(projectDir, "build")); diff --git a/integration/loader-test.tsx b/integration/loader-test.tsx index 10551ec799..9f8b8a3537 100644 --- a/integration/loader-test.tsx +++ b/integration/loader-test.tsx @@ -1,4 +1,4 @@ -import { createFixture } from "./helpers/create-fixture"; +import { createFixture, js } from "./helpers/create-fixture"; describe("loader export", () => { it("returns responses for a specific route", async () => { @@ -7,7 +7,7 @@ describe("loader export", () => { let fixture = await createFixture({ files: { - "app/root.jsx": ` + "app/root.jsx": js` import { Outlet } from "remix"; export function loader() { @@ -19,7 +19,7 @@ describe("loader export", () => { } `, - "app/routes/index.jsx": ` + "app/routes/index.jsx": js` import { json } from "remix"; export function loader() { From 334e1f99de010c2e52ef179fb39abd887828b290 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 1 Feb 2022 21:33:12 -0700 Subject: [PATCH 0206/1690] chore(tests): migrate catchall tests --- integration/catch-all-routes-test.ts | 123 +++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 integration/catch-all-routes-test.ts diff --git a/integration/catch-all-routes-test.ts b/integration/catch-all-routes-test.ts new file mode 100644 index 0000000000..8d18fbf3a7 --- /dev/null +++ b/integration/catch-all-routes-test.ts @@ -0,0 +1,123 @@ +import { createFixture, js } from "./helpers/create-fixture"; +import type { Fixture } from "./helpers/create-fixture"; + +describe("rendering", () => { + let fixture: Fixture; + + const ROOT_$ = "FLAT"; + const ROOT_INDEX = "ROOT_INDEX"; + const FLAT_$ = "FLAT"; + const PARENT = "PARENT"; + const NESTED_$ = "NESTED_$"; + const NESTED_INDEX = "NESTED_INDEX"; + const PARENTLESS_$ = "PARENTLESS_$"; + + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/root.jsx": js` + import { Outlet, Scripts } from "remix"; + export default function Root() { + return ( + + + + + + + + ) + } + `, + + "app/routes/index.jsx": js` + export default function() { + return

${ROOT_INDEX}

; + } + `, + + "app/routes/$.jsx": js` + export default function() { + return

${ROOT_$}

; + } + `, + + "app/routes/flat.$.jsx": js` + export default function() { + return

${FLAT_$}

+ } + `, + + "app/routes/nested.jsx": js` + import { Outlet } from "remix"; + export default function() { + return ( +
+

${PARENT}

+ +
+ ) + } + `, + + "app/routes/nested/$.jsx": js` + export default function() { + return

${NESTED_$}

+ } + `, + + "app/routes/nested/index.jsx": js` + export default function() { + return

${NESTED_INDEX}

+ } + `, + + "app/routes/parentless/$.jsx": js` + export default function() { + return

${PARENTLESS_$}

+ } + ` + } + }); + }); + + test("flat exact match", async () => { + let res = await fixture.requestDocument("/flat"); + expect(await res.text()).toMatch(FLAT_$); + }); + + test("flat deep match", async () => { + let res = await fixture.requestDocument("/flat/swig"); + expect(await res.text()).toMatch(FLAT_$); + }); + + it("prioritizes index over root splat", async () => { + let res = await fixture.requestDocument("/"); + expect(await res.text()).toMatch(ROOT_INDEX); + }); + + it("matches root splat", async () => { + let res = await fixture.requestDocument("/twisted/sugar"); + expect(await res.text()).toMatch(ROOT_$); + }); + + it("prioritizes index over splat for parent route match", async () => { + let res = await fixture.requestDocument("/nested"); + expect(await res.text()).toMatch(NESTED_INDEX); + }); + + test("nested child", async () => { + let res = await fixture.requestDocument("/nested/sodalicious"); + expect(await res.text()).toMatch(NESTED_$); + }); + + test("parentless exact match", async () => { + let res = await fixture.requestDocument("/parentless"); + expect(await res.text()).toMatch(PARENTLESS_$); + }); + + test("parentless deep match", async () => { + let res = await fixture.requestDocument("/parentless/chip"); + expect(await res.text()).toMatch(PARENTLESS_$); + }); +}); From cd2a892881d99c319afe32d171130f59e5cee83d Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 1 Feb 2022 21:34:02 -0700 Subject: [PATCH 0207/1690] chore(tests): migrate (ssr) rendering tests --- integration/helpers/create-fixture.tsx | 4 +- integration/rendering-test.ts | 69 ++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 integration/rendering-test.ts diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index 48086eaf5d..1a673c38d8 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -280,7 +280,7 @@ function writeTestFiles(init: FixtureInit, dir: string) { export async function getHtml(page: Page, selector?: string) { let html = await page.content(); - return prettyHtml(selector ? selectHtml(html, selector) : html).trim(); + return selector ? selectHtml(html, selector) : prettyHtml(html); } export function selectHtml(source: string, selector: string) { @@ -290,7 +290,7 @@ export function selectHtml(source: string, selector: string) { throw new Error(`No element matches selector "${selector}"`); } - return cheerio.html(el); + return prettyHtml(cheerio.html(el)).trim(); } export function prettyHtml(source: string): string { diff --git a/integration/rendering-test.ts b/integration/rendering-test.ts new file mode 100644 index 0000000000..4987dc1da7 --- /dev/null +++ b/integration/rendering-test.ts @@ -0,0 +1,69 @@ +import { + createAppFixture, + createFixture, + js, + selectHtml +} from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; + +describe("rendering", () => { + let fixture: Fixture; + let app: AppFixture; + + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/root.jsx": js` + import { Outlet, Scripts } from "remix"; + export default function Root() { + return ( + + + +
+

Root

+ +
+ + + + ) + } + `, + + "app/routes/index.jsx": js` + export default function() { + return

Index

; + } + ` + } + }); + + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); + + it("server renders matching routes", async () => { + let res = await fixture.requestDocument("/"); + expect(res.status).toBe(200); + expect(selectHtml(await res.text(), "#content")).toMatchInlineSnapshot(` + "
+

Root

+

Index

+
" + `); + }); + + it("hydrates", async () => { + await app.goto("/"); + expect(selectHtml(await app.getHtml(), "#content")).toMatchInlineSnapshot(` + "
+

Root

+

Index

+
" + `); + }); +}); From 9f09c109d5d42b1bf0cafc2a2fe6efe6d06de63b Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 1 Feb 2022 21:37:12 -0700 Subject: [PATCH 0208/1690] chore(tests): file renames --- integration/{action-test.tsx => action-test.ts} | 0 integration/{compiler-test.tsx => compiler-test.ts} | 0 integration/{headers-test.tsx => headers-test.ts} | 0 integration/{loader-test.tsx => loader-test.ts} | 0 integration/{catch-all-routes-test.ts => splat-routes-test.ts} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename integration/{action-test.tsx => action-test.ts} (100%) rename integration/{compiler-test.tsx => compiler-test.ts} (100%) rename integration/{headers-test.tsx => headers-test.ts} (100%) rename integration/{loader-test.tsx => loader-test.ts} (100%) rename integration/{catch-all-routes-test.ts => splat-routes-test.ts} (100%) diff --git a/integration/action-test.tsx b/integration/action-test.ts similarity index 100% rename from integration/action-test.tsx rename to integration/action-test.ts diff --git a/integration/compiler-test.tsx b/integration/compiler-test.ts similarity index 100% rename from integration/compiler-test.tsx rename to integration/compiler-test.ts diff --git a/integration/headers-test.tsx b/integration/headers-test.ts similarity index 100% rename from integration/headers-test.tsx rename to integration/headers-test.ts diff --git a/integration/loader-test.tsx b/integration/loader-test.ts similarity index 100% rename from integration/loader-test.tsx rename to integration/loader-test.ts diff --git a/integration/catch-all-routes-test.ts b/integration/splat-routes-test.ts similarity index 100% rename from integration/catch-all-routes-test.ts rename to integration/splat-routes-test.ts From 28655d1835495ff5088b37733533fb7b40025684 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 1 Feb 2022 22:40:50 -0700 Subject: [PATCH 0209/1690] chore(tests): migrated data loading tests renamed it to "transitions" because that's what it's really more about --- integration/helpers/create-fixture.tsx | 50 ++++++- integration/transition-test.ts | 176 +++++++++++++++++++++++++ 2 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 integration/transition-test.ts diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index 1a673c38d8..884ba913fd 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -3,7 +3,7 @@ import fs from "fs/promises"; import fse from "fs-extra"; import cp from "child_process"; import puppeteer from "puppeteer"; -import type { Page } from "puppeteer"; +import type { Page, HTTPResponse } from "puppeteer"; import express from "express"; import cheerio from "cheerio"; import prettier from "prettier"; @@ -174,7 +174,7 @@ export async function createAppFixture(fixture: Fixture) { let selector = `a[href="${href}"]`; let el = await page.$(selector); if (options.wait) { - await doAndWait(page, () => el.click(), 200, 2000); + await doAndWait(page, () => el.click()); } else { await el.click(); } @@ -196,12 +196,31 @@ export async function createAppFixture(fixture: Fixture) { let el = await page.$(selector); if (!el) throw new Error(`Can't find button: ${selector}`); if (options.wait) { - await doAndWait(page, () => el.click(), 200, 2000); + await doAndWait(page, () => el.click()); } else { await el.click(); } }, + /** + * "Clicks" the back button and optionally waits for the network to be + * idle (defaults to waiting). + */ + goBack: async (options: { wait: boolean } = { wait: true }) => { + if (options.wait) { + await doAndWait(page, () => page.goBack()); + } else { + await page.goBack(); + } + }, + + /** + * Collects data responses from the network, usually after a link click or + * form submission. This is useful for asserting that specific loaders + * were called (or not). + */ + collectDataResponses: () => collectDataResponses(page), + /** * Get HTML from the page. Useful for asserting something rendered that * you expected. @@ -302,8 +321,8 @@ export function prettyHtml(source: string): string { async function doAndWait( page: puppeteer.Page, fun: () => Promise, - pollTime: number = 1000, - timeout: number = 10000 + pollTime: number = 20, + timeout: number = 2000 ) { let waiting: puppeteer.HTTPRequest[] = []; @@ -338,3 +357,24 @@ async function doAndWait( }, pollTime); }); } + +type UrlFilter = (url: URL) => boolean; + +export function collectResponses( + page: Page, + filter?: UrlFilter +): HTTPResponse[] { + let responses: HTTPResponse[] = []; + + page.on("response", res => { + if (!filter || filter(new URL(res.url()))) { + responses.push(res); + } + }); + + return responses; +} + +export function collectDataResponses(page: Page) { + return collectResponses(page, url => url.searchParams.has("_data")); +} diff --git a/integration/transition-test.ts b/integration/transition-test.ts new file mode 100644 index 0000000000..7bdae41065 --- /dev/null +++ b/integration/transition-test.ts @@ -0,0 +1,176 @@ +import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; + +describe("rendering", () => { + let fixture: Fixture; + let app: AppFixture; + + const PAGE = "page"; + const PAGE_TEXT = "PAGE_TEXT"; + const PAGE_INDEX_TEXT = "PAGE_INDEX_TEXT"; + const CHILD = "child"; + const CHILD_TEXT = "CHILD_TEXT"; + const REDIRECT = "redirect"; + const REDIRECT_TARGET = "page"; + + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/root.jsx": js` + import { Outlet, Scripts } from "remix"; + export default function Root() { + return ( + + + +
+ +
+ + + + ) + } + + `, + "app/routes/index.jsx": js` + import { Link } from "remix"; + export default function() { + return ( +
+

Index

+ ${PAGE} + ${REDIRECT} +
+ ); + } + `, + + [`app/routes/${PAGE}.jsx`]: js` + import { Outlet, useLoaderData } from "remix"; + + export function loader() { + return "${PAGE_TEXT}" + } + + export default function() { + let text = useLoaderData(); + return ( + <> +

{text}

+ + + ); + } + `, + + [`app/routes/${PAGE}/index.jsx`]: js` + import { useLoaderData, Link } from "remix"; + + export function loader() { + return "${PAGE_INDEX_TEXT}" + } + + export default function() { + let text = useLoaderData(); + return ( + <> +

{text}

+ ${CHILD} + + ); + } + `, + + [`app/routes/${PAGE}/${CHILD}.jsx`]: js` + import { useLoaderData } from "remix"; + + export function loader() { + return "${CHILD_TEXT}" + } + + export default function() { + let text = useLoaderData(); + return

{text}

; + } + `, + + [`app/routes/${REDIRECT}.jsx`]: js` + import { redirect } from "remix"; + export function loader() { + return redirect("/${REDIRECT_TARGET}") + } + export default function() { + return null; + } + ` + } + }); + + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); + + it("calls all loaders for new routes", async () => { + await app.goto("/"); + let responses = app.collectDataResponses(); + await app.clickLink(`/${PAGE}`); + + expect( + responses.map(res => new URL(res.url()).searchParams.get("_data")) + ).toEqual([`routes/${PAGE}`, `routes/${PAGE}/index`]); + + let html = await app.getHtml("main"); + expect(html).toMatch(PAGE_TEXT); + expect(html).toMatch(PAGE_INDEX_TEXT); + }); + + it("calls only loaders for changing routes", async () => { + await app.goto(`/${PAGE}`); + let responses = app.collectDataResponses(); + await app.clickLink(`/${PAGE}/${CHILD}`); + + expect( + responses.map(res => new URL(res.url()).searchParams.get("_data")) + ).toEqual([`routes/${PAGE}/${CHILD}`]); + + let html = await app.getHtml("main"); + expect(html).toMatch(PAGE_TEXT); + expect(html).toMatch(CHILD_TEXT); + }); + + test("loader redirect", async () => { + await app.goto("/"); + + let responses = app.collectDataResponses(); + await app.clickLink(`/${REDIRECT}`); + expect(new URL(app.page.url()).pathname).toBe(`/${REDIRECT_TARGET}`); + + expect( + responses.map(res => new URL(res.url()).searchParams.get("_data")) + ).toEqual([`routes/${REDIRECT}`, `routes/${PAGE}`, `routes/${PAGE}/index`]); + + let html = await app.getHtml("main"); + expect(html).toMatch(PAGE_TEXT); + expect(html).toMatch(PAGE_INDEX_TEXT); + }); + + it("calls changing routes on POP", async () => { + await app.goto(`/${PAGE}`); + await app.clickLink(`/${PAGE}/${CHILD}`); + + let responses = app.collectDataResponses(); + await app.goBack(); + + expect( + responses.map(res => new URL(res.url()).searchParams.get("_data")) + ).toEqual([`routes/${PAGE}/index`]); + + let html = await app.getHtml("main"); + expect(html).toMatch(PAGE_TEXT); + expect(html).toMatch(PAGE_INDEX_TEXT); + }); +}); From a5a8cecae5c2d3abf70a58305df5ecdd04231ac2 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 1 Feb 2022 22:43:07 -0700 Subject: [PATCH 0210/1690] chore(tests): fixup readConfig test --- .../remix-dev/__tests__/readConfig-test.ts | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index e5647bf0a8..9a23f798e7 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -198,46 +198,6 @@ describe("readConfig", () => { "parentId": "routes/blog", "path": "third", }, - "routes/catchall-nested": Object { - "caseSensitive": undefined, - "file": "routes/catchall-nested.jsx", - "id": "routes/catchall-nested", - "index": undefined, - "parentId": "root", - "path": "catchall-nested", - }, - "routes/catchall-nested-no-layout/$": Object { - "caseSensitive": undefined, - "file": "routes/catchall-nested-no-layout/$.jsx", - "id": "routes/catchall-nested-no-layout/$", - "index": undefined, - "parentId": "root", - "path": "catchall-nested-no-layout/*", - }, - "routes/catchall-nested/$": Object { - "caseSensitive": undefined, - "file": "routes/catchall-nested/$.jsx", - "id": "routes/catchall-nested/$", - "index": undefined, - "parentId": "routes/catchall-nested", - "path": "*", - }, - "routes/catchall-nested/index": Object { - "caseSensitive": undefined, - "file": "routes/catchall-nested/index.jsx", - "id": "routes/catchall-nested/index", - "index": true, - "parentId": "routes/catchall-nested", - "path": undefined, - }, - "routes/catchall.flat.$": Object { - "caseSensitive": undefined, - "file": "routes/catchall.flat.$.jsx", - "id": "routes/catchall.flat.$", - "index": undefined, - "parentId": "root", - "path": "catchall/flat/*", - }, "routes/empty": Object { "caseSensitive": undefined, "file": "routes/empty.jsx", From 43190351b6654369cbc6947e44a47ce838e89e6f Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 1 Feb 2022 23:11:29 -0700 Subject: [PATCH 0211/1690] chore(tests): match conventions in integration tests We generally just want one fixture per test module since they are expensive. Having the fixture defined at the top of the file encourages us to keep adding to that fixture instead of making a bunch of them in each test. Also moved the global-setup out of the root of integration/ since it just felt a little messy --- integration/action-test.ts | 116 +++++++++++----------- integration/helpers/create-fixture.tsx | 2 +- integration/{ => helpers}/global-setup.ts | 0 integration/loader-test.ts | 15 ++- 4 files changed, 68 insertions(+), 65 deletions(-) rename integration/{ => helpers}/global-setup.ts (100%) diff --git a/integration/action-test.ts b/integration/action-test.ts index 358a186cdd..62b6dcf3aa 100644 --- a/integration/action-test.ts +++ b/integration/action-test.ts @@ -6,79 +6,77 @@ import { } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; -describe("action + useActionData", () => { - describe("with x-www-form-urlencoded", () => { - let fixture: Fixture; - let app: AppFixture; +describe("actions", () => { + let fixture: Fixture; + let app: AppFixture; - const FIELD_NAME = "message"; - const WAITING_VALUE = "Waiting..."; - const SUBMITTED_VALUE = "Submission"; + const FIELD_NAME = "message"; + const WAITING_VALUE = "Waiting..."; + const SUBMITTED_VALUE = "Submission"; - beforeAll(async () => { - fixture = await createFixture({ - files: { - "app/routes/urlencoded.jsx": js` - import { Form, useActionData } from "remix"; + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/routes/urlencoded.jsx": js` + import { Form, useActionData } from "remix"; - export let action = async ({ request }) => { - let formData = await request.formData(); - return formData.get("${FIELD_NAME}"); - }; + export let action = async ({ request }) => { + let formData = await request.formData(); + return formData.get("${FIELD_NAME}"); + }; - export default function Actions() { - let data = useActionData() + export default function Actions() { + let data = useActionData() - return ( -
-

- {data ? {data} : "${WAITING_VALUE}"} -

-

- - -

-
- ); - } - ` + return ( +
+

+ {data ? {data} : "${WAITING_VALUE}"} +

+

+ + +

+
+ ); } - }); - - app = await createAppFixture(fixture); + ` + } }); - afterAll(async () => { - await app.close(); - }); + app = await createAppFixture(fixture); + }); - it("returns undefined for action data on GET", async () => { - let res = await fixture.requestDocument("/urlencoded"); - let html = selectHtml(await res.text(), "#text"); - expect(html).toMatch(WAITING_VALUE); - }); + afterAll(async () => { + await app.close(); + }); - it("returns data from the action after POST", async () => { - const FIELD_VALUE = "cheeseburger"; + it("returns undefined for action data on GET", async () => { + let res = await fixture.requestDocument("/urlencoded"); + let html = selectHtml(await res.text(), "#text"); + expect(html).toMatch(WAITING_VALUE); + }); - let params = new URLSearchParams(); - params.append(FIELD_NAME, FIELD_VALUE); + it("returns data from the action after POST", async () => { + const FIELD_VALUE = "cheeseburger"; - let res = await fixture.postDocument("/urlencoded", params); + let params = new URLSearchParams(); + params.append(FIELD_NAME, FIELD_VALUE); - let html = selectHtml(await res.text(), "#text"); - expect(html).toMatch(FIELD_VALUE); - }); + let res = await fixture.postDocument("/urlencoded", params); - it("returns data after a form submission with JavaScript", async () => { - await app.goto(`/urlencoded`); - let html = await app.getHtml("#text"); - expect(html).toMatch(WAITING_VALUE); + let html = selectHtml(await res.text(), "#text"); + expect(html).toMatch(FIELD_VALUE); + }); - await app.page.click("button[type=submit]"); - await app.page.waitForSelector("#action-text"); - html = await app.getHtml("#text"); - expect(html).toMatch(SUBMITTED_VALUE); - }); + it("returns data after a form submission with JavaScript", async () => { + await app.goto(`/urlencoded`); + let html = await app.getHtml("#text"); + expect(html).toMatch(WAITING_VALUE); + + await app.page.click("button[type=submit]"); + await app.page.waitForSelector("#action-text"); + html = await app.getHtml("#text"); + expect(html).toMatch(SUBMITTED_VALUE); }); }); diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index 884ba913fd..d23710004d 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -15,7 +15,7 @@ import { createApp } from "../../packages/create-remix"; import { createRequestHandler as createExpressHandler } from "../../packages/remix-express"; import type { ServerBuild } from "../../packages/remix-server-runtime"; import type { CreateAppArgs } from "../../packages/create-remix"; -import { TMP_DIR } from "../global-setup"; +import { TMP_DIR } from "./global-setup"; const REMIX_SOURCE_BUILD_DIR = path.join(process.cwd(), "build"); diff --git a/integration/global-setup.ts b/integration/helpers/global-setup.ts similarity index 100% rename from integration/global-setup.ts rename to integration/helpers/global-setup.ts diff --git a/integration/loader-test.ts b/integration/loader-test.ts index 9f8b8a3537..55fb374918 100644 --- a/integration/loader-test.ts +++ b/integration/loader-test.ts @@ -1,11 +1,14 @@ import { createFixture, js } from "./helpers/create-fixture"; +import type { Fixture } from "./helpers/create-fixture"; -describe("loader export", () => { - it("returns responses for a specific route", async () => { - const ROOT_DATA = "ROOT_DATA"; - const INDEX_DATA = "INDEX_DATA"; +describe("loader", () => { + let fixture: Fixture; + + const ROOT_DATA = "ROOT_DATA"; + const INDEX_DATA = "INDEX_DATA"; - let fixture = await createFixture({ + beforeAll(async () => { + fixture = await createFixture({ files: { "app/root.jsx": js` import { Outlet } from "remix"; @@ -32,7 +35,9 @@ describe("loader export", () => { ` } }); + }); + it("returns responses for a specific route", async () => { let [root, index] = await Promise.all([ fixture.requestData("/", "root"), fixture.requestData("/", "routes/index") From 69a8a6f75a80b4e618ff5a501ead904fe0cb3f04 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 1 Feb 2022 23:18:42 -0700 Subject: [PATCH 0212/1690] chore: fix test indentation --- integration/action-test.ts | 40 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/integration/action-test.ts b/integration/action-test.ts index 62b6dcf3aa..bdc2db62b1 100644 --- a/integration/action-test.ts +++ b/integration/action-test.ts @@ -18,29 +18,29 @@ describe("actions", () => { fixture = await createFixture({ files: { "app/routes/urlencoded.jsx": js` - import { Form, useActionData } from "remix"; + import { Form, useActionData } from "remix"; - export let action = async ({ request }) => { - let formData = await request.formData(); - return formData.get("${FIELD_NAME}"); - }; + export let action = async ({ request }) => { + let formData = await request.formData(); + return formData.get("${FIELD_NAME}"); + }; - export default function Actions() { - let data = useActionData() + export default function Actions() { + let data = useActionData() - return ( -
-

- {data ? {data} : "${WAITING_VALUE}"} -

-

- - -

-
- ); - } - ` + return ( +
+

+ {data ? {data} : "${WAITING_VALUE}"} +

+

+ + +

+
+ ); + } + ` } }); From a03ca0485691cfdb570b4272c11e5ca436d4436a Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 2 Feb 2022 08:45:06 -0700 Subject: [PATCH 0213/1690] chore(tests): migrate error boundary tests --- integration/action-test.ts | 48 ++++- integration/errory-boundary-test.ts | 266 +++++++++++++++++++++++++ integration/helpers/create-fixture.tsx | 14 +- 3 files changed, 321 insertions(+), 7 deletions(-) create mode 100644 integration/errory-boundary-test.ts diff --git a/integration/action-test.ts b/integration/action-test.ts index bdc2db62b1..503a25b2b1 100644 --- a/integration/action-test.ts +++ b/integration/action-test.ts @@ -13,6 +13,9 @@ describe("actions", () => { const FIELD_NAME = "message"; const WAITING_VALUE = "Waiting..."; const SUBMITTED_VALUE = "Submission"; + const THROWS_REDIRECT = "redirect-throw"; + const REDIRECT_TARGET = "page"; + const PAGE_TEXT = "PAGE_TEXT"; beforeAll(async () => { fixture = await createFixture({ @@ -40,6 +43,28 @@ describe("actions", () => { ); } + `, + + [`app/routes/${THROWS_REDIRECT}.jsx`]: js` + import { Form, redirect } from "remix"; + + export function action() { + throw redirect("/${REDIRECT_TARGET}") + } + + export default function () { + return ( +
+ +
+ ) + } + `, + + [`app/routes/${REDIRECT_TARGET}.jsx`]: js` + export default function () { + return
${PAGE_TEXT}
+ } ` } }); @@ -51,13 +76,13 @@ describe("actions", () => { await app.close(); }); - it("returns undefined for action data on GET", async () => { + it("is not called on document GET requests", async () => { let res = await fixture.requestDocument("/urlencoded"); let html = selectHtml(await res.text(), "#text"); expect(html).toMatch(WAITING_VALUE); }); - it("returns data from the action after POST", async () => { + it("is called on document POST requests", async () => { const FIELD_VALUE = "cheeseburger"; let params = new URLSearchParams(); @@ -69,7 +94,7 @@ describe("actions", () => { expect(html).toMatch(FIELD_VALUE); }); - it("returns data after a form submission with JavaScript", async () => { + it("is called on script transition POST requests", async () => { await app.goto(`/urlencoded`); let html = await app.getHtml("#text"); expect(html).toMatch(WAITING_VALUE); @@ -79,4 +104,21 @@ describe("actions", () => { html = await app.getHtml("#text"); expect(html).toMatch(SUBMITTED_VALUE); }); + + it("redirects a thrown response on document requests", async () => { + let params = new URLSearchParams(); + let res = await fixture.postDocument(`/${THROWS_REDIRECT}`, params); + expect(res.status).toBe(302); + expect(res.headers.get("Location")).toBe(`/${REDIRECT_TARGET}`); + }); + + it("redirects a thrown response on script transitions", async () => { + await app.goto(`/${THROWS_REDIRECT}`); + let responses = app.collectDataResponses(); + await app.clickSubmitButton(`/${THROWS_REDIRECT}`); + expect(responses.length).toBe(1); + expect(responses[0].status()).toBe(204); + expect(new URL(app.page.url()).pathname).toBe(`/${REDIRECT_TARGET}`); + expect(await app.getHtml()).toMatch(PAGE_TEXT); + }); }); diff --git a/integration/errory-boundary-test.ts b/integration/errory-boundary-test.ts new file mode 100644 index 0000000000..f4f68aa78d --- /dev/null +++ b/integration/errory-boundary-test.ts @@ -0,0 +1,266 @@ +import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; + +describe("ErrorBoundary", () => { + let fixture: Fixture; + let app: AppFixture; + let _consoleError: any; + + const ROOT_BOUNDARY_TEXT = "ROOT_BOUNDARY_TEXT"; + const OWN_BOUNDARY_TEXT = "OWN_BOUNDARY_TEXT"; + + const HAS_BOUNDARY_LOADER = "/yes/loader"; + const HAS_BOUNDARY_ACTION = "/yes/action"; + const HAS_BOUNDARY_RENDER = "/yes/render"; + + const NO_BOUNDARY_ACTION = "/no/action"; + const NO_BOUNDARY_LOADER = "/no/loader"; + const NO_BOUNDARY_RENDER = "/no/render"; + + const NOT_FOUND_HREF = "/not/found"; + + beforeAll(async () => { + _consoleError = console.error; + console.error = () => {}; + fixture = await createFixture({ + files: { + "app/root.jsx": js` + import { Outlet, Scripts } from "remix"; + + export default function () { + return ( + + + +
+ +
+ + + + ) + } + + export function ErrorBoundary() { + return ( + + + +
+
${ROOT_BOUNDARY_TEXT}
+
+ + + + ) + } + `, + + "app/routes/index.jsx": js` + import { Link, Form } from "remix"; + export default function () { + return ( +
+ ${NOT_FOUND_HREF} + +
+ + +
+ + + ${HAS_BOUNDARY_LOADER} + + + ${NO_BOUNDARY_LOADER} + + + ${HAS_BOUNDARY_RENDER} + + + ${NO_BOUNDARY_RENDER} + +
+ ) + } + `, + + [`app/routes${HAS_BOUNDARY_ACTION}.jsx`]: js` + import { Form } from "remix"; + export async function action() { + throw new Error("Kaboom!") + } + export function ErrorBoundary() { + return

${OWN_BOUNDARY_TEXT}

+ } + export default function () { + return ( +
+ +
+ ); + } + `, + + [`app/routes${NO_BOUNDARY_ACTION}.jsx`]: js` + import { Form } from "remix"; + export function action() { + throw new Error("Kaboom!") + } + export default function () { + return ( +
+ +
+ ) + } + `, + + [`app/routes${HAS_BOUNDARY_LOADER}.jsx`]: js` + export function loader() { + throw new Error("Kaboom!") + } + export function ErrorBoundary() { + return
${OWN_BOUNDARY_TEXT}
+ } + export default function () { + return
+ } + `, + + [`app/routes${NO_BOUNDARY_LOADER}.jsx`]: js` + export function loader() { + throw new Error("Kaboom!") + } + export default function () { + return
+ } + `, + + [`app/routes${NO_BOUNDARY_RENDER}.jsx`]: js` + export default function () { + throw new Error("Kaboom!") + return
+ } + `, + + [`app/routes${HAS_BOUNDARY_RENDER}.jsx`]: js` + export default function () { + throw new Error("Kaboom!") + return
+ } + + export function ErrorBoundary() { + return
${OWN_BOUNDARY_TEXT}
+ } + ` + } + }); + + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + console.error = _consoleError; + await app.close(); + }); + + test("own boundary, action, document request", async () => { + let params = new URLSearchParams(); + let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); + + // FIXME: this is broken, it renders the root boundary logging in `RemixRoute` + // it's because the route module hasn't been loaded, my gut tells me that we + // didn't load the route module but tried to render it's boundary, we need the + // module for that! this will probably fix the twin test over in + // catch-boundary-test + test.skip("own boundary, action, client transition from other route", async () => { + await app.goto("/"); + await app.clickSubmitButton(HAS_BOUNDARY_ACTION); + expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); + }); + + test("own boundary, action, client transition from itself", async () => { + await app.goto(HAS_BOUNDARY_ACTION); + await app.clickSubmitButton(HAS_BOUNDARY_ACTION); + expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); + }); + + it("bubbles to parent in action document requests", async () => { + let params = new URLSearchParams(); + let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); + + it("bubbles to parent in action script transitions from other routes", async () => { + await app.goto("/"); + await app.clickSubmitButton(NO_BOUNDARY_ACTION); + expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); + }); + + it("bubbles to parent in action script transitions from self", async () => { + await app.goto(NO_BOUNDARY_ACTION); + await app.clickSubmitButton(NO_BOUNDARY_ACTION); + expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); + }); + + test("own boundary, loader, document request", async () => { + let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); + + test("own boundary, loader, client transition", async () => { + await app.goto("/"); + await app.clickLink(HAS_BOUNDARY_LOADER); + expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); + }); + + it("bubbles to parent in loader document requests", async () => { + let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); + + it("bubbles to parent in action script transitions from other routes", async () => { + await app.goto("/"); + await app.clickLink(NO_BOUNDARY_LOADER); + expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); + }); + + test("ssr rendering errors with no boundary", async () => { + let res = await fixture.requestDocument(NO_BOUNDARY_RENDER); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); + + test("script transition rendering errors with no boundary", async () => { + await app.goto("/"); + await app.clickLink(NO_BOUNDARY_RENDER); + expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); + }); + + test("ssr rendering errors with boundary", async () => { + let res = await fixture.requestDocument(HAS_BOUNDARY_RENDER); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); + + test("script transition rendering errors with boundary", async () => { + await app.goto("/"); + await app.clickLink(HAS_BOUNDARY_RENDER); + expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); + }); +}); diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index d23710004d..89938929a6 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -185,16 +185,22 @@ export async function createAppFixture(fixture: Fixture) { * `action` supplied, clicks it, and optionally waits for the network to * be idle before contininuing. * - * @param formAction The formAction of the button you want to click + * @param action The formAction of the button you want to click * @param options `{ wait }` waits for the network to be idle before moving on */ clickSubmitButton: async ( - formAction: string, + action: string, options: { wait: boolean } = { wait: true } ) => { - let selector = `button[formaction="${formAction}"]`; + let selector = `button[formaction="${action}"]`; let el = await page.$(selector); - if (!el) throw new Error(`Can't find button: ${selector}`); + if (!el) { + selector = `form[action="${action}"] button[type="submit"]`; + el = await page.$(selector); + if (!el) { + throw new Error(`Can't find button for: ${action}`); + } + } if (options.wait) { await doAndWait(page, () => el.click()); } else { From cfba589d825db7b96feb2ecd99a6d624a49555c0 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 2 Feb 2022 10:15:52 -0800 Subject: [PATCH 0214/1690] feat: update esbuild (#1764) * feat: update esbuild fix: move node polyfills to last plugin for non node platforms This resolves the issue of not being able to import node built-ins in a route module. * feat: add node built-ins polyfill to browser build chore: added tests * removed dup fuc --- integration/compiler-test.ts | 83 ++++++++++++++++++++++---- integration/helpers/create-fixture.tsx | 12 +++- packages/remix-dev/compiler.ts | 16 ++--- packages/remix-dev/package.json | 2 +- 4 files changed, 90 insertions(+), 23 deletions(-) diff --git a/integration/compiler-test.ts b/integration/compiler-test.ts index af40074e11..a278d4ad86 100644 --- a/integration/compiler-test.ts +++ b/integration/compiler-test.ts @@ -1,34 +1,93 @@ -import { createFixture, createAppFixture } from "./helpers/create-fixture"; +import { createFixture, createAppFixture, js } from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; describe("compiler", () => { - it("removes server code with `*.server` files", async () => { - let fixture = await createFixture({ + let fixture: Fixture; + let app: AppFixture; + + beforeAll(async () => { + fixture = await createFixture({ files: { - "app/fake.server.js": ` - import fs from "fs"; - export default fs; + "app/fake.server.js": js` + export default { hello: "world" }; `, - "app/routes/index.jsx": ` - import fs from "~/fake.server.js"; + "app/routes/index.jsx": js` + import fake from "~/fake.server.js"; export default function Index() { - return
{Object.keys(fs).length}
+ return
{Object.keys(fake).length}
+ } + `, + "app/routes/built-ins.jsx": js` + import { useLoaderData } from "remix"; + import * as path from "path"; + + export let loader = () => { + return path.join("test", "file.txt"); + } + + export default function BuiltIns() { + return
{useLoaderData()}
+ } + `, + "app/routes/built-ins-polyfill.jsx": js` + import { useLoaderData } from "remix"; + import * as path from "path"; + + export default function BuiltIns() { + return
{path.join("test", "file.txt")}
} ` } }); - let app = await createAppFixture(fixture); + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); - let res = await app.goto("/"); + it("removes server code with `*.server` files", async () => { + let res = await app.goto("/", true); expect(res.status()).toBe(200); // server rendered fine // rendered the page instead of the error boundary expect(await app.getHtml("#index")).toMatchInlineSnapshot( `"
0
"` ); + }); - await app.close(); + it("removes node built-ins from client bundle when used in just loader", async () => { + let res = await app.goto("/built-ins", true); + expect(res.status()).toBe(200); // server rendered fine + + // rendered the page instead of the error boundary + expect(await app.getHtml("#built-ins")).toMatchInlineSnapshot( + `"
test/file.txt
"` + ); + + let routeModule = await fixture.getBrowserAsset( + fixture.build.assets.routes["routes/built-ins"].module + ); + // does not include `import bla from "path"` in the output bundle + expect(routeModule).not.toMatch(/from\s*"path/); + }); + + it("bundles node built-ins polyfill for client bundle when used in client code", async () => { + let res = await app.goto("/built-ins-polyfill", true); + expect(res.status()).toBe(200); // server rendered fine + + // rendered the page instead of the error boundary + expect(await app.getHtml("#built-ins-polyfill")).toMatchInlineSnapshot( + `"
test/file.txt
"` + ); + + let routeModule = await fixture.getBrowserAsset( + fixture.build.assets.routes["routes/built-ins-polyfill"].module + ); + // does not include `import bla from "path"` in the output bundle + expect(routeModule).not.toMatch(/from\s*"path/); }); }); diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index 89938929a6..fcf07e7f93 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -65,12 +65,20 @@ export async function createFixture(init: FixtureInit) { }); }; + let getBrowserAsset = async (asset: string) => { + return fs.readFile( + path.join(projectDir, "public", asset.replace(/^\//, "")), + "utf8" + ); + }; + return { projectDir, build: app, requestDocument, requestData, - postDocument + postDocument, + getBrowserAsset }; } @@ -321,7 +329,7 @@ export function selectHtml(source: string, selector: string) { export function prettyHtml(source: string): string { return prettier.format(source, { parser: "html" }); } - + // Taken from https://github.com/puppeteer/puppeteer/issues/5328#issuecomment-986175620 // Seems to work? async function doAndWait( diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index dafabf791e..fc28411f55 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -372,7 +372,8 @@ async function createBrowserBuild( plugins: [ mdxPlugin(config), browserRouteModulesPlugin(config, /\?browser$/), - emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/) + emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/), + NodeModulesPolyfillPlugin() ] }); } @@ -397,19 +398,18 @@ async function createServerBuild( }; } - let plugins: esbuild.Plugin[] = []; - if (config.serverPlatform !== "node") { - plugins.push(NodeModulesPolyfillPlugin()); - } - - plugins.push( + let plugins: esbuild.Plugin[] = [ mdxPlugin(config), emptyModulesPlugin(config, /\.client\.[tj]sx?$/), serverRouteModulesPlugin(config), serverEntryModulesPlugin(config), serverAssetsPlugin(browserManifestPromiseRef), serverBareModulesPlugin(config, dependencies) - ); + ]; + + if (config.serverPlatform !== "node") { + plugins.push(NodeModulesPolyfillPlugin()); + } return esbuild .build({ diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index e818619ae4..0bf70d7e37 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -19,7 +19,7 @@ "@remix-run/server-runtime": "1.1.3", "cacache": "^15.0.5", "chokidar": "^3.5.1", - "esbuild": "0.13.14", + "esbuild": "0.14.16", "exit-hook": "2.2.1", "fs-extra": "^10.0.0", "get-port": "^5.1.1", From c4f5ca1fc0416896c4813f877ceadb9ea7e75285 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Wed, 2 Feb 2022 18:17:56 +0000 Subject: [PATCH 0215/1690] chore: format formatted cfba589d825db7b96feb2ecd99a6d624a49555c0 --- integration/helpers/create-fixture.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index fcf07e7f93..b2d192f132 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -329,7 +329,7 @@ export function selectHtml(source: string, selector: string) { export function prettyHtml(source: string): string { return prettier.format(source, { parser: "html" }); } - + // Taken from https://github.com/puppeteer/puppeteer/issues/5328#issuecomment-986175620 // Seems to work? async function doAndWait( From 49079afc0b7a0c224f4c5535d28ba40a811c6350 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 2 Feb 2022 14:34:08 -0700 Subject: [PATCH 0216/1690] chore: added bug report template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit we can direct people reporting issues to this file, making it easy for them and us to get on the same page about potential issues with Remix 😁 --- integration/bug-report-test.ts | 95 ++++++++++++++++++++++++++ integration/helpers/create-fixture.tsx | 26 ++++++- 2 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 integration/bug-report-test.ts diff --git a/integration/bug-report-test.ts b/integration/bug-report-test.ts new file mode 100644 index 0000000000..a83a6b081f --- /dev/null +++ b/integration/bug-report-test.ts @@ -0,0 +1,95 @@ +import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; + +let fixture: Fixture; +let app: AppFixture; + +//////////////////////////////////////////////////////////////////////////////// +// 💿 👋 Hola! It's me, Dora the Remix Disc, I'm here to help you write a great +// bug report pull request. You don't need to fix the bug, this is just to +// report one. +// +// First, make sure to install dependencies and build Remix. From the root of +// the project, run this: +// +// ``` +// yarn && yarn build +// ``` +// +// Now try running this test: +// +// ``` +// jest integration/bug-report-test.ts +// ``` +// +// You can add `--watch` to the end to have it re-run on file changes: +// +// ``` +// jest integration/bug-report-test.ts --watch +// ``` +//////////////////////////////////////////////////////////////////////////////// + +beforeAll(async () => { + fixture = await createFixture({ + //////////////////////////////////////////////////////////////////////////// + // 💿 Next, add files to this object, just like files in a real app, + // `createFixture` will make an app and run your tests against it. + //////////////////////////////////////////////////////////////////////////// + files: { + "app/routes/index.jsx": js` + import { json, useLoaderData, Link } from "remix"; + + export function loader() { + return json("pizza"); + } + + export default function Index() { + let data = useLoaderData(); + return ( +
+ {data} + Other Route +
+ ) + } + `, + + "app/routes/burgers.jsx": js` + export default function Index() { + return
cheeseburger
; + } + ` + } + }); + + // This creates an interactive app using puppeteer. + app = await createAppFixture(fixture); +}); + +afterAll(async () => app.close()); + +//////////////////////////////////////////////////////////////////////////////// +// 💿 Almost done, now write your failing test case(s) down here Make sure to +// add a good description for what you expect Remix to do 👇🏽 +//////////////////////////////////////////////////////////////////////////////// + +it("[description of what you expect it to do]", async () => { + // 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"); + expect(await app.getHtml()).toMatch("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! + // await app.poke(20); + + // Go check out the other tests to see what else you can do. +}); + +//////////////////////////////////////////////////////////////////////////////// +// 💿 Finally, push your changes to your fork of Remix and open a pull request! +//////////////////////////////////////////////////////////////////////////////// diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index b2d192f132..63647f851f 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -181,6 +181,9 @@ export async function createAppFixture(fixture: Fixture) { ) => { let selector = `a[href="${href}"]`; let el = await page.$(selector); + if (!el) { + throw new Error(`Could not find link for ${selector}`); + } if (options.wait) { await doAndWait(page, () => el.click()); } else { @@ -301,14 +304,33 @@ async function installRemix(projectDir: string) { await fse.copy(buildDir, installDir); } -function writeTestFiles(init: FixtureInit, dir: string) { - return Promise.all( +async function writeTestFiles(init: FixtureInit, dir: string) { + await Promise.all( Object.keys(init.files).map(async filename => { let filePath = path.join(dir, filename); await fse.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, init.files[filename]); }) ); + await renamePkgJsonApp(dir); +} + +/** + * This prevents the console for spitting out a bunch of junk like this for + * every fixture: + * + * jest-haste-map: Haste module naming collision: remix-app-template-js + * + * I found some github issues that says that `modulePathIgnorePatterns` should + * help, so I added it to our `jest.config.js`, but it doesn't seem to help, so + * I bruteforced it here. + */ +async function renamePkgJsonApp(dir: string) { + let pkgPath = path.join(dir, "package.json"); + let pkg = await fs.readFile(pkgPath); + let obj = JSON.parse(pkg.toString()); + obj.name = path.basename(dir); + await fs.writeFile(pkgPath, JSON.stringify(obj, null, 2) + "\n"); } export async function getHtml(page: Page, selector?: string) { From d0644c9a1d7fb68cacd004c7b66c7d23e06d1e49 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 2 Feb 2022 16:11:02 -0700 Subject: [PATCH 0217/1690] chore: migrate form tests --- integration/form-test.ts | 149 +++++++++++++++++++++++++ integration/helpers/create-fixture.tsx | 46 ++++++++ integration/rendering-test.ts | 2 +- 3 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 integration/form-test.ts diff --git a/integration/form-test.ts b/integration/form-test.ts new file mode 100644 index 0000000000..a8369d5e1b --- /dev/null +++ b/integration/form-test.ts @@ -0,0 +1,149 @@ +import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; + +describe("Forms", () => { + let fixture: Fixture; + let app: AppFixture; + + const KEYBOARD_INPUT = "KEYBOARD_INPUT"; + const CHECKBOX_BUTTON = "CHECKBOX_BUTTON"; + const ORPHAN_BUTTON = "ORPHAN_BUTTON"; + const FORM_WITH_ORPHAN = "FORM_WITH_ORPHAN"; + const LUNCH = "LUNCH"; + const CHEESESTEAK = "CHEESESTEAK"; + const LAKSA = "LAKSA"; + const SQUID_INK_HOTDOG = "SQUID_INK_HOTDOG"; + + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/root.jsx": js` + import { Outlet, Scripts } from "remix"; + export default function Root() { + return ( + + + + + + + + ) + } + `, + + "app/routes/get-submission.jsx": js` + import { Fragment } from "react" + import { useLoaderData, Form } from "remix"; + + export function loader({ request }) { + let url = new URL(request.url); + return url.searchParams.toString() + } + + export default function() { + let data = useLoaderData(); + return ( + <> +
+ + +
+ +
+ + +
+ + + +
+ + + + +
+ +
{data}
+ + ) + } + ` + } + }); + + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); + + it("posts to a loader without JavaScript", async () => { + let enableJavaScript = await app.disableJavaScript(); + await app.goto("/get-submission"); + await app.clickSubmitButton("/get-submission", { wait: false }); + await app.page.waitForNavigation(); + expect(await app.getHtml("pre")).toMatch(CHEESESTEAK); + await enableJavaScript(); + }); + + it("posts to a loader", async () => { + await app.goto("/get-submission"); + await app.clickSubmitButton("/get-submission"); + expect(await app.getHtml("pre")).toMatch(CHEESESTEAK); + }); + + it("posts to a loader with button data with click", async () => { + await app.goto("/get-submission"); + await app.clickElement("#buttonWithValue"); + expect(await app.getHtml("pre")).toMatch(LAKSA); + }); + + it("posts to a loader with button data with keyboard", async () => { + await app.goto("/get-submission"); + await app.waitForNetworkAfter(async () => { + await app.page.focus(`#${KEYBOARD_INPUT}`); + await app.page.keyboard.press("Enter"); + }); + expect(await app.getHtml("pre")).toMatch(LAKSA); + }); + + it("posts with the correct checkbox data", async () => { + await app.goto("/get-submission"); + await app.clickElement(`#${CHECKBOX_BUTTON}`); + expect(await app.getHtml("pre")).toMatchInlineSnapshot( + `"
LUNCH=CHEESESTEAK&LUNCH=LAKSA
"` + ); + }); + + it("posts button data from outside the form", async () => { + await app.goto("/get-submission"); + await app.clickElement(`#${ORPHAN_BUTTON}`); + expect(await app.getHtml("pre")).toMatch(SQUID_INK_HOTDOG); + }); +}); diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index 63647f851f..d96bcf4a65 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -219,6 +219,28 @@ export async function createAppFixture(fixture: Fixture) { } }, + /** + * Clicks any element and waits for the network to be idle. + */ + clickElement: async (selector: string) => { + let el = await page.$(selector); + if (!el) { + throw new Error(`Can't find element for: ${selector}`); + } + await doAndWait(page, () => el.click()); + }, + + /** + * Perform any interaction and wait for the network to be idle: + * + * ```js + * await app.waitForNetworkAfter(() => app.page.focus("#el")) + * ``` + */ + waitForNetworkAfter: async (fn: () => Promise) => { + await doAndWait(page, fn); + }, + /** * "Clicks" the back button and optionally waits for the network to be * idle (defaults to waiting). @@ -238,6 +260,30 @@ export async function createAppFixture(fixture: Fixture) { */ collectDataResponses: () => collectDataResponses(page), + /** + * Disables JavaScript for the page, make sure to enable it again by + * calling and awaiting the returned function! + * + * ```js + * let enable = await app.disableJavaScript(); + * // tests + * await enable(); + * ``` + */ + disableJavaScript: async () => { + await page.setRequestInterception(true); + let handler = (request: puppeteer.HTTPRequest) => { + if (request.resourceType() === "script") request.abort(); + else request.continue(); + }; + page.on("request", handler); + + return async () => { + await page.setRequestInterception(false); + page.off("request", handler); + }; + }, + /** * Get HTML from the page. Useful for asserting something rendered that * you expected. diff --git a/integration/rendering-test.ts b/integration/rendering-test.ts index 4987dc1da7..101555e17c 100644 --- a/integration/rendering-test.ts +++ b/integration/rendering-test.ts @@ -59,7 +59,7 @@ describe("rendering", () => { it("hydrates", async () => { await app.goto("/"); - expect(selectHtml(await app.getHtml(), "#content")).toMatchInlineSnapshot(` + expect(await app.getHtml("#content")).toMatchInlineSnapshot(` "

Root

Index

From c830910596df9e869aa1f2abbe5fabdf3e2246d2 Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 2 Feb 2022 16:25:51 -0700 Subject: [PATCH 0218/1690] chore: cleaned up unused test code --- integration/form-test.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/integration/form-test.ts b/integration/form-test.ts index a8369d5e1b..db1eddbc99 100644 --- a/integration/form-test.ts +++ b/integration/form-test.ts @@ -17,23 +17,7 @@ describe("Forms", () => { beforeAll(async () => { fixture = await createFixture({ files: { - "app/root.jsx": js` - import { Outlet, Scripts } from "remix"; - export default function Root() { - return ( - - - - - - - - ) - } - `, - "app/routes/get-submission.jsx": js` - import { Fragment } from "react" import { useLoaderData, Form } from "remix"; export function loader({ request }) { From 235d430b56cbb5fdef9c827b520742407171c12b Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Wed, 2 Feb 2022 16:48:19 -0700 Subject: [PATCH 0219/1690] chore: migrated server entry tests --- integration/server-entry-test.ts | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 integration/server-entry-test.ts diff --git a/integration/server-entry-test.ts b/integration/server-entry-test.ts new file mode 100644 index 0000000000..770c707e52 --- /dev/null +++ b/integration/server-entry-test.ts @@ -0,0 +1,40 @@ +import { createFixture, js } from "./helpers/create-fixture"; +import type { Fixture } from "./helpers/create-fixture"; + +describe("Server Entry", () => { + let fixture: Fixture; + + const DATA_HEADER_NAME = "X-Macaroni-Salad"; + const DATA_HEADER_VALUE = "Smoked Mozarella"; + + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/entry.server.jsx": js` + export default function handleRequest() { + return new Response(""); + } + + export function handleDataRequest(response) { + response.headers.set("${DATA_HEADER_NAME}", "${DATA_HEADER_VALUE}"); + return response; + } + `, + + "app/routes/index.jsx": js` + export function loader() { + return "" + } + export default function () { + return
+ } + ` + } + }); + }); + + it("can manipulate a data response", async () => { + let response = await fixture.requestData("/", "routes/index"); + expect(response.headers.get(DATA_HEADER_NAME)).toBe(DATA_HEADER_VALUE); + }); +}); From fa51de75e69f52949dc2179969de94c84f51d277 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 4 Feb 2022 12:10:27 -0800 Subject: [PATCH 0220/1690] Compiler cleanup and tweaks --- packages/remix-dev/compiler.ts | 103 +++++++++--------- packages/remix-dev/compiler/assets.ts | 2 +- .../compiler/plugins/serverAssetsPlugin.ts | 9 +- .../plugins/serverBareModulesPlugin.ts | 9 +- .../plugins/serverEntryModulesPlugin.ts | 9 +- packages/remix-dev/compiler/virtualModules.ts | 22 ++-- packages/remix-dev/config.ts | 4 +- 7 files changed, 83 insertions(+), 75 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index fc28411f55..144280a785 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -4,13 +4,13 @@ import * as esbuild from "esbuild"; import * as fse from "fs-extra"; import debounce from "lodash.debounce"; import chokidar from "chokidar"; -import type { AssetsManifest } from "@remix-run/server-runtime/entry"; import { NodeModulesPolyfillPlugin } from "@esbuild-plugins/node-modules-polyfill"; import { BuildMode, BuildTarget } from "./build"; import type { RemixConfig } from "./config"; import { readConfig } from "./config"; import { warnOnce } from "./warnings"; +import type { AssetsManifest } from "./compiler/assets"; import { createAssetsManifest } from "./compiler/assets"; import { getAppDependencies } from "./compiler/dependencies"; import { loaders } from "./compiler/loaders"; @@ -18,7 +18,7 @@ import { browserRouteModulesPlugin } from "./compiler/plugins/browserRouteModule import { emptyModulesPlugin } from "./compiler/plugins/emptyModulesPlugin"; import { mdxPlugin } from "./compiler/plugins/mdx"; import { serverAssetsPlugin } from "./compiler/plugins/serverAssetsPlugin"; -import type { BrowserManifestPromiseRef } from "./compiler/plugins/serverAssetsPlugin"; +import type { AssetsManifestPromiseRef } from "./compiler/plugins/serverAssetsPlugin"; import { serverBareModulesPlugin } from "./compiler/plugins/serverBareModulesPlugin"; import { serverEntryModulesPlugin } from "./compiler/plugins/serverEntryModulesPlugin"; import { serverRouteModulesPlugin } from "./compiler/plugins/serverRouteModulesPlugin"; @@ -76,9 +76,9 @@ export async function build( onBuildFailure = defaultBuildFailureHandler }: BuildOptions = {} ): Promise { - let ref: BrowserManifestPromiseRef = {}; + let assetsManifestPromiseRef: AssetsManifestPromiseRef = {}; - await buildEverything(config, ref, { + await buildEverything(config, assetsManifestPromiseRef, { mode, target, sourcemap, @@ -120,10 +120,11 @@ export async function watch( onWarning, incremental: true }; - let browserManifestPromiseRef: BrowserManifestPromiseRef = {}; + + let assetsManifestPromiseRef: AssetsManifestPromiseRef = {}; let [browserBuild, serverBuild] = await buildEverything( config, - browserManifestPromiseRef, + assetsManifestPromiseRef, options ); @@ -152,7 +153,7 @@ export async function watch( if (onRebuildStart) onRebuildStart(); let builders = await buildEverything( config, - browserManifestPromiseRef, + assetsManifestPromiseRef, options ); if (onRebuildFinish) onRebuildFinish(); @@ -169,7 +170,7 @@ export async function watch( try { [browserBuild, serverBuild] = await buildEverything( config, - browserManifestPromiseRef, + assetsManifestPromiseRef, options ); @@ -188,16 +189,20 @@ export async function watch( // If we get here and can't call rebuild something went wrong and we // should probably blow as it's not really recoverable. - let browserBuildPromise = browserBuild - .rebuild() - .then(build => generateManifests(config, build.metafile!)); - // Do not await the client build, instead assign the promise to a ref - // so the server build can await it to gain access to the client manifest. - browserManifestPromiseRef.current = browserBuildPromise; + let browserBuildPromise = browserBuild.rebuild(); + let assetsManifestPromise = browserBuildPromise.then(build => + generateAssetsManifest(config, build.metafile!) + ); + + // Assign the assetsManifestPromise to a ref so the server build can await + // it when loading the @remix-run/dev/assets-manifest virtual module. + assetsManifestPromiseRef.current = assetsManifestPromise; await Promise.all([ - browserBuildPromise, - serverBuild.rebuild().then(writeServerBuildResult(config)) + assetsManifestPromise, + serverBuild + .rebuild() + .then(build => writeServerBuildResult(config, build.outputFiles!)) ]).catch(err => { disposeBuilders(); onBuildFailure(err); @@ -275,33 +280,27 @@ function isEntryPoint(config: RemixConfig, file: string) { async function buildEverything( config: RemixConfig, - browserManifestPromiseRef: BrowserManifestPromiseRef, + assetsManifestPromiseRef: AssetsManifestPromiseRef, options: Required & { incremental?: boolean } ): Promise<(esbuild.BuildResult | undefined)[]> { - // TODO: - // When building for node, we build both the browser and server builds in - // parallel and emit the asset manifest as a separate file in the output - // directory. - // When building for Cloudflare Workers, we need to run the browser and server - // builds serially so we can inline the asset manifest into the server build - // in a single JavaScript file. - try { let browserBuildPromise = createBrowserBuild(config, options); - let manifestPromise = browserBuildPromise.then(build => { - return generateManifests(config, build.metafile!); - }); - // Do not await the client build, instead assign the promise to a ref - // so the server build can await it to gain access to the client manifest. - browserManifestPromiseRef.current = manifestPromise; + let assetsManifestPromise = browserBuildPromise.then(build => + generateAssetsManifest(config, build.metafile!) + ); + + // Assign the assetsManifestPromise to a ref so the server build can await + // it when loading the @remix-run/dev/assets-manifest virtual module. + assetsManifestPromiseRef.current = assetsManifestPromise; + let serverBuildPromise = createServerBuild( config, options, - browserManifestPromiseRef + assetsManifestPromiseRef ); return await Promise.all([ - manifestPromise.then(() => browserBuildPromise), + assetsManifestPromise.then(() => browserBuildPromise), serverBuildPromise ]); } catch (err) { @@ -381,7 +380,7 @@ async function createBrowserBuild( async function createServerBuild( config: RemixConfig, options: Required & { incremental?: boolean }, - browserManifestPromiseRef: BrowserManifestPromiseRef + assetsManifestPromiseRef: AssetsManifestPromiseRef ): Promise { let dependencies = await getAppDependencies(config); @@ -393,8 +392,8 @@ async function createServerBuild( } else { stdin = { contents: config.serverBuildTargetEntryModule, - loader: "ts", - resolveDir: config.rootDirectory + resolveDir: config.rootDirectory, + loader: "ts" }; } @@ -403,7 +402,7 @@ async function createServerBuild( emptyModulesPlugin(config, /\.client\.[tj]sx?$/), serverRouteModulesPlugin(config), serverEntryModulesPlugin(config), - serverAssetsPlugin(browserManifestPromiseRef), + serverAssetsPlugin(assetsManifestPromiseRef), serverBareModulesPlugin(config, dependencies) ]; @@ -450,16 +449,19 @@ async function createServerBuild( }, plugins }) - .then(writeServerBuildResult(config)); + .then(async build => { + await writeServerBuildResult(config, build.outputFiles); + return build; + }); } -async function generateManifests( +async function generateAssetsManifest( config: RemixConfig, metafile: esbuild.Metafile ): Promise { let assetsManifest = await createAssetsManifest(config, metafile); - let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; + assetsManifest.url = config.publicPath + filename; await writeFileSafe( @@ -467,22 +469,19 @@ async function generateManifests( `window.__remixManifest=${JSON.stringify(assetsManifest)};` ); - return assetsManifest as AssetsManifest; + return assetsManifest; } -function writeServerBuildResult(config: RemixConfig) { - return async (buildResult: esbuild.BuildResult) => { - await fse.ensureDir(path.dirname(config.serverBuildPath)); +async function writeServerBuildResult( + config: RemixConfig, + outputFiles: esbuild.OutputFile[] +) { + await fse.ensureDir(path.dirname(config.serverBuildPath)); - // manually write files to exclude assets from server build - for (let file of buildResult.outputFiles!) { - if (file.path !== config.serverBuildPath) { - continue; - } + for (let file of outputFiles) { + if (file.path === config.serverBuildPath) { await fse.writeFile(file.path, file.contents); break; } - - return buildResult; - }; + } } diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index ffe7c2ccdb..742b129755 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -9,7 +9,7 @@ import { createUrl } from "./utils/url"; type Route = RemixConfig["routes"][string]; -interface AssetsManifest { +export interface AssetsManifest { version: string; url?: string; entry: { diff --git a/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts b/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts index b82a4dbfc0..6f1e184312 100644 --- a/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts @@ -2,18 +2,17 @@ import type { Plugin } from "esbuild"; import jsesc from "jsesc"; import invariant from "../../invariant"; -import virtualModules from "../virtualModules"; -import type { serverEntryModulesPlugin } from "./serverEntryModulesPlugin"; +import { assetsManifestVirtualModule } from "../virtualModules"; -export type BrowserManifestPromiseRef = { current?: Promise }; +export type AssetsManifestPromiseRef = { current?: Promise }; /** * Creates a virtual module of the asset manifest for consumption. * See {@link serverEntryModulesPlugin} for consumption. */ export function serverAssetsPlugin( - browserManifestPromiseRef: BrowserManifestPromiseRef, - filter: RegExp = virtualModules.assetsManifestVirtualModule.filter + browserManifestPromiseRef: AssetsManifestPromiseRef, + filter: RegExp = assetsManifestVirtualModule.filter ): Plugin { return { name: "server-assets", diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts index 00c22f23b7..c13b34077c 100644 --- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -3,7 +3,10 @@ import { isAbsolute, relative } from "path"; import type { Plugin } from "esbuild"; import type { RemixConfig } from "../../config"; -import virtualModules from "../virtualModules"; +import { + serverBuildVirtualModule, + assetsManifestVirtualModule +} from "../virtualModules"; /** * A plugin responsible for resolving bare module ids based on server target. @@ -33,8 +36,8 @@ export function serverBareModulesPlugin( // These are our virutal modules, always bundle the because there is no // "real" file on disk to externalize. if ( - path === virtualModules.serverBuildVirutalModule.path || - path === virtualModules.assetsManifestVirtualModule.path + path === serverBuildVirtualModule.id || + path === assetsManifestVirtualModule.id ) { return undefined; } diff --git a/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts index c0b4fa5f6b..e62474ab26 100644 --- a/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts @@ -2,7 +2,10 @@ import * as path from "path"; import type { Plugin } from "esbuild"; import type { RemixConfig } from "../../config"; -import virtualModules from "../virtualModules"; +import { + serverBuildVirtualModule, + assetsManifestVirtualModule +} from "../virtualModules"; /** * Creates a virtual module called `@remix-run/dev/server-build` that exports the @@ -12,7 +15,7 @@ import virtualModules from "../virtualModules"; */ export function serverEntryModulesPlugin( remixConfig: RemixConfig, - filter: RegExp = virtualModules.serverBuildVirutalModule.filter + filter: RegExp = serverBuildVirtualModule.filter ): Plugin { return { name: "server-entry", @@ -41,7 +44,7 @@ ${Object.keys(remixConfig.routes) }) .join("\n")} export { default as assets } from ${JSON.stringify( - virtualModules.assetsManifestVirtualModule.path + assetsManifestVirtualModule.id )}; export const entry = { module: entryServer }; export const routes = { diff --git a/packages/remix-dev/compiler/virtualModules.ts b/packages/remix-dev/compiler/virtualModules.ts index 11c62564cf..05cb975ed4 100644 --- a/packages/remix-dev/compiler/virtualModules.ts +++ b/packages/remix-dev/compiler/virtualModules.ts @@ -1,10 +1,14 @@ -export default { - serverBuildVirutalModule: { - path: "@remix-run/dev/server-build", - filter: /^@remix-run\/dev\/server-build$/ - }, - assetsManifestVirtualModule: { - path: "@remix-run/dev/assets-manifest", - filter: /^@remix-run\/dev\/assets-manifest$/ - } +interface VirtualModule { + id: string; + filter: RegExp; +} + +export const serverBuildVirtualModule: VirtualModule = { + id: "@remix-run/dev/server-build", + filter: /^@remix-run\/dev\/server-build$/ +}; + +export const assetsManifestVirtualModule: VirtualModule = { + id: "@remix-run/dev/assets-manifest", + filter: /^@remix-run\/dev\/assets-manifest$/ }; diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index c9d4b12b2c..1f52e4da55 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -6,7 +6,7 @@ import type { RouteManifest, DefineRoutesFunction } from "./config/routes"; import { defineRoutes } from "./config/routes"; import { defineConventionalRoutes } from "./config/routesConvention"; import { ServerMode, isValidServerMode } from "./config/serverModes"; -import virtualModules from "./compiler/virtualModules"; +import { serverBuildVirtualModule } from "./compiler/virtualModules"; export interface RemixMdxConfig { rehypePlugins?: any[]; @@ -364,7 +364,7 @@ export async function readConfig( } let serverBuildTargetEntryModule = `export * from ${JSON.stringify( - virtualModules.serverBuildVirutalModule.path + serverBuildVirtualModule.id )};`; return { From a1cbd3ede2761ade51b774f765982d01de169b39 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 8 Feb 2022 23:25:29 +0700 Subject: [PATCH 0221/1690] fix: do not clone request for actions to avoid stream halting related bug in node-fetch (#1766) --- integration/file-uploads-test.ts | 121 +++++++++++++++++++++++++ integration/helpers/create-fixture.tsx | 13 +++ packages/remix-server-runtime/data.ts | 2 +- 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 integration/file-uploads-test.ts diff --git a/integration/file-uploads-test.ts b/integration/file-uploads-test.ts new file mode 100644 index 0000000000..f9f0d418d4 --- /dev/null +++ b/integration/file-uploads-test.ts @@ -0,0 +1,121 @@ +import * as fs from "fs/promises"; +import * as path from "path"; + +import { createFixture, createAppFixture, js } from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; + +describe("file-uploads", () => { + let fixture: Fixture; + let app: AppFixture; + + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/fileUploadHandler.js": js` + import * as path from "path"; + import { unstable_createFileUploadHandler as createFileUploadHandler } from "remix"; + + export let uploadHandler = createFileUploadHandler({ + directory: path.resolve(__dirname, "..", "uploads"), + maxFileSize: 3000000, // 3MB + // you probably want to avoid conflicts in production + // do not set to false or passthrough filename in real + // applications. + avoidFileConflicts: false, + file: ({ filename }) => filename + }); + `, + "app/routes/file-upload.jsx": js` + import { Form, unstable_parseMultipartFormData as parseMultipartFormData, useActionData } from "remix"; + import { uploadHandler } from "~/fileUploadHandler"; + + export let action = async ({ request }) => { + try { + let formData = await parseMultipartFormData(request, uploadHandler); + + let file = formData.get("file"); + + if (typeof file === "string" || !file) { + throw new Error("invalid file type"); + } + + return { name: file.name, size: file.size }; + } catch (error) { + return { errorMessage: error.message }; + } + }; + + export default function Upload() { + return ( + <> +
+ + + +
+
{JSON.stringify(useActionData(), null, 2)}
+ + ); + } + ` + } + }); + + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); + + it("handles files under upload size limit", async () => { + let uploadFile = path.join( + fixture.projectDir, + "toUpload", + "underLimit.txt" + ); + let uploadData = Array(1000000).fill("a").join(""); // 1MB + await fs + .mkdir(path.dirname(uploadFile), { recursive: true }) + .catch(() => {}); + await fs.writeFile(uploadFile, uploadData, "utf8"); + + await app.goto("/file-upload"); + await app.uploadFile("#file", uploadFile); + await app.clickSubmitButton("/file-upload"); + expect(await app.getHtml("pre")).toMatchInlineSnapshot(` + "
+      {
+        \\"name\\": \\"underLimit.txt\\",
+        \\"size\\": 1000000
+      }
" + `); + + let written = await fs.readFile( + path.join(fixture.projectDir, "uploads/underLimit.txt"), + "utf8" + ); + expect(written).toBe(uploadData); + }); + + it("rejects files over upload size limit", async () => { + let uploadFile = path.join(fixture.projectDir, "toUpload", "overLimit.txt"); + let uploadData = Array(3000001).fill("a").join(""); // 3.000001MB + await fs + .mkdir(path.dirname(uploadFile), { recursive: true }) + .catch(() => {}); + await fs.writeFile(uploadFile, uploadData, "utf8"); + + await app.goto("/file-upload"); + await app.uploadFile("#file", uploadFile); + await app.clickSubmitButton("/file-upload"); + expect(await app.getHtml("pre")).toMatchInlineSnapshot(` + "
+      {
+        \\"errorMessage\\": \\"Field \\\\\\"file\\\\\\" exceeded upload size of 3000000 bytes.\\"
+      }
" + `); + }); +}); diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index d96bcf4a65..ff8cace794 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -191,6 +191,19 @@ export async function createAppFixture(fixture: Fixture) { } }, + /** + * Find the input element and fill for file uploads. + * @param inputSelector The selector of the input you want to fill + * @param filePaths The paths to the files you want to upload + */ + uploadFile: async (inputSelector: string, ...filePaths: string[]) => { + let el = await page.$(inputSelector); + if (!el) { + throw new Error(`Could not find input for: ${inputSelector}`); + } + await el.uploadFile(...filePaths); + }, + /** * Finds the first submit button with `formAction` that matches the * `action` supplied, clicks it, and optionally waits for the network to diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index 973e1574fd..8e30917066 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -35,7 +35,7 @@ export async function callRouteAction({ let result; try { result = await action({ - request: stripDataParam(stripIndexParam(request.clone())), + request: stripDataParam(stripIndexParam(request)), context: loadContext, params: match.params }); From 5030728420a1c65a939affc84569ed933c820a00 Mon Sep 17 00:00:00 2001 From: Thomas Holland Date: Tue, 8 Feb 2022 21:35:40 +0100 Subject: [PATCH 0222/1690] chore: Add @see pointers to the docs (#1546) * docs(jsdoc): Add missing JSDocs, including @see pointers to the docs * Update contributors.yml Co-authored-by: Ryan Florence --- packages/remix-node/parseMultipartFormData.ts | 5 +++++ packages/remix-node/sessions/fileStorage.ts | 2 ++ packages/remix-server-runtime/cookies.ts | 11 ++++++++++- packages/remix-server-runtime/responses.ts | 7 ++++++- packages/remix-server-runtime/sessions.ts | 11 +++++++++++ .../remix-server-runtime/sessions/cookieStorage.ts | 2 ++ .../remix-server-runtime/sessions/memoryStorage.ts | 2 ++ 7 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/remix-node/parseMultipartFormData.ts b/packages/remix-node/parseMultipartFormData.ts index c73968688d..b8e5712c69 100644 --- a/packages/remix-node/parseMultipartFormData.ts +++ b/packages/remix-node/parseMultipartFormData.ts @@ -5,6 +5,11 @@ import type { Request as NodeRequest } from "./fetch"; import type { UploadHandler } from "./formData"; import { FormData as NodeFormData } from "./formData"; +/** + * Allows you to handle multipart forms (file uploads) for your app. + * + * @see https://remix.run/docs/en/v1/api/remix#parsemultipartformdata-node + */ export function parseMultipartFormData( request: Request, uploadHandler: UploadHandler diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts index 22dc047cce..09e6c1c1f7 100644 --- a/packages/remix-node/sessions/fileStorage.ts +++ b/packages/remix-node/sessions/fileStorage.ts @@ -25,6 +25,8 @@ interface FileSessionStorageOptions { * * The advantage of using this instead of cookie session storage is that * files may contain much more data than cookies. + * + * @see https://remix.run/docs/en/v1/api/remix#createfilesessionstorage-node */ export function createFileSessionStorage({ cookie, diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index a2e8c05406..18ab27520e 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -31,6 +31,8 @@ export type CookieOptions = CookieParseOptions & * and options. But it doesn't contain a value. Instead, it has `parse()` and * `serialize()` methods that allow a single instance to be reused for * parsing/encoding multiple different values. + * + * @see https://remix.run/docs/en/v1/api/remix#cookie-api */ export interface Cookie { /** @@ -68,7 +70,9 @@ export interface Cookie { } /** - * Creates and returns a new Cookie. + * Creates a logical container for managing a browser cookie from the server. + * + * @see https://remix.run/docs/en/v1/api/remix#createcookie */ export function createCookie( name: string, @@ -109,6 +113,11 @@ export function createCookie( }; } +/** + * Returns true if an object is a Remix cookie container. + * + * @see https://remix.run/docs/en/v1/api/remix#iscookie + */ export function isCookie(object: any): object is Cookie { return ( object != null && diff --git a/packages/remix-server-runtime/responses.ts b/packages/remix-server-runtime/responses.ts index 1285f404a9..ea1a8c6beb 100644 --- a/packages/remix-server-runtime/responses.ts +++ b/packages/remix-server-runtime/responses.ts @@ -1,5 +1,8 @@ /** - * A JSON response. Converts `data` to JSON and sets the `Content-Type` header. + * This is a shortcut for creating `application/json` responses. Converts `data` + * to JSON and sets the `Content-Type` header. + * + * @see https://remix.run/docs/en/v1/api/remix#json */ export function json( data: Data, @@ -24,6 +27,8 @@ export function json( /** * A redirect response. Sets the status code and the `Location` header. * Defaults to "302 Found". + * + * @see https://remix.run/docs/en/v1/api/remix#redirect */ export function redirect( url: string, diff --git a/packages/remix-server-runtime/sessions.ts b/packages/remix-server-runtime/sessions.ts index d5764cca12..ced656e5d4 100644 --- a/packages/remix-server-runtime/sessions.ts +++ b/packages/remix-server-runtime/sessions.ts @@ -13,6 +13,8 @@ export interface SessionData { /** * Session persists data across HTTP requests. + * + * @see https://remix.run/docs/en/v1/api/remix#session-api */ export interface Session { /** @@ -68,6 +70,8 @@ function flash(name: string): string { * * Note: This function is typically not invoked directly by application code. * Instead, use a `SessionStorage` object's `getSession` method. + * + * @see https://remix.run/docs/en/v1/api/remix#createsession */ export function createSession(initialData: SessionData = {}, id = ""): Session { let map = new Map(Object.entries(initialData)); @@ -106,6 +110,11 @@ export function createSession(initialData: SessionData = {}, id = ""): Session { }; } +/** + * Returns true if an object is a Remix session. + * + * @see https://remix.run/docs/en/v1/api/remix#issession + */ export function isSession(object: any): object is Session { return ( object != null && @@ -198,6 +207,8 @@ export interface SessionIdStorageStrategy { * * Note: This is a low-level API that should only be used if none of the * existing session storage options meet your requirements. + * + * @see https://remix.run/docs/en/v1/api/remix#createsessionstorage */ export function createSessionStorage({ cookie: cookieArg, diff --git a/packages/remix-server-runtime/sessions/cookieStorage.ts b/packages/remix-server-runtime/sessions/cookieStorage.ts index 45e62a0acb..5f243724d9 100644 --- a/packages/remix-server-runtime/sessions/cookieStorage.ts +++ b/packages/remix-server-runtime/sessions/cookieStorage.ts @@ -18,6 +18,8 @@ interface CookieSessionStorageOptions { * needed, and can help to simplify some load-balanced scenarios. However, it * also has the limitation that serialized session data may not exceed the * browser's maximum cookie size. Trade-offs! + * + * @see https://remix.run/docs/en/v1/api/remix#createcookiesessionstorage */ export function createCookieSessionStorage({ cookie: cookieArg diff --git a/packages/remix-server-runtime/sessions/memoryStorage.ts b/packages/remix-server-runtime/sessions/memoryStorage.ts index 2233e1d7a3..a0c29424c4 100644 --- a/packages/remix-server-runtime/sessions/memoryStorage.ts +++ b/packages/remix-server-runtime/sessions/memoryStorage.ts @@ -19,6 +19,8 @@ interface MemorySessionStorageOptions { * * Note: This storage does not scale beyond a single process, so it is not * suitable for most production scenarios. + * + * @see https://remix.run/docs/en/v1/api/remix#creatememorysessionstorage */ export function createMemorySessionStorage({ cookie From 0517d75e8bed99de5242c536a4152820c8e8bb1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Tue, 8 Feb 2022 23:35:40 +0100 Subject: [PATCH 0223/1690] chore: update docs links (`main` branch) (#1844) --- packages/remix-server-runtime/sessions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-server-runtime/sessions.ts b/packages/remix-server-runtime/sessions.ts index d5764cca12..5ca99a4e82 100644 --- a/packages/remix-server-runtime/sessions.ts +++ b/packages/remix-server-runtime/sessions.ts @@ -244,7 +244,7 @@ export function warnOnceAboutSigningSessionCookie(cookie: Cookie) { cookie.isSigned, `The "${cookie.name}" cookie is not signed, but session cookies should be ` + `signed to prevent tampering on the client before they are sent back to the ` + - `server. See https://remix.run/docs/en/v1/api/remix#signing-cookies ` + + `server. See https://remix.run/api/remix#signing-cookies ` + `for more information.` ); } From 48b91501de2f4c220e5b1f89ed2dd960f20f84ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20De=20Boey?= Date: Tue, 8 Feb 2022 23:35:56 +0100 Subject: [PATCH 0224/1690] chore: update docs links (`dev` branch) (#1843) --- packages/remix-node/parseMultipartFormData.ts | 2 +- packages/remix-node/sessions/fileStorage.ts | 2 +- packages/remix-server-runtime/cookies.ts | 6 +++--- packages/remix-server-runtime/responses.ts | 4 ++-- packages/remix-server-runtime/sessions.ts | 10 +++++----- .../remix-server-runtime/sessions/cookieStorage.ts | 2 +- .../remix-server-runtime/sessions/memoryStorage.ts | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/remix-node/parseMultipartFormData.ts b/packages/remix-node/parseMultipartFormData.ts index b8e5712c69..d9f43c1e46 100644 --- a/packages/remix-node/parseMultipartFormData.ts +++ b/packages/remix-node/parseMultipartFormData.ts @@ -8,7 +8,7 @@ import { FormData as NodeFormData } from "./formData"; /** * Allows you to handle multipart forms (file uploads) for your app. * - * @see https://remix.run/docs/en/v1/api/remix#parsemultipartformdata-node + * @see https://remix.run/api/remix#parsemultipartformdata-node */ export function parseMultipartFormData( request: Request, diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts index 09e6c1c1f7..a58ad41430 100644 --- a/packages/remix-node/sessions/fileStorage.ts +++ b/packages/remix-node/sessions/fileStorage.ts @@ -26,7 +26,7 @@ interface FileSessionStorageOptions { * The advantage of using this instead of cookie session storage is that * files may contain much more data than cookies. * - * @see https://remix.run/docs/en/v1/api/remix#createfilesessionstorage-node + * @see https://remix.run/api/remix#createfilesessionstorage-node */ export function createFileSessionStorage({ cookie, diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index 18ab27520e..307280a896 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -32,7 +32,7 @@ export type CookieOptions = CookieParseOptions & * `serialize()` methods that allow a single instance to be reused for * parsing/encoding multiple different values. * - * @see https://remix.run/docs/en/v1/api/remix#cookie-api + * @see https://remix.run/api/remix#cookie-api */ export interface Cookie { /** @@ -72,7 +72,7 @@ export interface Cookie { /** * Creates a logical container for managing a browser cookie from the server. * - * @see https://remix.run/docs/en/v1/api/remix#createcookie + * @see https://remix.run/api/remix#createcookie */ export function createCookie( name: string, @@ -116,7 +116,7 @@ export function createCookie( /** * Returns true if an object is a Remix cookie container. * - * @see https://remix.run/docs/en/v1/api/remix#iscookie + * @see https://remix.run/api/remix#iscookie */ export function isCookie(object: any): object is Cookie { return ( diff --git a/packages/remix-server-runtime/responses.ts b/packages/remix-server-runtime/responses.ts index ea1a8c6beb..aea4dcfcd3 100644 --- a/packages/remix-server-runtime/responses.ts +++ b/packages/remix-server-runtime/responses.ts @@ -2,7 +2,7 @@ * This is a shortcut for creating `application/json` responses. Converts `data` * to JSON and sets the `Content-Type` header. * - * @see https://remix.run/docs/en/v1/api/remix#json + * @see https://remix.run/api/remix#json */ export function json( data: Data, @@ -28,7 +28,7 @@ export function json( * A redirect response. Sets the status code and the `Location` header. * Defaults to "302 Found". * - * @see https://remix.run/docs/en/v1/api/remix#redirect + * @see https://remix.run/api/remix#redirect */ export function redirect( url: string, diff --git a/packages/remix-server-runtime/sessions.ts b/packages/remix-server-runtime/sessions.ts index ced656e5d4..bf09562fdc 100644 --- a/packages/remix-server-runtime/sessions.ts +++ b/packages/remix-server-runtime/sessions.ts @@ -14,7 +14,7 @@ export interface SessionData { /** * Session persists data across HTTP requests. * - * @see https://remix.run/docs/en/v1/api/remix#session-api + * @see https://remix.run/api/remix#session-api */ export interface Session { /** @@ -71,7 +71,7 @@ function flash(name: string): string { * Note: This function is typically not invoked directly by application code. * Instead, use a `SessionStorage` object's `getSession` method. * - * @see https://remix.run/docs/en/v1/api/remix#createsession + * @see https://remix.run/api/remix#createsession */ export function createSession(initialData: SessionData = {}, id = ""): Session { let map = new Map(Object.entries(initialData)); @@ -113,7 +113,7 @@ export function createSession(initialData: SessionData = {}, id = ""): Session { /** * Returns true if an object is a Remix session. * - * @see https://remix.run/docs/en/v1/api/remix#issession + * @see https://remix.run/api/remix#issession */ export function isSession(object: any): object is Session { return ( @@ -208,7 +208,7 @@ export interface SessionIdStorageStrategy { * Note: This is a low-level API that should only be used if none of the * existing session storage options meet your requirements. * - * @see https://remix.run/docs/en/v1/api/remix#createsessionstorage + * @see https://remix.run/api/remix#createsessionstorage */ export function createSessionStorage({ cookie: cookieArg, @@ -255,7 +255,7 @@ export function warnOnceAboutSigningSessionCookie(cookie: Cookie) { cookie.isSigned, `The "${cookie.name}" cookie is not signed, but session cookies should be ` + `signed to prevent tampering on the client before they are sent back to the ` + - `server. See https://remix.run/docs/en/v1/api/remix#signing-cookies ` + + `server. See https://remix.run/api/remix#signing-cookies ` + `for more information.` ); } diff --git a/packages/remix-server-runtime/sessions/cookieStorage.ts b/packages/remix-server-runtime/sessions/cookieStorage.ts index 5f243724d9..624cc5751f 100644 --- a/packages/remix-server-runtime/sessions/cookieStorage.ts +++ b/packages/remix-server-runtime/sessions/cookieStorage.ts @@ -19,7 +19,7 @@ interface CookieSessionStorageOptions { * also has the limitation that serialized session data may not exceed the * browser's maximum cookie size. Trade-offs! * - * @see https://remix.run/docs/en/v1/api/remix#createcookiesessionstorage + * @see https://remix.run/api/remix#createcookiesessionstorage */ export function createCookieSessionStorage({ cookie: cookieArg diff --git a/packages/remix-server-runtime/sessions/memoryStorage.ts b/packages/remix-server-runtime/sessions/memoryStorage.ts index a0c29424c4..bd1696cd5e 100644 --- a/packages/remix-server-runtime/sessions/memoryStorage.ts +++ b/packages/remix-server-runtime/sessions/memoryStorage.ts @@ -20,7 +20,7 @@ interface MemorySessionStorageOptions { * Note: This storage does not scale beyond a single process, so it is not * suitable for most production scenarios. * - * @see https://remix.run/docs/en/v1/api/remix#creatememorysessionstorage + * @see https://remix.run/api/remix#creatememorysessionstorage */ export function createMemorySessionStorage({ cookie From e566d6266da034d60fdbf9ada7193b72aa3c36f2 Mon Sep 17 00:00:00 2001 From: Steven Yung Date: Thu, 10 Feb 2022 07:10:37 +0100 Subject: [PATCH 0225/1690] fix: headers function access loaderHeaders when parent has no loader (#1847) We were keying loader data by index which assumes every route in the tree had a loader, but they might not! Co-authored-by: Ryan Florence --- integration/headers-test.ts | 3 +-- packages/remix-server-runtime/headers.ts | 7 ++++--- packages/remix-server-runtime/server.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/integration/headers-test.ts b/integration/headers-test.ts index 061ab6cf4f..08e352c73b 100644 --- a/integration/headers-test.ts +++ b/integration/headers-test.ts @@ -79,8 +79,7 @@ describe("headers export", () => { expect(response.headers.get(ROOT_HEADER_KEY)).toBe(ROOT_HEADER_VALUE); }); - // FIXME: this test is busted - it.skip("can use the loader headers when parents don't have loaders", async () => { + it("can use the loader headers when parents don't have loaders", async () => { const HEADER_KEY = "X-Test"; const HEADER_VALUE = "SUCCESS"; diff --git a/packages/remix-server-runtime/headers.ts b/packages/remix-server-runtime/headers.ts index 05730b96fa..70887eafda 100644 --- a/packages/remix-server-runtime/headers.ts +++ b/packages/remix-server-runtime/headers.ts @@ -7,13 +7,14 @@ import type { RouteMatch } from "./routeMatching"; export function getDocumentHeaders( build: ServerBuild, matches: RouteMatch[], - routeLoaderResponses: Response[], + routeLoaderResponses: Record, actionResponse?: Response ): Headers { return matches.reduce((parentHeaders, match, index) => { let routeModule = build.routes[match.route.id].module; - let loaderHeaders = routeLoaderResponses[index] - ? routeLoaderResponses[index].headers + let routeLoaderResponse = routeLoaderResponses[match.route.id]; + let loaderHeaders = routeLoaderResponse + ? routeLoaderResponse.headers : new Headers(); let actionHeaders = actionResponse ? actionResponse.headers : new Headers(); let headers = new Headers( diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index 087ca3ea35..456264d6db 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -324,7 +324,7 @@ async function renderDocumentRequest({ appState.error = undefined; let headerMatches: RouteMatch[] = []; - let routeLoaderResponses: Response[] = []; + let routeLoaderResponses: Record = {}; let loaderStatusCodes: number[] = []; let routeData: Record = {}; for (let index = 0; index < matchesToLoad.length; index++) { @@ -372,7 +372,7 @@ async function renderDocumentRequest({ break; } else if (response) { headerMatches.push(match); - routeLoaderResponses.push(response); + routeLoaderResponses[match.route.id] = response; loaderStatusCodes.push(response.status); if (isCatch) { From 285809f14a0ebe3766bee24c3b8db8494522e1a8 Mon Sep 17 00:00:00 2001 From: Roman Mkrtchian Date: Thu, 10 Feb 2022 15:12:10 +0100 Subject: [PATCH 0226/1690] fix(remix-node): do not change empty string to null when using get method on FormData (#1869) * fix(remix-node): do not change empty string to null when using get method on FormData * add name to contributors --- packages/remix-node/__tests__/formData-test.ts | 6 ++++++ packages/remix-node/formData.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/remix-node/__tests__/formData-test.ts b/packages/remix-node/__tests__/formData-test.ts index 6a4df1837a..b0cc11fb3f 100644 --- a/packages/remix-node/__tests__/formData-test.ts +++ b/packages/remix-node/__tests__/formData-test.ts @@ -18,6 +18,12 @@ describe("FormData", () => { ]); }); + it("restores correctly empty string values with get method", () => { + let formData = new NodeFormData(); + formData.set("single", ""); + expect(formData.get("single")).toBe(""); + }); + it("allows for mix of set and append with blobs and files", () => { let formData = new NodeFormData(); formData.set("single", new Blob([])); diff --git a/packages/remix-node/formData.ts b/packages/remix-node/formData.ts index 116a69491b..13cee0c213 100644 --- a/packages/remix-node/formData.ts +++ b/packages/remix-node/formData.ts @@ -58,7 +58,7 @@ class NodeFormData implements FormData { get(name: string): FormDataEntryValue | null { let arr = this._fields[name]; - return arr?.slice(-1)[0] || null; + return arr?.slice(-1)[0] ?? null; } getAll(name: string): FormDataEntryValue[] { From 2db0f6842cbee0a048541c5b329e2e6a7a15368f Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 4 Feb 2022 14:39:52 -0800 Subject: [PATCH 0227/1690] Clean up and small tweaks on virtual module plugins --- packages/remix-dev/compiler.ts | 10 ++++---- ...lugin.ts => serverAssetsManifestPlugin.ts} | 24 ++++++++++-------- ...esPlugin.ts => serverEntryModulePlugin.ts} | 25 +++++++++---------- 3 files changed, 31 insertions(+), 28 deletions(-) rename packages/remix-dev/compiler/plugins/{serverAssetsPlugin.ts => serverAssetsManifestPlugin.ts} (51%) rename packages/remix-dev/compiler/plugins/{serverEntryModulesPlugin.ts => serverEntryModulePlugin.ts} (73%) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 144280a785..bb1f50679b 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -17,10 +17,10 @@ import { loaders } from "./compiler/loaders"; import { browserRouteModulesPlugin } from "./compiler/plugins/browserRouteModulesPlugin"; import { emptyModulesPlugin } from "./compiler/plugins/emptyModulesPlugin"; import { mdxPlugin } from "./compiler/plugins/mdx"; -import { serverAssetsPlugin } from "./compiler/plugins/serverAssetsPlugin"; -import type { AssetsManifestPromiseRef } from "./compiler/plugins/serverAssetsPlugin"; +import type { AssetsManifestPromiseRef } from "./compiler/plugins/serverAssetsManifestPlugin"; +import { serverAssetsManifestPlugin } from "./compiler/plugins/serverAssetsManifestPlugin"; import { serverBareModulesPlugin } from "./compiler/plugins/serverBareModulesPlugin"; -import { serverEntryModulesPlugin } from "./compiler/plugins/serverEntryModulesPlugin"; +import { serverEntryModulePlugin } from "./compiler/plugins/serverEntryModulePlugin"; import { serverRouteModulesPlugin } from "./compiler/plugins/serverRouteModulesPlugin"; import { writeFileSafe } from "./compiler/utils/fs"; @@ -401,8 +401,8 @@ async function createServerBuild( mdxPlugin(config), emptyModulesPlugin(config, /\.client\.[tj]sx?$/), serverRouteModulesPlugin(config), - serverEntryModulesPlugin(config), - serverAssetsPlugin(assetsManifestPromiseRef), + serverEntryModulePlugin(config), + serverAssetsManifestPlugin(assetsManifestPromiseRef), serverBareModulesPlugin(config, dependencies) ]; diff --git a/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts b/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts similarity index 51% rename from packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts rename to packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts index 6f1e184312..9de59736d9 100644 --- a/packages/remix-dev/compiler/plugins/serverAssetsPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts @@ -7,28 +7,32 @@ import { assetsManifestVirtualModule } from "../virtualModules"; export type AssetsManifestPromiseRef = { current?: Promise }; /** - * Creates a virtual module of the asset manifest for consumption. - * See {@link serverEntryModulesPlugin} for consumption. + * Creates a virtual module called `@remix-run/dev/assets-manifest` that exports + * the assets manifest. This is used in the server entry module to access the + * assets manifest in the server build. */ -export function serverAssetsPlugin( - browserManifestPromiseRef: AssetsManifestPromiseRef, - filter: RegExp = assetsManifestVirtualModule.filter +export function serverAssetsManifestPlugin( + assetsManifestPromiseRef: AssetsManifestPromiseRef ): Plugin { + let filter = assetsManifestVirtualModule.filter; + return { - name: "server-assets", + name: "server-assets-manifest", setup(build) { build.onResolve({ filter }, ({ path }) => { return { path, - namespace: "assets" + namespace: "server-assets-manifest" }; }); + build.onLoad({ filter }, async () => { invariant( - browserManifestPromiseRef.current, - "Missing browser manifest assets ref in server build." + assetsManifestPromiseRef.current, + "Missing assets manifests in server build." ); - let manifest = await browserManifestPromiseRef.current; + + let manifest = await assetsManifestPromiseRef.current; return { contents: `export default ${jsesc(manifest, { es6: true })};`, diff --git a/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverEntryModulePlugin.ts similarity index 73% rename from packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts rename to packages/remix-dev/compiler/plugins/serverEntryModulePlugin.ts index e62474ab26..8820dadcb7 100644 --- a/packages/remix-dev/compiler/plugins/serverEntryModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverEntryModulePlugin.ts @@ -13,33 +13,32 @@ import { * for you to consume the build in a custom server entry that is also fed through * the compiler. */ -export function serverEntryModulesPlugin( - remixConfig: RemixConfig, - filter: RegExp = serverBuildVirtualModule.filter -): Plugin { +export function serverEntryModulePlugin(config: RemixConfig): Plugin { + let filter = serverBuildVirtualModule.filter; + return { - name: "server-entry", + name: "server-entry-module", setup(build) { build.onResolve({ filter }, ({ path }) => { return { path, - namespace: "server-entry" + namespace: "server-entry-module" }; }); build.onLoad({ filter }, async () => { return { - resolveDir: remixConfig.appDirectory, + resolveDir: config.appDirectory, loader: "js", contents: ` import * as entryServer from ${JSON.stringify( - path.resolve(remixConfig.appDirectory, remixConfig.entryServerFile) + path.resolve(config.appDirectory, config.entryServerFile) )}; -${Object.keys(remixConfig.routes) +${Object.keys(config.routes) .map((key, index) => { - let route = remixConfig.routes[key]; + let route = config.routes[key]; return `import * as route${index} from ${JSON.stringify( - path.resolve(remixConfig.appDirectory, route.file) + path.resolve(config.appDirectory, route.file) )};`; }) .join("\n")} @@ -48,9 +47,9 @@ ${Object.keys(remixConfig.routes) )}; export const entry = { module: entryServer }; export const routes = { - ${Object.keys(remixConfig.routes) + ${Object.keys(config.routes) .map((key, index) => { - let route = remixConfig.routes[key]; + let route = config.routes[key]; return `${JSON.stringify(key)}: { id: ${JSON.stringify(route.id)}, parentId: ${JSON.stringify(route.parentId)}, From b4d2cde19e40ffae9f611976ac1881497df84a3f Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Thu, 10 Feb 2022 22:52:15 +0100 Subject: [PATCH 0228/1690] fix: button data is missing if clicking on the svg element within the button (#1240) Co-authored-by: Ryan Florence --- integration/form-test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/integration/form-test.ts b/integration/form-test.ts index db1eddbc99..3b08fbfde3 100644 --- a/integration/form-test.ts +++ b/integration/form-test.ts @@ -31,7 +31,8 @@ describe("Forms", () => { <>
- +
@@ -41,7 +42,11 @@ describe("Forms", () => { type="submit" name="${LUNCH}" value="${LAKSA}" - >Go + > + + + +
+ +
+ ); + } ` } }); @@ -173,4 +201,12 @@ describe("rendering", () => { expect(html).toMatch(PAGE_TEXT); expect(html).toMatch(PAGE_INDEX_TEXT); }); + + it("useFetcher state should return to the idle when redirect from an action", async () => { + await app.goto("/gh-1691"); + expect(await app.getHtml("span")).toMatch("idle"); + + await app.clickSubmitButton("/gh-1691"); + expect(await app.getHtml("span")).toMatch("idle"); + }); }); From 98397cb0864c1a83dfb261a6c3f9af7ea7e2ffaf Mon Sep 17 00:00:00 2001 From: selfish <7327741+selfish@users.noreply.github.com> Date: Fri, 11 Feb 2022 00:30:19 +0200 Subject: [PATCH 0230/1690] feat: implement debug flag to attach debugger (#337, #795) (#1789) Apps using `remix dev` can now attach the Node.js inspector with the `--debug` flag ```sh remix --debug dev ``` Co-authored-by: Ryan Florence --- packages/remix-dev/__tests__/cli-test.ts | 8 +++++++- packages/remix-dev/cli.ts | 13 ++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts index a825e4e9de..b914a6a84b 100644 --- a/packages/remix-dev/__tests__/cli-test.ts +++ b/packages/remix-dev/__tests__/cli-test.ts @@ -34,16 +34,22 @@ describe("remix cli", () => { --version, -v Print the CLI version and exit --json Print the routes as JSON (remix routes only) - --sourcemap Generate source maps (remix build only) + --sourcemap Generate source maps for production (remix build only) + --debug Attach Node.js inspector (remix dev only) Values [remixPlatform] Can be one of: node, cloudflare-pages, cloudflare-workers, or deno Examples + $ remix build + $ remix build --sourcemap $ remix build my-website + $ remix dev + $ remix dev --debug $ remix dev my-website $ remix setup node $ remix routes my-website + $ remix routes my-website --json " `); diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index 6913320ea2..b4bf10f3a7 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -1,4 +1,5 @@ import meow from "meow"; +import inspector from "inspector"; import * as commands from "./cli/commands"; @@ -14,16 +15,22 @@ Options --version, -v Print the CLI version and exit --json Print the routes as JSON (remix routes only) - --sourcemap Generate source maps (remix build only) + --sourcemap Generate source maps for production (remix build only) + --debug Attach Node.js inspector (remix dev only) Values [remixPlatform] Can be one of: node, cloudflare-pages, cloudflare-workers, or deno Examples + $ remix build + $ remix build --sourcemap $ remix build my-website + $ remix dev + $ remix dev --debug $ remix dev my-website $ remix setup node $ remix routes my-website + $ remix routes my-website --json `; const cli = meow(helpText, { @@ -40,6 +47,9 @@ const cli = meow(helpText, { }, sourcemap: { type: "boolean" + }, + debug: { + type: "boolean" } } }); @@ -74,6 +84,7 @@ switch (cli.input[0]) { break; case "dev": if (!process.env.NODE_ENV) process.env.NODE_ENV = "development"; + if (cli.flags.debug) inspector.open(); commands.dev(cli.input[1], process.env.NODE_ENV).catch(handleError); break; default: From f9f640ca8c7822a01bd4c774511799caea75e1ac Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 11 Feb 2022 12:33:54 +0700 Subject: [PATCH 0231/1690] fix: add status text to express & vercel packages (#1234) cloudflare handles this for us via native use of fetch Response, note there doesn't seem to be a way to send status text in the other platforms. --- packages/remix-express/server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index e1022a1cfb..c65bbdbb38 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -120,6 +120,7 @@ function sendRemixResponse( response: NodeResponse, abortController: AbortController ): void { + res.statusMessage = response.statusText; res.status(response.status); for (let [key, values] of Object.entries(response.headers.raw())) { From 1b326ab230e8f5bf9c259bab979585cdfdba497c Mon Sep 17 00:00:00 2001 From: Can Rau Date: Fri, 11 Feb 2022 00:51:15 -0500 Subject: [PATCH 0232/1690] feat: add `.ico` to list of asset imports (#1203) can now import `.ico` files like other assets to use for favicons. --- packages/remix-dev/compiler/loaders.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/remix-dev/compiler/loaders.ts b/packages/remix-dev/compiler/loaders.ts index c137879f4a..f0d304f0be 100644 --- a/packages/remix-dev/compiler/loaders.ts +++ b/packages/remix-dev/compiler/loaders.ts @@ -7,6 +7,7 @@ export const loaders: { [ext: string]: esbuild.Loader } = { ".eot": "file", ".flac": "file", ".gif": "file", + ".ico": "file", ".jpeg": "file", ".jpg": "file", ".js": "jsx", From c326b51bed9514a88109910b798f1cba5f31e78c Mon Sep 17 00:00:00 2001 From: Alexandru Bereghici Date: Fri, 11 Feb 2022 07:56:06 +0200 Subject: [PATCH 0233/1690] feat: default cookies paths to "/" (#1181) lots of folks set cookies at `/login` or `/register` and wonder why their root loaders don't have cookies! This is a less surprising default than the current URL. closes #1162 --- .../remix-server-runtime/__tests__/cookies-test.ts | 14 ++++++++++++++ .../__tests__/sessions-test.ts | 10 ++++++++++ packages/remix-server-runtime/cookies.ts | 8 +++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/remix-server-runtime/__tests__/cookies-test.ts b/packages/remix-server-runtime/__tests__/cookies-test.ts index db47a95392..5e1411c740 100644 --- a/packages/remix-server-runtime/__tests__/cookies-test.ts +++ b/packages/remix-server-runtime/__tests__/cookies-test.ts @@ -118,4 +118,18 @@ describe("cookies", () => { let setCookie2 = await cookie.serialize(value); expect(setCookie).not.toEqual(setCookie2); }); + + it("makes the default path of cookies to be /", async () => { + let cookie = createCookie("my-cookie"); + + let setCookie = await cookie.serialize("hello world"); + expect(setCookie).toContain("Path=/"); + + let cookie2 = createCookie("my-cookie2"); + + let setCookie2 = await cookie2.serialize("hello world", { + path: "/about" + }); + expect(setCookie2).toContain("Path=/about"); + }); }); diff --git a/packages/remix-server-runtime/__tests__/sessions-test.ts b/packages/remix-server-runtime/__tests__/sessions-test.ts index 4dd7abf163..87d40b70d9 100644 --- a/packages/remix-server-runtime/__tests__/sessions-test.ts +++ b/packages/remix-server-runtime/__tests__/sessions-test.ts @@ -94,6 +94,16 @@ describe("Cookie session storage", () => { expect(session.get("user")).toBeUndefined(); }); + it('"makes the default path of cookies to be /', async () => { + let { getSession, commitSession } = createCookieSessionStorage({ + cookie: { secrets: ["secret1"] } + }); + let session = await getSession(); + session.set("user", "mjackson"); + let setCookie = await commitSession(session); + expect(setCookie).toContain("Path=/"); + }); + describe("when a new secret shows up in the rotation", () => { it("unsigns old session cookies using the old secret and encodes new cookies using the new secret", async () => { let { getSession, commitSession } = createCookieSessionStorage({ diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index 307280a896..c1b0d13b91 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -76,8 +76,14 @@ export interface Cookie { */ export function createCookie( name: string, - { secrets = [], ...options }: CookieOptions = {} + cookieOptions: CookieOptions = {} ): Cookie { + let { secrets, ...options } = { + secrets: [], + path: "/", + ...cookieOptions + }; + return { get name() { return name; From 19a19b5d341beae44c5d29068612d16b916d3c42 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Sat, 12 Feb 2022 10:20:11 +0700 Subject: [PATCH 0234/1690] feat: expose upload handler types (#1913) This is building on #1133 but adds the magic portion --- packages/remix-node/index.ts | 1 + packages/remix-node/magicExports/platform.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index e6df2457b1..4cfb190a73 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -11,6 +11,7 @@ export type { export { Headers, Request, Response, fetch } from "./fetch"; export { FormData } from "./formData"; +export type { UploadHandler, UploadHandlerArgs } from "./formData"; export { installGlobals } from "./globals"; diff --git a/packages/remix-node/magicExports/platform.ts b/packages/remix-node/magicExports/platform.ts index 719c256e48..dedf86de68 100644 --- a/packages/remix-node/magicExports/platform.ts +++ b/packages/remix-node/magicExports/platform.ts @@ -7,3 +7,5 @@ export { unstable_createMemoryUploadHandler, unstable_parseMultipartFormData } from "@remix-run/node"; + +export type { UploadHandler, UploadHandlerArgs } from "@remix-run/node"; From a0435e109da49be474df2bd06e56b4d225a65aee Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Mon, 14 Feb 2022 16:19:00 +0700 Subject: [PATCH 0235/1690] feat: add serverDependenciesToBundle to remix config (#1839) --- integration/compiler-test.ts | 32 ++++++++++++++++++- .../remix-dev/__tests__/readConfig-test.ts | 1 + .../plugins/serverBareModulesPlugin.ts | 13 +++++++- packages/remix-dev/config.ts | 17 ++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/integration/compiler-test.ts b/integration/compiler-test.ts index a278d4ad86..3b0bdfe4fd 100644 --- a/integration/compiler-test.ts +++ b/integration/compiler-test.ts @@ -36,8 +36,29 @@ describe("compiler", () => { import * as path from "path"; export default function BuiltIns() { - return
{path.join("test", "file.txt")}
+ return
{path.join("test", "file.txt")}
; } + `, + "app/routes/esm-only-pkg.jsx": js` + import esmOnlyPkg from "esm-only-pkg"; + + export default function EsmOnlyPkg() { + return
{esmOnlyPkg}
; + } + `, + "remix.config.js": js` + module.exports = { + serverDependenciesToBundle: ["esm-only-pkg"], + }; + `, + "node_modules/esm-only-pkg/package.json": `{ + "name": "esm-only-pkg", + "version": "1.0.0", + "type": "module", + "main": "./esm-only-pkg.js" + }`, + "node_modules/esm-only-pkg/esm-only-pkg.js": js` + export default "esm-only-pkg"; ` } }); @@ -90,4 +111,13 @@ describe("compiler", () => { // does not include `import bla from "path"` in the output bundle expect(routeModule).not.toMatch(/from\s*"path/); }); + + it("allows consumption of ESM modules in CJS builds with `serverDependenciesToBundle`", async () => { + let res = await app.goto("/esm-only-pkg", true); + expect(res.status()).toBe(200); // server rendered fine + // rendered the page instead of the error boundary + expect(await app.getHtml("#esm-only-pkg")).toMatchInlineSnapshot( + `"
esm-only-pkg
"` + ); + }); }); diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index 9a23f798e7..bf2c7092a0 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -410,6 +410,7 @@ describe("readConfig", () => { "serverBuildPath": Any, "serverBuildTarget": undefined, "serverBuildTargetEntryModule": "export * from \\"@remix-run/dev/server-build\\";", + "serverDependenciesToBundle": Array [], "serverEntryPoint": "./server.js", "serverMode": "production", "serverModuleFormat": "cjs", diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts index c13b34077c..feada86028 100644 --- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -33,7 +33,7 @@ export function serverBareModulesPlugin( return undefined; } - // These are our virutal modules, always bundle the because there is no + // These are our virutal modules, always bundle them because there is no // "real" file on disk to externalize. if ( path === serverBuildVirtualModule.id || @@ -82,6 +82,17 @@ export function serverBareModulesPlugin( return undefined; } + for (let pattern of remixConfig.serverDependenciesToBundle) { + // bundle it if the path matches the pattern + if ( + typeof pattern === "string" + ? path === pattern + : pattern.test(path) + ) { + return undefined; + } + } + // Externalize everything else if we've gotten here. return { path, diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 1f52e4da55..a23ecc810a 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -128,6 +128,13 @@ export interface AppConfig { * routes. */ ignoredRouteFiles?: 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; } /** @@ -224,6 +231,13 @@ export interface RemixConfig { * A server entrypoint relative to the root directory that becomes your server's main module. */ serverEntryPoint?: 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; } /** @@ -367,6 +381,8 @@ export async function readConfig( serverBuildVirtualModule.id )};`; + let serverDependenciesToBundle = appConfig.serverDependenciesToBundle || []; + return { appDirectory, cacheDirectory, @@ -385,6 +401,7 @@ export async function readConfig( serverBuildTarget, serverBuildTargetEntryModule, serverEntryPoint: customServerEntryPoint, + serverDependenciesToBundle, mdx }; } From d6db2c870a031d231c38c606ef93a42b65564ce4 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Mon, 14 Feb 2022 09:20:58 +0000 Subject: [PATCH 0236/1690] chore: format formatted a0435e109da49be474df2bd06e56b4d225a65aee --- .../remix-dev/compiler/plugins/serverBareModulesPlugin.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts index feada86028..8c639eca51 100644 --- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -85,9 +85,7 @@ export function serverBareModulesPlugin( for (let pattern of remixConfig.serverDependenciesToBundle) { // bundle it if the path matches the pattern if ( - typeof pattern === "string" - ? path === pattern - : pattern.test(path) + typeof pattern === "string" ? path === pattern : pattern.test(path) ) { return undefined; } From 9870cb1e789dfe58b423ddf752c89d8319b7425f Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 14 Feb 2022 18:30:17 -0800 Subject: [PATCH 0237/1690] Version 1.2.0-pre.0 --- packages/remix-dev/package.json | 4 ++-- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 0bf70d7e37..35339c7d54 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.1.3", + "version": "1.2.0-pre.0", "license": "MIT", "repository": { "type": "git", @@ -16,7 +16,7 @@ }, "dependencies": { "@esbuild-plugins/node-modules-polyfill": "^0.1.4", - "@remix-run/server-runtime": "1.1.3", + "@remix-run/server-runtime": "1.2.0-pre.0", "cacache": "^15.0.5", "chokidar": "^3.5.1", "esbuild": "0.14.16", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 1634451536..1aa687c7c7 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.1.3", + "version": "1.2.0-pre.0", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.1.3", - "@remix-run/server-runtime": "1.1.3" + "@remix-run/node": "1.2.0-pre.0", + "@remix-run/server-runtime": "1.2.0-pre.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 26be5f4588..81f31271e1 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.1.3", + "version": "1.2.0-pre.0", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.1.3", + "@remix-run/server-runtime": "1.2.0-pre.0", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index f94eb5d84a..e05541c31c 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.1.3", + "version": "1.2.0-pre.0", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.1.3", + "@remix-run/express": "1.2.0-pre.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index f48ccb0401..f90042d42f 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.1.3", + "version": "1.2.0-pre.0", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From 0c37cfc1ca736342ed8349d0d808deedfba9af29 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Wed, 16 Feb 2022 04:59:34 +0700 Subject: [PATCH 0238/1690] chore: revert esbuild version (#1975) --- packages/remix-dev/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 35339c7d54..6b818f9bb6 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -19,7 +19,7 @@ "@remix-run/server-runtime": "1.2.0-pre.0", "cacache": "^15.0.5", "chokidar": "^3.5.1", - "esbuild": "0.14.16", + "esbuild": "0.13.14", "exit-hook": "2.2.1", "fs-extra": "^10.0.0", "get-port": "^5.1.1", From 9f8b549af106d7fd474707f570c4375249f5edea Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 15 Feb 2022 14:41:48 -0800 Subject: [PATCH 0239/1690] Version 1.2.0-pre.1 --- packages/remix-dev/package.json | 4 ++-- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 6b818f9bb6..0b618e0171 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.2.0-pre.0", + "version": "1.2.0-pre.1", "license": "MIT", "repository": { "type": "git", @@ -16,7 +16,7 @@ }, "dependencies": { "@esbuild-plugins/node-modules-polyfill": "^0.1.4", - "@remix-run/server-runtime": "1.2.0-pre.0", + "@remix-run/server-runtime": "1.2.0-pre.1", "cacache": "^15.0.5", "chokidar": "^3.5.1", "esbuild": "0.13.14", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 1aa687c7c7..c5c5b5a969 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.2.0-pre.0", + "version": "1.2.0-pre.1", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.2.0-pre.0", - "@remix-run/server-runtime": "1.2.0-pre.0" + "@remix-run/node": "1.2.0-pre.1", + "@remix-run/server-runtime": "1.2.0-pre.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 81f31271e1..258bc5e9f4 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.2.0-pre.0", + "version": "1.2.0-pre.1", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.2.0-pre.0", + "@remix-run/server-runtime": "1.2.0-pre.1", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index e05541c31c..48f20fb414 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.2.0-pre.0", + "version": "1.2.0-pre.1", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.2.0-pre.0", + "@remix-run/express": "1.2.0-pre.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index f90042d42f..1166fca9ca 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.2.0-pre.0", + "version": "1.2.0-pre.1", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From 3544bdb51c85988a6e5293a02b82009d5c587ac5 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Wed, 16 Feb 2022 11:15:42 -0800 Subject: [PATCH 0240/1690] Version 1.2.0 --- packages/remix-dev/package.json | 4 ++-- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 0b618e0171..f84f6814b6 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.2.0-pre.1", + "version": "1.2.0", "license": "MIT", "repository": { "type": "git", @@ -16,7 +16,7 @@ }, "dependencies": { "@esbuild-plugins/node-modules-polyfill": "^0.1.4", - "@remix-run/server-runtime": "1.2.0-pre.1", + "@remix-run/server-runtime": "1.2.0", "cacache": "^15.0.5", "chokidar": "^3.5.1", "esbuild": "0.13.14", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index c5c5b5a969..ccaa08d714 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.2.0-pre.1", + "version": "1.2.0", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.2.0-pre.1", - "@remix-run/server-runtime": "1.2.0-pre.1" + "@remix-run/node": "1.2.0", + "@remix-run/server-runtime": "1.2.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 258bc5e9f4..8c1e62daa8 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.2.0-pre.1", + "version": "1.2.0", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.2.0-pre.1", + "@remix-run/server-runtime": "1.2.0", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 48f20fb414..64a675493a 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.2.0-pre.1", + "version": "1.2.0", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.2.0-pre.1", + "@remix-run/express": "1.2.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 1166fca9ca..bcb2375a50 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.2.0-pre.1", + "version": "1.2.0", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From 4d9ea7347b3b49fe1b987f389edaa056041d575e Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Wed, 16 Feb 2022 14:00:42 -0800 Subject: [PATCH 0241/1690] Version 1.2.1 --- packages/remix-dev/package.json | 4 ++-- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index f84f6814b6..a9afbcabfd 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.2.0", + "version": "1.2.1", "license": "MIT", "repository": { "type": "git", @@ -16,7 +16,7 @@ }, "dependencies": { "@esbuild-plugins/node-modules-polyfill": "^0.1.4", - "@remix-run/server-runtime": "1.2.0", + "@remix-run/server-runtime": "1.2.1", "cacache": "^15.0.5", "chokidar": "^3.5.1", "esbuild": "0.13.14", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index ccaa08d714..956de0ae89 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.2.0", + "version": "1.2.1", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.2.0", - "@remix-run/server-runtime": "1.2.0" + "@remix-run/node": "1.2.1", + "@remix-run/server-runtime": "1.2.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 8c1e62daa8..1967d40bfb 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.2.0", + "version": "1.2.1", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.2.0", + "@remix-run/server-runtime": "1.2.1", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 64a675493a..5340917028 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.2.0", + "version": "1.2.1", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.2.0", + "@remix-run/express": "1.2.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index bcb2375a50..ea804bb818 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.2.0", + "version": "1.2.1", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From a5054eb03fa6b7a8dd4e962d90a635595cea0cf1 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Wed, 16 Feb 2022 22:14:14 -0500 Subject: [PATCH 0242/1690] fix(remix-dev): update `serverModuleFormat` deprecation notice (#1992)!! --- packages/remix-dev/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index a23ecc810a..37445a83fd 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -102,7 +102,7 @@ export interface AppConfig { /** * The output format of the server build. Defaults to "cjs". - * * @deprecated Use {@link ServerConfig.serverBuildTarget} instead. + * @deprecated Use {@link ServerConfig.serverBuildTarget} instead. */ serverModuleFormat?: ServerModuleFormat; From 89465b6418d96042e9e4958d6d7bd9050a3f343b Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 18 Feb 2022 01:58:40 +0700 Subject: [PATCH 0243/1690] fix(node): add source-map-support to base node pkg (#2013) * fix(node): add source-map-support to base node pkg Since we are now inlining sourcemaps we can rely on the source-map-support library instead of having a custom implementation that has to be handed manually in our shared runtime. * remove test --- packages/remix-express/server.ts | 5 +- packages/remix-node/__tests__/errors-test.ts | 61 ---------- packages/remix-node/errors.ts | 112 ------------------- packages/remix-node/index.ts | 6 +- packages/remix-node/package.json | 5 +- packages/remix-server-runtime/platform.ts | 11 +- 6 files changed, 14 insertions(+), 186 deletions(-) delete mode 100644 packages/remix-node/__tests__/errors-test.ts delete mode 100644 packages/remix-node/errors.ts diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index c65bbdbb38..b0debf891c 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -14,8 +14,7 @@ import { // This has been added as a global in node 15+ AbortController, Headers as NodeHeaders, - Request as NodeRequest, - formatServerError + Request as NodeRequest } from "@remix-run/node"; /** @@ -44,7 +43,7 @@ export function createRequestHandler({ getLoadContext?: GetLoadContextFunction; mode?: string; }) { - let platform: ServerPlatform = { formatServerError }; + let platform: ServerPlatform = {}; let handleRequest = createRemixRequestHandler(build, platform, mode); return async ( diff --git a/packages/remix-node/__tests__/errors-test.ts b/packages/remix-node/__tests__/errors-test.ts deleted file mode 100644 index e3584007ca..0000000000 --- a/packages/remix-node/__tests__/errors-test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - getSourceContentForPosition, - relativeFilename, - UNKNOWN_LOCATION_POSITION -} from "../errors"; - -describe("getSourceContentForPosition", () => { - it("returns unknown position when no pos", () => { - expect(getSourceContentForPosition(null!, null)).toBe( - UNKNOWN_LOCATION_POSITION - ); - }); - - it("returns unknown position when no source", () => { - expect(getSourceContentForPosition(null!, {} as any)).toBe( - UNKNOWN_LOCATION_POSITION - ); - }); - - it("returns unknown position when no line", () => { - expect(getSourceContentForPosition(null!, { source: "yay!" } as any)).toBe( - UNKNOWN_LOCATION_POSITION - ); - }); - - it("returns trimmed source", () => { - let smc = { - sourceContentFor: jest.fn(() => "\n test() \n") - }; - - expect( - getSourceContentForPosition( - smc as any, - { source: "yay!", line: 2 } as any - ) - ).toBe("test()"); - }); -}); - -describe("relativeFilename", () => { - let root = process.cwd() + "/"; - let baseFilename = "./app/test.jsx"; - it("returns original filename", () => { - expect(relativeFilename(baseFilename)).toBe(baseFilename); - }); - - it("returns clean filename for route-module: prefix", () => { - let filename = "route-module:./app/test.jsx"; - expect(relativeFilename(filename)).toBe(baseFilename); - }); - - it("returns clean filename for absolute path route-module: prefix", () => { - let filename = `route-module:${root}app/test.jsx`; - expect(relativeFilename(filename)).toBe(baseFilename); - }); - - it("returns clean filename for absolute path route-module: prefix with extra stuff", () => { - let filename = `extra-stuff:route-module:${root}app/test.jsx`; - expect(relativeFilename(filename)).toBe(baseFilename); - }); -}); diff --git a/packages/remix-node/errors.ts b/packages/remix-node/errors.ts deleted file mode 100644 index b91aa759d1..0000000000 --- a/packages/remix-node/errors.ts +++ /dev/null @@ -1,112 +0,0 @@ -import fs from "fs"; -import fsp from "fs/promises"; -import path from "path"; -import type { NullableMappedPosition } from "source-map"; -import { SourceMapConsumer } from "source-map"; - -const ROOT = process.cwd() + path.sep; -const SOURCE_PATTERN = - /(?\s+at.+)\((?.+):(?\d+):(?\d+)\)/; - -export const UNKNOWN_LOCATION_POSITION = ""; - -export async function formatServerError(error: Error): Promise { - try { - error.stack = await formatStackTrace(error); - } catch {} - - return error; -} - -export async function formatStackTrace(error: Error) { - const cache = new Map(); - const lines = error.stack?.split("\n") || []; - const promises = lines.map(line => mapToSourceFile(cache, line)); - const stack = (await Promise.all(promises)).join("\n") || error.stack; - - return stack; -} - -export async function mapToSourceFile( - cache: Map, - stackLine: string -) { - let match = SOURCE_PATTERN.exec(stackLine); - - if (!match?.groups) { - // doesn't match pattern but may still have a filename - return relativeFilename(stackLine); - } - - let { at, filename } = match.groups; - let line: number | string = match.groups.line; - let column: number | string = match.groups.column; - let mapFilename = `${filename}.map`; - let smc = cache.get(mapFilename); - filename = relativeFilename(filename); - - if (!smc) { - if (await fileExists(mapFilename)) { - // read source map and setup consumer - const map = JSON.parse(await fsp.readFile(mapFilename, "utf-8")); - map.sourceRoot = path.dirname(mapFilename); - smc = await new SourceMapConsumer(map); - cache.set(mapFilename, smc); - } - } - - if (smc) { - const pos = getOriginalSourcePosition( - smc, - parseInt(line, 10), - parseInt(column, 10) - ); - - if (pos.source) { - filename = relativeFilename(pos.source); - line = pos.line || "?"; - column = pos.column || "?"; - at = ` at \`${getSourceContentForPosition(smc, pos)}\` `; - } - } - - return `${at}(${filename}:${line}:${column})`; -} - -export function relativeFilename(filename: string) { - if (filename.includes("route-module:")) { - filename = filename.substring(filename.indexOf("route-module:")); - } - return filename.replace("route-module:", "").replace(ROOT, "./"); -} - -export function getOriginalSourcePosition( - smc: SourceMapConsumer, - line: number, - column: number -) { - return smc.originalPositionFor({ line, column }); -} - -export function getSourceContentForPosition( - smc: SourceMapConsumer, - pos: NullableMappedPosition -) { - let src: string | null = null; - if (pos?.source && typeof pos.line === "number") { - src = smc.sourceContentFor(pos.source); - } - - if (!src) { - return UNKNOWN_LOCATION_POSITION; - } - - return src.split("\n")[pos.line! - 1].trim(); -} - -function fileExists(filename: string) { - return fsp - .access(filename, fs.constants.F_OK) - .then(() => true) - .catch(() => false); -} diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index 4cfb190a73..74fc3bbb72 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -1,6 +1,8 @@ -export { AbortController } from "abort-controller"; +import sourceMapSupport from "source-map-support"; + +sourceMapSupport.install(); -export { formatServerError } from "./errors"; +export { AbortController } from "abort-controller"; export type { HeadersInit, diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 1967d40bfb..d1597e9f57 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -22,11 +22,12 @@ "cookie-signature": "^1.1.0", "form-data": "^4.0.0", "node-fetch": "^2.6.1", - "source-map": "^0.7.3" + "source-map-support": "^0.5.21" }, "devDependencies": { "@types/blob-stream": "^0.1.30", - "@types/cookie-signature": "^1.0.3" + "@types/cookie-signature": "^1.0.3", + "@types/source-map-support": "^0.5.4" }, "sideEffects": false } diff --git a/packages/remix-server-runtime/platform.ts b/packages/remix-server-runtime/platform.ts index 9862bb57e9..1008823e8c 100644 --- a/packages/remix-server-runtime/platform.ts +++ b/packages/remix-server-runtime/platform.ts @@ -4,14 +4,13 @@ * The whole point here is to abstract out the server functionality that is required * by the server runtime but is dependent on the platform runtime. * - * An example of this is error beautification as it depends on loading sourcemaps from - * the file system in node, while functions hosted on cloudflare workers will not need - * to format as they have built in sourcemap support. + * The origional use of this was error beautification as it depends on loading sourcemaps + * from the file system in node, while functions hosted on cloudflare workers will not + * need to format as they have built in sourcemap support. This is no longer needed though + * as we utlize the `source-map-support` library to do this for us. */ /** * Abstracts functionality that is platform specific (node vs workers, etc.) */ -export interface ServerPlatform { - formatServerError?(error: Error): Promise; -} +export interface ServerPlatform {} From ffaf018712733f2cbdf414e62b99081299a1fd5f Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Thu, 17 Feb 2022 13:12:46 -0800 Subject: [PATCH 0244/1690] chore: update docblocks for app config --- packages/remix-dev/config.ts | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 37445a83fd..292beb0f2a 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -35,19 +35,20 @@ export type ServerPlatform = "node" | "neutral"; export interface AppConfig { /** * The path to the `app` directory, relative to `remix.config.js`. Defaults - * to "app". + * to `"app"`. */ appDirectory?: string; /** * The path to a directory Remix can use for caching things in development, - * relative to `remix.config.js`. Defaults to ".cache". + * relative to `remix.config.js`. Defaults to `".cache"`. */ cacheDirectory?: string; /** * A function for defining custom routes, in addition to those already defined - * using the filesystem convention in `app/routes`. + * using the filesystem convention in `app/routes`. Both sets of routes will + * be merged. */ routes?: ( defineRoutes: DefineRoutesFunction @@ -56,13 +57,17 @@ export interface AppConfig { /** * 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. This file should end in a `.js`. Defaults - * are based on {@link ServerConfig.serverBuildTarget}. + * 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; @@ -82,7 +87,7 @@ export interface AppConfig { /** * The URL prefix of the browser build with a trailing slash. Defaults to - * "/build/". + * `"/build/"`. This is the path the browser will use to find assets. */ publicPath?: string; @@ -90,8 +95,10 @@ export interface AppConfig { * The port number to use for the dev server. Defaults to 8002. */ devServerPort?: number; + /** - * The delay before the dev server broadcasts a reload event. + * The delay, in milliseconds, before the dev server broadcasts a reload + * event. There is no delay by default. */ devServerBroadcastDelay?: number; @@ -102,12 +109,14 @@ export interface AppConfig { /** * The output format of the server build. Defaults to "cjs". + * * @deprecated Use {@link ServerConfig.serverBuildTarget} instead. */ serverModuleFormat?: ServerModuleFormat; /** * The platform the server build is targeting. Defaults to "node". + * * @deprecated Use {@link ServerConfig.serverBuildTarget} instead. */ serverPlatform?: ServerPlatform; @@ -118,7 +127,10 @@ export interface AppConfig { serverBuildTarget?: ServerBuildTarget; /** - * A server entrypoint relative to the root directory that becomes your server's main module. + * 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. */ server?: string; From 94701dcb509aec7406f5346262bdcc818fcb6265 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 18 Feb 2022 05:10:51 +0700 Subject: [PATCH 0245/1690] chore: update esbuild to latest (#1998) --- packages/remix-dev/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index a9afbcabfd..7c086ce854 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -19,7 +19,7 @@ "@remix-run/server-runtime": "1.2.1", "cacache": "^15.0.5", "chokidar": "^3.5.1", - "esbuild": "0.13.14", + "esbuild": "0.14.22", "exit-hook": "2.2.1", "fs-extra": "^10.0.0", "get-port": "^5.1.1", From 1d81519dc72bf31a2762bee53639cf296b0fb78e Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 18 Feb 2022 13:14:24 +0700 Subject: [PATCH 0246/1690] fix: strip ".client" files from server bundle (#2019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The regex is wrong? Not sure how this got changed or stopped working between release 🤷‍♀️ --- integration/compiler-test.ts | 27 +++++++++++++++++++++++---- packages/remix-dev/compiler.ts | 2 +- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/integration/compiler-test.ts b/integration/compiler-test.ts index 3b0bdfe4fd..5a6ae3ef2e 100644 --- a/integration/compiler-test.ts +++ b/integration/compiler-test.ts @@ -9,14 +9,23 @@ describe("compiler", () => { fixture = await createFixture({ files: { "app/fake.server.js": js` - export default { hello: "world" }; + export const hello = "server"; + `, + "app/fake.client.js": js` + export const hello = "client"; + `, + "app/fake.js": js` + import { hello as clientHello } from "./fake.client.js"; + import { hello as serverHello } from "./fake.server.js"; + export default clientHello || serverHello; `, "app/routes/index.jsx": js` - import fake from "~/fake.server.js"; + import fake from "~/fake.js"; export default function Index() { - return
{Object.keys(fake).length}
+ let hasRightModule = fake === (typeof document === "undefined" ? "server" : "client"); + return
{String(hasRightModule)}
} `, "app/routes/built-ins.jsx": js` @@ -76,7 +85,17 @@ describe("compiler", () => { // rendered the page instead of the error boundary expect(await app.getHtml("#index")).toMatchInlineSnapshot( - `"
0
"` + `"
true
"` + ); + }); + it("removes server code with `*.client` files", async () => { + await app.disableJavaScript(); + let res = await app.goto("/", true); + expect(res.status()).toBe(200); // server rendered fine + + // rendered the page instead of the error boundary + expect(await app.getHtml("#index")).toMatchInlineSnapshot( + `"
true
"` ); }); diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index bb1f50679b..673bf27e75 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -399,7 +399,7 @@ async function createServerBuild( let plugins: esbuild.Plugin[] = [ mdxPlugin(config), - emptyModulesPlugin(config, /\.client\.[tj]sx?$/), + emptyModulesPlugin(config, /\.client(\.[jt]sx?)?$/), serverRouteModulesPlugin(config), serverEntryModulePlugin(config), serverAssetsManifestPlugin(assetsManifestPromiseRef), From f0af3a0b3f483b4f54c05c0dc0399472b4ab0ded Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Fri, 18 Feb 2022 16:25:42 -0800 Subject: [PATCH 0247/1690] chore: fix test fixture creator `formatServerError` was removed from `@remix-run/node` in #2013 but we were still using it internally for fixture tests. I'm kind of confused why tests weren't failing as a result but this just removes it like we do in our adapters. --- integration/helpers/create-fixture.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index ff8cace794..189d33140d 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -10,10 +10,12 @@ import prettier from "prettier"; import getPort from "get-port"; import { createRequestHandler } from "../../packages/remix-server-runtime"; -import { formatServerError } from "../../packages/remix-node"; import { createApp } from "../../packages/create-remix"; import { createRequestHandler as createExpressHandler } from "../../packages/remix-express"; -import type { ServerBuild } from "../../packages/remix-server-runtime"; +import type { + ServerBuild, + ServerPlatform +} from "../../packages/remix-server-runtime"; import type { CreateAppArgs } from "../../packages/create-remix"; import { TMP_DIR } from "./global-setup"; @@ -32,7 +34,7 @@ export let js = String.raw; export async function createFixture(init: FixtureInit) { let projectDir = await createFixtureProject(init); let app: ServerBuild = await import(path.resolve(projectDir, "build")); - let platform = { formatServerError }; + let platform: ServerPlatform = {}; let handler = createRequestHandler(app, platform); let requestDocument = async (href: string, init?: RequestInit) => { From a2a8805132ac710e62b58887f040a613c325c038 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 21 Feb 2022 11:52:51 -0800 Subject: [PATCH 0248/1690] Version 1.2.2-pre.0 --- packages/remix-dev/package.json | 4 ++-- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 7c086ce854..b113372077 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.2.1", + "version": "1.2.2-pre.0", "license": "MIT", "repository": { "type": "git", @@ -16,7 +16,7 @@ }, "dependencies": { "@esbuild-plugins/node-modules-polyfill": "^0.1.4", - "@remix-run/server-runtime": "1.2.1", + "@remix-run/server-runtime": "1.2.2-pre.0", "cacache": "^15.0.5", "chokidar": "^3.5.1", "esbuild": "0.14.22", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 956de0ae89..d3a8bebe78 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.2.1", + "version": "1.2.2-pre.0", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.2.1", - "@remix-run/server-runtime": "1.2.1" + "@remix-run/node": "1.2.2-pre.0", + "@remix-run/server-runtime": "1.2.2-pre.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index d1597e9f57..5d34cfb5b2 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.2.1", + "version": "1.2.2-pre.0", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.2.1", + "@remix-run/server-runtime": "1.2.2-pre.0", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 5340917028..778563b28d 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.2.1", + "version": "1.2.2-pre.0", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.2.1", + "@remix-run/express": "1.2.2-pre.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index ea804bb818..cdf008eed2 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.2.1", + "version": "1.2.2-pre.0", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From 52999a14c563c9b76b48fd0b6cca247d36c80e1e Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 21 Feb 2022 15:41:23 -0800 Subject: [PATCH 0249/1690] Version 1.2.2 --- packages/remix-dev/package.json | 4 ++-- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index b113372077..98cfde52f7 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.2.2-pre.0", + "version": "1.2.2", "license": "MIT", "repository": { "type": "git", @@ -16,7 +16,7 @@ }, "dependencies": { "@esbuild-plugins/node-modules-polyfill": "^0.1.4", - "@remix-run/server-runtime": "1.2.2-pre.0", + "@remix-run/server-runtime": "1.2.2", "cacache": "^15.0.5", "chokidar": "^3.5.1", "esbuild": "0.14.22", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index d3a8bebe78..8634f6811c 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.2.2-pre.0", + "version": "1.2.2", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.2.2-pre.0", - "@remix-run/server-runtime": "1.2.2-pre.0" + "@remix-run/node": "1.2.2", + "@remix-run/server-runtime": "1.2.2" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 5d34cfb5b2..d0707fa19e 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.2.2-pre.0", + "version": "1.2.2", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.2.2-pre.0", + "@remix-run/server-runtime": "1.2.2", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 778563b28d..3f5ca3c1e6 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.2.2-pre.0", + "version": "1.2.2", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.2.2-pre.0", + "@remix-run/express": "1.2.2", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index cdf008eed2..0a63b79b6c 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.2.2-pre.0", + "version": "1.2.2", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From 1489e7637294e7b48683f6b80b4bc9ed49282298 Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 22 Feb 2022 08:38:53 +0700 Subject: [PATCH 0250/1690] fix: use browser polyfills for deno node builtins (#1997) Esbuild can't hoist `require("xyz")` calls to `import "xyz"`, this leads to an issue every time you try to re-map a require'd version in external code URL. --- packages/remix-dev/compiler.ts | 2 +- .../compiler/plugins/serverBareModulesPlugin.ts | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 673bf27e75..72c3874321 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -407,7 +407,7 @@ async function createServerBuild( ]; if (config.serverPlatform !== "node") { - plugins.push(NodeModulesPolyfillPlugin()); + plugins.unshift(NodeModulesPolyfillPlugin()); } return esbuild diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts index 8c639eca51..08472566a2 100644 --- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -71,15 +71,6 @@ export function serverBareModulesPlugin( case "cloudflare-pages": case "cloudflare-workers": return undefined; - // Map node externals to deno std libs and bundle everything else. - case "deno": - if (isNodeBuiltIn(packageName)) { - return { - path: `https://deno.land/std/node/${packageName}/mod.ts`, - external: true - }; - } - return undefined; } for (let pattern of remixConfig.serverDependenciesToBundle) { From bd83a6b038ca118cea3f794d1cde4965b122c9e1 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 21 Feb 2022 22:53:17 -0800 Subject: [PATCH 0251/1690] feat: Refactor `@remix-run/eslint-config` with added support for Testing Library (#2066) * Update testing dependencies * Break eslint rules into separate modules * Fix internal eslint issues --- packages/remix-dev/server-build.ts | 1 + .../__tests__/data-test.ts | 32 ++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/remix-dev/server-build.ts b/packages/remix-dev/server-build.ts index 458eb43d52..9f98585372 100644 --- a/packages/remix-dev/server-build.ts +++ b/packages/remix-dev/server-build.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-unreachable */ import type { ServerBuild } from "@remix-run/server-runtime"; throw new Error( diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index e79611f249..5c08f6c525 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -165,13 +165,21 @@ describe("loaders", () => { } } as unknown as RouteMatch; + let possibleError: any; try { - await callRouteLoader({ request, match, loadContext: {} }); + possibleError = await callRouteLoader({ + request, + match, + loadContext: {} + }); } catch (error) { - expect(error.message).toMatchInlineSnapshot( - '"You defined a loader for route \\"routes/random\\" but didn\'t return anything from your `loader` function. Please return a value or `null`."' - ); + possibleError = error; } + + expect(possibleError).toBeInstanceOf(Error); + expect(possibleError.message).toMatchInlineSnapshot( + '"You defined a loader for route \\"routes/random\\" but didn\'t return anything from your `loader` function. Please return a value or `null`."' + ); }); }); @@ -194,12 +202,20 @@ describe("actions", () => { } } as unknown as RouteMatch; + let possibleError: any; try { - await callRouteAction({ request, match, loadContext: {} }); + possibleError = await callRouteAction({ + request, + match, + loadContext: {} + }); } catch (error) { - expect(error.message).toMatchInlineSnapshot( - '"You defined an action for route \\"routes/random\\" but didn\'t return anything from your `action` function. Please return a value or `null`."' - ); + possibleError = error; } + + expect(possibleError).toBeInstanceOf(Error); + expect(possibleError.message).toMatchInlineSnapshot( + '"You defined an action for route \\"routes/random\\" but didn\'t return anything from your `action` function. Please return a value or `null`."' + ); }); }); From 59a135dd0d0ba0af5d6ad2be12684c411cea6045 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 21 Feb 2022 22:55:50 -0800 Subject: [PATCH 0252/1690] fix: `onSubmit`: Get submission values from form attributes instead of props (#1854) * get form values from form attributes instead of DOM props to prevent overrides by inputs with the same name * add test for * add tests to ensure form action resolves relative to route hierarchy * Refactor test utils --- integration/form-test.ts | 337 ++++++++++++++++++++++++- integration/helpers/create-fixture.tsx | 25 +- 2 files changed, 358 insertions(+), 4 deletions(-) diff --git a/integration/form-test.ts b/integration/form-test.ts index 3b08fbfde3..0d90e8b38e 100644 --- a/integration/form-test.ts +++ b/integration/form-test.ts @@ -1,4 +1,9 @@ -import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; +import { + createAppFixture, + createFixture, + getElement, + js +} from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; describe("Forms", () => { @@ -8,11 +13,35 @@ describe("Forms", () => { const KEYBOARD_INPUT = "KEYBOARD_INPUT"; const CHECKBOX_BUTTON = "CHECKBOX_BUTTON"; const ORPHAN_BUTTON = "ORPHAN_BUTTON"; + const FORM_WITH_ACTION_INPUT = "FORM_WITH_ACTION_INPUT"; const FORM_WITH_ORPHAN = "FORM_WITH_ORPHAN"; const LUNCH = "LUNCH"; const CHEESESTEAK = "CHEESESTEAK"; const LAKSA = "LAKSA"; const SQUID_INK_HOTDOG = "SQUID_INK_HOTDOG"; + const ACTION = "action"; + const EAT = "EAT"; + + const STATIC_ROUTE_ABSOLUTE_ACTION = "static-route-abs"; + const STATIC_ROUTE_CURRENT_ACTION = "static-route-cur"; + const STATIC_ROUTE_PARENT_ACTION = "static-route-parent"; + const STATIC_ROUTE_TOO_MANY_DOTS_ACTION = "static-route-too-many-dots"; + const INDEX_ROUTE_ABSOLUTE_ACTION = "index-route-abs"; + const INDEX_ROUTE_CURRENT_ACTION = "index-route-cur"; + const INDEX_ROUTE_PARENT_ACTION = "index-route-parent"; + const INDEX_ROUTE_TOO_MANY_DOTS_ACTION = "index-route-too-many-dots"; + const DYNAMIC_ROUTE_ABSOLUTE_ACTION = "dynamic-route-abs"; + const DYNAMIC_ROUTE_CURRENT_ACTION = "dynamic-route-cur"; + const DYNAMIC_ROUTE_PARENT_ACTION = "dynamic-route-parent"; + const DYNAMIC_ROUTE_TOO_MANY_DOTS_ACTION = "dynamic-route-too-many-dots"; + const LAYOUT_ROUTE_ABSOLUTE_ACTION = "layout-route-abs"; + const LAYOUT_ROUTE_CURRENT_ACTION = "layout-route-cur"; + const LAYOUT_ROUTE_PARENT_ACTION = "layout-route-parent"; + const LAYOUT_ROUTE_TOO_MANY_DOTS_ACTION = "layout-route-too-many-dots"; + const SPLAT_ROUTE_ABSOLUTE_ACTION = "splat-route-abs"; + const SPLAT_ROUTE_CURRENT_ACTION = "splat-route-cur"; + const SPLAT_ROUTE_PARENT_ACTION = "splat-route-parent"; + const SPLAT_ROUTE_TOO_MANY_DOTS_ACTION = "splat-route-too-many-dots"; beforeAll(async () => { fixture = await createFixture({ @@ -31,6 +60,13 @@ describe("Forms", () => { <>
+ + +
+ +
+
@@ -45,7 +81,7 @@ describe("Forms", () => { > - + @@ -81,6 +117,145 @@ describe("Forms", () => { ) } + `, + + "app/routes/about.jsx": js` + export async function action({ request }) { + return json({ submitted: true }); + } + export default function () { + return

About

; + } + `, + + "app/routes/inbox.jsx": js` + import { Form } from "remix"; + export default function() { + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+ + ) + } + `, + + "app/routes/blog.jsx": js` + import { Form, Outlet } from "remix"; + export default function() { + return ( + <> +

Blog

+
+ +
+
+ +
+
+ +
+
+ +
+ + + ) + } + `, + + "app/routes/blog/index.jsx": js` + import { Form } from "remix"; + export default function() { + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+ + ) + } + `, + + "app/routes/blog/$postId.jsx": js` + import { Form } from "remix"; + export default function() { + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+ + ) + } + `, + + "app/routes/projects.jsx": js` + import { Form, Outlet } from "remix"; + export default function() { + return ( + <> +

Projects

+ + + ) + } + `, + + "app/routes/projects/index.jsx": js` + export default function() { + return

All projects

+ } + `, + + "app/routes/projects/$.jsx": js` + import { Form } from "remix"; + export default function() { + return ( + <> +
+ +
+
+ +
+
+ +
+
+ +
+ + ) + } ` } }); @@ -108,6 +283,12 @@ describe("Forms", () => { expect(await app.getHtml("pre")).toMatch(CHEESESTEAK); }); + it("posts to a loader with an ", async () => { + await app.goto("/get-submission"); + await app.clickElement(`#${FORM_WITH_ACTION_INPUT} button`); + expect(await app.getHtml("pre")).toMatch(EAT); + }); + it("posts to a loader with button data with click", async () => { await app.goto("/get-submission"); await app.clickElement("#buttonWithValue"); @@ -136,4 +317,156 @@ describe("Forms", () => { await app.clickElement(`#${ORPHAN_BUTTON}`); expect(await app.getHtml("pre")).toMatch(SQUID_INK_HOTDOG); }); + + describe("
action", () => { + describe("in a static route", () => { + test("absolute action resolves relative to the root route", async () => { + await app.goto("/inbox"); + let html = await app.getHtml(); + let el = getElement(html, `#${STATIC_ROUTE_ABSOLUTE_ACTION}`); + expect(el.attr("action")).toMatch("/about"); + }); + + test("'.' action resolves relative to the current route", async () => { + await app.goto("/inbox"); + let html = await app.getHtml(); + let el = getElement(html, `#${STATIC_ROUTE_CURRENT_ACTION}`); + expect(el.attr("action")).toMatch("/inbox"); + }); + + test("'..' action resolves relative to the parent route", async () => { + await app.goto("/inbox"); + let html = await app.getHtml(); + let el = getElement(html, `#${STATIC_ROUTE_PARENT_ACTION}`); + expect(el.attr("action")).toMatch("/"); + }); + + test("'..' action with more .. segments than parent routes resolves relative to the root route", async () => { + await app.goto("/inbox"); + let html = await app.getHtml(); + let el = getElement(html, `#${STATIC_ROUTE_TOO_MANY_DOTS_ACTION}`); + expect(el.attr("action")).toMatch("/"); + }); + }); + + describe("in a dynamic route", () => { + test("absolute action resolves relative to the root route", async () => { + await app.goto("/blog/abc"); + let html = await app.getHtml(); + let el = getElement(html, `#${DYNAMIC_ROUTE_ABSOLUTE_ACTION}`); + expect(el.attr("action")).toMatch("/about"); + }); + + test("'.' action resolves relative to the current route", async () => { + await app.goto("/blog/abc"); + let html = await app.getHtml(); + let el = getElement(html, `#${DYNAMIC_ROUTE_CURRENT_ACTION}`); + expect(el.attr("action")).toMatch("/blog/abc"); + }); + + test("'..' action resolves relative to the parent route", async () => { + await app.goto("/blog/abc"); + let html = await app.getHtml(); + let el = getElement(html, `#${DYNAMIC_ROUTE_PARENT_ACTION}`); + expect(el.attr("action")).toMatch("/blog"); + }); + + test("'..' action with more .. segments than parent routes resolves relative to the root route", async () => { + await app.goto("/blog/abc"); + let html = await app.getHtml(); + let el = getElement(html, `#${DYNAMIC_ROUTE_TOO_MANY_DOTS_ACTION}`); + expect(el.attr("action")).toMatch("/"); + }); + }); + + describe("in an index route", () => { + test("absolute action resolves relative to the root route", async () => { + await app.goto("/blog"); + let html = await app.getHtml(); + let el = getElement(html, `#${INDEX_ROUTE_ABSOLUTE_ACTION}`); + expect(el.attr("action")).toMatch("/about"); + }); + + test("'.' action resolves relative to the current route", async () => { + await app.goto("/blog"); + let html = await app.getHtml(); + let el = getElement(html, `#${INDEX_ROUTE_CURRENT_ACTION}`); + expect(el.attr("action")).toMatch("/blog"); + }); + + test("'..' action resolves relative to the parent route", async () => { + await app.goto("/blog"); + let html = await app.getHtml(); + let el = getElement(html, `#${INDEX_ROUTE_PARENT_ACTION}`); + expect(el.attr("action")).toMatch("/"); + }); + + test("'..' action with more .. segments than parent routes resolves relative to the root route", async () => { + await app.goto("/blog"); + let html = await app.getHtml(); + let el = getElement(html, `#${INDEX_ROUTE_TOO_MANY_DOTS_ACTION}`); + expect(el.attr("action")).toMatch("/"); + }); + }); + + describe("in a layout route", () => { + test("absolute action resolves relative to the root route", async () => { + await app.goto("/blog"); + let html = await app.getHtml(); + let el = getElement(html, `#${LAYOUT_ROUTE_ABSOLUTE_ACTION}`); + expect(el.attr("action")).toMatch("/about"); + }); + + test("'.' action resolves relative to the current route", async () => { + await app.goto("/blog"); + let html = await app.getHtml(); + let el = getElement(html, `#${LAYOUT_ROUTE_CURRENT_ACTION}`); + expect(el.attr("action")).toMatch("/blog"); + }); + + test("'..' action resolves relative to the parent route", async () => { + await app.goto("/blog"); + let html = await app.getHtml(); + let el = getElement(html, `#${LAYOUT_ROUTE_PARENT_ACTION}`); + expect(el.attr("action")).toMatch("/"); + }); + + test("'..' action with more .. segments than parent routes resolves relative to the root route", async () => { + await app.goto("/blog"); + let html = await app.getHtml(); + let el = getElement(html, `#${LAYOUT_ROUTE_TOO_MANY_DOTS_ACTION}`); + expect(el.attr("action")).toMatch("/"); + }); + }); + + describe("in a splat route", () => { + test("absolute action resolves relative to the root route", async () => { + await app.goto("/projects/blarg"); + let html = await app.getHtml(); + let el = getElement(html, `#${SPLAT_ROUTE_ABSOLUTE_ACTION}`); + expect(el.attr("action")).toMatch("/about"); + }); + + test("'.' action resolves relative to the current route", async () => { + await app.goto("/projects/blarg"); + let html = await app.getHtml(); + let el = getElement(html, `#${SPLAT_ROUTE_CURRENT_ACTION}`); + expect(el.attr("action")).toMatch("/projects"); + }); + + test("'..' action resolves relative to the parent route", async () => { + await app.goto("/projects/blarg"); + let html = await app.getHtml(); + let el = getElement(html, `#${SPLAT_ROUTE_PARENT_ACTION}`); + expect(el.attr("action")).toMatch("/projects"); + }); + + test("'..' action with more .. segments than parent routes resolves relative to the root route", async () => { + await app.goto("/projects/blarg"); + let html = await app.getHtml(); + let el = getElement(html, `#${SPLAT_ROUTE_TOO_MANY_DOTS_ACTION}`); + expect(el.attr("action")).toMatch("/"); + }); + }); + }); }); diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index 189d33140d..a41b24c030 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -307,6 +307,15 @@ export async function createAppFixture(fixture: Fixture) { */ getHtml: (selector?: string) => getHtml(page, selector), + /** + * Get a cheerio instance of an element from the page. + * + * @param selector CSS Selector for the element's HTML you want + */ + getElement: async (selector: string) => { + return getElement(await getHtml(page), selector); + }, + /** * Keeps the fixture running for as many seconds as you want so you can go * poke around in the browser to see what's up. @@ -399,13 +408,25 @@ export async function getHtml(page: Page, selector?: string) { return selector ? selectHtml(html, selector) : prettyHtml(html); } -export function selectHtml(source: string, selector: string) { - let el = cheerio(selector, source); +export function getAttribute( + source: string, + selector: string, + attributeName: string +) { + let el = getElement(source, selector); + return el.attr(attributeName); +} +export function getElement(source: string, selector: string) { + let el = cheerio(selector, source); if (!el.length) { throw new Error(`No element matches selector "${selector}"`); } + return el; +} +export function selectHtml(source: string, selector: string) { + let el = getElement(source, selector); return prettyHtml(cheerio.html(el)).trim(); } From afa5b8c36fc165841b7eaf21aa0522a441b84d0c Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 22 Feb 2022 09:15:24 -0500 Subject: [PATCH 0253/1690] fix(remix-dev): Remove x-powered-by header from Express dev server (#2061) * fix(remix-dev): Remove x-powered-by header from Express dev server * chore: sign CLA --- packages/remix-dev/cli/commands.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 37e03e031f..498a72f235 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -167,6 +167,7 @@ export async function dev(remixRoot: string, modeArg?: string) { } let app = express(); + app.disable("x-powered-by"); app.use((_, __, next) => { purgeAppRequireCache(config.serverBuildPath); next(); From cb6c8530c540d81a9a43592a4bc6b83e1318bf96 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 22 Feb 2022 09:42:10 -0800 Subject: [PATCH 0254/1690] fix: allow `process.env.NODE_ENV === "test"` in build (#2059) --- packages/remix-dev/build.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/remix-dev/build.ts b/packages/remix-dev/build.ts index 18052ddbf2..dd3aceed8f 100644 --- a/packages/remix-dev/build.ts +++ b/packages/remix-dev/build.ts @@ -1,10 +1,15 @@ export enum BuildMode { Development = "development", - Production = "production" + Production = "production", + Test = "test" } export function isBuildMode(mode: any): mode is BuildMode { - return mode === BuildMode.Development || mode === BuildMode.Production; + return ( + mode === BuildMode.Development || + mode === BuildMode.Production || + mode === BuildMode.Test + ); } export enum BuildTarget { From 9b1112941609c8e46eb9db1af2f0c2415c6710b4 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 22 Feb 2022 13:12:33 -0800 Subject: [PATCH 0255/1690] chore: Format with Prettier defaults (#2084) --- integration/action-test.ts | 6 +- integration/bug-report-test.ts | 4 +- integration/catch-boundary-test.ts | 6 +- integration/compiler-test.ts | 4 +- integration/errory-boundary-test.ts | 4 +- integration/file-uploads-test.ts | 4 +- integration/form-test.ts | 6 +- integration/headers-test.ts | 8 +- integration/helpers/create-fixture.tsx | 38 +- integration/helpers/global-setup.ts | 2 +- integration/loader-test.ts | 6 +- integration/rendering-test.ts | 6 +- integration/server-entry-test.ts | 4 +- integration/splat-routes-test.ts | 4 +- integration/transition-test.ts | 12 +- packages/remix-dev/__tests__/build-test.ts | 10 +- .../remix-dev/__tests__/defineRoutes-test.ts | 4 +- .../remix-dev/__tests__/readConfig-test.ts | 2 +- .../__tests__/routesConvention-test.ts | 2 +- packages/remix-dev/build.ts | 4 +- packages/remix-dev/cache.ts | 2 +- packages/remix-dev/cli.ts | 12 +- packages/remix-dev/cli/commands.ts | 12 +- packages/remix-dev/compiler.ts | 56 +-- packages/remix-dev/compiler/assets.ts | 10 +- packages/remix-dev/compiler/loaders.ts | 2 +- .../plugins/browserRouteModulesPlugin.ts | 20 +- .../compiler/plugins/emptyModulesPlugin.ts | 6 +- packages/remix-dev/compiler/plugins/mdx.ts | 28 +- .../plugins/serverAssetsManifestPlugin.ts | 6 +- .../plugins/serverBareModulesPlugin.ts | 6 +- .../plugins/serverEntryModulePlugin.ts | 8 +- .../plugins/serverRouteModulesPlugin.ts | 10 +- packages/remix-dev/compiler/routes.ts | 4 +- packages/remix-dev/compiler/utils/crypto.ts | 4 +- packages/remix-dev/compiler/virtualModules.ts | 4 +- packages/remix-dev/config.ts | 4 +- packages/remix-dev/config/format.ts | 8 +- packages/remix-dev/config/routes.ts | 2 +- packages/remix-dev/config/routesConvention.ts | 12 +- packages/remix-dev/config/serverModes.ts | 2 +- packages/remix-dev/setup.ts | 6 +- .../remix-express/__tests__/server-test.ts | 16 +- packages/remix-express/server.ts | 10 +- packages/remix-node/__tests__/fetch-test.ts | 22 +- .../remix-node/__tests__/formData-test.ts | 2 +- .../__tests__/parseMultipartFormData-test.ts | 2 +- .../remix-node/__tests__/sessions-test.ts | 8 +- packages/remix-node/cookieSigning.ts | 2 +- packages/remix-node/fetch.ts | 10 +- packages/remix-node/formData.ts | 6 +- packages/remix-node/globals.ts | 4 +- packages/remix-node/index.ts | 4 +- packages/remix-node/magicExports/platform.ts | 2 +- packages/remix-node/parseMultipartFormData.ts | 6 +- packages/remix-node/sessions/fileStorage.ts | 6 +- .../remix-node/upload/fileUploadHandler.ts | 6 +- .../remix-node/upload/memoryUploadHandler.ts | 4 +- packages/remix-serve/cli.ts | 2 +- .../__tests__/cookies-test.ts | 18 +- .../__tests__/data-test.ts | 68 ++-- .../__tests__/responses-test.ts | 12 +- .../__tests__/server-test.ts | 338 +++++++++--------- .../__tests__/sessions-test.ts | 12 +- .../remix-server-runtime/__tests__/utils.ts | 22 +- packages/remix-server-runtime/cookies.ts | 6 +- packages/remix-server-runtime/data.ts | 8 +- packages/remix-server-runtime/entry.ts | 6 +- packages/remix-server-runtime/errors.ts | 2 +- packages/remix-server-runtime/headers.ts | 2 +- packages/remix-server-runtime/index.ts | 10 +- .../magicExports/server.ts | 4 +- packages/remix-server-runtime/mode.ts | 2 +- packages/remix-server-runtime/responses.ts | 4 +- .../remix-server-runtime/routeMatching.ts | 4 +- packages/remix-server-runtime/routes.ts | 6 +- packages/remix-server-runtime/server.ts | 62 ++-- packages/remix-server-runtime/sessions.ts | 8 +- .../sessions/cookieStorage.ts | 6 +- .../sessions/memoryStorage.ts | 6 +- 80 files changed, 539 insertions(+), 539 deletions(-) diff --git a/integration/action-test.ts b/integration/action-test.ts index 503a25b2b1..f50073314f 100644 --- a/integration/action-test.ts +++ b/integration/action-test.ts @@ -2,7 +2,7 @@ import { createFixture, createAppFixture, selectHtml, - js + js, } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; @@ -65,8 +65,8 @@ describe("actions", () => { export default function () { return
${PAGE_TEXT}
} - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/bug-report-test.ts b/integration/bug-report-test.ts index a83a6b081f..cf268a06b3 100644 --- a/integration/bug-report-test.ts +++ b/integration/bug-report-test.ts @@ -58,8 +58,8 @@ beforeAll(async () => { export default function Index() { return
cheeseburger
; } - ` - } + `, + }, }); // This creates an interactive app using puppeteer. diff --git a/integration/catch-boundary-test.ts b/integration/catch-boundary-test.ts index 80dce4b882..e87a647e06 100644 --- a/integration/catch-boundary-test.ts +++ b/integration/catch-boundary-test.ts @@ -121,8 +121,8 @@ describe("CatchBoundary", () => { export default function Index() { return
} - ` - } + `, + }, }); app = await createAppFixture(fixture); @@ -146,7 +146,7 @@ describe("CatchBoundary", () => { test("invalid request methods", async () => { let res = await fixture.requestDocument("/", { - method: "OPTIONS" + method: "OPTIONS", }); expect(res.status).toBe(405); expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); diff --git a/integration/compiler-test.ts b/integration/compiler-test.ts index 5a6ae3ef2e..3e2c91c8ee 100644 --- a/integration/compiler-test.ts +++ b/integration/compiler-test.ts @@ -68,8 +68,8 @@ describe("compiler", () => { }`, "node_modules/esm-only-pkg/esm-only-pkg.js": js` export default "esm-only-pkg"; - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/errory-boundary-test.ts b/integration/errory-boundary-test.ts index f4f68aa78d..0ee18a44af 100644 --- a/integration/errory-boundary-test.ts +++ b/integration/errory-boundary-test.ts @@ -161,8 +161,8 @@ describe("ErrorBoundary", () => { export function ErrorBoundary() { return
${OWN_BOUNDARY_TEXT}
} - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/file-uploads-test.ts b/integration/file-uploads-test.ts index f9f0d418d4..398c9b8cb6 100644 --- a/integration/file-uploads-test.ts +++ b/integration/file-uploads-test.ts @@ -57,8 +57,8 @@ describe("file-uploads", () => { ); } - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/form-test.ts b/integration/form-test.ts index 0d90e8b38e..a035b3c8c1 100644 --- a/integration/form-test.ts +++ b/integration/form-test.ts @@ -2,7 +2,7 @@ import { createAppFixture, createFixture, getElement, - js + js, } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; @@ -256,8 +256,8 @@ describe("Forms", () => { ) } - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/headers-test.ts b/integration/headers-test.ts index 08e352c73b..966231c71c 100644 --- a/integration/headers-test.ts +++ b/integration/headers-test.ts @@ -64,8 +64,8 @@ describe("headers export", () => { } export default function Action() { return
} - ` - } + `, + }, }); }); @@ -113,8 +113,8 @@ describe("headers export", () => { export default function Index() { return
Heyo!
} - ` - } + `, + }, }); let response = await fixture.requestDocument("/"); expect(response.headers.get(HEADER_KEY)).toBe(HEADER_VALUE); diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index a41b24c030..0d297633cd 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -14,7 +14,7 @@ import { createApp } from "../../packages/create-remix"; import { createRequestHandler as createExpressHandler } from "../../packages/remix-express"; import type { ServerBuild, - ServerPlatform + ServerPlatform, } from "../../packages/remix-server-runtime"; import type { CreateAppArgs } from "../../packages/create-remix"; import { TMP_DIR } from "./global-setup"; @@ -62,8 +62,8 @@ export async function createFixture(init: FixtureInit) { "Content-Type": data instanceof URLSearchParams ? "application/x-www-form-urlencoded" - : "multipart/form-data" - } + : "multipart/form-data", + }, }); }; @@ -80,7 +80,7 @@ export async function createFixture(init: FixtureInit) { requestDocument, requestData, postDocument, - getBrowserAsset + getBrowserAsset, }; } @@ -89,7 +89,7 @@ export async function createAppFixture(fixture: Fixture) { port: number; stop: () => Promise; }> => { - return new Promise(async accept => { + return new Promise(async (accept) => { let port = await getPort(); let app = express(); app.use(express.static(path.join(fixture.projectDir, "public"))); @@ -101,7 +101,7 @@ export async function createAppFixture(fixture: Fixture) { let server = app.listen(port); let stop = (): Promise => { - return new Promise(res => { + return new Promise((res) => { server.close(() => res()); }); }; @@ -119,7 +119,7 @@ export async function createAppFixture(fixture: Fixture) { let start = async () => { let [{ stop, port }, { browser, page }] = await Promise.all([ startAppServer(), - launchPuppeteer() + launchPuppeteer(), ]); let serverUrl = `http://localhost:${port}`; @@ -166,7 +166,7 @@ export async function createAppFixture(fixture: Fixture) { */ goto: async (href: string, waitForHydration?: true) => { return page.goto(`${serverUrl}${href}`, { - waitUntil: waitForHydration ? "networkidle0" : undefined + waitUntil: waitForHydration ? "networkidle0" : undefined, }); }, @@ -327,8 +327,8 @@ export async function createAppFixture(fixture: Fixture) { jest.setTimeout(ms); console.log(`🙈 Poke around for ${seconds} seconds 👉 ${serverUrl}`); cp.exec(`open ${serverUrl}${href}`); - return new Promise(res => setTimeout(res, ms)); - } + return new Promise((res) => setTimeout(res, ms)); + }, }; }; @@ -344,11 +344,11 @@ export async function createFixtureProject(init: FixtureInit): Promise { lang: "js", server: init.server || "remix", projectDir, - quiet: true + quiet: true, }); await Promise.all([ writeTestFiles(init, projectDir), - installRemix(projectDir) + installRemix(projectDir), ]); build(projectDir); @@ -358,10 +358,10 @@ export async function createFixtureProject(init: FixtureInit): Promise { function build(projectDir: string) { // TODO: log errors (like syntax errors in the fixture file strings) cp.spawnSync("node", ["node_modules/@remix-run/dev/cli.js", "setup"], { - cwd: projectDir + cwd: projectDir, }); cp.spawnSync("node", ["node_modules/@remix-run/dev/cli.js", "build"], { - cwd: projectDir + cwd: projectDir, }); } @@ -376,7 +376,7 @@ async function installRemix(projectDir: string) { async function writeTestFiles(init: FixtureInit, dir: string) { await Promise.all( - Object.keys(init.files).map(async filename => { + Object.keys(init.files).map(async (filename) => { let filePath = path.join(dir, filename); await fse.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, init.files[filename]); @@ -464,14 +464,14 @@ async function doAndWait( }; timeoutEvent = setTimeout(() => { console.warn("Warning, wait for the address below to time out:"); - console.warn(waiting.map(a => a.url()).join("\n")); + console.warn(waiting.map((a) => a.url()).join("\n")); return clear().then(() => res(null)); }, timeout); pollEvent = setInterval(() => { if (waiting.length == 0) { return clear().then(() => res(null)); } - waiting = waiting.filter(a => a.response() == null); + waiting = waiting.filter((a) => a.response() == null); }, pollTime); }); } @@ -484,7 +484,7 @@ export function collectResponses( ): HTTPResponse[] { let responses: HTTPResponse[] = []; - page.on("response", res => { + page.on("response", (res) => { if (!filter || filter(new URL(res.url()))) { responses.push(res); } @@ -494,5 +494,5 @@ export function collectResponses( } export function collectDataResponses(page: Page) { - return collectResponses(page, url => url.searchParams.has("_data")); + return collectResponses(page, (url) => url.searchParams.has("_data")); } diff --git a/integration/helpers/global-setup.ts b/integration/helpers/global-setup.ts index 8122431cf3..c4eb8a409a 100644 --- a/integration/helpers/global-setup.ts +++ b/integration/helpers/global-setup.ts @@ -9,7 +9,7 @@ console.warn = () => {}; export default async function setup() { await fs.rm(TMP_DIR, { force: true, - recursive: true + recursive: true, }); await fs.mkdir(TMP_DIR); } diff --git a/integration/loader-test.ts b/integration/loader-test.ts index 55fb374918..f13c362672 100644 --- a/integration/loader-test.ts +++ b/integration/loader-test.ts @@ -32,15 +32,15 @@ describe("loader", () => { export default function Index() { return
} - ` - } + `, + }, }); }); it("returns responses for a specific route", async () => { let [root, index] = await Promise.all([ fixture.requestData("/", "root"), - fixture.requestData("/", "routes/index") + fixture.requestData("/", "routes/index"), ]); expect(root.headers.get("Content-Type")).toBe( diff --git a/integration/rendering-test.ts b/integration/rendering-test.ts index 101555e17c..2579fc8b55 100644 --- a/integration/rendering-test.ts +++ b/integration/rendering-test.ts @@ -2,7 +2,7 @@ import { createAppFixture, createFixture, js, - selectHtml + selectHtml, } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; @@ -35,8 +35,8 @@ describe("rendering", () => { export default function() { return

Index

; } - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/server-entry-test.ts b/integration/server-entry-test.ts index 770c707e52..ac06c427a4 100644 --- a/integration/server-entry-test.ts +++ b/integration/server-entry-test.ts @@ -28,8 +28,8 @@ describe("Server Entry", () => { export default function () { return
} - ` - } + `, + }, }); }); diff --git a/integration/splat-routes-test.ts b/integration/splat-routes-test.ts index 8d18fbf3a7..e3914f68e5 100644 --- a/integration/splat-routes-test.ts +++ b/integration/splat-routes-test.ts @@ -76,8 +76,8 @@ describe("rendering", () => { export default function() { return

${PARENTLESS_$}

} - ` - } + `, + }, }); }); diff --git a/integration/transition-test.ts b/integration/transition-test.ts index 109f8a2e3e..b92d0f7728 100644 --- a/integration/transition-test.ts +++ b/integration/transition-test.ts @@ -131,8 +131,8 @@ describe("rendering", () => {
); } - ` - } + `, + }, }); app = await createAppFixture(fixture); @@ -148,7 +148,7 @@ describe("rendering", () => { await app.clickLink(`/${PAGE}`); expect( - responses.map(res => new URL(res.url()).searchParams.get("_data")) + responses.map((res) => new URL(res.url()).searchParams.get("_data")) ).toEqual([`routes/${PAGE}`, `routes/${PAGE}/index`]); let html = await app.getHtml("main"); @@ -162,7 +162,7 @@ describe("rendering", () => { await app.clickLink(`/${PAGE}/${CHILD}`); expect( - responses.map(res => new URL(res.url()).searchParams.get("_data")) + responses.map((res) => new URL(res.url()).searchParams.get("_data")) ).toEqual([`routes/${PAGE}/${CHILD}`]); let html = await app.getHtml("main"); @@ -178,7 +178,7 @@ describe("rendering", () => { expect(new URL(app.page.url()).pathname).toBe(`/${REDIRECT_TARGET}`); expect( - responses.map(res => new URL(res.url()).searchParams.get("_data")) + responses.map((res) => new URL(res.url()).searchParams.get("_data")) ).toEqual([`routes/${REDIRECT}`, `routes/${PAGE}`, `routes/${PAGE}/index`]); let html = await app.getHtml("main"); @@ -194,7 +194,7 @@ describe("rendering", () => { await app.goBack(); expect( - responses.map(res => new URL(res.url()).searchParams.get("_data")) + responses.map((res) => new URL(res.url()).searchParams.get("_data")) ).toEqual([`routes/${PAGE}/index`]); let html = await app.getHtml("main"); diff --git a/packages/remix-dev/__tests__/build-test.ts b/packages/remix-dev/__tests__/build-test.ts index 42ba09eb8d..69595bff34 100644 --- a/packages/remix-dev/__tests__/build-test.ts +++ b/packages/remix-dev/__tests__/build-test.ts @@ -14,7 +14,7 @@ async function generateBuild(config: RemixConfig, options: BuildOptions) { } function getFilenames(output: RollupOutput) { - return output.output.map(item => item.fileName).sort(); + return output.output.map((item) => item.fileName).sort(); } describe.skip("building", () => { @@ -32,7 +32,7 @@ describe.skip("building", () => { it("generates the correct bundles", async () => { let output = await generateBuild(config, { mode: BuildMode.Development, - target: BuildTarget.Server + target: BuildTarget.Server, }); expect(getFilenames(output)).toMatchInlineSnapshot(` @@ -68,7 +68,7 @@ describe.skip("building", () => { it("generates the correct bundles", async () => { let output = await generateBuild(config, { mode: BuildMode.Production, - target: BuildTarget.Server + target: BuildTarget.Server, }); expect(getFilenames(output)).toMatchInlineSnapshot(` @@ -104,7 +104,7 @@ describe.skip("building", () => { it("generates the correct bundles", async () => { let output = await generateBuild(config, { mode: BuildMode.Development, - target: BuildTarget.Browser + target: BuildTarget.Browser, }); expect(getFilenames(output)).toMatchInlineSnapshot(` @@ -162,7 +162,7 @@ describe.skip("building", () => { it("generates the correct bundles", async () => { let output = await generateBuild(config, { mode: BuildMode.Production, - target: BuildTarget.Browser + target: BuildTarget.Browser, }); expect(getFilenames(output)).toMatchInlineSnapshot(` diff --git a/packages/remix-dev/__tests__/defineRoutes-test.ts b/packages/remix-dev/__tests__/defineRoutes-test.ts index cb5fa80341..d4766d004f 100644 --- a/packages/remix-dev/__tests__/defineRoutes-test.ts +++ b/packages/remix-dev/__tests__/defineRoutes-test.ts @@ -2,7 +2,7 @@ import { defineRoutes } from "../config/routes"; describe("defineRoutes", () => { it("returns an array of routes", () => { - let routes = defineRoutes(route => { + let routes = defineRoutes((route) => { route("/", "routes/home.js"); route("inbox", "routes/inbox.js", () => { route("/", "routes/inbox/index.js", { index: true }); @@ -60,7 +60,7 @@ describe("defineRoutes", () => { it("works with async data", async () => { // Read everything *before* calling defineRoutes. let fakeDirectory = await Promise.resolve(["one.md", "two.md"]); - let routes = defineRoutes(route => { + let routes = defineRoutes((route) => { for (let file of fakeDirectory) { route(file.replace(/\.md$/, ""), file); } diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index bf2c7092a0..d6ffd5e3ab 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -18,7 +18,7 @@ describe("readConfig", () => { appDirectory: expect.any(String), cacheDirectory: expect.any(String), serverBuildPath: expect.any(String), - assetsBuildDirectory: expect.any(String) + assetsBuildDirectory: expect.any(String), }, ` Object { diff --git a/packages/remix-dev/__tests__/routesConvention-test.ts b/packages/remix-dev/__tests__/routesConvention-test.ts index bf29137997..599dba4483 100644 --- a/packages/remix-dev/__tests__/routesConvention-test.ts +++ b/packages/remix-dev/__tests__/routesConvention-test.ts @@ -35,7 +35,7 @@ describe("createRoutePath", () => { ["beef]", "beef]"], ["[index]", "index"], ["test/inde[x]", "test/index"], - ["[i]ndex/[[].[[]]", "index/[/[]"] + ["[i]ndex/[[].[[]]", "index/[/[]"], ]; for (let [input, expected] of tests) { diff --git a/packages/remix-dev/build.ts b/packages/remix-dev/build.ts index dd3aceed8f..a598949dda 100644 --- a/packages/remix-dev/build.ts +++ b/packages/remix-dev/build.ts @@ -1,7 +1,7 @@ export enum BuildMode { Development = "development", Production = "production", - Test = "test" + Test = "test", } export function isBuildMode(mode: any): mode is BuildMode { @@ -16,7 +16,7 @@ export enum BuildTarget { Browser = "browser", // TODO: remove Server = "server", // TODO: remove CloudflareWorkers = "cloudflare-workers", - Node14 = "node14" + Node14 = "node14", } export function isBuildTarget(target: any): target is BuildTarget { diff --git a/packages/remix-dev/cache.ts b/packages/remix-dev/cache.ts index 0ce3e3df61..bbec3ceee3 100644 --- a/packages/remix-dev/cache.ts +++ b/packages/remix-dev/cache.ts @@ -7,7 +7,7 @@ export function putJson(cachePath: string, key: string, data: any) { } export function getJson(cachePath: string, key: string) { - return get(cachePath, key).then(obj => + return get(cachePath, key).then((obj) => JSON.parse(obj.data.toString("utf-8")) ); } diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index b4bf10f3a7..5ef23e9fac 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -40,18 +40,18 @@ const cli = meow(helpText, { flags: { version: { type: "boolean", - alias: "v" + alias: "v", }, json: { - type: "boolean" + type: "boolean", }, sourcemap: { - type: "boolean" + type: "boolean", }, debug: { - type: "boolean" - } - } + type: "boolean", + }, + }, }); if (cli.flags.version) { diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 498a72f235..2657da6fff 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -89,7 +89,7 @@ export async function watch( let wss = new WebSocket.Server({ port: config.devServerPort }); function broadcast(event: { type: string; [key: string]: any }) { setTimeout(() => { - wss.clients.forEach(client => { + wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(event)); } @@ -123,7 +123,7 @@ export async function watch( }, onFileDeleted(file) { log(`File deleted: ${path.relative(process.cwd(), file)}`); - } + }, }); console.log(`💿 Built in ${prettyMs(Date.now() - start)}`); @@ -132,7 +132,7 @@ export async function watch( exitHook(() => { resolve(); }); - return new Promise(r => { + return new Promise((r) => { resolve = r; }).then(async () => { wss.close(); @@ -159,7 +159,7 @@ export async function dev(remixRoot: string, modeArg?: string) { let config = await readConfig(remixRoot); let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Development; let port = await getPort({ - port: process.env.PORT ? Number(process.env.PORT) : 3000 + port: process.env.PORT ? Number(process.env.PORT) : 3000, }); if (config.serverEntryPoint) { @@ -181,7 +181,7 @@ export async function dev(remixRoot: string, modeArg?: string) { onInitialBuild: () => { let address = Object.values(os.networkInterfaces()) .flat() - .find(ip => ip?.family == "IPv4" && !ip.internal)?.address; + .find((ip) => ip?.family == "IPv4" && !ip.internal)?.address; if (!address) { address = "localhost"; @@ -190,7 +190,7 @@ export async function dev(remixRoot: string, modeArg?: string) { server = app.listen(port, () => { console.log(`Remix App Server started at http://${address}:${port}`); }); - } + }, }); } finally { server!?.close(); diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 72c3874321..def1c9c7f9 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -44,7 +44,7 @@ function defaultBuildFailureHandler(failure: Error | esbuild.BuildFailure) { if (failure.warnings) { let messages = esbuild.formatMessagesSync(failure.warnings, { kind: "warning", - color: true + color: true, }); console.warn(...messages); } @@ -52,7 +52,7 @@ function defaultBuildFailureHandler(failure: Error | esbuild.BuildFailure) { if (failure.errors) { let messages = esbuild.formatMessagesSync(failure.errors, { kind: "error", - color: true + color: true, }); console.error(...messages); } @@ -73,7 +73,7 @@ export async function build( target = BuildTarget.Node14, sourcemap = false, onWarning = defaultWarningHandler, - onBuildFailure = defaultBuildFailureHandler + onBuildFailure = defaultBuildFailureHandler, }: BuildOptions = {} ): Promise { let assetsManifestPromiseRef: AssetsManifestPromiseRef = {}; @@ -83,7 +83,7 @@ export async function build( target, sourcemap, onWarning, - onBuildFailure + onBuildFailure, }); } @@ -109,7 +109,7 @@ export async function watch( onFileCreated, onFileChanged, onFileDeleted, - onInitialBuild + onInitialBuild, }: WatchOptions = {} ): Promise<() => Promise> { let options = { @@ -118,7 +118,7 @@ export async function watch( sourcemap, onBuildFailure, onWarning, - incremental: true + incremental: true, }; let assetsManifestPromiseRef: AssetsManifestPromiseRef = {}; @@ -190,7 +190,7 @@ export async function watch( // If we get here and can't call rebuild something went wrong and we // should probably blow as it's not really recoverable. let browserBuildPromise = browserBuild.rebuild(); - let assetsManifestPromise = browserBuildPromise.then(build => + let assetsManifestPromise = browserBuildPromise.then((build) => generateAssetsManifest(config, build.metafile!) ); @@ -202,8 +202,8 @@ export async function watch( assetsManifestPromise, serverBuild .rebuild() - .then(build => writeServerBuildResult(config, build.outputFiles!)) - ]).catch(err => { + .then((build) => writeServerBuildResult(config, build.outputFiles!)), + ]).catch((err) => { disposeBuilders(); onBuildFailure(err); }); @@ -221,15 +221,15 @@ export async function watch( ignoreInitial: true, awaitWriteFinish: { stabilityThreshold: 100, - pollInterval: 100 - } + pollInterval: 100, + }, }) - .on("error", error => console.error(error)) - .on("change", async file => { + .on("error", (error) => console.error(error)) + .on("change", async (file) => { if (onFileChanged) onFileChanged(file); await rebuildEverything(); }) - .on("add", async file => { + .on("add", async (file) => { if (onFileCreated) onFileCreated(file); let newConfig: RemixConfig; try { @@ -245,7 +245,7 @@ export async function watch( await rebuildEverything(); } }) - .on("unlink", async file => { + .on("unlink", async (file) => { if (onFileDeleted) onFileDeleted(file); if (isEntryPoint(config, file)) { await restartBuilders(); @@ -285,7 +285,7 @@ async function buildEverything( ): Promise<(esbuild.BuildResult | undefined)[]> { try { let browserBuildPromise = createBrowserBuild(config, options); - let assetsManifestPromise = browserBuildPromise.then(build => + let assetsManifestPromise = browserBuildPromise.then((build) => generateAssetsManifest(config, build.metafile!) ); @@ -301,7 +301,7 @@ async function buildEverything( return await Promise.all([ assetsManifestPromise.then(() => browserBuildPromise), - serverBuildPromise + serverBuildPromise, ]); } catch (err) { options.onBuildFailure(err as Error); @@ -319,8 +319,8 @@ async function createBrowserBuild( // this is really just making sure we don't accidentally have any dependencies // on node built-ins in browser bundles. let dependencies = Object.keys(await getAppDependencies(config)); - let externals = nodeBuiltins.filter(mod => !dependencies.includes(mod)); - let fakeBuiltins = nodeBuiltins.filter(mod => dependencies.includes(mod)); + let externals = nodeBuiltins.filter((mod) => !dependencies.includes(mod)); + let fakeBuiltins = nodeBuiltins.filter((mod) => dependencies.includes(mod)); if (fakeBuiltins.length > 0) { throw new Error( @@ -331,7 +331,7 @@ async function createBrowserBuild( } let entryPoints: esbuild.BuildOptions["entryPoints"] = { - "entry.client": path.resolve(config.appDirectory, config.entryClientFile) + "entry.client": path.resolve(config.appDirectory, config.entryClientFile), }; for (let id of Object.keys(config.routes)) { // All route entry points are virtual modules that will be loaded by the @@ -366,14 +366,14 @@ async function createBrowserBuild( "process.env.NODE_ENV": JSON.stringify(options.mode), "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( config.devServerPort - ) + ), }, plugins: [ mdxPlugin(config), browserRouteModulesPlugin(config, /\?browser$/), emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/), - NodeModulesPolyfillPlugin() - ] + NodeModulesPolyfillPlugin(), + ], }); } @@ -393,7 +393,7 @@ async function createServerBuild( stdin = { contents: config.serverBuildTargetEntryModule, resolveDir: config.rootDirectory, - loader: "ts" + loader: "ts", }; } @@ -403,7 +403,7 @@ async function createServerBuild( serverRouteModulesPlugin(config), serverEntryModulePlugin(config), serverAssetsManifestPlugin(assetsManifestPromiseRef), - serverBareModulesPlugin(config, dependencies) + serverBareModulesPlugin(config, dependencies), ]; if (config.serverPlatform !== "node") { @@ -445,11 +445,11 @@ async function createServerBuild( "process.env.NODE_ENV": JSON.stringify(options.mode), "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( config.devServerPort - ) + ), }, - plugins + plugins, }) - .then(async build => { + .then(async (build) => { await writeServerBuildResult(config, build.outputFiles); return build; }); diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index 742b129755..4656c8328e 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -48,8 +48,8 @@ export async function createAssetsManifest( imports: esbuild.Metafile["outputs"][string]["imports"] ): string[] { return imports - .filter(im => im.kind === "import-statement") - .map(im => resolveUrl(im.path)); + .filter((im) => im.kind === "import-statement") + .map((im) => resolveUrl(im.path)); } let entryClientFile = path.resolve( @@ -78,7 +78,7 @@ export async function createAssetsManifest( if (entryPointFile === entryClientFile) { entry = { module: resolveUrl(key), - imports: resolveImports(output.imports) + imports: resolveImports(output.imports), }; // Only parse routes otherwise dynamic imports can fall into here and fail the build } else if (output.entryPoint.startsWith("browser-route-module:")) { @@ -96,7 +96,7 @@ export async function createAssetsManifest( hasAction: sourceExports.includes("action"), hasLoader: sourceExports.includes("loader"), hasCatchBoundary: sourceExports.includes("CatchBoundary"), - hasErrorBoundary: sourceExports.includes("ErrorBoundary") + hasErrorBoundary: sourceExports.includes("ErrorBoundary"), }; } } @@ -141,7 +141,7 @@ function optimizeRouteImports( } let routeImports = (route.imports || []).filter( - url => !parentImports.includes(url) + (url) => !parentImports.includes(url) ); // Setting `route.imports = undefined` prevents `imports: []` from showing up diff --git a/packages/remix-dev/compiler/loaders.ts b/packages/remix-dev/compiler/loaders.ts index f0d304f0be..83bee9b821 100644 --- a/packages/remix-dev/compiler/loaders.ts +++ b/packages/remix-dev/compiler/loaders.ts @@ -30,7 +30,7 @@ export const loaders: { [ext: string]: esbuild.Loader } = { ".webm": "file", ".webp": "file", ".woff": "file", - ".woff2": "file" + ".woff2": "file", }; export function getLoaderForFile(file: string): esbuild.Loader { diff --git a/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts index a5a456776e..6af2f9c091 100644 --- a/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts @@ -14,7 +14,7 @@ const browserSafeRouteExports: { [name: string]: boolean } = { handle: true, links: true, meta: true, - unstable_shouldReload: true + unstable_shouldReload: true, }; /** @@ -37,16 +37,16 @@ export function browserRouteModulesPlugin( new Map() ); - build.onResolve({ filter: suffixMatcher }, args => { + build.onResolve({ filter: suffixMatcher }, (args) => { return { path: args.path, - namespace: "browser-route-module" + namespace: "browser-route-module", }; }); build.onLoad( { filter: suffixMatcher, namespace: "browser-route-module" }, - async args => { + async (args) => { let theExports; let file = args.path.replace(suffixMatcher, ""); let route = routesByFile.get(file); @@ -56,15 +56,15 @@ export function browserRouteModulesPlugin( theExports = ( await getRouteModuleExportsCached(config, route.id) - ).filter(ex => !!browserSafeRouteExports[ex]); + ).filter((ex) => !!browserSafeRouteExports[ex]); } catch (error: any) { return { errors: [ { text: error.message, - pluginName: "browser-route-module" - } - ] + pluginName: "browser-route-module", + }, + ], }; } let spec = @@ -74,10 +74,10 @@ export function browserRouteModulesPlugin( return { contents, resolveDir: path.dirname(file), - loader: "js" + loader: "js", }; } ); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts b/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts index 54f512038c..34fc3cbb6a 100644 --- a/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts @@ -14,7 +14,7 @@ export function emptyModulesPlugin( return { name: "empty-modules", setup(build) { - build.onResolve({ filter }, args => { + build.onResolve({ filter }, (args) => { let resolved = path.resolve(args.resolveDir, args.path); if ( // Limit this behavior to modules found in only the `app` directory. @@ -32,9 +32,9 @@ export function emptyModulesPlugin( // matching export" errors in esbuild for stuff that is imported // from this file. contents: "module.exports = {};", - loader: "js" + loader: "js", }; }); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/mdx.ts b/packages/remix-dev/compiler/plugins/mdx.ts index a18b4228db..287014da15 100644 --- a/packages/remix-dev/compiler/plugins/mdx.ts +++ b/packages/remix-dev/compiler/plugins/mdx.ts @@ -12,26 +12,26 @@ export function mdxPlugin(config: RemixConfig): esbuild.Plugin { async setup(build) { let [xdm, { default: remarkFrontmatter }] = await Promise.all([ import("xdm"), - import("remark-frontmatter") as any + import("remark-frontmatter") as any, ]); - build.onResolve({ filter: /\.mdx?$/ }, args => { + build.onResolve({ filter: /\.mdx?$/ }, (args) => { return { path: args.path.startsWith("~/") ? path.resolve(config.appDirectory, args.path.replace(/^~\//, "")) : path.resolve(args.resolveDir, args.path), - namespace: "mdx" + namespace: "mdx", }; }); - build.onLoad({ filter: /\.mdx?$/ }, async args => { + build.onLoad({ filter: /\.mdx?$/ }, async (args) => { try { let contents = await fsp.readFile(args.path, "utf-8"); let rehypePlugins = []; let remarkPlugins = [ remarkFrontmatter, - [remarkMdxFrontmatter, { name: "attributes" }] + [remarkMdxFrontmatter, { name: "attributes" }], ]; switch (typeof config.mdx) { @@ -60,7 +60,7 @@ export const links = undefined; pragma: "React.createElement", pragmaFrag: "React.Fragment", rehypePlugins, - remarkPlugins + remarkPlugins, }); contents = ` @@ -70,7 +70,7 @@ ${remixExports}`; let errors: esbuild.PartialMessage[] = []; let warnings: esbuild.PartialMessage[] = []; - compiled.messages.forEach(message => { + compiled.messages.forEach((message) => { let toPush = message.fatal ? errors : warnings; toPush.push({ location: @@ -83,12 +83,12 @@ ${remixExports}`; line: typeof message.line === "number" ? message.line - : undefined + : undefined, } : undefined, text: message.message, detail: - typeof message.note === "string" ? message.note : undefined + typeof message.note === "string" ? message.note : undefined, }); }); @@ -97,18 +97,18 @@ ${remixExports}`; warnings: warnings.length ? warnings : undefined, contents, resolveDir: path.dirname(args.path), - loader: getLoaderForFile(args.path) + loader: getLoaderForFile(args.path), }; } catch (err: any) { return { errors: [ { - text: err.message - } - ] + text: err.message, + }, + ], }; } }); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts b/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts index 9de59736d9..fd434042cc 100644 --- a/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts @@ -22,7 +22,7 @@ export function serverAssetsManifestPlugin( build.onResolve({ filter }, ({ path }) => { return { path, - namespace: "server-assets-manifest" + namespace: "server-assets-manifest", }; }); @@ -36,9 +36,9 @@ export function serverAssetsManifestPlugin( return { contents: `export default ${jsesc(manifest, { es6: true })};`, - loader: "js" + loader: "js", }; }); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts index 08472566a2..14e978c157 100644 --- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -5,7 +5,7 @@ import type { Plugin } from "esbuild"; import type { RemixConfig } from "../../config"; import { serverBuildVirtualModule, - assetsManifestVirtualModule + assetsManifestVirtualModule, } from "../virtualModules"; /** @@ -85,10 +85,10 @@ export function serverBareModulesPlugin( // Externalize everything else if we've gotten here. return { path, - external: true + external: true, }; }); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/serverEntryModulePlugin.ts b/packages/remix-dev/compiler/plugins/serverEntryModulePlugin.ts index 8820dadcb7..79e20f4f41 100644 --- a/packages/remix-dev/compiler/plugins/serverEntryModulePlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverEntryModulePlugin.ts @@ -4,7 +4,7 @@ import type { Plugin } from "esbuild"; import type { RemixConfig } from "../../config"; import { serverBuildVirtualModule, - assetsManifestVirtualModule + assetsManifestVirtualModule, } from "../virtualModules"; /** @@ -22,7 +22,7 @@ export function serverEntryModulePlugin(config: RemixConfig): Plugin { build.onResolve({ filter }, ({ path }) => { return { path, - namespace: "server-entry-module" + namespace: "server-entry-module", }; }); @@ -60,9 +60,9 @@ ${Object.keys(config.routes) }`; }) .join(",\n ")} - };` + };`, }; }); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts index 567c2a0aa5..08f94eabd1 100644 --- a/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts @@ -14,18 +14,18 @@ export function serverRouteModulesPlugin(config: RemixConfig): esbuild.Plugin { name: "server-route-modules", setup(build) { let routeFiles = new Set( - Object.keys(config.routes).map(key => + Object.keys(config.routes).map((key) => path.resolve(config.appDirectory, config.routes[key].file) ) ); - build.onResolve({ filter: /.*/ }, args => { + build.onResolve({ filter: /.*/ }, (args) => { if (routeFiles.has(args.path)) { return { path: args.path, namespace: "route" }; } }); - build.onLoad({ filter: /.*/, namespace: "route" }, async args => { + build.onLoad({ filter: /.*/, namespace: "route" }, async (args) => { let file = args.path; let contents = await fsp.readFile(file, "utf-8"); @@ -40,9 +40,9 @@ export function serverRouteModulesPlugin(config: RemixConfig): esbuild.Plugin { return { contents, resolveDir: path.dirname(file), - loader: getLoaderForFile(file) + loader: getLoaderForFile(file), }; }); - } + }, }; } diff --git a/packages/remix-dev/compiler/routes.ts b/packages/remix-dev/compiler/routes.ts index 87a49c2e6b..06b9f36a59 100644 --- a/packages/remix-dev/compiler/routes.ts +++ b/packages/remix-dev/compiler/routes.ts @@ -47,14 +47,14 @@ export async function getRouteModuleExports( ): Promise { let result = await esbuild.build({ entryPoints: [ - path.resolve(config.appDirectory, config.routes[routeId].file) + path.resolve(config.appDirectory, config.routes[routeId].file), ], platform: "neutral", format: "esm", metafile: true, write: false, logLevel: "silent", - plugins: [mdxPlugin(config)] + plugins: [mdxPlugin(config)], }); let metafile = result.metafile!; diff --git a/packages/remix-dev/compiler/utils/crypto.ts b/packages/remix-dev/compiler/utils/crypto.ts index cc8eae86c3..dd38a03eac 100644 --- a/packages/remix-dev/compiler/utils/crypto.ts +++ b/packages/remix-dev/compiler/utils/crypto.ts @@ -10,8 +10,8 @@ export async function getFileHash(file: string): Promise { return new Promise((accept, reject) => { let hash = createHash("sha256"); fs.createReadStream(file) - .on("error", error => reject(error)) - .on("data", data => hash.update(data)) + .on("error", (error) => reject(error)) + .on("data", (data) => hash.update(data)) .on("close", () => { accept(hash.digest("hex")); }); diff --git a/packages/remix-dev/compiler/virtualModules.ts b/packages/remix-dev/compiler/virtualModules.ts index 05cb975ed4..e7df45fbd9 100644 --- a/packages/remix-dev/compiler/virtualModules.ts +++ b/packages/remix-dev/compiler/virtualModules.ts @@ -5,10 +5,10 @@ interface VirtualModule { export const serverBuildVirtualModule: VirtualModule = { id: "@remix-run/dev/server-build", - filter: /^@remix-run\/dev\/server-build$/ + filter: /^@remix-run\/dev\/server-build$/, }; export const assetsManifestVirtualModule: VirtualModule = { id: "@remix-run/dev/assets-manifest", - filter: /^@remix-run\/dev\/assets-manifest$/ + filter: /^@remix-run\/dev\/assets-manifest$/, }; diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 292beb0f2a..be5e2ad224 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -369,7 +369,7 @@ export async function readConfig( } let routes: RouteManifest = { - root: { path: "", id: "root", file: rootRouteFile } + root: { path: "", id: "root", file: rootRouteFile }, }; if (fse.existsSync(path.resolve(appDirectory, "routes"))) { let conventionalRoutes = defineConventionalRoutes( @@ -414,7 +414,7 @@ export async function readConfig( serverBuildTargetEntryModule, serverEntryPoint: customServerEntryPoint, serverDependenciesToBundle, - mdx + mdx, }; } diff --git a/packages/remix-dev/config/format.ts b/packages/remix-dev/config/format.ts index 1317a54aae..c3227f6232 100644 --- a/packages/remix-dev/config/format.ts +++ b/packages/remix-dev/config/format.ts @@ -2,7 +2,7 @@ import type { RouteManifest } from "./routes"; export enum RoutesFormat { json = "json", - jsx = "jsx" + jsx = "jsx", } export function isRoutesFormat(format: any): format is RoutesFormat { @@ -35,7 +35,7 @@ export function formatRoutesAsJson(routeManifest: RouteManifest): string { parentId?: string ): JsonFormattedRoute[] | undefined { let routes = Object.values(routeManifest).filter( - route => route.parentId === parentId + (route) => route.parentId === parentId ); let children = []; @@ -47,7 +47,7 @@ export function formatRoutesAsJson(routeManifest: RouteManifest): string { path: route.path, caseSensitive: route.caseSensitive, file: route.file, - children: handleRoutesRecursive(route.id) + children: handleRoutesRecursive(route.id), }); } @@ -65,7 +65,7 @@ export function formatRoutesAsJsx(routeManifest: RouteManifest) { function handleRoutesRecursive(parentId?: string, level = 1): boolean { let routes = Object.values(routeManifest).filter( - route => route.parentId === parentId + (route) => route.parentId === parentId ); let indent = Array(level * 2) diff --git a/packages/remix-dev/config/routes.ts b/packages/remix-dev/config/routes.ts index 1fad6a03a3..9d97158d03 100644 --- a/packages/remix-dev/config/routes.ts +++ b/packages/remix-dev/config/routes.ts @@ -146,7 +146,7 @@ export function defineRoutes( parentRoutes.length > 0 ? parentRoutes[parentRoutes.length - 1].id : undefined, - file + file, }; routes[route.id] = route; diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 2ff0143af1..4fb5fc3211 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -29,10 +29,10 @@ export function defineConventionalRoutes( let files: { [routeId: string]: string } = {}; // First, find all route modules in app/routes - visitFiles(path.join(appDir, "routes"), file => { + visitFiles(path.join(appDir, "routes"), (file) => { if ( ignoredFilePatterns && - ignoredFilePatterns.some(pattern => minimatch(file, pattern)) + ignoredFilePatterns.some((pattern) => minimatch(file, pattern)) ) { return; } @@ -58,7 +58,7 @@ export function defineConventionalRoutes( parentId?: string ): void { let childRouteIds = routeIds.filter( - id => findParentRouteId(routeIds, id) === parentId + (id) => findParentRouteId(routeIds, id) === parentId ); for (let routeId of childRouteIds) { @@ -86,7 +86,7 @@ export function defineConventionalRoutes( if (isIndexRoute) { let invalidChildRoutes = routeIds.filter( - id => findParentRouteId(routeIds, id) === routeId + (id) => findParentRouteId(routeIds, id) === routeId ); if (invalidChildRoutes.length > 0) { @@ -96,7 +96,7 @@ export function defineConventionalRoutes( } defineRoute(routePath, files[routeId], { - index: true + index: true, }); } else { defineRoute(routePath, files[routeId], () => { @@ -197,7 +197,7 @@ function findParentRouteId( routeIds: string[], childRouteId: string ): string | undefined { - return routeIds.find(id => childRouteId.startsWith(`${id}/`)); + return routeIds.find((id) => childRouteId.startsWith(`${id}/`)); } function byLongestFirst(a: string, b: string): number { diff --git a/packages/remix-dev/config/serverModes.ts b/packages/remix-dev/config/serverModes.ts index 387cfa5cf2..a04829da43 100644 --- a/packages/remix-dev/config/serverModes.ts +++ b/packages/remix-dev/config/serverModes.ts @@ -4,7 +4,7 @@ export enum ServerMode { Development = "development", Production = "production", - Test = "test" + Test = "test", } export function isValidServerMode(mode: string): mode is ServerMode { diff --git a/packages/remix-dev/setup.ts b/packages/remix-dev/setup.ts index ce281c3368..31306eb182 100644 --- a/packages/remix-dev/setup.ts +++ b/packages/remix-dev/setup.ts @@ -5,7 +5,7 @@ export enum SetupPlatform { CloudflarePages = "cloudflare-pages", CloudflareWorkers = "cloudflare-workers", Node = "node", - Deno = "deno" + Deno = "deno", } export function isSetupPlatform(platform: any): platform is SetupPlatform { @@ -13,7 +13,7 @@ export function isSetupPlatform(platform: any): platform is SetupPlatform { SetupPlatform.CloudflarePages, SetupPlatform.CloudflareWorkers, SetupPlatform.Node, - SetupPlatform.Deno + SetupPlatform.Deno, ].includes(platform); } @@ -74,7 +74,7 @@ export async function setupRemix(platform: SetupPlatform): Promise { [ path.join(platformExportsDir, "esm"), path.join(serverExportsDir, "esm"), - path.join(clientExportsDir, "esm") + path.join(clientExportsDir, "esm"), ], ".js" ); diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index de9667973d..a342980096 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -8,7 +8,7 @@ import { Readable } from "stream"; import { createRemixHeaders, createRemixRequest, - createRequestHandler + createRequestHandler, } from "../server"; // We don't want to test that the remix server works here (that's what the @@ -27,7 +27,7 @@ function createApp() { createRequestHandler({ // We don't have a real app to test, but it doesn't matter. We // won't ever call through to the real createRequestHandler - build: undefined + build: undefined, }) ); @@ -45,7 +45,7 @@ describe("express createRequestHandler", () => { }); it("handles requests", async () => { - mockedCreateRequestHandler.mockImplementation(() => async req => { + mockedCreateRequestHandler.mockImplementation(() => async (req) => { return new Response(`URL: ${new URL(req.url).pathname}`); }); @@ -119,7 +119,7 @@ describe("express createRequestHandler", () => { expect(res.headers["set-cookie"]).toEqual([ "first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax", "second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax", - "third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax" + "third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax", ]); }); }); @@ -197,8 +197,8 @@ describe("express createRemixHeaders", () => { createRemixHeaders({ "set-cookie": [ "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax", - "__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax" - ] + "__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax", + ], }) ).toMatchInlineSnapshot(` Headers { @@ -223,8 +223,8 @@ describe("express createRemixRequest", () => { hostname: "localhost", headers: { "Cache-Control": "max-age=300, s-maxage=3600", - Host: "localhost:3000" - } + Host: "localhost:3000", + }, }); expect(createRemixRequest(expressRequest)).toMatchInlineSnapshot(` diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index b0debf891c..ff65695bdd 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -3,18 +3,18 @@ import type * as express from "express"; import type { AppLoadContext, ServerBuild, - ServerPlatform + ServerPlatform, } from "@remix-run/server-runtime"; import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; import type { RequestInit as NodeRequestInit, - Response as NodeResponse + Response as NodeResponse, } from "@remix-run/node"; import { // This has been added as a global in node 15+ AbortController, Headers as NodeHeaders, - Request as NodeRequest + Request as NodeRequest, } from "@remix-run/node"; /** @@ -37,7 +37,7 @@ export type RequestHandler = ReturnType; export function createRequestHandler({ build, getLoadContext, - mode = process.env.NODE_ENV + mode = process.env.NODE_ENV, }: { build: ServerBuild; getLoadContext?: GetLoadContextFunction; @@ -104,7 +104,7 @@ export function createRemixRequest( method: req.method, headers: createRemixHeaders(req.headers), signal: abortController?.signal, - abortController + abortController, }; if (req.method !== "GET" && req.method !== "HEAD") { diff --git a/packages/remix-node/__tests__/fetch-test.ts b/packages/remix-node/__tests__/fetch-test.ts index 3062e3e6d5..e1dda7584d 100644 --- a/packages/remix-node/__tests__/fetch-test.ts +++ b/packages/remix-node/__tests__/fetch-test.ts @@ -24,8 +24,8 @@ let test = { "Content-Type: application/octet-stream", "", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", - "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" - ].join("\r\n") + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--", + ].join("\r\n"), ], boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", expected: [ @@ -36,7 +36,7 @@ let test = { false, false, "7bit", - "text/plain" + "text/plain", ], [ "field", @@ -45,7 +45,7 @@ let test = { false, false, "7bit", - "text/plain" + "text/plain", ], [ "file", @@ -54,7 +54,7 @@ let test = { 0, "1k_a.dat", "7bit", - "application/octet-stream" + "application/octet-stream", ], [ "file", @@ -63,10 +63,10 @@ let test = { 0, "1k_b.dat", "7bit", - "application/octet-stream" - ] + "application/octet-stream", + ], ], - what: "Fields and files" + what: "Fields and files", }; describe("Request", () => { @@ -74,14 +74,14 @@ describe("Request", () => { it("clones", async () => { let body = new PassThrough(); - test.source.forEach(chunk => body.write(chunk)); + test.source.forEach((chunk) => body.write(chunk)); let req = new Request("http://test.com", { method: "post", body, headers: { - "Content-Type": "multipart/form-data; boundary=" + test.boundary - } + "Content-Type": "multipart/form-data; boundary=" + test.boundary, + }, }); let cloned = req.clone(); diff --git a/packages/remix-node/__tests__/formData-test.ts b/packages/remix-node/__tests__/formData-test.ts index b0cc11fb3f..1f483ef44f 100644 --- a/packages/remix-node/__tests__/formData-test.ts +++ b/packages/remix-node/__tests__/formData-test.ts @@ -14,7 +14,7 @@ describe("FormData", () => { expect(results).toEqual([ ["single", "heyo"], ["multi", "one"], - ["multi", "two"] + ["multi", "two"], ]); }); diff --git a/packages/remix-node/__tests__/parseMultipartFormData-test.ts b/packages/remix-node/__tests__/parseMultipartFormData-test.ts index 2dd3847047..7cd02866d1 100644 --- a/packages/remix-node/__tests__/parseMultipartFormData-test.ts +++ b/packages/remix-node/__tests__/parseMultipartFormData-test.ts @@ -14,7 +14,7 @@ describe("internalParseFormData", () => { let req = new NodeRequest("https://test.com", { method: "post", - body: formData as any + body: formData as any, }); let uploadHandler = createMemoryUploadHandler({}); diff --git a/packages/remix-node/__tests__/sessions-test.ts b/packages/remix-node/__tests__/sessions-test.ts index 6e617c96af..005c4d1fa7 100644 --- a/packages/remix-node/__tests__/sessions-test.ts +++ b/packages/remix-node/__tests__/sessions-test.ts @@ -22,7 +22,7 @@ describe("File session storage", () => { it("persists session data across requests", async () => { let { getSession, commitSession } = createFileSessionStorage({ dir, - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -35,7 +35,7 @@ describe("File session storage", () => { it("returns an empty session for cookies that are not signed properly", async () => { let { getSession, commitSession } = createFileSessionStorage({ dir, - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -55,7 +55,7 @@ describe("File session storage", () => { it("unsigns old session cookies using the old secret and encodes new cookies using the new secret", async () => { let { getSession, commitSession } = createFileSessionStorage({ dir, - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -67,7 +67,7 @@ describe("File session storage", () => { // A new secret enters the rotation... let storage = createFileSessionStorage({ dir, - cookie: { secrets: ["secret2", "secret1"] } + cookie: { secrets: ["secret2", "secret1"] }, }); getSession = storage.getSession; commitSession = storage.commitSession; diff --git a/packages/remix-node/cookieSigning.ts b/packages/remix-node/cookieSigning.ts index 1cbb5e5ad9..10c56a71aa 100644 --- a/packages/remix-node/cookieSigning.ts +++ b/packages/remix-node/cookieSigning.ts @@ -1,7 +1,7 @@ import cookieSignature from "cookie-signature"; import type { InternalSignFunctionDoNotUseMe, - InternalUnsignFunctionDoNotUseMe + InternalUnsignFunctionDoNotUseMe, } from "@remix-run/server-runtime/cookieSigning"; export const sign: InternalSignFunctionDoNotUseMe = async (value, secret) => { diff --git a/packages/remix-node/fetch.ts b/packages/remix-node/fetch.ts index 0ca71d1ea8..a79f872082 100644 --- a/packages/remix-node/fetch.ts +++ b/packages/remix-node/fetch.ts @@ -34,7 +34,7 @@ function formDataToStream(formData: NodeFormData): FormStream { } passthrough.push(null); }) - .catch(error => { + .catch((error) => { passthrough.emit("error", error); }); @@ -49,13 +49,13 @@ function formDataToStream(formData: NodeFormData): FormStream { formStream.append(key, stream, { filename: value.name, contentType: value.type, - knownLength: value.size + knownLength: value.size, }); } else { let file = value as File; let stream = toNodeStream(file.stream()); formStream.append(key, stream, { - filename: "unknown" + filename: "unknown", }); } } @@ -74,7 +74,7 @@ class NodeRequest extends BaseNodeRequest { if (init?.body instanceof NodeFormData) { init = { ...init, - body: formDataToStream(init.body) + body: formDataToStream(init.body), }; } @@ -124,7 +124,7 @@ export function fetch( if (init?.body instanceof NodeFormData) { init = { ...init, - body: formDataToStream(init.body) + body: formDataToStream(init.body), }; } diff --git a/packages/remix-node/formData.ts b/packages/remix-node/formData.ts index 13cee0c213..2e4cb8ceb0 100644 --- a/packages/remix-node/formData.ts +++ b/packages/remix-node/formData.ts @@ -91,14 +91,14 @@ class NodeFormData implements FormData { thisArg?: any ): void { Object.entries(this._fields).forEach(([name, values]) => { - values.forEach(value => callbackfn(value, name, thisArg), thisArg); + values.forEach((value) => callbackfn(value, name, thisArg), thisArg); }); } entries(): IterableIterator<[string, FormDataEntryValue]> { return Object.entries(this._fields) .reduce((entries, [name, values]) => { - values.forEach(value => entries.push([name, value])); + values.forEach((value) => entries.push([name, value])); return entries; }, [] as [string, FormDataEntryValue][]) .values(); @@ -111,7 +111,7 @@ class NodeFormData implements FormData { values(): IterableIterator { return Object.entries(this._fields) .reduce((results, [name, values]) => { - values.forEach(value => results.push(value)); + values.forEach((value) => results.push(value)); return results; }, [] as FormDataEntryValue[]) .values(); diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts index 168bf02f7a..4a3a94090a 100644 --- a/packages/remix-node/globals.ts +++ b/packages/remix-node/globals.ts @@ -1,6 +1,6 @@ import type { InternalSignFunctionDoNotUseMe, - InternalUnsignFunctionDoNotUseMe + InternalUnsignFunctionDoNotUseMe, } from "@remix-run/server-runtime/cookieSigning"; import { Blob as NodeBlob, File as NodeFile } from "@web-std/file"; @@ -10,7 +10,7 @@ import { Headers as NodeHeaders, Request as NodeRequest, Response as NodeResponse, - fetch as nodeFetch + fetch as nodeFetch, } from "./fetch"; import { FormData as NodeFormData } from "./formData"; diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index 74fc3bbb72..34eef8735c 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -8,7 +8,7 @@ export type { HeadersInit, RequestInfo, RequestInit, - ResponseInit + ResponseInit, } from "./fetch"; export { Headers, Request, Response, fetch } from "./fetch"; @@ -23,6 +23,6 @@ export { createFileSessionStorage } from "./sessions/fileStorage"; export { createFileUploadHandler as unstable_createFileUploadHandler, - NodeOnDiskFile + NodeOnDiskFile, } from "./upload/fileUploadHandler"; export { createMemoryUploadHandler as unstable_createMemoryUploadHandler } from "./upload/memoryUploadHandler"; diff --git a/packages/remix-node/magicExports/platform.ts b/packages/remix-node/magicExports/platform.ts index dedf86de68..7d3c27fc3a 100644 --- a/packages/remix-node/magicExports/platform.ts +++ b/packages/remix-node/magicExports/platform.ts @@ -5,7 +5,7 @@ export { createFileSessionStorage, unstable_createFileUploadHandler, unstable_createMemoryUploadHandler, - unstable_parseMultipartFormData + unstable_parseMultipartFormData, } from "@remix-run/node"; export type { UploadHandler, UploadHandlerArgs } from "@remix-run/node"; diff --git a/packages/remix-node/parseMultipartFormData.ts b/packages/remix-node/parseMultipartFormData.ts index d9f43c1e46..7597c1ca62 100644 --- a/packages/remix-node/parseMultipartFormData.ts +++ b/packages/remix-node/parseMultipartFormData.ts @@ -37,8 +37,8 @@ export async function internalParseFormData( let busboy = new Busboy({ highWaterMark: 2 * 1024 * 1024, headers: { - "content-type": contentType - } + "content-type": contentType, + }, }); let aborted = false; @@ -68,7 +68,7 @@ export async function internalParseFormData( stream: filestream, filename, encoding, - mimetype + mimetype, }); if (typeof value !== "undefined") { diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts index a58ad41430..ca0f94a369 100644 --- a/packages/remix-node/sessions/fileStorage.ts +++ b/packages/remix-node/sessions/fileStorage.ts @@ -3,7 +3,7 @@ import { promises as fsp } from "fs"; import * as path from "path"; import type { SessionStorage, - SessionIdStorageStrategy + SessionIdStorageStrategy, } from "@remix-run/server-runtime"; import { createSessionStorage } from "@remix-run/server-runtime"; @@ -30,7 +30,7 @@ interface FileSessionStorageOptions { */ export function createFileSessionStorage({ cookie, - dir + dir, }: FileSessionStorageOptions): SessionStorage { return createSessionStorage({ cookie, @@ -92,7 +92,7 @@ export function createFileSessionStorage({ } catch (error: any) { if (error.code !== "ENOENT") throw error; } - } + }, }); } diff --git a/packages/remix-node/upload/fileUploadHandler.ts b/packages/remix-node/upload/fileUploadHandler.ts index 5dc5ddf7eb..b9b8aec687 100644 --- a/packages/remix-node/upload/fileUploadHandler.ts +++ b/packages/remix-node/upload/fileUploadHandler.ts @@ -85,7 +85,7 @@ export function createFileUploadHandler({ avoidFileConflicts = true, file = defaultFilePathResolver, filter, - maxFileSize = 3000000 + maxFileSize = 3000000, }: FileUploadHandlerOptions): UploadHandler { return async ({ name, stream, filename, encoding, mimetype }) => { if (filter && !(await filter({ filename, encoding, mimetype }))) { @@ -170,9 +170,9 @@ export class NodeOnDiskFile implements File { return new Promise((resolve, reject) => { const buf: any[] = []; - stream.on("data", chunk => buf.push(chunk)); + stream.on("data", (chunk) => buf.push(chunk)); stream.on("end", () => resolve(Buffer.concat(buf))); - stream.on("error", err => reject(err)); + stream.on("error", (err) => reject(err)); }); } diff --git a/packages/remix-node/upload/memoryUploadHandler.ts b/packages/remix-node/upload/memoryUploadHandler.ts index 572cb592e6..5dde5b803f 100644 --- a/packages/remix-node/upload/memoryUploadHandler.ts +++ b/packages/remix-node/upload/memoryUploadHandler.ts @@ -28,7 +28,7 @@ export type MemoryUploadHandlerOptions = { export function createMemoryUploadHandler({ filter, - maxFileSize = 3000000 + maxFileSize = 3000000, }: MemoryUploadHandlerOptions): UploadHandler { return async ({ name, stream, filename, encoding, mimetype }) => { if (filter && !(await filter({ filename, encoding, mimetype }))) { @@ -63,7 +63,7 @@ export function createMemoryUploadHandler({ }); return new BufferFile(bufferStream.data, filename, { - type: mimetype + type: mimetype, }); }; } diff --git a/packages/remix-serve/cli.ts b/packages/remix-serve/cli.ts index 1a1e33511a..c8b8036377 100644 --- a/packages/remix-serve/cli.ts +++ b/packages/remix-serve/cli.ts @@ -18,7 +18,7 @@ let buildPath = path.resolve(process.cwd(), buildPathArg); createApp(buildPath).listen(port, () => { let address = Object.values(os.networkInterfaces()) .flat() - .find(ip => ip?.family == "IPv4" && !ip.internal)?.address; + .find((ip) => ip?.family == "IPv4" && !ip.internal)?.address; if (!address) { address = "localhost"; diff --git a/packages/remix-server-runtime/__tests__/cookies-test.ts b/packages/remix-server-runtime/__tests__/cookies-test.ts index 5e1411c740..2b6552b21f 100644 --- a/packages/remix-server-runtime/__tests__/cookies-test.ts +++ b/packages/remix-server-runtime/__tests__/cookies-test.ts @@ -44,7 +44,7 @@ describe("cookies", () => { it("parses/serializes signed string values", async () => { let cookie = createCookie("my-cookie", { - secrets: ["secret1"] + secrets: ["secret1"], }); let setCookie = await cookie.serialize("hello michael"); let value = await cookie.parse(getCookieFromSetCookie(setCookie)); @@ -54,11 +54,11 @@ describe("cookies", () => { it("fails to parses signed string values with invalid signature", async () => { let cookie = createCookie("my-cookie", { - secrets: ["secret1"] + secrets: ["secret1"], }); let setCookie = await cookie.serialize("hello michael"); let cookie2 = createCookie("my-cookie", { - secrets: ["secret2"] + secrets: ["secret2"], }); let value = await cookie2.parse(getCookieFromSetCookie(setCookie)); @@ -67,7 +67,7 @@ describe("cookies", () => { it("parses/serializes signed object values", async () => { let cookie = createCookie("my-cookie", { - secrets: ["secret1"] + secrets: ["secret1"], }); let setCookie = await cookie.serialize({ hello: "mjackson" }); let value = await cookie.parse(getCookieFromSetCookie(setCookie)); @@ -81,11 +81,11 @@ describe("cookies", () => { it("fails to parse signed object values with invalid signature", async () => { let cookie = createCookie("my-cookie", { - secrets: ["secret1"] + secrets: ["secret1"], }); let setCookie = await cookie.serialize({ hello: "mjackson" }); let cookie2 = createCookie("my-cookie", { - secrets: ["secret2"] + secrets: ["secret2"], }); let value = await cookie2.parse(getCookieFromSetCookie(setCookie)); @@ -94,7 +94,7 @@ describe("cookies", () => { it("supports secret rotation", async () => { let cookie = createCookie("my-cookie", { - secrets: ["secret1"] + secrets: ["secret1"], }); let setCookie = await cookie.serialize({ hello: "mjackson" }); let value = await cookie.parse(getCookieFromSetCookie(setCookie)); @@ -107,7 +107,7 @@ describe("cookies", () => { // A new secret enters the rotation... cookie = createCookie("my-cookie", { - secrets: ["secret2", "secret1"] + secrets: ["secret2", "secret1"], }); // cookie should still be able to parse old cookies. @@ -128,7 +128,7 @@ describe("cookies", () => { let cookie2 = createCookie("my-cookie2"); let setCookie2 = await cookie2.serialize("hello world", { - path: "/about" + path: "/about", }); expect(setCookie2).toContain("Path=/about"); }); diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index 5c08f6c525..a5187d23f2 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -19,11 +19,11 @@ describe("loaders", () => { id: routeId, path: "/random", module: { - loader - } - } + loader, + }, + }, }, - entry: { module: {} } + entry: { module: {} }, } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -32,8 +32,8 @@ describe("loaders", () => { "http://example.com/random?_data=routes/random&foo=bar", { headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, } ); @@ -45,8 +45,8 @@ describe("loaders", () => { let loader = async ({ request }) => { throw new Response("null", { headers: { - "Content-type": "application/json" - } + "Content-type": "application/json", + }, }); }; @@ -57,11 +57,11 @@ describe("loaders", () => { id: routeId, path: "/random", module: { - loader - } - } + loader, + }, + }, }, - entry: { module: {} } + entry: { module: {} }, } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -70,8 +70,8 @@ describe("loaders", () => { "http://example.com/random?_data=routes/random&foo=bar", { headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, } ); @@ -91,11 +91,11 @@ describe("loaders", () => { id: routeId, path: "/random", module: { - loader - } - } + loader, + }, + }, }, - entry: { module: {} } + entry: { module: {} }, } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -104,8 +104,8 @@ describe("loaders", () => { "http://example.com/random?_data=routes/random&index&foo=bar", { headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, } ); @@ -125,11 +125,11 @@ describe("loaders", () => { id: routeId, path: "/random", module: { - loader - } - } + loader, + }, + }, }, - entry: { module: {} } + entry: { module: {} }, } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -138,8 +138,8 @@ describe("loaders", () => { "http://example.com/random?_data=routes/random&index&foo=bar&index=test", { headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, } ); @@ -160,9 +160,9 @@ describe("loaders", () => { route: { id: routeId, module: { - loader - } - } + loader, + }, + }, } as unknown as RouteMatch; let possibleError: any; @@ -170,7 +170,7 @@ describe("loaders", () => { possibleError = await callRouteLoader({ request, match, - loadContext: {} + loadContext: {}, }); } catch (error) { possibleError = error; @@ -197,9 +197,9 @@ describe("actions", () => { route: { id: routeId, module: { - action - } - } + action, + }, + }, } as unknown as RouteMatch; let possibleError: any; @@ -207,7 +207,7 @@ describe("actions", () => { possibleError = await callRouteAction({ request, match, - loadContext: {} + loadContext: {}, }); } catch (error) { possibleError = error; diff --git a/packages/remix-server-runtime/__tests__/responses-test.ts b/packages/remix-server-runtime/__tests__/responses-test.ts index c431bd75ac..d680717af0 100644 --- a/packages/remix-server-runtime/__tests__/responses-test.ts +++ b/packages/remix-server-runtime/__tests__/responses-test.ts @@ -14,8 +14,8 @@ describe("json", () => { { headers: { "Content-Type": "application/json; charset=iso-8859-1", - "X-Remix": "is awesome" - } + "X-Remix": "is awesome", + }, } ); @@ -45,8 +45,8 @@ describe("redirect", () => { it("sets the status to 302 when only headers are given", () => { let response = redirect("/login", { headers: { - "X-Remix": "is awesome" - } + "X-Remix": "is awesome", + }, }); expect(response.status).toEqual(302); }); @@ -60,8 +60,8 @@ describe("redirect", () => { let response = redirect("/login", { headers: { Location: "/", - "X-Remix": "is awesome" - } + "X-Remix": "is awesome", + }, }); expect(response.headers.get("Location")).toEqual("/login"); diff --git a/packages/remix-server-runtime/__tests__/server-test.ts b/packages/remix-server-runtime/__tests__/server-test.ts index b3f479be61..be6f6041cb 100644 --- a/packages/remix-server-runtime/__tests__/server-test.ts +++ b/packages/remix-server-runtime/__tests__/server-test.ts @@ -23,10 +23,10 @@ describe("server", () => { let build: ServerBuild = { entry: { module: { - default: async request => { + default: async (request) => { return new Response(`${request.method}, ${request.url}`); - } - } + }, + }, }, routes: { [routeId]: { @@ -35,9 +35,9 @@ describe("server", () => { module: { action: () => "ACTION", loader: () => "LOADER", - default: () => "COMPONENT" - } - } + default: () => "COMPONENT", + }, + }, }, assets: { routes: { @@ -47,10 +47,10 @@ describe("server", () => { hasLoader: true, id: routeId, module: routeId, - path: "" - } - } - } + path: "", + }, + }, + }, } as unknown as ServerBuild; describe("createRequestHandler", () => { @@ -64,14 +64,14 @@ describe("server", () => { ["DELETE", "/"], ["DELETE", "/_data=root"], ["PATCH", "/"], - ["PATCH", "/_data=root"] + ["PATCH", "/_data=root"], ]; for (let [method, to] of allowThrough) { it(`allows through ${method} request to ${to}`, async () => { let handler = createRequestHandler(build, {}); let response = await handler( new Request(`http://localhost:3000${to}`, { - method + method, }) ); @@ -83,7 +83,7 @@ describe("server", () => { let handler = createRequestHandler(build, {}); let response = await handler( new Request("http://localhost:3000/", { - method: "HEAD" + method: "HEAD", }) ); @@ -112,12 +112,12 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/resource": { loader: resourceLoader, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -143,16 +143,16 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/resource": { loader: resourceLoader, - path: "resource" + path: "resource", }, "routes/resource.sub": { loader: subResourceLoader, - path: "resource/sub" - } + path: "resource/sub", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -176,12 +176,12 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/resource": { loader: resourceLoader, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -202,8 +202,8 @@ describe("shared server runtime", () => { let build = mockServerBuild({ "routes/resource": { loader, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -221,8 +221,8 @@ describe("shared server runtime", () => { let build = mockServerBuild({ "routes/resource": { loader, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Development); @@ -243,12 +243,12 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - action: rootAction + action: rootAction, }, "routes/resource": { action: resourceAction, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -274,16 +274,16 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - action: rootAction + action: rootAction, }, "routes/resource": { action: resourceAction, - path: "resource" + path: "resource", }, "routes/resource.sub": { action: subResourceAction, - path: "resource/sub" - } + path: "resource/sub", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -307,12 +307,12 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - action: rootAction + action: rootAction, }, "routes/resource": { action: resourceAction, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -333,8 +333,8 @@ describe("shared server runtime", () => { let build = mockServerBuild({ "routes/resource": { action, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -352,8 +352,8 @@ describe("shared server runtime", () => { let build = mockServerBuild({ "routes/resource": { action, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Development); @@ -369,17 +369,17 @@ describe("shared server runtime", () => { test("data request that does not match loader surfaces error for boundary", async () => { let build = mockServerBuild({ root: { - default: {} + default: {}, }, "routes/index": { parentId: "root", - index: true - } + index: true, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/?_data=routes/index`, { - method: "get" + method: "get", }); let result = await handler(request); @@ -398,18 +398,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/index": { parentId: "root", loader: indexLoader, - index: true - } + index: true, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/?_data=routes/index`, { - method: "get" + method: "get", }); let result = await handler(request); @@ -429,18 +429,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=root`, { - method: "get" + method: "get", }); let result = await handler(request); @@ -463,18 +463,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Development); let request = new Request(`${baseUrl}/test?_data=root`, { - method: "get" + method: "get", }); let result = await handler(request); @@ -496,18 +496,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=root`, { - method: "get" + method: "get", }); let result = await handler(request); @@ -521,17 +521,17 @@ describe("shared server runtime", () => { test("data request that does not match action surfaces error for boundary", async () => { let build = mockServerBuild({ root: { - default: {} + default: {}, }, "routes/index": { parentId: "root", - index: true - } + index: true, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/?index&_data=routes/index`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -550,18 +550,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=routes/test`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -581,18 +581,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=routes/test`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -615,18 +615,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Development); let request = new Request(`${baseUrl}/test?_data=routes/test`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -648,18 +648,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=routes/test`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -681,12 +681,12 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - action: rootAction + action: rootAction, }, "routes/index": { parentId: "root", - index: true - } + index: true, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -709,18 +709,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/index": { parentId: "root", action: indexAction, - index: true - } + index: true, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/?index&_data=routes/index`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -739,8 +739,8 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader - } + loader: rootLoader, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -767,8 +767,8 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} - } + CatchBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -799,14 +799,14 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, - loader: indexLoader - } + loader: indexLoader, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -825,7 +825,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -840,15 +840,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, loader: indexLoader, - CatchBoundary: {} - } + CatchBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -867,7 +867,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("routes/index"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -885,15 +885,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/test": { parentId: "root", path: "test", default: {}, loader: testLoader, - action: testAction - } + action: testAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -913,7 +913,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -931,15 +931,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, loader: indexLoader, - action: indexAction - } + action: indexAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -959,7 +959,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -977,7 +977,7 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/test": { parentId: "root", @@ -985,8 +985,8 @@ describe("shared server runtime", () => { default: {}, loader: testLoader, action: testAction, - CatchBoundary: {} - } + CatchBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1006,7 +1006,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("routes/test"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1024,7 +1024,7 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/index": { parentId: "root", @@ -1032,8 +1032,8 @@ describe("shared server runtime", () => { default: {}, loader: indexLoader, action: indexAction, - CatchBoundary: {} - } + CatchBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1053,7 +1053,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("routes/index"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1074,21 +1074,21 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/__layout": { parentId: "root", default: {}, loader: layoutLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/__layout/test": { parentId: "routes/__layout", path: "test", default: {}, loader: testLoader, - action: testAction - } + action: testAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1110,7 +1110,7 @@ describe("shared server runtime", () => { "routes/__layout" ); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1131,21 +1131,21 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/__layout": { parentId: "root", default: {}, loader: layoutLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/__layout/index": { parentId: "routes/__layout", index: true, default: {}, loader: indexLoader, - action: indexAction - } + action: indexAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1167,7 +1167,7 @@ describe("shared server runtime", () => { "routes/__layout" ); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1182,14 +1182,14 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, - loader: indexLoader - } + loader: indexLoader, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1208,7 +1208,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("index"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1223,15 +1223,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, loader: indexLoader, - ErrorBoundary: {} - } + ErrorBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1250,7 +1250,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("index"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("routes/index"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1268,15 +1268,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/test": { parentId: "root", path: "test", default: {}, loader: testLoader, - action: testAction - } + action: testAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1296,7 +1296,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("test"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1314,15 +1314,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, loader: indexLoader, - action: indexAction - } + action: indexAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1342,7 +1342,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("index"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1360,7 +1360,7 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/test": { parentId: "root", @@ -1368,8 +1368,8 @@ describe("shared server runtime", () => { default: {}, loader: testLoader, action: testAction, - ErrorBoundary: {} - } + ErrorBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1389,7 +1389,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("test"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("routes/test"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1407,7 +1407,7 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", @@ -1415,8 +1415,8 @@ describe("shared server runtime", () => { default: {}, loader: indexLoader, action: indexAction, - ErrorBoundary: {} - } + ErrorBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1436,7 +1436,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("index"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("routes/index"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1457,21 +1457,21 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/__layout": { parentId: "root", default: {}, loader: layoutLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/__layout/test": { parentId: "routes/__layout", path: "test", default: {}, loader: testLoader, - action: testAction - } + action: testAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1493,7 +1493,7 @@ describe("shared server runtime", () => { "routes/__layout" ); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1514,21 +1514,21 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/__layout": { parentId: "root", default: {}, loader: layoutLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/__layout/index": { parentId: "routes/__layout", index: true, default: {}, loader: indexLoader, - action: indexAction - } + action: indexAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1550,7 +1550,7 @@ describe("shared server runtime", () => { "routes/__layout" ); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1565,13 +1565,13 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", default: {}, - loader: indexLoader - } + loader: indexLoader, + }, }); let calledBefore = false; let ogHandleDocumentRequest = build.entry.module.default; @@ -1611,13 +1611,13 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", default: {}, - loader: indexLoader - } + loader: indexLoader, + }, }); let lastThrownError; build.entry.module.default = jest.fn(function () { @@ -1649,13 +1649,13 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", default: {}, - loader: indexLoader - } + loader: indexLoader, + }, }); let errorMessage = "thrown from handleDocumentRequest and expected to be logged in console only once"; diff --git a/packages/remix-server-runtime/__tests__/sessions-test.ts b/packages/remix-server-runtime/__tests__/sessions-test.ts index 87d40b70d9..7fe66e8e16 100644 --- a/packages/remix-server-runtime/__tests__/sessions-test.ts +++ b/packages/remix-server-runtime/__tests__/sessions-test.ts @@ -52,7 +52,7 @@ describe("isSession", () => { describe("In-memory session storage", () => { it("persists session data across requests", async () => { let { getSession, commitSession } = createMemorySessionStorage({ - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -66,7 +66,7 @@ describe("In-memory session storage", () => { describe("Cookie session storage", () => { it("persists session data across requests", async () => { let { getSession, commitSession } = createCookieSessionStorage({ - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -78,7 +78,7 @@ describe("Cookie session storage", () => { it("returns an empty session for cookies that are not signed properly", async () => { let { getSession, commitSession } = createCookieSessionStorage({ - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -96,7 +96,7 @@ describe("Cookie session storage", () => { it('"makes the default path of cookies to be /', async () => { let { getSession, commitSession } = createCookieSessionStorage({ - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -107,7 +107,7 @@ describe("Cookie session storage", () => { describe("when a new secret shows up in the rotation", () => { it("unsigns old session cookies using the old secret and encodes new cookies using the new secret", async () => { let { getSession, commitSession } = createCookieSessionStorage({ - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -118,7 +118,7 @@ describe("Cookie session storage", () => { // A new secret enters the rotation... let storage = createCookieSessionStorage({ - cookie: { secrets: ["secret2", "secret1"] } + cookie: { secrets: ["secret2", "secret1"] }, }); getSession = storage.getSession; commitSession = storage.commitSession; diff --git a/packages/remix-server-runtime/__tests__/utils.ts b/packages/remix-server-runtime/__tests__/utils.ts index e5a6ddbc8b..ff9ec7681a 100644 --- a/packages/remix-server-runtime/__tests__/utils.ts +++ b/packages/remix-server-runtime/__tests__/utils.ts @@ -23,7 +23,7 @@ export function mockServerBuild( assets: { entry: { imports: [""], - module: "" + module: "", }, routes: Object.entries(routes).reduce((p, [id, config]) => { let route: EntryRoute = { @@ -35,15 +35,15 @@ export function mockServerBuild( module: "", index: config.index, path: config.path, - parentId: config.parentId + parentId: config.parentId, }; return { ...p, - [id]: route + [id]: route, }; }, {}), url: "", - version: "" + version: "", }, entry: { module: { @@ -51,11 +51,11 @@ export function mockServerBuild( async (request, responseStatusCode, responseHeaders, entryContext) => new Response(null, { status: responseStatusCode, - headers: responseHeaders + headers: responseHeaders, }) ), - handleDataRequest: jest.fn(async response => response) - } + handleDataRequest: jest.fn(async (response) => response), + }, }, routes: Object.entries(routes).reduce( (p, [id, config]) => { @@ -70,16 +70,16 @@ export function mockServerBuild( ErrorBoundary: config.ErrorBoundary, action: config.action, headers: config.headers, - loader: config.loader - } + loader: config.loader, + }, }; return { ...p, - [id]: route + [id]: route, }; }, {} - ) + ), }; } diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index c1b0d13b91..4c86f65182 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -81,7 +81,7 @@ export function createCookie( let { secrets, ...options } = { secrets: [], path: "/", - ...cookieOptions + ...cookieOptions, }; return { @@ -112,10 +112,10 @@ export function createCookie( value === "" ? "" : await encodeCookieValue(value, secrets), { ...options, - ...serializeOptions + ...serializeOptions, } ); - } + }, }; } diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index 8e30917066..60df3fef7c 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -16,7 +16,7 @@ export type AppData = any; export async function callRouteAction({ loadContext, match, - request + request, }: { loadContext: unknown; match: RouteMatch; @@ -37,7 +37,7 @@ export async function callRouteAction({ result = await action({ request: stripDataParam(stripIndexParam(request)), context: loadContext, - params: match.params + params: match.params, }); } catch (error: unknown) { if (!isResponse(error)) { @@ -63,7 +63,7 @@ export async function callRouteAction({ export async function callRouteLoader({ loadContext, match, - request + request, }: { request: Request; match: RouteMatch; @@ -84,7 +84,7 @@ export async function callRouteLoader({ result = await loader({ request: stripDataParam(stripIndexParam(request.clone())), context: loadContext, - params: match.params + params: match.params, }); } catch (error: unknown) { if (!isResponse(error)) { diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index c14c740d90..8a555c429a 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -3,7 +3,7 @@ import type { RouteManifest, ServerRouteManifest, EntryRoute, - ServerRoute + ServerRoute, } from "./routes"; import type { RouteData } from "./routeData"; import type { RouteMatch } from "./routeMatching"; @@ -33,10 +33,10 @@ export function createEntryMatches( matches: RouteMatch[], routes: RouteManifest ): RouteMatch[] { - return matches.map(match => ({ + return matches.map((match) => ({ params: match.params, pathname: match.pathname, - route: routes[match.route.id] + route: routes[match.route.id], })); } diff --git a/packages/remix-server-runtime/errors.ts b/packages/remix-server-runtime/errors.ts index 7dad52c404..ffd5bd7f22 100644 --- a/packages/remix-server-runtime/errors.ts +++ b/packages/remix-server-runtime/errors.ts @@ -65,6 +65,6 @@ export interface SerializedError { export async function serializeError(error: Error): Promise { return { message: error.message, - stack: error.stack + stack: error.stack, }; } diff --git a/packages/remix-server-runtime/headers.ts b/packages/remix-server-runtime/headers.ts index 70887eafda..19b494266e 100644 --- a/packages/remix-server-runtime/headers.ts +++ b/packages/remix-server-runtime/headers.ts @@ -40,7 +40,7 @@ function prependCookies(parentHeaders: Headers, childHeaders: Headers): void { if (parentSetCookieString) { let cookies = splitCookiesString(parentSetCookieString); - cookies.forEach(cookie => { + cookies.forEach((cookie) => { childHeaders.append("Set-Cookie", cookie); }); } diff --git a/packages/remix-server-runtime/index.ts b/packages/remix-server-runtime/index.ts index fd51daea14..4ff6a3152b 100644 --- a/packages/remix-server-runtime/index.ts +++ b/packages/remix-server-runtime/index.ts @@ -2,7 +2,7 @@ export type { ServerBuild, ServerEntryModule, HandleDataRequestFunction, - HandleDocumentRequestFunction + HandleDocumentRequestFunction, } from "./build"; export type { @@ -10,7 +10,7 @@ export type { CookieSerializeOptions, CookieSignatureOptions, CookieOptions, - Cookie + Cookie, } from "./cookies"; export { createCookie, isCookie } from "./cookies"; @@ -21,7 +21,7 @@ export type { EntryContext } from "./entry"; export type { LinkDescriptor, HtmlLinkDescriptor, - PageLinkDescriptor + PageLinkDescriptor, } from "./links"; export type { ServerPlatform } from "./platform"; @@ -37,7 +37,7 @@ export type { MetaDescriptor, MetaFunction, RouteComponent, - RouteHandle + RouteHandle, } from "./routeModules"; export { json, redirect } from "./responses"; @@ -49,7 +49,7 @@ export type { SessionData, Session, SessionStorage, - SessionIdStorageStrategy + SessionIdStorageStrategy, } from "./sessions"; export { createSession, isSession, createSessionStorage } from "./sessions"; export { createCookieSessionStorage } from "./sessions/cookieStorage"; diff --git a/packages/remix-server-runtime/magicExports/server.ts b/packages/remix-server-runtime/magicExports/server.ts index 37d570c18e..8a06a1d578 100644 --- a/packages/remix-server-runtime/magicExports/server.ts +++ b/packages/remix-server-runtime/magicExports/server.ts @@ -31,7 +31,7 @@ export type { SessionData, Session, SessionStorage, - SessionIdStorageStrategy + SessionIdStorageStrategy, } from "@remix-run/server-runtime"; export { @@ -43,5 +43,5 @@ export { createCookieSessionStorage, createMemorySessionStorage, json, - redirect + redirect, } from "@remix-run/server-runtime"; diff --git a/packages/remix-server-runtime/mode.ts b/packages/remix-server-runtime/mode.ts index 817352d294..903aaa3bee 100644 --- a/packages/remix-server-runtime/mode.ts +++ b/packages/remix-server-runtime/mode.ts @@ -4,7 +4,7 @@ export enum ServerMode { Development = "development", Production = "production", - Test = "test" + Test = "test", } export function isServerMode(value: any): value is ServerMode { diff --git a/packages/remix-server-runtime/responses.ts b/packages/remix-server-runtime/responses.ts index aea4dcfcd3..5a48e9d87b 100644 --- a/packages/remix-server-runtime/responses.ts +++ b/packages/remix-server-runtime/responses.ts @@ -20,7 +20,7 @@ export function json( return new Response(JSON.stringify(data), { ...responseInit, - headers + headers, }); } @@ -46,7 +46,7 @@ export function redirect( return new Response(null, { ...responseInit, - headers + headers, }); } diff --git a/packages/remix-server-runtime/routeMatching.ts b/packages/remix-server-runtime/routeMatching.ts index ccfc8238cf..4373fd6f01 100644 --- a/packages/remix-server-runtime/routeMatching.ts +++ b/packages/remix-server-runtime/routeMatching.ts @@ -16,9 +16,9 @@ export function matchServerRoutes( let matches = matchRoutes(routes as unknown as RouteObject[], pathname); if (!matches) return null; - return matches.map(match => ({ + return matches.map((match) => ({ params: match.params, pathname: match.pathname, - route: match.route as unknown as ServerRoute + route: match.route as unknown as ServerRoute, })); } diff --git a/packages/remix-server-runtime/routes.ts b/packages/remix-server-runtime/routes.ts index 712388270f..7c45e8b273 100644 --- a/packages/remix-server-runtime/routes.ts +++ b/packages/remix-server-runtime/routes.ts @@ -35,9 +35,9 @@ export function createRoutes( parentId?: string ): ServerRoute[] { return Object.keys(manifest) - .filter(key => manifest[key].parentId === parentId) - .map(id => ({ + .filter((key) => manifest[key].parentId === parentId) + .map((id) => ({ ...manifest[id], - children: createRoutes(manifest, id) + children: createRoutes(manifest, id), })); } diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index 456264d6db..3b8c1ae997 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -48,7 +48,7 @@ export function createRequestHandler( loadContext, matches: matches!, handleDataRequest: build.entry.module.handleDataRequest, - serverMode + serverMode, }); break; case "document": @@ -58,7 +58,7 @@ export function createRequestHandler( matches, request, routes, - serverMode + serverMode, }); break; case "resource": @@ -66,7 +66,7 @@ export function createRequestHandler( request, loadContext, matches: matches!, - serverMode + serverMode, }); break; } @@ -75,7 +75,7 @@ export function createRequestHandler( return new Response(null, { headers: response.headers, status: response.status, - statusText: response.statusText + statusText: response.statusText, }); } @@ -87,7 +87,7 @@ async function handleDataRequest({ loadContext, matches, request, - serverMode + serverMode, }: { handleDataRequest?: HandleDataRequestFunction; loadContext: unknown; @@ -120,7 +120,7 @@ async function handleDataRequest({ response = await callRouteAction({ loadContext, match, - request: request + request: request, }); } else { let routeId = url.searchParams.get("_data"); @@ -128,7 +128,7 @@ async function handleDataRequest({ return errorBoundaryError(new Error(`Missing route id in ?_data`), 403); } - let tempMatch = matches.find(match => match.route.id === routeId); + let tempMatch = matches.find((match) => match.route.id === routeId); if (!tempMatch) { return errorBoundaryError( new Error(`Route "${routeId}" does not match URL "${url.pathname}"`), @@ -150,7 +150,7 @@ async function handleDataRequest({ return new Response(null, { status: 204, - headers + headers, }); } @@ -158,7 +158,7 @@ async function handleDataRequest({ response = await handleDataRequest(response.clone(), { context: loadContext, params: match.params, - request: request.clone() + request: request.clone(), }); } @@ -182,7 +182,7 @@ async function renderDocumentRequest({ matches, request, routes, - serverMode + serverMode, }: { build: ServerBuild; loadContext: unknown; @@ -200,7 +200,7 @@ async function renderDocumentRequest({ renderBoundaryRouteId: null, loaderBoundaryRouteId: null, error: undefined, - catch: undefined + catch: undefined, }; if (!isValidRequestMethod(request)) { @@ -209,14 +209,14 @@ async function renderDocumentRequest({ appState.catch = { data: null, status: 405, - statusText: "Method Not Allowed" + statusText: "Method Not Allowed", }; } else if (!matches) { appState.trackCatchBoundaries = false; appState.catch = { data: null, status: 404, - statusText: "Not Found" + statusText: "Not Found", }; } @@ -232,7 +232,7 @@ async function renderDocumentRequest({ actionResponse = await callRouteAction({ loadContext, match: actionMatch, - request: request + request: request, }); if (isRedirectResponse(actionResponse)) { @@ -241,7 +241,7 @@ async function renderDocumentRequest({ actionStatus = { status: actionResponse.status, - statusText: actionResponse.statusText + statusText: actionResponse.statusText, }; if (isCatchResponse(actionResponse)) { @@ -252,11 +252,11 @@ async function renderDocumentRequest({ appState.trackCatchBoundaries = false; appState.catch = { ...actionStatus, - data: await extractData(actionResponse) + data: await extractData(actionResponse), }; } else { actionData = { - [actionMatch.route.id]: await extractData(actionResponse) + [actionMatch.route.id]: await extractData(actionResponse), }; } } catch (error: any) { @@ -299,12 +299,12 @@ async function renderDocumentRequest({ } let routeLoaderResults = await Promise.allSettled( - matchesToLoad.map(match => + matchesToLoad.map((match) => match.route.module.loader ? callRouteLoader({ loadContext, match, - request + request, }) : Promise.resolve(undefined) ) @@ -381,7 +381,7 @@ async function renderDocumentRequest({ appState.catch = { data: await extractData(response), status: response.status, - statusText: response.statusText + statusText: response.statusText, }; break; } else { @@ -416,7 +416,7 @@ async function renderDocumentRequest({ renderableMatches.push({ params: {}, pathname: "", - route: routes[0] + route: routes[0], }); } } @@ -426,7 +426,7 @@ async function renderDocumentRequest({ let notOkResponse = actionStatus && actionStatus.status !== 200 ? actionStatus.status - : loaderStatusCodes.find(status => status !== 200); + : loaderStatusCodes.find((status) => status !== 200); let responseStatusCode = appState.error ? 500 @@ -449,14 +449,14 @@ async function renderDocumentRequest({ actionData, appState: appState, matches: entryMatches, - routeData + routeData, }; let entryContext: EntryContext = { ...serverHandoff, manifest: build.assets, routeModules, - serverHandoffString: createServerHandoffString(serverHandoff) + serverHandoffString: createServerHandoffString(serverHandoff), }; let handleDocumentRequest = build.entry.module.default; @@ -502,8 +502,8 @@ async function renderDocumentRequest({ return new Response(message, { status: 500, headers: { - "Content-Type": "text/plain" - } + "Content-Type": "text/plain", + }, }); } } @@ -513,7 +513,7 @@ async function handleResourceRequest({ loadContext, matches, request, - serverMode + serverMode, }: { request: Request; loadContext: unknown; @@ -543,8 +543,8 @@ async function handleResourceRequest({ return new Response(message, { status: 500, headers: { - "Content-Type": "text/plain" - } + "Content-Type": "text/plain", + }, }); } } @@ -597,8 +597,8 @@ async function errorBoundaryError(error: Error, status: number) { return json(await serializeError(error), { status, headers: { - "X-Remix-Error": "yes" - } + "X-Remix-Error": "yes", + }, }); } diff --git a/packages/remix-server-runtime/sessions.ts b/packages/remix-server-runtime/sessions.ts index bf09562fdc..bd8b1e2dbb 100644 --- a/packages/remix-server-runtime/sessions.ts +++ b/packages/remix-server-runtime/sessions.ts @@ -106,7 +106,7 @@ export function createSession(initialData: SessionData = {}, id = ""): Session { }, unset(name) { map.delete(name); - } + }, }; } @@ -215,7 +215,7 @@ export function createSessionStorage({ createData, readData, updateData, - deleteData + deleteData, }: SessionIdStorageStrategy): SessionStorage { let cookie = isCookie(cookieArg) ? cookieArg @@ -244,9 +244,9 @@ export function createSessionStorage({ await deleteData(session.id); return cookie.serialize("", { ...options, - expires: new Date(0) + expires: new Date(0), }); - } + }, }; } diff --git a/packages/remix-server-runtime/sessions/cookieStorage.ts b/packages/remix-server-runtime/sessions/cookieStorage.ts index 624cc5751f..8e6ee92d9a 100644 --- a/packages/remix-server-runtime/sessions/cookieStorage.ts +++ b/packages/remix-server-runtime/sessions/cookieStorage.ts @@ -22,7 +22,7 @@ interface CookieSessionStorageOptions { * @see https://remix.run/api/remix#createcookiesessionstorage */ export function createCookieSessionStorage({ - cookie: cookieArg + cookie: cookieArg, }: CookieSessionStorageOptions = {}): SessionStorage { let cookie = isCookie(cookieArg) ? cookieArg @@ -42,8 +42,8 @@ export function createCookieSessionStorage({ async destroySession(_session, options) { return cookie.serialize("", { ...options, - expires: new Date(0) + expires: new Date(0), }); - } + }, }; } diff --git a/packages/remix-server-runtime/sessions/memoryStorage.ts b/packages/remix-server-runtime/sessions/memoryStorage.ts index bd1696cd5e..edc7f3668c 100644 --- a/packages/remix-server-runtime/sessions/memoryStorage.ts +++ b/packages/remix-server-runtime/sessions/memoryStorage.ts @@ -1,7 +1,7 @@ import type { SessionData, SessionStorage, - SessionIdStorageStrategy + SessionIdStorageStrategy, } from "../sessions"; import { createSessionStorage } from "../sessions"; @@ -23,7 +23,7 @@ interface MemorySessionStorageOptions { * @see https://remix.run/api/remix#creatememorysessionstorage */ export function createMemorySessionStorage({ - cookie + cookie, }: MemorySessionStorageOptions = {}): SessionStorage { let uniqueId = 0; let map = new Map(); @@ -54,6 +54,6 @@ export function createMemorySessionStorage({ }, async deleteData(id) { map.delete(id); - } + }, }); } From d00d5a9b943bdd578693d0ea6389cba0aef3efae Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Tue, 22 Feb 2022 13:19:29 -0800 Subject: [PATCH 0256/1690] chore: Format `main` with Prettier defaults (#2085) --- integration/action-test.ts | 6 +- integration/bug-report-test.ts | 4 +- integration/catch-boundary-test.ts | 6 +- integration/compiler-test.ts | 4 +- integration/errory-boundary-test.ts | 4 +- integration/file-uploads-test.ts | 4 +- integration/form-test.ts | 4 +- integration/headers-test.ts | 8 +- integration/helpers/create-fixture.tsx | 38 +- integration/helpers/global-setup.ts | 2 +- integration/loader-test.ts | 6 +- integration/rendering-test.ts | 6 +- integration/server-entry-test.ts | 4 +- integration/splat-routes-test.ts | 4 +- integration/transition-test.ts | 12 +- packages/remix-dev/__tests__/build-test.ts | 10 +- .../remix-dev/__tests__/defineRoutes-test.ts | 4 +- .../remix-dev/__tests__/readConfig-test.ts | 2 +- .../__tests__/routesConvention-test.ts | 2 +- packages/remix-dev/build.ts | 4 +- packages/remix-dev/cache.ts | 2 +- packages/remix-dev/cli.ts | 12 +- packages/remix-dev/cli/commands.ts | 12 +- packages/remix-dev/compiler.ts | 56 +-- packages/remix-dev/compiler/assets.ts | 10 +- packages/remix-dev/compiler/loaders.ts | 2 +- .../plugins/browserRouteModulesPlugin.ts | 20 +- .../compiler/plugins/emptyModulesPlugin.ts | 6 +- packages/remix-dev/compiler/plugins/mdx.ts | 28 +- .../plugins/serverAssetsManifestPlugin.ts | 6 +- .../plugins/serverBareModulesPlugin.ts | 8 +- .../plugins/serverEntryModulePlugin.ts | 8 +- .../plugins/serverRouteModulesPlugin.ts | 10 +- packages/remix-dev/compiler/routes.ts | 4 +- packages/remix-dev/compiler/utils/crypto.ts | 4 +- packages/remix-dev/compiler/virtualModules.ts | 4 +- packages/remix-dev/config.ts | 4 +- packages/remix-dev/config/format.ts | 8 +- packages/remix-dev/config/routes.ts | 2 +- packages/remix-dev/config/routesConvention.ts | 12 +- packages/remix-dev/config/serverModes.ts | 2 +- packages/remix-dev/setup.ts | 6 +- .../remix-express/__tests__/server-test.ts | 16 +- packages/remix-express/server.ts | 10 +- packages/remix-node/__tests__/fetch-test.ts | 22 +- .../remix-node/__tests__/formData-test.ts | 2 +- .../__tests__/parseMultipartFormData-test.ts | 2 +- .../remix-node/__tests__/sessions-test.ts | 8 +- packages/remix-node/cookieSigning.ts | 2 +- packages/remix-node/fetch.ts | 10 +- packages/remix-node/formData.ts | 6 +- packages/remix-node/globals.ts | 4 +- packages/remix-node/index.ts | 4 +- packages/remix-node/magicExports/platform.ts | 2 +- packages/remix-node/parseMultipartFormData.ts | 6 +- packages/remix-node/sessions/fileStorage.ts | 6 +- .../remix-node/upload/fileUploadHandler.ts | 6 +- .../remix-node/upload/memoryUploadHandler.ts | 4 +- packages/remix-serve/cli.ts | 2 +- .../__tests__/cookies-test.ts | 18 +- .../__tests__/data-test.ts | 64 ++-- .../__tests__/responses-test.ts | 12 +- .../__tests__/server-test.ts | 338 +++++++++--------- .../__tests__/sessions-test.ts | 12 +- .../remix-server-runtime/__tests__/utils.ts | 22 +- packages/remix-server-runtime/cookies.ts | 6 +- packages/remix-server-runtime/data.ts | 8 +- packages/remix-server-runtime/entry.ts | 6 +- packages/remix-server-runtime/errors.ts | 2 +- packages/remix-server-runtime/headers.ts | 2 +- packages/remix-server-runtime/index.ts | 10 +- .../magicExports/server.ts | 4 +- packages/remix-server-runtime/mode.ts | 2 +- packages/remix-server-runtime/responses.ts | 4 +- .../remix-server-runtime/routeMatching.ts | 4 +- packages/remix-server-runtime/routes.ts | 6 +- packages/remix-server-runtime/server.ts | 62 ++-- packages/remix-server-runtime/sessions.ts | 8 +- .../sessions/cookieStorage.ts | 6 +- .../sessions/memoryStorage.ts | 6 +- 80 files changed, 537 insertions(+), 537 deletions(-) diff --git a/integration/action-test.ts b/integration/action-test.ts index 503a25b2b1..f50073314f 100644 --- a/integration/action-test.ts +++ b/integration/action-test.ts @@ -2,7 +2,7 @@ import { createFixture, createAppFixture, selectHtml, - js + js, } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; @@ -65,8 +65,8 @@ describe("actions", () => { export default function () { return
${PAGE_TEXT}
} - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/bug-report-test.ts b/integration/bug-report-test.ts index a83a6b081f..cf268a06b3 100644 --- a/integration/bug-report-test.ts +++ b/integration/bug-report-test.ts @@ -58,8 +58,8 @@ beforeAll(async () => { export default function Index() { return
cheeseburger
; } - ` - } + `, + }, }); // This creates an interactive app using puppeteer. diff --git a/integration/catch-boundary-test.ts b/integration/catch-boundary-test.ts index 80dce4b882..e87a647e06 100644 --- a/integration/catch-boundary-test.ts +++ b/integration/catch-boundary-test.ts @@ -121,8 +121,8 @@ describe("CatchBoundary", () => { export default function Index() { return
} - ` - } + `, + }, }); app = await createAppFixture(fixture); @@ -146,7 +146,7 @@ describe("CatchBoundary", () => { test("invalid request methods", async () => { let res = await fixture.requestDocument("/", { - method: "OPTIONS" + method: "OPTIONS", }); expect(res.status).toBe(405); expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); diff --git a/integration/compiler-test.ts b/integration/compiler-test.ts index 5a6ae3ef2e..3e2c91c8ee 100644 --- a/integration/compiler-test.ts +++ b/integration/compiler-test.ts @@ -68,8 +68,8 @@ describe("compiler", () => { }`, "node_modules/esm-only-pkg/esm-only-pkg.js": js` export default "esm-only-pkg"; - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/errory-boundary-test.ts b/integration/errory-boundary-test.ts index f4f68aa78d..0ee18a44af 100644 --- a/integration/errory-boundary-test.ts +++ b/integration/errory-boundary-test.ts @@ -161,8 +161,8 @@ describe("ErrorBoundary", () => { export function ErrorBoundary() { return
${OWN_BOUNDARY_TEXT}
} - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/file-uploads-test.ts b/integration/file-uploads-test.ts index f9f0d418d4..398c9b8cb6 100644 --- a/integration/file-uploads-test.ts +++ b/integration/file-uploads-test.ts @@ -57,8 +57,8 @@ describe("file-uploads", () => { ); } - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/form-test.ts b/integration/form-test.ts index 3b08fbfde3..59fc4329ec 100644 --- a/integration/form-test.ts +++ b/integration/form-test.ts @@ -81,8 +81,8 @@ describe("Forms", () => { ) } - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/headers-test.ts b/integration/headers-test.ts index 08e352c73b..966231c71c 100644 --- a/integration/headers-test.ts +++ b/integration/headers-test.ts @@ -64,8 +64,8 @@ describe("headers export", () => { } export default function Action() { return
} - ` - } + `, + }, }); }); @@ -113,8 +113,8 @@ describe("headers export", () => { export default function Index() { return
Heyo!
} - ` - } + `, + }, }); let response = await fixture.requestDocument("/"); expect(response.headers.get(HEADER_KEY)).toBe(HEADER_VALUE); diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index 189d33140d..f602c111bc 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -14,7 +14,7 @@ import { createApp } from "../../packages/create-remix"; import { createRequestHandler as createExpressHandler } from "../../packages/remix-express"; import type { ServerBuild, - ServerPlatform + ServerPlatform, } from "../../packages/remix-server-runtime"; import type { CreateAppArgs } from "../../packages/create-remix"; import { TMP_DIR } from "./global-setup"; @@ -62,8 +62,8 @@ export async function createFixture(init: FixtureInit) { "Content-Type": data instanceof URLSearchParams ? "application/x-www-form-urlencoded" - : "multipart/form-data" - } + : "multipart/form-data", + }, }); }; @@ -80,7 +80,7 @@ export async function createFixture(init: FixtureInit) { requestDocument, requestData, postDocument, - getBrowserAsset + getBrowserAsset, }; } @@ -89,7 +89,7 @@ export async function createAppFixture(fixture: Fixture) { port: number; stop: () => Promise; }> => { - return new Promise(async accept => { + return new Promise(async (accept) => { let port = await getPort(); let app = express(); app.use(express.static(path.join(fixture.projectDir, "public"))); @@ -101,7 +101,7 @@ export async function createAppFixture(fixture: Fixture) { let server = app.listen(port); let stop = (): Promise => { - return new Promise(res => { + return new Promise((res) => { server.close(() => res()); }); }; @@ -119,7 +119,7 @@ export async function createAppFixture(fixture: Fixture) { let start = async () => { let [{ stop, port }, { browser, page }] = await Promise.all([ startAppServer(), - launchPuppeteer() + launchPuppeteer(), ]); let serverUrl = `http://localhost:${port}`; @@ -166,7 +166,7 @@ export async function createAppFixture(fixture: Fixture) { */ goto: async (href: string, waitForHydration?: true) => { return page.goto(`${serverUrl}${href}`, { - waitUntil: waitForHydration ? "networkidle0" : undefined + waitUntil: waitForHydration ? "networkidle0" : undefined, }); }, @@ -318,8 +318,8 @@ export async function createAppFixture(fixture: Fixture) { jest.setTimeout(ms); console.log(`🙈 Poke around for ${seconds} seconds 👉 ${serverUrl}`); cp.exec(`open ${serverUrl}${href}`); - return new Promise(res => setTimeout(res, ms)); - } + return new Promise((res) => setTimeout(res, ms)); + }, }; }; @@ -335,11 +335,11 @@ export async function createFixtureProject(init: FixtureInit): Promise { lang: "js", server: init.server || "remix", projectDir, - quiet: true + quiet: true, }); await Promise.all([ writeTestFiles(init, projectDir), - installRemix(projectDir) + installRemix(projectDir), ]); build(projectDir); @@ -349,10 +349,10 @@ export async function createFixtureProject(init: FixtureInit): Promise { function build(projectDir: string) { // TODO: log errors (like syntax errors in the fixture file strings) cp.spawnSync("node", ["node_modules/@remix-run/dev/cli.js", "setup"], { - cwd: projectDir + cwd: projectDir, }); cp.spawnSync("node", ["node_modules/@remix-run/dev/cli.js", "build"], { - cwd: projectDir + cwd: projectDir, }); } @@ -367,7 +367,7 @@ async function installRemix(projectDir: string) { async function writeTestFiles(init: FixtureInit, dir: string) { await Promise.all( - Object.keys(init.files).map(async filename => { + Object.keys(init.files).map(async (filename) => { let filePath = path.join(dir, filename); await fse.ensureDir(path.dirname(filePath)); await fs.writeFile(filePath, init.files[filename]); @@ -443,14 +443,14 @@ async function doAndWait( }; timeoutEvent = setTimeout(() => { console.warn("Warning, wait for the address below to time out:"); - console.warn(waiting.map(a => a.url()).join("\n")); + console.warn(waiting.map((a) => a.url()).join("\n")); return clear().then(() => res(null)); }, timeout); pollEvent = setInterval(() => { if (waiting.length == 0) { return clear().then(() => res(null)); } - waiting = waiting.filter(a => a.response() == null); + waiting = waiting.filter((a) => a.response() == null); }, pollTime); }); } @@ -463,7 +463,7 @@ export function collectResponses( ): HTTPResponse[] { let responses: HTTPResponse[] = []; - page.on("response", res => { + page.on("response", (res) => { if (!filter || filter(new URL(res.url()))) { responses.push(res); } @@ -473,5 +473,5 @@ export function collectResponses( } export function collectDataResponses(page: Page) { - return collectResponses(page, url => url.searchParams.has("_data")); + return collectResponses(page, (url) => url.searchParams.has("_data")); } diff --git a/integration/helpers/global-setup.ts b/integration/helpers/global-setup.ts index 8122431cf3..c4eb8a409a 100644 --- a/integration/helpers/global-setup.ts +++ b/integration/helpers/global-setup.ts @@ -9,7 +9,7 @@ console.warn = () => {}; export default async function setup() { await fs.rm(TMP_DIR, { force: true, - recursive: true + recursive: true, }); await fs.mkdir(TMP_DIR); } diff --git a/integration/loader-test.ts b/integration/loader-test.ts index 55fb374918..f13c362672 100644 --- a/integration/loader-test.ts +++ b/integration/loader-test.ts @@ -32,15 +32,15 @@ describe("loader", () => { export default function Index() { return
} - ` - } + `, + }, }); }); it("returns responses for a specific route", async () => { let [root, index] = await Promise.all([ fixture.requestData("/", "root"), - fixture.requestData("/", "routes/index") + fixture.requestData("/", "routes/index"), ]); expect(root.headers.get("Content-Type")).toBe( diff --git a/integration/rendering-test.ts b/integration/rendering-test.ts index 101555e17c..2579fc8b55 100644 --- a/integration/rendering-test.ts +++ b/integration/rendering-test.ts @@ -2,7 +2,7 @@ import { createAppFixture, createFixture, js, - selectHtml + selectHtml, } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; @@ -35,8 +35,8 @@ describe("rendering", () => { export default function() { return

Index

; } - ` - } + `, + }, }); app = await createAppFixture(fixture); diff --git a/integration/server-entry-test.ts b/integration/server-entry-test.ts index 770c707e52..ac06c427a4 100644 --- a/integration/server-entry-test.ts +++ b/integration/server-entry-test.ts @@ -28,8 +28,8 @@ describe("Server Entry", () => { export default function () { return
} - ` - } + `, + }, }); }); diff --git a/integration/splat-routes-test.ts b/integration/splat-routes-test.ts index 8d18fbf3a7..e3914f68e5 100644 --- a/integration/splat-routes-test.ts +++ b/integration/splat-routes-test.ts @@ -76,8 +76,8 @@ describe("rendering", () => { export default function() { return

${PARENTLESS_$}

} - ` - } + `, + }, }); }); diff --git a/integration/transition-test.ts b/integration/transition-test.ts index 109f8a2e3e..b92d0f7728 100644 --- a/integration/transition-test.ts +++ b/integration/transition-test.ts @@ -131,8 +131,8 @@ describe("rendering", () => {
); } - ` - } + `, + }, }); app = await createAppFixture(fixture); @@ -148,7 +148,7 @@ describe("rendering", () => { await app.clickLink(`/${PAGE}`); expect( - responses.map(res => new URL(res.url()).searchParams.get("_data")) + responses.map((res) => new URL(res.url()).searchParams.get("_data")) ).toEqual([`routes/${PAGE}`, `routes/${PAGE}/index`]); let html = await app.getHtml("main"); @@ -162,7 +162,7 @@ describe("rendering", () => { await app.clickLink(`/${PAGE}/${CHILD}`); expect( - responses.map(res => new URL(res.url()).searchParams.get("_data")) + responses.map((res) => new URL(res.url()).searchParams.get("_data")) ).toEqual([`routes/${PAGE}/${CHILD}`]); let html = await app.getHtml("main"); @@ -178,7 +178,7 @@ describe("rendering", () => { expect(new URL(app.page.url()).pathname).toBe(`/${REDIRECT_TARGET}`); expect( - responses.map(res => new URL(res.url()).searchParams.get("_data")) + responses.map((res) => new URL(res.url()).searchParams.get("_data")) ).toEqual([`routes/${REDIRECT}`, `routes/${PAGE}`, `routes/${PAGE}/index`]); let html = await app.getHtml("main"); @@ -194,7 +194,7 @@ describe("rendering", () => { await app.goBack(); expect( - responses.map(res => new URL(res.url()).searchParams.get("_data")) + responses.map((res) => new URL(res.url()).searchParams.get("_data")) ).toEqual([`routes/${PAGE}/index`]); let html = await app.getHtml("main"); diff --git a/packages/remix-dev/__tests__/build-test.ts b/packages/remix-dev/__tests__/build-test.ts index 42ba09eb8d..69595bff34 100644 --- a/packages/remix-dev/__tests__/build-test.ts +++ b/packages/remix-dev/__tests__/build-test.ts @@ -14,7 +14,7 @@ async function generateBuild(config: RemixConfig, options: BuildOptions) { } function getFilenames(output: RollupOutput) { - return output.output.map(item => item.fileName).sort(); + return output.output.map((item) => item.fileName).sort(); } describe.skip("building", () => { @@ -32,7 +32,7 @@ describe.skip("building", () => { it("generates the correct bundles", async () => { let output = await generateBuild(config, { mode: BuildMode.Development, - target: BuildTarget.Server + target: BuildTarget.Server, }); expect(getFilenames(output)).toMatchInlineSnapshot(` @@ -68,7 +68,7 @@ describe.skip("building", () => { it("generates the correct bundles", async () => { let output = await generateBuild(config, { mode: BuildMode.Production, - target: BuildTarget.Server + target: BuildTarget.Server, }); expect(getFilenames(output)).toMatchInlineSnapshot(` @@ -104,7 +104,7 @@ describe.skip("building", () => { it("generates the correct bundles", async () => { let output = await generateBuild(config, { mode: BuildMode.Development, - target: BuildTarget.Browser + target: BuildTarget.Browser, }); expect(getFilenames(output)).toMatchInlineSnapshot(` @@ -162,7 +162,7 @@ describe.skip("building", () => { it("generates the correct bundles", async () => { let output = await generateBuild(config, { mode: BuildMode.Production, - target: BuildTarget.Browser + target: BuildTarget.Browser, }); expect(getFilenames(output)).toMatchInlineSnapshot(` diff --git a/packages/remix-dev/__tests__/defineRoutes-test.ts b/packages/remix-dev/__tests__/defineRoutes-test.ts index cb5fa80341..d4766d004f 100644 --- a/packages/remix-dev/__tests__/defineRoutes-test.ts +++ b/packages/remix-dev/__tests__/defineRoutes-test.ts @@ -2,7 +2,7 @@ import { defineRoutes } from "../config/routes"; describe("defineRoutes", () => { it("returns an array of routes", () => { - let routes = defineRoutes(route => { + let routes = defineRoutes((route) => { route("/", "routes/home.js"); route("inbox", "routes/inbox.js", () => { route("/", "routes/inbox/index.js", { index: true }); @@ -60,7 +60,7 @@ describe("defineRoutes", () => { it("works with async data", async () => { // Read everything *before* calling defineRoutes. let fakeDirectory = await Promise.resolve(["one.md", "two.md"]); - let routes = defineRoutes(route => { + let routes = defineRoutes((route) => { for (let file of fakeDirectory) { route(file.replace(/\.md$/, ""), file); } diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index bf2c7092a0..d6ffd5e3ab 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -18,7 +18,7 @@ describe("readConfig", () => { appDirectory: expect.any(String), cacheDirectory: expect.any(String), serverBuildPath: expect.any(String), - assetsBuildDirectory: expect.any(String) + assetsBuildDirectory: expect.any(String), }, ` Object { diff --git a/packages/remix-dev/__tests__/routesConvention-test.ts b/packages/remix-dev/__tests__/routesConvention-test.ts index bf29137997..599dba4483 100644 --- a/packages/remix-dev/__tests__/routesConvention-test.ts +++ b/packages/remix-dev/__tests__/routesConvention-test.ts @@ -35,7 +35,7 @@ describe("createRoutePath", () => { ["beef]", "beef]"], ["[index]", "index"], ["test/inde[x]", "test/index"], - ["[i]ndex/[[].[[]]", "index/[/[]"] + ["[i]ndex/[[].[[]]", "index/[/[]"], ]; for (let [input, expected] of tests) { diff --git a/packages/remix-dev/build.ts b/packages/remix-dev/build.ts index 18052ddbf2..2703d5d041 100644 --- a/packages/remix-dev/build.ts +++ b/packages/remix-dev/build.ts @@ -1,6 +1,6 @@ export enum BuildMode { Development = "development", - Production = "production" + Production = "production", } export function isBuildMode(mode: any): mode is BuildMode { @@ -11,7 +11,7 @@ export enum BuildTarget { Browser = "browser", // TODO: remove Server = "server", // TODO: remove CloudflareWorkers = "cloudflare-workers", - Node14 = "node14" + Node14 = "node14", } export function isBuildTarget(target: any): target is BuildTarget { diff --git a/packages/remix-dev/cache.ts b/packages/remix-dev/cache.ts index 0ce3e3df61..bbec3ceee3 100644 --- a/packages/remix-dev/cache.ts +++ b/packages/remix-dev/cache.ts @@ -7,7 +7,7 @@ export function putJson(cachePath: string, key: string, data: any) { } export function getJson(cachePath: string, key: string) { - return get(cachePath, key).then(obj => + return get(cachePath, key).then((obj) => JSON.parse(obj.data.toString("utf-8")) ); } diff --git a/packages/remix-dev/cli.ts b/packages/remix-dev/cli.ts index b4bf10f3a7..5ef23e9fac 100644 --- a/packages/remix-dev/cli.ts +++ b/packages/remix-dev/cli.ts @@ -40,18 +40,18 @@ const cli = meow(helpText, { flags: { version: { type: "boolean", - alias: "v" + alias: "v", }, json: { - type: "boolean" + type: "boolean", }, sourcemap: { - type: "boolean" + type: "boolean", }, debug: { - type: "boolean" - } - } + type: "boolean", + }, + }, }); if (cli.flags.version) { diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 37e03e031f..bcaab5bfa9 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -89,7 +89,7 @@ export async function watch( let wss = new WebSocket.Server({ port: config.devServerPort }); function broadcast(event: { type: string; [key: string]: any }) { setTimeout(() => { - wss.clients.forEach(client => { + wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(event)); } @@ -123,7 +123,7 @@ export async function watch( }, onFileDeleted(file) { log(`File deleted: ${path.relative(process.cwd(), file)}`); - } + }, }); console.log(`💿 Built in ${prettyMs(Date.now() - start)}`); @@ -132,7 +132,7 @@ export async function watch( exitHook(() => { resolve(); }); - return new Promise(r => { + return new Promise((r) => { resolve = r; }).then(async () => { wss.close(); @@ -159,7 +159,7 @@ export async function dev(remixRoot: string, modeArg?: string) { let config = await readConfig(remixRoot); let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Development; let port = await getPort({ - port: process.env.PORT ? Number(process.env.PORT) : 3000 + port: process.env.PORT ? Number(process.env.PORT) : 3000, }); if (config.serverEntryPoint) { @@ -180,7 +180,7 @@ export async function dev(remixRoot: string, modeArg?: string) { onInitialBuild: () => { let address = Object.values(os.networkInterfaces()) .flat() - .find(ip => ip?.family == "IPv4" && !ip.internal)?.address; + .find((ip) => ip?.family == "IPv4" && !ip.internal)?.address; if (!address) { address = "localhost"; @@ -189,7 +189,7 @@ export async function dev(remixRoot: string, modeArg?: string) { server = app.listen(port, () => { console.log(`Remix App Server started at http://${address}:${port}`); }); - } + }, }); } finally { server!?.close(); diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts index 673bf27e75..0630dd769c 100644 --- a/packages/remix-dev/compiler.ts +++ b/packages/remix-dev/compiler.ts @@ -44,7 +44,7 @@ function defaultBuildFailureHandler(failure: Error | esbuild.BuildFailure) { if (failure.warnings) { let messages = esbuild.formatMessagesSync(failure.warnings, { kind: "warning", - color: true + color: true, }); console.warn(...messages); } @@ -52,7 +52,7 @@ function defaultBuildFailureHandler(failure: Error | esbuild.BuildFailure) { if (failure.errors) { let messages = esbuild.formatMessagesSync(failure.errors, { kind: "error", - color: true + color: true, }); console.error(...messages); } @@ -73,7 +73,7 @@ export async function build( target = BuildTarget.Node14, sourcemap = false, onWarning = defaultWarningHandler, - onBuildFailure = defaultBuildFailureHandler + onBuildFailure = defaultBuildFailureHandler, }: BuildOptions = {} ): Promise { let assetsManifestPromiseRef: AssetsManifestPromiseRef = {}; @@ -83,7 +83,7 @@ export async function build( target, sourcemap, onWarning, - onBuildFailure + onBuildFailure, }); } @@ -109,7 +109,7 @@ export async function watch( onFileCreated, onFileChanged, onFileDeleted, - onInitialBuild + onInitialBuild, }: WatchOptions = {} ): Promise<() => Promise> { let options = { @@ -118,7 +118,7 @@ export async function watch( sourcemap, onBuildFailure, onWarning, - incremental: true + incremental: true, }; let assetsManifestPromiseRef: AssetsManifestPromiseRef = {}; @@ -190,7 +190,7 @@ export async function watch( // If we get here and can't call rebuild something went wrong and we // should probably blow as it's not really recoverable. let browserBuildPromise = browserBuild.rebuild(); - let assetsManifestPromise = browserBuildPromise.then(build => + let assetsManifestPromise = browserBuildPromise.then((build) => generateAssetsManifest(config, build.metafile!) ); @@ -202,8 +202,8 @@ export async function watch( assetsManifestPromise, serverBuild .rebuild() - .then(build => writeServerBuildResult(config, build.outputFiles!)) - ]).catch(err => { + .then((build) => writeServerBuildResult(config, build.outputFiles!)), + ]).catch((err) => { disposeBuilders(); onBuildFailure(err); }); @@ -221,15 +221,15 @@ export async function watch( ignoreInitial: true, awaitWriteFinish: { stabilityThreshold: 100, - pollInterval: 100 - } + pollInterval: 100, + }, }) - .on("error", error => console.error(error)) - .on("change", async file => { + .on("error", (error) => console.error(error)) + .on("change", async (file) => { if (onFileChanged) onFileChanged(file); await rebuildEverything(); }) - .on("add", async file => { + .on("add", async (file) => { if (onFileCreated) onFileCreated(file); let newConfig: RemixConfig; try { @@ -245,7 +245,7 @@ export async function watch( await rebuildEverything(); } }) - .on("unlink", async file => { + .on("unlink", async (file) => { if (onFileDeleted) onFileDeleted(file); if (isEntryPoint(config, file)) { await restartBuilders(); @@ -285,7 +285,7 @@ async function buildEverything( ): Promise<(esbuild.BuildResult | undefined)[]> { try { let browserBuildPromise = createBrowserBuild(config, options); - let assetsManifestPromise = browserBuildPromise.then(build => + let assetsManifestPromise = browserBuildPromise.then((build) => generateAssetsManifest(config, build.metafile!) ); @@ -301,7 +301,7 @@ async function buildEverything( return await Promise.all([ assetsManifestPromise.then(() => browserBuildPromise), - serverBuildPromise + serverBuildPromise, ]); } catch (err) { options.onBuildFailure(err as Error); @@ -319,8 +319,8 @@ async function createBrowserBuild( // this is really just making sure we don't accidentally have any dependencies // on node built-ins in browser bundles. let dependencies = Object.keys(await getAppDependencies(config)); - let externals = nodeBuiltins.filter(mod => !dependencies.includes(mod)); - let fakeBuiltins = nodeBuiltins.filter(mod => dependencies.includes(mod)); + let externals = nodeBuiltins.filter((mod) => !dependencies.includes(mod)); + let fakeBuiltins = nodeBuiltins.filter((mod) => dependencies.includes(mod)); if (fakeBuiltins.length > 0) { throw new Error( @@ -331,7 +331,7 @@ async function createBrowserBuild( } let entryPoints: esbuild.BuildOptions["entryPoints"] = { - "entry.client": path.resolve(config.appDirectory, config.entryClientFile) + "entry.client": path.resolve(config.appDirectory, config.entryClientFile), }; for (let id of Object.keys(config.routes)) { // All route entry points are virtual modules that will be loaded by the @@ -366,14 +366,14 @@ async function createBrowserBuild( "process.env.NODE_ENV": JSON.stringify(options.mode), "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( config.devServerPort - ) + ), }, plugins: [ mdxPlugin(config), browserRouteModulesPlugin(config, /\?browser$/), emptyModulesPlugin(config, /\.server(\.[jt]sx?)?$/), - NodeModulesPolyfillPlugin() - ] + NodeModulesPolyfillPlugin(), + ], }); } @@ -393,7 +393,7 @@ async function createServerBuild( stdin = { contents: config.serverBuildTargetEntryModule, resolveDir: config.rootDirectory, - loader: "ts" + loader: "ts", }; } @@ -403,7 +403,7 @@ async function createServerBuild( serverRouteModulesPlugin(config), serverEntryModulePlugin(config), serverAssetsManifestPlugin(assetsManifestPromiseRef), - serverBareModulesPlugin(config, dependencies) + serverBareModulesPlugin(config, dependencies), ]; if (config.serverPlatform !== "node") { @@ -445,11 +445,11 @@ async function createServerBuild( "process.env.NODE_ENV": JSON.stringify(options.mode), "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( config.devServerPort - ) + ), }, - plugins + plugins, }) - .then(async build => { + .then(async (build) => { await writeServerBuildResult(config, build.outputFiles); return build; }); diff --git a/packages/remix-dev/compiler/assets.ts b/packages/remix-dev/compiler/assets.ts index 742b129755..4656c8328e 100644 --- a/packages/remix-dev/compiler/assets.ts +++ b/packages/remix-dev/compiler/assets.ts @@ -48,8 +48,8 @@ export async function createAssetsManifest( imports: esbuild.Metafile["outputs"][string]["imports"] ): string[] { return imports - .filter(im => im.kind === "import-statement") - .map(im => resolveUrl(im.path)); + .filter((im) => im.kind === "import-statement") + .map((im) => resolveUrl(im.path)); } let entryClientFile = path.resolve( @@ -78,7 +78,7 @@ export async function createAssetsManifest( if (entryPointFile === entryClientFile) { entry = { module: resolveUrl(key), - imports: resolveImports(output.imports) + imports: resolveImports(output.imports), }; // Only parse routes otherwise dynamic imports can fall into here and fail the build } else if (output.entryPoint.startsWith("browser-route-module:")) { @@ -96,7 +96,7 @@ export async function createAssetsManifest( hasAction: sourceExports.includes("action"), hasLoader: sourceExports.includes("loader"), hasCatchBoundary: sourceExports.includes("CatchBoundary"), - hasErrorBoundary: sourceExports.includes("ErrorBoundary") + hasErrorBoundary: sourceExports.includes("ErrorBoundary"), }; } } @@ -141,7 +141,7 @@ function optimizeRouteImports( } let routeImports = (route.imports || []).filter( - url => !parentImports.includes(url) + (url) => !parentImports.includes(url) ); // Setting `route.imports = undefined` prevents `imports: []` from showing up diff --git a/packages/remix-dev/compiler/loaders.ts b/packages/remix-dev/compiler/loaders.ts index f0d304f0be..83bee9b821 100644 --- a/packages/remix-dev/compiler/loaders.ts +++ b/packages/remix-dev/compiler/loaders.ts @@ -30,7 +30,7 @@ export const loaders: { [ext: string]: esbuild.Loader } = { ".webm": "file", ".webp": "file", ".woff": "file", - ".woff2": "file" + ".woff2": "file", }; export function getLoaderForFile(file: string): esbuild.Loader { diff --git a/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts index a5a456776e..6af2f9c091 100644 --- a/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/browserRouteModulesPlugin.ts @@ -14,7 +14,7 @@ const browserSafeRouteExports: { [name: string]: boolean } = { handle: true, links: true, meta: true, - unstable_shouldReload: true + unstable_shouldReload: true, }; /** @@ -37,16 +37,16 @@ export function browserRouteModulesPlugin( new Map() ); - build.onResolve({ filter: suffixMatcher }, args => { + build.onResolve({ filter: suffixMatcher }, (args) => { return { path: args.path, - namespace: "browser-route-module" + namespace: "browser-route-module", }; }); build.onLoad( { filter: suffixMatcher, namespace: "browser-route-module" }, - async args => { + async (args) => { let theExports; let file = args.path.replace(suffixMatcher, ""); let route = routesByFile.get(file); @@ -56,15 +56,15 @@ export function browserRouteModulesPlugin( theExports = ( await getRouteModuleExportsCached(config, route.id) - ).filter(ex => !!browserSafeRouteExports[ex]); + ).filter((ex) => !!browserSafeRouteExports[ex]); } catch (error: any) { return { errors: [ { text: error.message, - pluginName: "browser-route-module" - } - ] + pluginName: "browser-route-module", + }, + ], }; } let spec = @@ -74,10 +74,10 @@ export function browserRouteModulesPlugin( return { contents, resolveDir: path.dirname(file), - loader: "js" + loader: "js", }; } ); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts b/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts index 54f512038c..34fc3cbb6a 100644 --- a/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/emptyModulesPlugin.ts @@ -14,7 +14,7 @@ export function emptyModulesPlugin( return { name: "empty-modules", setup(build) { - build.onResolve({ filter }, args => { + build.onResolve({ filter }, (args) => { let resolved = path.resolve(args.resolveDir, args.path); if ( // Limit this behavior to modules found in only the `app` directory. @@ -32,9 +32,9 @@ export function emptyModulesPlugin( // matching export" errors in esbuild for stuff that is imported // from this file. contents: "module.exports = {};", - loader: "js" + loader: "js", }; }); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/mdx.ts b/packages/remix-dev/compiler/plugins/mdx.ts index a18b4228db..287014da15 100644 --- a/packages/remix-dev/compiler/plugins/mdx.ts +++ b/packages/remix-dev/compiler/plugins/mdx.ts @@ -12,26 +12,26 @@ export function mdxPlugin(config: RemixConfig): esbuild.Plugin { async setup(build) { let [xdm, { default: remarkFrontmatter }] = await Promise.all([ import("xdm"), - import("remark-frontmatter") as any + import("remark-frontmatter") as any, ]); - build.onResolve({ filter: /\.mdx?$/ }, args => { + build.onResolve({ filter: /\.mdx?$/ }, (args) => { return { path: args.path.startsWith("~/") ? path.resolve(config.appDirectory, args.path.replace(/^~\//, "")) : path.resolve(args.resolveDir, args.path), - namespace: "mdx" + namespace: "mdx", }; }); - build.onLoad({ filter: /\.mdx?$/ }, async args => { + build.onLoad({ filter: /\.mdx?$/ }, async (args) => { try { let contents = await fsp.readFile(args.path, "utf-8"); let rehypePlugins = []; let remarkPlugins = [ remarkFrontmatter, - [remarkMdxFrontmatter, { name: "attributes" }] + [remarkMdxFrontmatter, { name: "attributes" }], ]; switch (typeof config.mdx) { @@ -60,7 +60,7 @@ export const links = undefined; pragma: "React.createElement", pragmaFrag: "React.Fragment", rehypePlugins, - remarkPlugins + remarkPlugins, }); contents = ` @@ -70,7 +70,7 @@ ${remixExports}`; let errors: esbuild.PartialMessage[] = []; let warnings: esbuild.PartialMessage[] = []; - compiled.messages.forEach(message => { + compiled.messages.forEach((message) => { let toPush = message.fatal ? errors : warnings; toPush.push({ location: @@ -83,12 +83,12 @@ ${remixExports}`; line: typeof message.line === "number" ? message.line - : undefined + : undefined, } : undefined, text: message.message, detail: - typeof message.note === "string" ? message.note : undefined + typeof message.note === "string" ? message.note : undefined, }); }); @@ -97,18 +97,18 @@ ${remixExports}`; warnings: warnings.length ? warnings : undefined, contents, resolveDir: path.dirname(args.path), - loader: getLoaderForFile(args.path) + loader: getLoaderForFile(args.path), }; } catch (err: any) { return { errors: [ { - text: err.message - } - ] + text: err.message, + }, + ], }; } }); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts b/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts index 9de59736d9..fd434042cc 100644 --- a/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverAssetsManifestPlugin.ts @@ -22,7 +22,7 @@ export function serverAssetsManifestPlugin( build.onResolve({ filter }, ({ path }) => { return { path, - namespace: "server-assets-manifest" + namespace: "server-assets-manifest", }; }); @@ -36,9 +36,9 @@ export function serverAssetsManifestPlugin( return { contents: `export default ${jsesc(manifest, { es6: true })};`, - loader: "js" + loader: "js", }; }); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts index 8c639eca51..4b066cb688 100644 --- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -5,7 +5,7 @@ import type { Plugin } from "esbuild"; import type { RemixConfig } from "../../config"; import { serverBuildVirtualModule, - assetsManifestVirtualModule + assetsManifestVirtualModule, } from "../virtualModules"; /** @@ -76,7 +76,7 @@ export function serverBareModulesPlugin( if (isNodeBuiltIn(packageName)) { return { path: `https://deno.land/std/node/${packageName}/mod.ts`, - external: true + external: true, }; } return undefined; @@ -94,10 +94,10 @@ export function serverBareModulesPlugin( // Externalize everything else if we've gotten here. return { path, - external: true + external: true, }; }); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/serverEntryModulePlugin.ts b/packages/remix-dev/compiler/plugins/serverEntryModulePlugin.ts index 8820dadcb7..79e20f4f41 100644 --- a/packages/remix-dev/compiler/plugins/serverEntryModulePlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverEntryModulePlugin.ts @@ -4,7 +4,7 @@ import type { Plugin } from "esbuild"; import type { RemixConfig } from "../../config"; import { serverBuildVirtualModule, - assetsManifestVirtualModule + assetsManifestVirtualModule, } from "../virtualModules"; /** @@ -22,7 +22,7 @@ export function serverEntryModulePlugin(config: RemixConfig): Plugin { build.onResolve({ filter }, ({ path }) => { return { path, - namespace: "server-entry-module" + namespace: "server-entry-module", }; }); @@ -60,9 +60,9 @@ ${Object.keys(config.routes) }`; }) .join(",\n ")} - };` + };`, }; }); - } + }, }; } diff --git a/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts index 567c2a0aa5..08f94eabd1 100644 --- a/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverRouteModulesPlugin.ts @@ -14,18 +14,18 @@ export function serverRouteModulesPlugin(config: RemixConfig): esbuild.Plugin { name: "server-route-modules", setup(build) { let routeFiles = new Set( - Object.keys(config.routes).map(key => + Object.keys(config.routes).map((key) => path.resolve(config.appDirectory, config.routes[key].file) ) ); - build.onResolve({ filter: /.*/ }, args => { + build.onResolve({ filter: /.*/ }, (args) => { if (routeFiles.has(args.path)) { return { path: args.path, namespace: "route" }; } }); - build.onLoad({ filter: /.*/, namespace: "route" }, async args => { + build.onLoad({ filter: /.*/, namespace: "route" }, async (args) => { let file = args.path; let contents = await fsp.readFile(file, "utf-8"); @@ -40,9 +40,9 @@ export function serverRouteModulesPlugin(config: RemixConfig): esbuild.Plugin { return { contents, resolveDir: path.dirname(file), - loader: getLoaderForFile(file) + loader: getLoaderForFile(file), }; }); - } + }, }; } diff --git a/packages/remix-dev/compiler/routes.ts b/packages/remix-dev/compiler/routes.ts index 87a49c2e6b..06b9f36a59 100644 --- a/packages/remix-dev/compiler/routes.ts +++ b/packages/remix-dev/compiler/routes.ts @@ -47,14 +47,14 @@ export async function getRouteModuleExports( ): Promise { let result = await esbuild.build({ entryPoints: [ - path.resolve(config.appDirectory, config.routes[routeId].file) + path.resolve(config.appDirectory, config.routes[routeId].file), ], platform: "neutral", format: "esm", metafile: true, write: false, logLevel: "silent", - plugins: [mdxPlugin(config)] + plugins: [mdxPlugin(config)], }); let metafile = result.metafile!; diff --git a/packages/remix-dev/compiler/utils/crypto.ts b/packages/remix-dev/compiler/utils/crypto.ts index cc8eae86c3..dd38a03eac 100644 --- a/packages/remix-dev/compiler/utils/crypto.ts +++ b/packages/remix-dev/compiler/utils/crypto.ts @@ -10,8 +10,8 @@ export async function getFileHash(file: string): Promise { return new Promise((accept, reject) => { let hash = createHash("sha256"); fs.createReadStream(file) - .on("error", error => reject(error)) - .on("data", data => hash.update(data)) + .on("error", (error) => reject(error)) + .on("data", (data) => hash.update(data)) .on("close", () => { accept(hash.digest("hex")); }); diff --git a/packages/remix-dev/compiler/virtualModules.ts b/packages/remix-dev/compiler/virtualModules.ts index 05cb975ed4..e7df45fbd9 100644 --- a/packages/remix-dev/compiler/virtualModules.ts +++ b/packages/remix-dev/compiler/virtualModules.ts @@ -5,10 +5,10 @@ interface VirtualModule { export const serverBuildVirtualModule: VirtualModule = { id: "@remix-run/dev/server-build", - filter: /^@remix-run\/dev\/server-build$/ + filter: /^@remix-run\/dev\/server-build$/, }; export const assetsManifestVirtualModule: VirtualModule = { id: "@remix-run/dev/assets-manifest", - filter: /^@remix-run\/dev\/assets-manifest$/ + filter: /^@remix-run\/dev\/assets-manifest$/, }; diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 292beb0f2a..be5e2ad224 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -369,7 +369,7 @@ export async function readConfig( } let routes: RouteManifest = { - root: { path: "", id: "root", file: rootRouteFile } + root: { path: "", id: "root", file: rootRouteFile }, }; if (fse.existsSync(path.resolve(appDirectory, "routes"))) { let conventionalRoutes = defineConventionalRoutes( @@ -414,7 +414,7 @@ export async function readConfig( serverBuildTargetEntryModule, serverEntryPoint: customServerEntryPoint, serverDependenciesToBundle, - mdx + mdx, }; } diff --git a/packages/remix-dev/config/format.ts b/packages/remix-dev/config/format.ts index 1317a54aae..c3227f6232 100644 --- a/packages/remix-dev/config/format.ts +++ b/packages/remix-dev/config/format.ts @@ -2,7 +2,7 @@ import type { RouteManifest } from "./routes"; export enum RoutesFormat { json = "json", - jsx = "jsx" + jsx = "jsx", } export function isRoutesFormat(format: any): format is RoutesFormat { @@ -35,7 +35,7 @@ export function formatRoutesAsJson(routeManifest: RouteManifest): string { parentId?: string ): JsonFormattedRoute[] | undefined { let routes = Object.values(routeManifest).filter( - route => route.parentId === parentId + (route) => route.parentId === parentId ); let children = []; @@ -47,7 +47,7 @@ export function formatRoutesAsJson(routeManifest: RouteManifest): string { path: route.path, caseSensitive: route.caseSensitive, file: route.file, - children: handleRoutesRecursive(route.id) + children: handleRoutesRecursive(route.id), }); } @@ -65,7 +65,7 @@ export function formatRoutesAsJsx(routeManifest: RouteManifest) { function handleRoutesRecursive(parentId?: string, level = 1): boolean { let routes = Object.values(routeManifest).filter( - route => route.parentId === parentId + (route) => route.parentId === parentId ); let indent = Array(level * 2) diff --git a/packages/remix-dev/config/routes.ts b/packages/remix-dev/config/routes.ts index 1fad6a03a3..9d97158d03 100644 --- a/packages/remix-dev/config/routes.ts +++ b/packages/remix-dev/config/routes.ts @@ -146,7 +146,7 @@ export function defineRoutes( parentRoutes.length > 0 ? parentRoutes[parentRoutes.length - 1].id : undefined, - file + file, }; routes[route.id] = route; diff --git a/packages/remix-dev/config/routesConvention.ts b/packages/remix-dev/config/routesConvention.ts index 2ff0143af1..4fb5fc3211 100644 --- a/packages/remix-dev/config/routesConvention.ts +++ b/packages/remix-dev/config/routesConvention.ts @@ -29,10 +29,10 @@ export function defineConventionalRoutes( let files: { [routeId: string]: string } = {}; // First, find all route modules in app/routes - visitFiles(path.join(appDir, "routes"), file => { + visitFiles(path.join(appDir, "routes"), (file) => { if ( ignoredFilePatterns && - ignoredFilePatterns.some(pattern => minimatch(file, pattern)) + ignoredFilePatterns.some((pattern) => minimatch(file, pattern)) ) { return; } @@ -58,7 +58,7 @@ export function defineConventionalRoutes( parentId?: string ): void { let childRouteIds = routeIds.filter( - id => findParentRouteId(routeIds, id) === parentId + (id) => findParentRouteId(routeIds, id) === parentId ); for (let routeId of childRouteIds) { @@ -86,7 +86,7 @@ export function defineConventionalRoutes( if (isIndexRoute) { let invalidChildRoutes = routeIds.filter( - id => findParentRouteId(routeIds, id) === routeId + (id) => findParentRouteId(routeIds, id) === routeId ); if (invalidChildRoutes.length > 0) { @@ -96,7 +96,7 @@ export function defineConventionalRoutes( } defineRoute(routePath, files[routeId], { - index: true + index: true, }); } else { defineRoute(routePath, files[routeId], () => { @@ -197,7 +197,7 @@ function findParentRouteId( routeIds: string[], childRouteId: string ): string | undefined { - return routeIds.find(id => childRouteId.startsWith(`${id}/`)); + return routeIds.find((id) => childRouteId.startsWith(`${id}/`)); } function byLongestFirst(a: string, b: string): number { diff --git a/packages/remix-dev/config/serverModes.ts b/packages/remix-dev/config/serverModes.ts index 387cfa5cf2..a04829da43 100644 --- a/packages/remix-dev/config/serverModes.ts +++ b/packages/remix-dev/config/serverModes.ts @@ -4,7 +4,7 @@ export enum ServerMode { Development = "development", Production = "production", - Test = "test" + Test = "test", } export function isValidServerMode(mode: string): mode is ServerMode { diff --git a/packages/remix-dev/setup.ts b/packages/remix-dev/setup.ts index ce281c3368..31306eb182 100644 --- a/packages/remix-dev/setup.ts +++ b/packages/remix-dev/setup.ts @@ -5,7 +5,7 @@ export enum SetupPlatform { CloudflarePages = "cloudflare-pages", CloudflareWorkers = "cloudflare-workers", Node = "node", - Deno = "deno" + Deno = "deno", } export function isSetupPlatform(platform: any): platform is SetupPlatform { @@ -13,7 +13,7 @@ export function isSetupPlatform(platform: any): platform is SetupPlatform { SetupPlatform.CloudflarePages, SetupPlatform.CloudflareWorkers, SetupPlatform.Node, - SetupPlatform.Deno + SetupPlatform.Deno, ].includes(platform); } @@ -74,7 +74,7 @@ export async function setupRemix(platform: SetupPlatform): Promise { [ path.join(platformExportsDir, "esm"), path.join(serverExportsDir, "esm"), - path.join(clientExportsDir, "esm") + path.join(clientExportsDir, "esm"), ], ".js" ); diff --git a/packages/remix-express/__tests__/server-test.ts b/packages/remix-express/__tests__/server-test.ts index de9667973d..a342980096 100644 --- a/packages/remix-express/__tests__/server-test.ts +++ b/packages/remix-express/__tests__/server-test.ts @@ -8,7 +8,7 @@ import { Readable } from "stream"; import { createRemixHeaders, createRemixRequest, - createRequestHandler + createRequestHandler, } from "../server"; // We don't want to test that the remix server works here (that's what the @@ -27,7 +27,7 @@ function createApp() { createRequestHandler({ // We don't have a real app to test, but it doesn't matter. We // won't ever call through to the real createRequestHandler - build: undefined + build: undefined, }) ); @@ -45,7 +45,7 @@ describe("express createRequestHandler", () => { }); it("handles requests", async () => { - mockedCreateRequestHandler.mockImplementation(() => async req => { + mockedCreateRequestHandler.mockImplementation(() => async (req) => { return new Response(`URL: ${new URL(req.url).pathname}`); }); @@ -119,7 +119,7 @@ describe("express createRequestHandler", () => { expect(res.headers["set-cookie"]).toEqual([ "first=one; Expires=0; Path=/; HttpOnly; Secure; SameSite=Lax", "second=two; MaxAge=1209600; Path=/; HttpOnly; Secure; SameSite=Lax", - "third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax" + "third=three; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Path=/; HttpOnly; Secure; SameSite=Lax", ]); }); }); @@ -197,8 +197,8 @@ describe("express createRemixHeaders", () => { createRemixHeaders({ "set-cookie": [ "__session=some_value; Path=/; Secure; HttpOnly; MaxAge=7200; SameSite=Lax", - "__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax" - ] + "__other=some_other_value; Path=/; Secure; HttpOnly; MaxAge=3600; SameSite=Lax", + ], }) ).toMatchInlineSnapshot(` Headers { @@ -223,8 +223,8 @@ describe("express createRemixRequest", () => { hostname: "localhost", headers: { "Cache-Control": "max-age=300, s-maxage=3600", - Host: "localhost:3000" - } + Host: "localhost:3000", + }, }); expect(createRemixRequest(expressRequest)).toMatchInlineSnapshot(` diff --git a/packages/remix-express/server.ts b/packages/remix-express/server.ts index b0debf891c..ff65695bdd 100644 --- a/packages/remix-express/server.ts +++ b/packages/remix-express/server.ts @@ -3,18 +3,18 @@ import type * as express from "express"; import type { AppLoadContext, ServerBuild, - ServerPlatform + ServerPlatform, } from "@remix-run/server-runtime"; import { createRequestHandler as createRemixRequestHandler } from "@remix-run/server-runtime"; import type { RequestInit as NodeRequestInit, - Response as NodeResponse + Response as NodeResponse, } from "@remix-run/node"; import { // This has been added as a global in node 15+ AbortController, Headers as NodeHeaders, - Request as NodeRequest + Request as NodeRequest, } from "@remix-run/node"; /** @@ -37,7 +37,7 @@ export type RequestHandler = ReturnType; export function createRequestHandler({ build, getLoadContext, - mode = process.env.NODE_ENV + mode = process.env.NODE_ENV, }: { build: ServerBuild; getLoadContext?: GetLoadContextFunction; @@ -104,7 +104,7 @@ export function createRemixRequest( method: req.method, headers: createRemixHeaders(req.headers), signal: abortController?.signal, - abortController + abortController, }; if (req.method !== "GET" && req.method !== "HEAD") { diff --git a/packages/remix-node/__tests__/fetch-test.ts b/packages/remix-node/__tests__/fetch-test.ts index 3062e3e6d5..e1dda7584d 100644 --- a/packages/remix-node/__tests__/fetch-test.ts +++ b/packages/remix-node/__tests__/fetch-test.ts @@ -24,8 +24,8 @@ let test = { "Content-Type: application/octet-stream", "", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", - "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--" - ].join("\r\n") + "-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--", + ].join("\r\n"), ], boundary: "---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k", expected: [ @@ -36,7 +36,7 @@ let test = { false, false, "7bit", - "text/plain" + "text/plain", ], [ "field", @@ -45,7 +45,7 @@ let test = { false, false, "7bit", - "text/plain" + "text/plain", ], [ "file", @@ -54,7 +54,7 @@ let test = { 0, "1k_a.dat", "7bit", - "application/octet-stream" + "application/octet-stream", ], [ "file", @@ -63,10 +63,10 @@ let test = { 0, "1k_b.dat", "7bit", - "application/octet-stream" - ] + "application/octet-stream", + ], ], - what: "Fields and files" + what: "Fields and files", }; describe("Request", () => { @@ -74,14 +74,14 @@ describe("Request", () => { it("clones", async () => { let body = new PassThrough(); - test.source.forEach(chunk => body.write(chunk)); + test.source.forEach((chunk) => body.write(chunk)); let req = new Request("http://test.com", { method: "post", body, headers: { - "Content-Type": "multipart/form-data; boundary=" + test.boundary - } + "Content-Type": "multipart/form-data; boundary=" + test.boundary, + }, }); let cloned = req.clone(); diff --git a/packages/remix-node/__tests__/formData-test.ts b/packages/remix-node/__tests__/formData-test.ts index b0cc11fb3f..1f483ef44f 100644 --- a/packages/remix-node/__tests__/formData-test.ts +++ b/packages/remix-node/__tests__/formData-test.ts @@ -14,7 +14,7 @@ describe("FormData", () => { expect(results).toEqual([ ["single", "heyo"], ["multi", "one"], - ["multi", "two"] + ["multi", "two"], ]); }); diff --git a/packages/remix-node/__tests__/parseMultipartFormData-test.ts b/packages/remix-node/__tests__/parseMultipartFormData-test.ts index 2dd3847047..7cd02866d1 100644 --- a/packages/remix-node/__tests__/parseMultipartFormData-test.ts +++ b/packages/remix-node/__tests__/parseMultipartFormData-test.ts @@ -14,7 +14,7 @@ describe("internalParseFormData", () => { let req = new NodeRequest("https://test.com", { method: "post", - body: formData as any + body: formData as any, }); let uploadHandler = createMemoryUploadHandler({}); diff --git a/packages/remix-node/__tests__/sessions-test.ts b/packages/remix-node/__tests__/sessions-test.ts index 6e617c96af..005c4d1fa7 100644 --- a/packages/remix-node/__tests__/sessions-test.ts +++ b/packages/remix-node/__tests__/sessions-test.ts @@ -22,7 +22,7 @@ describe("File session storage", () => { it("persists session data across requests", async () => { let { getSession, commitSession } = createFileSessionStorage({ dir, - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -35,7 +35,7 @@ describe("File session storage", () => { it("returns an empty session for cookies that are not signed properly", async () => { let { getSession, commitSession } = createFileSessionStorage({ dir, - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -55,7 +55,7 @@ describe("File session storage", () => { it("unsigns old session cookies using the old secret and encodes new cookies using the new secret", async () => { let { getSession, commitSession } = createFileSessionStorage({ dir, - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -67,7 +67,7 @@ describe("File session storage", () => { // A new secret enters the rotation... let storage = createFileSessionStorage({ dir, - cookie: { secrets: ["secret2", "secret1"] } + cookie: { secrets: ["secret2", "secret1"] }, }); getSession = storage.getSession; commitSession = storage.commitSession; diff --git a/packages/remix-node/cookieSigning.ts b/packages/remix-node/cookieSigning.ts index 1cbb5e5ad9..10c56a71aa 100644 --- a/packages/remix-node/cookieSigning.ts +++ b/packages/remix-node/cookieSigning.ts @@ -1,7 +1,7 @@ import cookieSignature from "cookie-signature"; import type { InternalSignFunctionDoNotUseMe, - InternalUnsignFunctionDoNotUseMe + InternalUnsignFunctionDoNotUseMe, } from "@remix-run/server-runtime/cookieSigning"; export const sign: InternalSignFunctionDoNotUseMe = async (value, secret) => { diff --git a/packages/remix-node/fetch.ts b/packages/remix-node/fetch.ts index 0ca71d1ea8..a79f872082 100644 --- a/packages/remix-node/fetch.ts +++ b/packages/remix-node/fetch.ts @@ -34,7 +34,7 @@ function formDataToStream(formData: NodeFormData): FormStream { } passthrough.push(null); }) - .catch(error => { + .catch((error) => { passthrough.emit("error", error); }); @@ -49,13 +49,13 @@ function formDataToStream(formData: NodeFormData): FormStream { formStream.append(key, stream, { filename: value.name, contentType: value.type, - knownLength: value.size + knownLength: value.size, }); } else { let file = value as File; let stream = toNodeStream(file.stream()); formStream.append(key, stream, { - filename: "unknown" + filename: "unknown", }); } } @@ -74,7 +74,7 @@ class NodeRequest extends BaseNodeRequest { if (init?.body instanceof NodeFormData) { init = { ...init, - body: formDataToStream(init.body) + body: formDataToStream(init.body), }; } @@ -124,7 +124,7 @@ export function fetch( if (init?.body instanceof NodeFormData) { init = { ...init, - body: formDataToStream(init.body) + body: formDataToStream(init.body), }; } diff --git a/packages/remix-node/formData.ts b/packages/remix-node/formData.ts index 13cee0c213..2e4cb8ceb0 100644 --- a/packages/remix-node/formData.ts +++ b/packages/remix-node/formData.ts @@ -91,14 +91,14 @@ class NodeFormData implements FormData { thisArg?: any ): void { Object.entries(this._fields).forEach(([name, values]) => { - values.forEach(value => callbackfn(value, name, thisArg), thisArg); + values.forEach((value) => callbackfn(value, name, thisArg), thisArg); }); } entries(): IterableIterator<[string, FormDataEntryValue]> { return Object.entries(this._fields) .reduce((entries, [name, values]) => { - values.forEach(value => entries.push([name, value])); + values.forEach((value) => entries.push([name, value])); return entries; }, [] as [string, FormDataEntryValue][]) .values(); @@ -111,7 +111,7 @@ class NodeFormData implements FormData { values(): IterableIterator { return Object.entries(this._fields) .reduce((results, [name, values]) => { - values.forEach(value => results.push(value)); + values.forEach((value) => results.push(value)); return results; }, [] as FormDataEntryValue[]) .values(); diff --git a/packages/remix-node/globals.ts b/packages/remix-node/globals.ts index 168bf02f7a..4a3a94090a 100644 --- a/packages/remix-node/globals.ts +++ b/packages/remix-node/globals.ts @@ -1,6 +1,6 @@ import type { InternalSignFunctionDoNotUseMe, - InternalUnsignFunctionDoNotUseMe + InternalUnsignFunctionDoNotUseMe, } from "@remix-run/server-runtime/cookieSigning"; import { Blob as NodeBlob, File as NodeFile } from "@web-std/file"; @@ -10,7 +10,7 @@ import { Headers as NodeHeaders, Request as NodeRequest, Response as NodeResponse, - fetch as nodeFetch + fetch as nodeFetch, } from "./fetch"; import { FormData as NodeFormData } from "./formData"; diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index 74fc3bbb72..34eef8735c 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -8,7 +8,7 @@ export type { HeadersInit, RequestInfo, RequestInit, - ResponseInit + ResponseInit, } from "./fetch"; export { Headers, Request, Response, fetch } from "./fetch"; @@ -23,6 +23,6 @@ export { createFileSessionStorage } from "./sessions/fileStorage"; export { createFileUploadHandler as unstable_createFileUploadHandler, - NodeOnDiskFile + NodeOnDiskFile, } from "./upload/fileUploadHandler"; export { createMemoryUploadHandler as unstable_createMemoryUploadHandler } from "./upload/memoryUploadHandler"; diff --git a/packages/remix-node/magicExports/platform.ts b/packages/remix-node/magicExports/platform.ts index dedf86de68..7d3c27fc3a 100644 --- a/packages/remix-node/magicExports/platform.ts +++ b/packages/remix-node/magicExports/platform.ts @@ -5,7 +5,7 @@ export { createFileSessionStorage, unstable_createFileUploadHandler, unstable_createMemoryUploadHandler, - unstable_parseMultipartFormData + unstable_parseMultipartFormData, } from "@remix-run/node"; export type { UploadHandler, UploadHandlerArgs } from "@remix-run/node"; diff --git a/packages/remix-node/parseMultipartFormData.ts b/packages/remix-node/parseMultipartFormData.ts index d9f43c1e46..7597c1ca62 100644 --- a/packages/remix-node/parseMultipartFormData.ts +++ b/packages/remix-node/parseMultipartFormData.ts @@ -37,8 +37,8 @@ export async function internalParseFormData( let busboy = new Busboy({ highWaterMark: 2 * 1024 * 1024, headers: { - "content-type": contentType - } + "content-type": contentType, + }, }); let aborted = false; @@ -68,7 +68,7 @@ export async function internalParseFormData( stream: filestream, filename, encoding, - mimetype + mimetype, }); if (typeof value !== "undefined") { diff --git a/packages/remix-node/sessions/fileStorage.ts b/packages/remix-node/sessions/fileStorage.ts index a58ad41430..ca0f94a369 100644 --- a/packages/remix-node/sessions/fileStorage.ts +++ b/packages/remix-node/sessions/fileStorage.ts @@ -3,7 +3,7 @@ import { promises as fsp } from "fs"; import * as path from "path"; import type { SessionStorage, - SessionIdStorageStrategy + SessionIdStorageStrategy, } from "@remix-run/server-runtime"; import { createSessionStorage } from "@remix-run/server-runtime"; @@ -30,7 +30,7 @@ interface FileSessionStorageOptions { */ export function createFileSessionStorage({ cookie, - dir + dir, }: FileSessionStorageOptions): SessionStorage { return createSessionStorage({ cookie, @@ -92,7 +92,7 @@ export function createFileSessionStorage({ } catch (error: any) { if (error.code !== "ENOENT") throw error; } - } + }, }); } diff --git a/packages/remix-node/upload/fileUploadHandler.ts b/packages/remix-node/upload/fileUploadHandler.ts index 5dc5ddf7eb..b9b8aec687 100644 --- a/packages/remix-node/upload/fileUploadHandler.ts +++ b/packages/remix-node/upload/fileUploadHandler.ts @@ -85,7 +85,7 @@ export function createFileUploadHandler({ avoidFileConflicts = true, file = defaultFilePathResolver, filter, - maxFileSize = 3000000 + maxFileSize = 3000000, }: FileUploadHandlerOptions): UploadHandler { return async ({ name, stream, filename, encoding, mimetype }) => { if (filter && !(await filter({ filename, encoding, mimetype }))) { @@ -170,9 +170,9 @@ export class NodeOnDiskFile implements File { return new Promise((resolve, reject) => { const buf: any[] = []; - stream.on("data", chunk => buf.push(chunk)); + stream.on("data", (chunk) => buf.push(chunk)); stream.on("end", () => resolve(Buffer.concat(buf))); - stream.on("error", err => reject(err)); + stream.on("error", (err) => reject(err)); }); } diff --git a/packages/remix-node/upload/memoryUploadHandler.ts b/packages/remix-node/upload/memoryUploadHandler.ts index 572cb592e6..5dde5b803f 100644 --- a/packages/remix-node/upload/memoryUploadHandler.ts +++ b/packages/remix-node/upload/memoryUploadHandler.ts @@ -28,7 +28,7 @@ export type MemoryUploadHandlerOptions = { export function createMemoryUploadHandler({ filter, - maxFileSize = 3000000 + maxFileSize = 3000000, }: MemoryUploadHandlerOptions): UploadHandler { return async ({ name, stream, filename, encoding, mimetype }) => { if (filter && !(await filter({ filename, encoding, mimetype }))) { @@ -63,7 +63,7 @@ export function createMemoryUploadHandler({ }); return new BufferFile(bufferStream.data, filename, { - type: mimetype + type: mimetype, }); }; } diff --git a/packages/remix-serve/cli.ts b/packages/remix-serve/cli.ts index 1a1e33511a..c8b8036377 100644 --- a/packages/remix-serve/cli.ts +++ b/packages/remix-serve/cli.ts @@ -18,7 +18,7 @@ let buildPath = path.resolve(process.cwd(), buildPathArg); createApp(buildPath).listen(port, () => { let address = Object.values(os.networkInterfaces()) .flat() - .find(ip => ip?.family == "IPv4" && !ip.internal)?.address; + .find((ip) => ip?.family == "IPv4" && !ip.internal)?.address; if (!address) { address = "localhost"; diff --git a/packages/remix-server-runtime/__tests__/cookies-test.ts b/packages/remix-server-runtime/__tests__/cookies-test.ts index 5e1411c740..2b6552b21f 100644 --- a/packages/remix-server-runtime/__tests__/cookies-test.ts +++ b/packages/remix-server-runtime/__tests__/cookies-test.ts @@ -44,7 +44,7 @@ describe("cookies", () => { it("parses/serializes signed string values", async () => { let cookie = createCookie("my-cookie", { - secrets: ["secret1"] + secrets: ["secret1"], }); let setCookie = await cookie.serialize("hello michael"); let value = await cookie.parse(getCookieFromSetCookie(setCookie)); @@ -54,11 +54,11 @@ describe("cookies", () => { it("fails to parses signed string values with invalid signature", async () => { let cookie = createCookie("my-cookie", { - secrets: ["secret1"] + secrets: ["secret1"], }); let setCookie = await cookie.serialize("hello michael"); let cookie2 = createCookie("my-cookie", { - secrets: ["secret2"] + secrets: ["secret2"], }); let value = await cookie2.parse(getCookieFromSetCookie(setCookie)); @@ -67,7 +67,7 @@ describe("cookies", () => { it("parses/serializes signed object values", async () => { let cookie = createCookie("my-cookie", { - secrets: ["secret1"] + secrets: ["secret1"], }); let setCookie = await cookie.serialize({ hello: "mjackson" }); let value = await cookie.parse(getCookieFromSetCookie(setCookie)); @@ -81,11 +81,11 @@ describe("cookies", () => { it("fails to parse signed object values with invalid signature", async () => { let cookie = createCookie("my-cookie", { - secrets: ["secret1"] + secrets: ["secret1"], }); let setCookie = await cookie.serialize({ hello: "mjackson" }); let cookie2 = createCookie("my-cookie", { - secrets: ["secret2"] + secrets: ["secret2"], }); let value = await cookie2.parse(getCookieFromSetCookie(setCookie)); @@ -94,7 +94,7 @@ describe("cookies", () => { it("supports secret rotation", async () => { let cookie = createCookie("my-cookie", { - secrets: ["secret1"] + secrets: ["secret1"], }); let setCookie = await cookie.serialize({ hello: "mjackson" }); let value = await cookie.parse(getCookieFromSetCookie(setCookie)); @@ -107,7 +107,7 @@ describe("cookies", () => { // A new secret enters the rotation... cookie = createCookie("my-cookie", { - secrets: ["secret2", "secret1"] + secrets: ["secret2", "secret1"], }); // cookie should still be able to parse old cookies. @@ -128,7 +128,7 @@ describe("cookies", () => { let cookie2 = createCookie("my-cookie2"); let setCookie2 = await cookie2.serialize("hello world", { - path: "/about" + path: "/about", }); expect(setCookie2).toContain("Path=/about"); }); diff --git a/packages/remix-server-runtime/__tests__/data-test.ts b/packages/remix-server-runtime/__tests__/data-test.ts index e79611f249..585c7cd2f6 100644 --- a/packages/remix-server-runtime/__tests__/data-test.ts +++ b/packages/remix-server-runtime/__tests__/data-test.ts @@ -19,11 +19,11 @@ describe("loaders", () => { id: routeId, path: "/random", module: { - loader - } - } + loader, + }, + }, }, - entry: { module: {} } + entry: { module: {} }, } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -32,8 +32,8 @@ describe("loaders", () => { "http://example.com/random?_data=routes/random&foo=bar", { headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, } ); @@ -45,8 +45,8 @@ describe("loaders", () => { let loader = async ({ request }) => { throw new Response("null", { headers: { - "Content-type": "application/json" - } + "Content-type": "application/json", + }, }); }; @@ -57,11 +57,11 @@ describe("loaders", () => { id: routeId, path: "/random", module: { - loader - } - } + loader, + }, + }, }, - entry: { module: {} } + entry: { module: {} }, } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -70,8 +70,8 @@ describe("loaders", () => { "http://example.com/random?_data=routes/random&foo=bar", { headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, } ); @@ -91,11 +91,11 @@ describe("loaders", () => { id: routeId, path: "/random", module: { - loader - } - } + loader, + }, + }, }, - entry: { module: {} } + entry: { module: {} }, } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -104,8 +104,8 @@ describe("loaders", () => { "http://example.com/random?_data=routes/random&index&foo=bar", { headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, } ); @@ -125,11 +125,11 @@ describe("loaders", () => { id: routeId, path: "/random", module: { - loader - } - } + loader, + }, + }, }, - entry: { module: {} } + entry: { module: {} }, } as unknown as ServerBuild; let handler = createRequestHandler(build, {}); @@ -138,8 +138,8 @@ describe("loaders", () => { "http://example.com/random?_data=routes/random&index&foo=bar&index=test", { headers: { - "Content-Type": "application/json" - } + "Content-Type": "application/json", + }, } ); @@ -160,9 +160,9 @@ describe("loaders", () => { route: { id: routeId, module: { - loader - } - } + loader, + }, + }, } as unknown as RouteMatch; try { @@ -189,9 +189,9 @@ describe("actions", () => { route: { id: routeId, module: { - action - } - } + action, + }, + }, } as unknown as RouteMatch; try { diff --git a/packages/remix-server-runtime/__tests__/responses-test.ts b/packages/remix-server-runtime/__tests__/responses-test.ts index c431bd75ac..d680717af0 100644 --- a/packages/remix-server-runtime/__tests__/responses-test.ts +++ b/packages/remix-server-runtime/__tests__/responses-test.ts @@ -14,8 +14,8 @@ describe("json", () => { { headers: { "Content-Type": "application/json; charset=iso-8859-1", - "X-Remix": "is awesome" - } + "X-Remix": "is awesome", + }, } ); @@ -45,8 +45,8 @@ describe("redirect", () => { it("sets the status to 302 when only headers are given", () => { let response = redirect("/login", { headers: { - "X-Remix": "is awesome" - } + "X-Remix": "is awesome", + }, }); expect(response.status).toEqual(302); }); @@ -60,8 +60,8 @@ describe("redirect", () => { let response = redirect("/login", { headers: { Location: "/", - "X-Remix": "is awesome" - } + "X-Remix": "is awesome", + }, }); expect(response.headers.get("Location")).toEqual("/login"); diff --git a/packages/remix-server-runtime/__tests__/server-test.ts b/packages/remix-server-runtime/__tests__/server-test.ts index b3f479be61..be6f6041cb 100644 --- a/packages/remix-server-runtime/__tests__/server-test.ts +++ b/packages/remix-server-runtime/__tests__/server-test.ts @@ -23,10 +23,10 @@ describe("server", () => { let build: ServerBuild = { entry: { module: { - default: async request => { + default: async (request) => { return new Response(`${request.method}, ${request.url}`); - } - } + }, + }, }, routes: { [routeId]: { @@ -35,9 +35,9 @@ describe("server", () => { module: { action: () => "ACTION", loader: () => "LOADER", - default: () => "COMPONENT" - } - } + default: () => "COMPONENT", + }, + }, }, assets: { routes: { @@ -47,10 +47,10 @@ describe("server", () => { hasLoader: true, id: routeId, module: routeId, - path: "" - } - } - } + path: "", + }, + }, + }, } as unknown as ServerBuild; describe("createRequestHandler", () => { @@ -64,14 +64,14 @@ describe("server", () => { ["DELETE", "/"], ["DELETE", "/_data=root"], ["PATCH", "/"], - ["PATCH", "/_data=root"] + ["PATCH", "/_data=root"], ]; for (let [method, to] of allowThrough) { it(`allows through ${method} request to ${to}`, async () => { let handler = createRequestHandler(build, {}); let response = await handler( new Request(`http://localhost:3000${to}`, { - method + method, }) ); @@ -83,7 +83,7 @@ describe("server", () => { let handler = createRequestHandler(build, {}); let response = await handler( new Request("http://localhost:3000/", { - method: "HEAD" + method: "HEAD", }) ); @@ -112,12 +112,12 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/resource": { loader: resourceLoader, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -143,16 +143,16 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/resource": { loader: resourceLoader, - path: "resource" + path: "resource", }, "routes/resource.sub": { loader: subResourceLoader, - path: "resource/sub" - } + path: "resource/sub", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -176,12 +176,12 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/resource": { loader: resourceLoader, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -202,8 +202,8 @@ describe("shared server runtime", () => { let build = mockServerBuild({ "routes/resource": { loader, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -221,8 +221,8 @@ describe("shared server runtime", () => { let build = mockServerBuild({ "routes/resource": { loader, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Development); @@ -243,12 +243,12 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - action: rootAction + action: rootAction, }, "routes/resource": { action: resourceAction, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -274,16 +274,16 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - action: rootAction + action: rootAction, }, "routes/resource": { action: resourceAction, - path: "resource" + path: "resource", }, "routes/resource.sub": { action: subResourceAction, - path: "resource/sub" - } + path: "resource/sub", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -307,12 +307,12 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - action: rootAction + action: rootAction, }, "routes/resource": { action: resourceAction, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -333,8 +333,8 @@ describe("shared server runtime", () => { let build = mockServerBuild({ "routes/resource": { action, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -352,8 +352,8 @@ describe("shared server runtime", () => { let build = mockServerBuild({ "routes/resource": { action, - path: "resource" - } + path: "resource", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Development); @@ -369,17 +369,17 @@ describe("shared server runtime", () => { test("data request that does not match loader surfaces error for boundary", async () => { let build = mockServerBuild({ root: { - default: {} + default: {}, }, "routes/index": { parentId: "root", - index: true - } + index: true, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/?_data=routes/index`, { - method: "get" + method: "get", }); let result = await handler(request); @@ -398,18 +398,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/index": { parentId: "root", loader: indexLoader, - index: true - } + index: true, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/?_data=routes/index`, { - method: "get" + method: "get", }); let result = await handler(request); @@ -429,18 +429,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=root`, { - method: "get" + method: "get", }); let result = await handler(request); @@ -463,18 +463,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Development); let request = new Request(`${baseUrl}/test?_data=root`, { - method: "get" + method: "get", }); let result = await handler(request); @@ -496,18 +496,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=root`, { - method: "get" + method: "get", }); let result = await handler(request); @@ -521,17 +521,17 @@ describe("shared server runtime", () => { test("data request that does not match action surfaces error for boundary", async () => { let build = mockServerBuild({ root: { - default: {} + default: {}, }, "routes/index": { parentId: "root", - index: true - } + index: true, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/?index&_data=routes/index`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -550,18 +550,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=routes/test`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -581,18 +581,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=routes/test`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -615,18 +615,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Development); let request = new Request(`${baseUrl}/test?_data=routes/test`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -648,18 +648,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/test": { parentId: "root", action: testAction, - path: "test" - } + path: "test", + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/test?_data=routes/test`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -681,12 +681,12 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - action: rootAction + action: rootAction, }, "routes/index": { parentId: "root", - index: true - } + index: true, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -709,18 +709,18 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader + loader: rootLoader, }, "routes/index": { parentId: "root", action: indexAction, - index: true - } + index: true, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); let request = new Request(`${baseUrl}/?index&_data=routes/index`, { - method: "post" + method: "post", }); let result = await handler(request); @@ -739,8 +739,8 @@ describe("shared server runtime", () => { let build = mockServerBuild({ root: { default: {}, - loader: rootLoader - } + loader: rootLoader, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -767,8 +767,8 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} - } + CatchBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -799,14 +799,14 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, - loader: indexLoader - } + loader: indexLoader, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -825,7 +825,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -840,15 +840,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, loader: indexLoader, - CatchBoundary: {} - } + CatchBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -867,7 +867,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("routes/index"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -885,15 +885,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/test": { parentId: "root", path: "test", default: {}, loader: testLoader, - action: testAction - } + action: testAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -913,7 +913,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -931,15 +931,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, loader: indexLoader, - action: indexAction - } + action: indexAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -959,7 +959,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -977,7 +977,7 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/test": { parentId: "root", @@ -985,8 +985,8 @@ describe("shared server runtime", () => { default: {}, loader: testLoader, action: testAction, - CatchBoundary: {} - } + CatchBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1006,7 +1006,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("routes/test"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1024,7 +1024,7 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/index": { parentId: "root", @@ -1032,8 +1032,8 @@ describe("shared server runtime", () => { default: {}, loader: indexLoader, action: indexAction, - CatchBoundary: {} - } + CatchBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1053,7 +1053,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.catch!.status).toBe(400); expect(entryContext.appState.catchBoundaryRouteId).toBe("routes/index"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1074,21 +1074,21 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/__layout": { parentId: "root", default: {}, loader: layoutLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/__layout/test": { parentId: "routes/__layout", path: "test", default: {}, loader: testLoader, - action: testAction - } + action: testAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1110,7 +1110,7 @@ describe("shared server runtime", () => { "routes/__layout" ); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1131,21 +1131,21 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/__layout": { parentId: "root", default: {}, loader: layoutLoader, - CatchBoundary: {} + CatchBoundary: {}, }, "routes/__layout/index": { parentId: "routes/__layout", index: true, default: {}, loader: indexLoader, - action: indexAction - } + action: indexAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1167,7 +1167,7 @@ describe("shared server runtime", () => { "routes/__layout" ); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1182,14 +1182,14 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, - loader: indexLoader - } + loader: indexLoader, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1208,7 +1208,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("index"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1223,15 +1223,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, loader: indexLoader, - ErrorBoundary: {} - } + ErrorBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1250,7 +1250,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("index"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("routes/index"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1268,15 +1268,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/test": { parentId: "root", path: "test", default: {}, loader: testLoader, - action: testAction - } + action: testAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1296,7 +1296,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("test"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1314,15 +1314,15 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", index: true, default: {}, loader: indexLoader, - action: indexAction - } + action: indexAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1342,7 +1342,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("index"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("root"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1360,7 +1360,7 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/test": { parentId: "root", @@ -1368,8 +1368,8 @@ describe("shared server runtime", () => { default: {}, loader: testLoader, action: testAction, - ErrorBoundary: {} - } + ErrorBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1389,7 +1389,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("test"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("routes/test"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1407,7 +1407,7 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", @@ -1415,8 +1415,8 @@ describe("shared server runtime", () => { default: {}, loader: indexLoader, action: indexAction, - ErrorBoundary: {} - } + ErrorBoundary: {}, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1436,7 +1436,7 @@ describe("shared server runtime", () => { expect(entryContext.appState.error.message).toBe("index"); expect(entryContext.appState.loaderBoundaryRouteId).toBe("routes/index"); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1457,21 +1457,21 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/__layout": { parentId: "root", default: {}, loader: layoutLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/__layout/test": { parentId: "routes/__layout", path: "test", default: {}, loader: testLoader, - action: testAction - } + action: testAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1493,7 +1493,7 @@ describe("shared server runtime", () => { "routes/__layout" ); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1514,21 +1514,21 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/__layout": { parentId: "root", default: {}, loader: layoutLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/__layout/index": { parentId: "routes/__layout", index: true, default: {}, loader: indexLoader, - action: indexAction - } + action: indexAction, + }, }); let handler = createRequestHandler(build, {}, ServerMode.Test); @@ -1550,7 +1550,7 @@ describe("shared server runtime", () => { "routes/__layout" ); expect(entryContext.routeData).toEqual({ - root: "root" + root: "root", }); }); @@ -1565,13 +1565,13 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", default: {}, - loader: indexLoader - } + loader: indexLoader, + }, }); let calledBefore = false; let ogHandleDocumentRequest = build.entry.module.default; @@ -1611,13 +1611,13 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", default: {}, - loader: indexLoader - } + loader: indexLoader, + }, }); let lastThrownError; build.entry.module.default = jest.fn(function () { @@ -1649,13 +1649,13 @@ describe("shared server runtime", () => { root: { default: {}, loader: rootLoader, - ErrorBoundary: {} + ErrorBoundary: {}, }, "routes/index": { parentId: "root", default: {}, - loader: indexLoader - } + loader: indexLoader, + }, }); let errorMessage = "thrown from handleDocumentRequest and expected to be logged in console only once"; diff --git a/packages/remix-server-runtime/__tests__/sessions-test.ts b/packages/remix-server-runtime/__tests__/sessions-test.ts index 87d40b70d9..7fe66e8e16 100644 --- a/packages/remix-server-runtime/__tests__/sessions-test.ts +++ b/packages/remix-server-runtime/__tests__/sessions-test.ts @@ -52,7 +52,7 @@ describe("isSession", () => { describe("In-memory session storage", () => { it("persists session data across requests", async () => { let { getSession, commitSession } = createMemorySessionStorage({ - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -66,7 +66,7 @@ describe("In-memory session storage", () => { describe("Cookie session storage", () => { it("persists session data across requests", async () => { let { getSession, commitSession } = createCookieSessionStorage({ - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -78,7 +78,7 @@ describe("Cookie session storage", () => { it("returns an empty session for cookies that are not signed properly", async () => { let { getSession, commitSession } = createCookieSessionStorage({ - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -96,7 +96,7 @@ describe("Cookie session storage", () => { it('"makes the default path of cookies to be /', async () => { let { getSession, commitSession } = createCookieSessionStorage({ - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -107,7 +107,7 @@ describe("Cookie session storage", () => { describe("when a new secret shows up in the rotation", () => { it("unsigns old session cookies using the old secret and encodes new cookies using the new secret", async () => { let { getSession, commitSession } = createCookieSessionStorage({ - cookie: { secrets: ["secret1"] } + cookie: { secrets: ["secret1"] }, }); let session = await getSession(); session.set("user", "mjackson"); @@ -118,7 +118,7 @@ describe("Cookie session storage", () => { // A new secret enters the rotation... let storage = createCookieSessionStorage({ - cookie: { secrets: ["secret2", "secret1"] } + cookie: { secrets: ["secret2", "secret1"] }, }); getSession = storage.getSession; commitSession = storage.commitSession; diff --git a/packages/remix-server-runtime/__tests__/utils.ts b/packages/remix-server-runtime/__tests__/utils.ts index e5a6ddbc8b..ff9ec7681a 100644 --- a/packages/remix-server-runtime/__tests__/utils.ts +++ b/packages/remix-server-runtime/__tests__/utils.ts @@ -23,7 +23,7 @@ export function mockServerBuild( assets: { entry: { imports: [""], - module: "" + module: "", }, routes: Object.entries(routes).reduce((p, [id, config]) => { let route: EntryRoute = { @@ -35,15 +35,15 @@ export function mockServerBuild( module: "", index: config.index, path: config.path, - parentId: config.parentId + parentId: config.parentId, }; return { ...p, - [id]: route + [id]: route, }; }, {}), url: "", - version: "" + version: "", }, entry: { module: { @@ -51,11 +51,11 @@ export function mockServerBuild( async (request, responseStatusCode, responseHeaders, entryContext) => new Response(null, { status: responseStatusCode, - headers: responseHeaders + headers: responseHeaders, }) ), - handleDataRequest: jest.fn(async response => response) - } + handleDataRequest: jest.fn(async (response) => response), + }, }, routes: Object.entries(routes).reduce( (p, [id, config]) => { @@ -70,16 +70,16 @@ export function mockServerBuild( ErrorBoundary: config.ErrorBoundary, action: config.action, headers: config.headers, - loader: config.loader - } + loader: config.loader, + }, }; return { ...p, - [id]: route + [id]: route, }; }, {} - ) + ), }; } diff --git a/packages/remix-server-runtime/cookies.ts b/packages/remix-server-runtime/cookies.ts index c1b0d13b91..4c86f65182 100644 --- a/packages/remix-server-runtime/cookies.ts +++ b/packages/remix-server-runtime/cookies.ts @@ -81,7 +81,7 @@ export function createCookie( let { secrets, ...options } = { secrets: [], path: "/", - ...cookieOptions + ...cookieOptions, }; return { @@ -112,10 +112,10 @@ export function createCookie( value === "" ? "" : await encodeCookieValue(value, secrets), { ...options, - ...serializeOptions + ...serializeOptions, } ); - } + }, }; } diff --git a/packages/remix-server-runtime/data.ts b/packages/remix-server-runtime/data.ts index 8e30917066..60df3fef7c 100644 --- a/packages/remix-server-runtime/data.ts +++ b/packages/remix-server-runtime/data.ts @@ -16,7 +16,7 @@ export type AppData = any; export async function callRouteAction({ loadContext, match, - request + request, }: { loadContext: unknown; match: RouteMatch; @@ -37,7 +37,7 @@ export async function callRouteAction({ result = await action({ request: stripDataParam(stripIndexParam(request)), context: loadContext, - params: match.params + params: match.params, }); } catch (error: unknown) { if (!isResponse(error)) { @@ -63,7 +63,7 @@ export async function callRouteAction({ export async function callRouteLoader({ loadContext, match, - request + request, }: { request: Request; match: RouteMatch; @@ -84,7 +84,7 @@ export async function callRouteLoader({ result = await loader({ request: stripDataParam(stripIndexParam(request.clone())), context: loadContext, - params: match.params + params: match.params, }); } catch (error: unknown) { if (!isResponse(error)) { diff --git a/packages/remix-server-runtime/entry.ts b/packages/remix-server-runtime/entry.ts index c14c740d90..8a555c429a 100644 --- a/packages/remix-server-runtime/entry.ts +++ b/packages/remix-server-runtime/entry.ts @@ -3,7 +3,7 @@ import type { RouteManifest, ServerRouteManifest, EntryRoute, - ServerRoute + ServerRoute, } from "./routes"; import type { RouteData } from "./routeData"; import type { RouteMatch } from "./routeMatching"; @@ -33,10 +33,10 @@ export function createEntryMatches( matches: RouteMatch[], routes: RouteManifest ): RouteMatch[] { - return matches.map(match => ({ + return matches.map((match) => ({ params: match.params, pathname: match.pathname, - route: routes[match.route.id] + route: routes[match.route.id], })); } diff --git a/packages/remix-server-runtime/errors.ts b/packages/remix-server-runtime/errors.ts index 7dad52c404..ffd5bd7f22 100644 --- a/packages/remix-server-runtime/errors.ts +++ b/packages/remix-server-runtime/errors.ts @@ -65,6 +65,6 @@ export interface SerializedError { export async function serializeError(error: Error): Promise { return { message: error.message, - stack: error.stack + stack: error.stack, }; } diff --git a/packages/remix-server-runtime/headers.ts b/packages/remix-server-runtime/headers.ts index 70887eafda..19b494266e 100644 --- a/packages/remix-server-runtime/headers.ts +++ b/packages/remix-server-runtime/headers.ts @@ -40,7 +40,7 @@ function prependCookies(parentHeaders: Headers, childHeaders: Headers): void { if (parentSetCookieString) { let cookies = splitCookiesString(parentSetCookieString); - cookies.forEach(cookie => { + cookies.forEach((cookie) => { childHeaders.append("Set-Cookie", cookie); }); } diff --git a/packages/remix-server-runtime/index.ts b/packages/remix-server-runtime/index.ts index fd51daea14..4ff6a3152b 100644 --- a/packages/remix-server-runtime/index.ts +++ b/packages/remix-server-runtime/index.ts @@ -2,7 +2,7 @@ export type { ServerBuild, ServerEntryModule, HandleDataRequestFunction, - HandleDocumentRequestFunction + HandleDocumentRequestFunction, } from "./build"; export type { @@ -10,7 +10,7 @@ export type { CookieSerializeOptions, CookieSignatureOptions, CookieOptions, - Cookie + Cookie, } from "./cookies"; export { createCookie, isCookie } from "./cookies"; @@ -21,7 +21,7 @@ export type { EntryContext } from "./entry"; export type { LinkDescriptor, HtmlLinkDescriptor, - PageLinkDescriptor + PageLinkDescriptor, } from "./links"; export type { ServerPlatform } from "./platform"; @@ -37,7 +37,7 @@ export type { MetaDescriptor, MetaFunction, RouteComponent, - RouteHandle + RouteHandle, } from "./routeModules"; export { json, redirect } from "./responses"; @@ -49,7 +49,7 @@ export type { SessionData, Session, SessionStorage, - SessionIdStorageStrategy + SessionIdStorageStrategy, } from "./sessions"; export { createSession, isSession, createSessionStorage } from "./sessions"; export { createCookieSessionStorage } from "./sessions/cookieStorage"; diff --git a/packages/remix-server-runtime/magicExports/server.ts b/packages/remix-server-runtime/magicExports/server.ts index 37d570c18e..8a06a1d578 100644 --- a/packages/remix-server-runtime/magicExports/server.ts +++ b/packages/remix-server-runtime/magicExports/server.ts @@ -31,7 +31,7 @@ export type { SessionData, Session, SessionStorage, - SessionIdStorageStrategy + SessionIdStorageStrategy, } from "@remix-run/server-runtime"; export { @@ -43,5 +43,5 @@ export { createCookieSessionStorage, createMemorySessionStorage, json, - redirect + redirect, } from "@remix-run/server-runtime"; diff --git a/packages/remix-server-runtime/mode.ts b/packages/remix-server-runtime/mode.ts index 817352d294..903aaa3bee 100644 --- a/packages/remix-server-runtime/mode.ts +++ b/packages/remix-server-runtime/mode.ts @@ -4,7 +4,7 @@ export enum ServerMode { Development = "development", Production = "production", - Test = "test" + Test = "test", } export function isServerMode(value: any): value is ServerMode { diff --git a/packages/remix-server-runtime/responses.ts b/packages/remix-server-runtime/responses.ts index aea4dcfcd3..5a48e9d87b 100644 --- a/packages/remix-server-runtime/responses.ts +++ b/packages/remix-server-runtime/responses.ts @@ -20,7 +20,7 @@ export function json( return new Response(JSON.stringify(data), { ...responseInit, - headers + headers, }); } @@ -46,7 +46,7 @@ export function redirect( return new Response(null, { ...responseInit, - headers + headers, }); } diff --git a/packages/remix-server-runtime/routeMatching.ts b/packages/remix-server-runtime/routeMatching.ts index ccfc8238cf..4373fd6f01 100644 --- a/packages/remix-server-runtime/routeMatching.ts +++ b/packages/remix-server-runtime/routeMatching.ts @@ -16,9 +16,9 @@ export function matchServerRoutes( let matches = matchRoutes(routes as unknown as RouteObject[], pathname); if (!matches) return null; - return matches.map(match => ({ + return matches.map((match) => ({ params: match.params, pathname: match.pathname, - route: match.route as unknown as ServerRoute + route: match.route as unknown as ServerRoute, })); } diff --git a/packages/remix-server-runtime/routes.ts b/packages/remix-server-runtime/routes.ts index 712388270f..7c45e8b273 100644 --- a/packages/remix-server-runtime/routes.ts +++ b/packages/remix-server-runtime/routes.ts @@ -35,9 +35,9 @@ export function createRoutes( parentId?: string ): ServerRoute[] { return Object.keys(manifest) - .filter(key => manifest[key].parentId === parentId) - .map(id => ({ + .filter((key) => manifest[key].parentId === parentId) + .map((id) => ({ ...manifest[id], - children: createRoutes(manifest, id) + children: createRoutes(manifest, id), })); } diff --git a/packages/remix-server-runtime/server.ts b/packages/remix-server-runtime/server.ts index 456264d6db..3b8c1ae997 100644 --- a/packages/remix-server-runtime/server.ts +++ b/packages/remix-server-runtime/server.ts @@ -48,7 +48,7 @@ export function createRequestHandler( loadContext, matches: matches!, handleDataRequest: build.entry.module.handleDataRequest, - serverMode + serverMode, }); break; case "document": @@ -58,7 +58,7 @@ export function createRequestHandler( matches, request, routes, - serverMode + serverMode, }); break; case "resource": @@ -66,7 +66,7 @@ export function createRequestHandler( request, loadContext, matches: matches!, - serverMode + serverMode, }); break; } @@ -75,7 +75,7 @@ export function createRequestHandler( return new Response(null, { headers: response.headers, status: response.status, - statusText: response.statusText + statusText: response.statusText, }); } @@ -87,7 +87,7 @@ async function handleDataRequest({ loadContext, matches, request, - serverMode + serverMode, }: { handleDataRequest?: HandleDataRequestFunction; loadContext: unknown; @@ -120,7 +120,7 @@ async function handleDataRequest({ response = await callRouteAction({ loadContext, match, - request: request + request: request, }); } else { let routeId = url.searchParams.get("_data"); @@ -128,7 +128,7 @@ async function handleDataRequest({ return errorBoundaryError(new Error(`Missing route id in ?_data`), 403); } - let tempMatch = matches.find(match => match.route.id === routeId); + let tempMatch = matches.find((match) => match.route.id === routeId); if (!tempMatch) { return errorBoundaryError( new Error(`Route "${routeId}" does not match URL "${url.pathname}"`), @@ -150,7 +150,7 @@ async function handleDataRequest({ return new Response(null, { status: 204, - headers + headers, }); } @@ -158,7 +158,7 @@ async function handleDataRequest({ response = await handleDataRequest(response.clone(), { context: loadContext, params: match.params, - request: request.clone() + request: request.clone(), }); } @@ -182,7 +182,7 @@ async function renderDocumentRequest({ matches, request, routes, - serverMode + serverMode, }: { build: ServerBuild; loadContext: unknown; @@ -200,7 +200,7 @@ async function renderDocumentRequest({ renderBoundaryRouteId: null, loaderBoundaryRouteId: null, error: undefined, - catch: undefined + catch: undefined, }; if (!isValidRequestMethod(request)) { @@ -209,14 +209,14 @@ async function renderDocumentRequest({ appState.catch = { data: null, status: 405, - statusText: "Method Not Allowed" + statusText: "Method Not Allowed", }; } else if (!matches) { appState.trackCatchBoundaries = false; appState.catch = { data: null, status: 404, - statusText: "Not Found" + statusText: "Not Found", }; } @@ -232,7 +232,7 @@ async function renderDocumentRequest({ actionResponse = await callRouteAction({ loadContext, match: actionMatch, - request: request + request: request, }); if (isRedirectResponse(actionResponse)) { @@ -241,7 +241,7 @@ async function renderDocumentRequest({ actionStatus = { status: actionResponse.status, - statusText: actionResponse.statusText + statusText: actionResponse.statusText, }; if (isCatchResponse(actionResponse)) { @@ -252,11 +252,11 @@ async function renderDocumentRequest({ appState.trackCatchBoundaries = false; appState.catch = { ...actionStatus, - data: await extractData(actionResponse) + data: await extractData(actionResponse), }; } else { actionData = { - [actionMatch.route.id]: await extractData(actionResponse) + [actionMatch.route.id]: await extractData(actionResponse), }; } } catch (error: any) { @@ -299,12 +299,12 @@ async function renderDocumentRequest({ } let routeLoaderResults = await Promise.allSettled( - matchesToLoad.map(match => + matchesToLoad.map((match) => match.route.module.loader ? callRouteLoader({ loadContext, match, - request + request, }) : Promise.resolve(undefined) ) @@ -381,7 +381,7 @@ async function renderDocumentRequest({ appState.catch = { data: await extractData(response), status: response.status, - statusText: response.statusText + statusText: response.statusText, }; break; } else { @@ -416,7 +416,7 @@ async function renderDocumentRequest({ renderableMatches.push({ params: {}, pathname: "", - route: routes[0] + route: routes[0], }); } } @@ -426,7 +426,7 @@ async function renderDocumentRequest({ let notOkResponse = actionStatus && actionStatus.status !== 200 ? actionStatus.status - : loaderStatusCodes.find(status => status !== 200); + : loaderStatusCodes.find((status) => status !== 200); let responseStatusCode = appState.error ? 500 @@ -449,14 +449,14 @@ async function renderDocumentRequest({ actionData, appState: appState, matches: entryMatches, - routeData + routeData, }; let entryContext: EntryContext = { ...serverHandoff, manifest: build.assets, routeModules, - serverHandoffString: createServerHandoffString(serverHandoff) + serverHandoffString: createServerHandoffString(serverHandoff), }; let handleDocumentRequest = build.entry.module.default; @@ -502,8 +502,8 @@ async function renderDocumentRequest({ return new Response(message, { status: 500, headers: { - "Content-Type": "text/plain" - } + "Content-Type": "text/plain", + }, }); } } @@ -513,7 +513,7 @@ async function handleResourceRequest({ loadContext, matches, request, - serverMode + serverMode, }: { request: Request; loadContext: unknown; @@ -543,8 +543,8 @@ async function handleResourceRequest({ return new Response(message, { status: 500, headers: { - "Content-Type": "text/plain" - } + "Content-Type": "text/plain", + }, }); } } @@ -597,8 +597,8 @@ async function errorBoundaryError(error: Error, status: number) { return json(await serializeError(error), { status, headers: { - "X-Remix-Error": "yes" - } + "X-Remix-Error": "yes", + }, }); } diff --git a/packages/remix-server-runtime/sessions.ts b/packages/remix-server-runtime/sessions.ts index bf09562fdc..bd8b1e2dbb 100644 --- a/packages/remix-server-runtime/sessions.ts +++ b/packages/remix-server-runtime/sessions.ts @@ -106,7 +106,7 @@ export function createSession(initialData: SessionData = {}, id = ""): Session { }, unset(name) { map.delete(name); - } + }, }; } @@ -215,7 +215,7 @@ export function createSessionStorage({ createData, readData, updateData, - deleteData + deleteData, }: SessionIdStorageStrategy): SessionStorage { let cookie = isCookie(cookieArg) ? cookieArg @@ -244,9 +244,9 @@ export function createSessionStorage({ await deleteData(session.id); return cookie.serialize("", { ...options, - expires: new Date(0) + expires: new Date(0), }); - } + }, }; } diff --git a/packages/remix-server-runtime/sessions/cookieStorage.ts b/packages/remix-server-runtime/sessions/cookieStorage.ts index 624cc5751f..8e6ee92d9a 100644 --- a/packages/remix-server-runtime/sessions/cookieStorage.ts +++ b/packages/remix-server-runtime/sessions/cookieStorage.ts @@ -22,7 +22,7 @@ interface CookieSessionStorageOptions { * @see https://remix.run/api/remix#createcookiesessionstorage */ export function createCookieSessionStorage({ - cookie: cookieArg + cookie: cookieArg, }: CookieSessionStorageOptions = {}): SessionStorage { let cookie = isCookie(cookieArg) ? cookieArg @@ -42,8 +42,8 @@ export function createCookieSessionStorage({ async destroySession(_session, options) { return cookie.serialize("", { ...options, - expires: new Date(0) + expires: new Date(0), }); - } + }, }; } diff --git a/packages/remix-server-runtime/sessions/memoryStorage.ts b/packages/remix-server-runtime/sessions/memoryStorage.ts index bd1696cd5e..edc7f3668c 100644 --- a/packages/remix-server-runtime/sessions/memoryStorage.ts +++ b/packages/remix-server-runtime/sessions/memoryStorage.ts @@ -1,7 +1,7 @@ import type { SessionData, SessionStorage, - SessionIdStorageStrategy + SessionIdStorageStrategy, } from "../sessions"; import { createSessionStorage } from "../sessions"; @@ -23,7 +23,7 @@ interface MemorySessionStorageOptions { * @see https://remix.run/api/remix#creatememorysessionstorage */ export function createMemorySessionStorage({ - cookie + cookie, }: MemorySessionStorageOptions = {}): SessionStorage { let uniqueId = 0; let map = new Map(); @@ -54,6 +54,6 @@ export function createMemorySessionStorage({ }, async deleteData(id) { map.delete(id); - } + }, }); } From 9d263832a9fd5a1a3bb01f006a685aeb1a59a28e Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Tue, 22 Feb 2022 16:42:01 -0500 Subject: [PATCH 0257/1690] fix(dev/compiler): mark imports using node protocol as external (#1232) --- .../remix-dev/compiler/plugins/serverBareModulesPlugin.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts index 14e978c157..174e1bdc11 100644 --- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts +++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts @@ -104,5 +104,10 @@ function getNpmPackageName(id: string): string { } function isBareModuleId(id: string): boolean { - return !id.startsWith(".") && !id.startsWith("~") && !isAbsolute(id); + return ( + !id.startsWith("node:") && + !id.startsWith(".") && + !id.startsWith("~") && + !isAbsolute(id) + ); } From c16bc9252ebd559f29bf1d37bc5617cafc01bd71 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 23 Feb 2022 13:38:33 -0500 Subject: [PATCH 0258/1690] feat: load environment variables via dotenv for remix dev (#2063) * feat: load environment variables via dotenv for remix dev * chore: sign CLA * chore: move loadEnv to standalone file since its not a command * chore: add .env/.env.* to all create-remix template gitignore files * docs: Add environment variable documentation * docs: update guide/envvars.md * feat: remove support for additional .env files * chore: cleanup some uneccesary diffs --- packages/remix-dev/cli/commands.ts | 5 ++++- packages/remix-dev/env.ts | 18 ++++++++++++++++++ packages/remix-dev/package.json | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 packages/remix-dev/env.ts diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 2657da6fff..0214b62027 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -14,6 +14,7 @@ import * as compiler from "../compiler"; import type { RemixConfig } from "../config"; import { readConfig } from "../config"; import { formatRoutes, RoutesFormat, isRoutesFormat } from "../config/format"; +import { loadEnv } from "../env"; import { setupRemix, isSetupPlatform, SetupPlatform } from "../setup"; import { log } from "../log"; @@ -143,7 +144,6 @@ export async function watch( } export async function dev(remixRoot: string, modeArg?: string) { - // TODO: Warn about the need to install @remix-run/serve if it isn't there? let createApp: typeof createAppType; let express: typeof Express; try { @@ -158,6 +158,9 @@ export async function dev(remixRoot: string, modeArg?: string) { let config = await readConfig(remixRoot); let mode = isBuildMode(modeArg) ? modeArg : BuildMode.Development; + + await loadEnv(config.rootDirectory); + let port = await getPort({ port: process.env.PORT ? Number(process.env.PORT) : 3000, }); diff --git a/packages/remix-dev/env.ts b/packages/remix-dev/env.ts new file mode 100644 index 0000000000..0905bd10fc --- /dev/null +++ b/packages/remix-dev/env.ts @@ -0,0 +1,18 @@ +import * as fsp from "fs/promises"; +import * as path from "path"; + +// Import environment variables from: .env, failing gracefully if it doesn't exist +export async function loadEnv(rootDirectory: string): Promise { + const envPath = path.join(rootDirectory, ".env"); + try { + await fsp.readFile(envPath); + } catch (e) { + return; + } + + console.log(`Loading environment variables from .env`); + const result = require("dotenv").config({ path: envPath }); + if (result.error) { + throw result.error; + } +} diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 98cfde52f7..080dde31d3 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -19,6 +19,7 @@ "@remix-run/server-runtime": "1.2.2", "cacache": "^15.0.5", "chokidar": "^3.5.1", + "dotenv": "^16.0.0", "esbuild": "0.14.22", "exit-hook": "2.2.1", "fs-extra": "^10.0.0", From 006d7fef11d423921fb17fd453ed5cd01aa7cbab Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 23 Feb 2022 13:39:35 -0500 Subject: [PATCH 0259/1690] fix: prefetch asset chunks for routes without loaders (#2096) * fix: prefetch asset chunks for routes without loaders * fix: update to use waitForSelector * fix: switch to waitForSelector * chore: remove unused imports --- integration/prefetch-test.ts | 212 +++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 integration/prefetch-test.ts diff --git a/integration/prefetch-test.ts b/integration/prefetch-test.ts new file mode 100644 index 0000000000..f47cb5f077 --- /dev/null +++ b/integration/prefetch-test.ts @@ -0,0 +1,212 @@ +import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; + +// Generate the test app using the given prefetch mode +function fixtureFactory(mode) { + return { + files: { + "app/root.jsx": js` + import { Outlet, Scripts, Link, useLoaderData } from "remix"; + export default function Root() { + const styles = + 'a:hover { color: red; } a:hover:after { content: " (hovered)"; }' + + 'a:focus { color: green; } a:focus:after { content: " (focused)"; }'; + + return ( + + + + +

Root

+ + + + + + ) + } + `, + + "app/routes/index.jsx": js` + export default function() { + return

Index

; + } + `, + + "app/routes/with-loader.jsx": js` + export function loader() { + return { message: 'data from the loader' }; + } + export default function() { + return

With Loader

; + } + `, + + "app/routes/without-loader.jsx": js` + export default function() { + return

Without Loader

; + } + `, + }, + }; +} + +describe("prefetch=none", () => { + let fixture: Fixture; + let app: AppFixture; + + beforeAll(async () => { + fixture = await createFixture(fixtureFactory("none")); + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); + + it("does not render prefetch tags during SSR", async () => { + let res = await fixture.requestDocument("/"); + expect(res.status).toBe(200); + expect((await app.page.$$("#nav link")).length).toBe(0); + }); + + it("does not add prefetch tags on hydration", async () => { + await app.goto("/"); + expect((await app.page.$$("#nav link")).length).toBe(0); + }); +}); + +describe("prefetch=render", () => { + let fixture: Fixture; + let app: AppFixture; + + beforeAll(async () => { + fixture = await createFixture(fixtureFactory("render")); + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); + + it("does not render prefetch tags during SSR", async () => { + let res = await fixture.requestDocument("/"); + expect(res.status).toBe(200); + expect((await app.page.$$("#nav link")).length).toBe(0); + }); + + it("adds prefetch tags on hydration", async () => { + await app.goto("/"); + // Both data and asset fetch for /with-loader + await app.page.waitForSelector( + "#nav link[rel='prefetch'][as='fetch'][href='/with-loader?_data=routes%2Fwith-loader']" + ); + await app.page.waitForSelector( + "#nav link[rel='modulepreload'][href^='/build/routes/with-loader-']" + ); + // Only asset fetch for /without-loader + await app.page.waitForSelector( + "#nav link[rel='modulepreload'][href^='/build/routes/without-loader-']" + ); + + // Ensure no other links in the #nav element + expect((await app.page.$$("#nav link")).length).toBe(3); + }); +}); + +describe("prefetch=intent (hover)", () => { + let fixture: Fixture; + let app: AppFixture; + + beforeAll(async () => { + fixture = await createFixture(fixtureFactory("intent")); + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); + + it("does not render prefetch tags during SSR", async () => { + let res = await fixture.requestDocument("/"); + expect(res.status).toBe(200); + expect((await app.page.$$("#nav link")).length).toBe(0); + }); + + it("does not add prefetch tags on hydration", async () => { + await app.goto("/"); + expect((await app.page.$$("#nav link")).length).toBe(0); + }); + + it("adds prefetch tags on hover", async () => { + await app.page.hover("a[href='/with-loader']"); + await app.page.waitForSelector( + "#nav link[rel='prefetch'][as='fetch'][href='/with-loader?_data=routes%2Fwith-loader']" + ); + // Check href prefix due to hashed filenames + await app.page.waitForSelector( + "#nav link[rel='modulepreload'][href^='/build/routes/with-loader-']" + ); + expect((await app.page.$$("#nav link")).length).toBe(2); + + await app.page.hover("a[href='/without-loader']"); + await app.page.waitForSelector( + "#nav link[rel='modulepreload'][href^='/build/routes/without-loader-']" + ); + expect((await app.page.$$("#nav link")).length).toBe(3); + }); +}); + +describe("prefetch=intent (focus)", () => { + let fixture: Fixture; + let app: AppFixture; + + beforeAll(async () => { + fixture = await createFixture(fixtureFactory("intent")); + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); + + it("does not render prefetch tags during SSR", async () => { + let res = await fixture.requestDocument("/"); + expect(res.status).toBe(200); + expect((await app.page.$$("#nav link")).length).toBe(0); + }); + + it("does not add prefetch tags on hydration", async () => { + await app.goto("/"); + expect((await app.page.$$("#nav link")).length).toBe(0); + }); + + it("adds prefetch tags on focus", async () => { + // This click is needed to transfer focus to the main window, allowing + // subsequent focus events to fire + await app.page.click("body"); + await app.page.focus("a[href='/with-loader']"); + await app.page.waitForSelector( + "#nav link[rel='prefetch'][as='fetch'][href='/with-loader?_data=routes%2Fwith-loader']" + ); + // Check href prefix due to hashed filenames + await app.page.waitForSelector( + "#nav link[rel='modulepreload'][href^='/build/routes/with-loader-']" + ); + expect((await app.page.$$("#nav link")).length).toBe(2); + + await app.page.focus("a[href='/without-loader']"); + await app.page.waitForSelector( + "#nav link[rel='modulepreload'][href^='/build/routes/without-loader-']" + ); + expect((await app.page.$$("#nav link")).length).toBe(3); + }); +}); From 3475732c2e7e1b6a7211830062369043d9035453 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 28 Feb 2022 14:11:45 -0800 Subject: [PATCH 0260/1690] Version 1.2.3-pre.0 --- packages/remix-dev/package.json | 4 ++-- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 080dde31d3..9067eb9d82 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.2.2", + "version": "1.2.3-pre.0", "license": "MIT", "repository": { "type": "git", @@ -16,7 +16,7 @@ }, "dependencies": { "@esbuild-plugins/node-modules-polyfill": "^0.1.4", - "@remix-run/server-runtime": "1.2.2", + "@remix-run/server-runtime": "1.2.3-pre.0", "cacache": "^15.0.5", "chokidar": "^3.5.1", "dotenv": "^16.0.0", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 8634f6811c..92f57a22e6 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.2.2", + "version": "1.2.3-pre.0", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.2.2", - "@remix-run/server-runtime": "1.2.2" + "@remix-run/node": "1.2.3-pre.0", + "@remix-run/server-runtime": "1.2.3-pre.0" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index d0707fa19e..e98e910ebc 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.2.2", + "version": "1.2.3-pre.0", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.2.2", + "@remix-run/server-runtime": "1.2.3-pre.0", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 3f5ca3c1e6..377c70cca6 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.2.2", + "version": "1.2.3-pre.0", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.2.2", + "@remix-run/express": "1.2.3-pre.0", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 0a63b79b6c..48ea50b2d8 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.2.2", + "version": "1.2.3-pre.0", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From 5e18d998bc89f8a688fffa7134cd075621842b9e Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 28 Feb 2022 14:45:13 -0800 Subject: [PATCH 0261/1690] Version 1.2.3-pre.1 --- packages/remix-dev/package.json | 4 ++-- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 9067eb9d82..9c10d9773e 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.2.3-pre.0", + "version": "1.2.3-pre.1", "license": "MIT", "repository": { "type": "git", @@ -16,7 +16,7 @@ }, "dependencies": { "@esbuild-plugins/node-modules-polyfill": "^0.1.4", - "@remix-run/server-runtime": "1.2.3-pre.0", + "@remix-run/server-runtime": "1.2.3-pre.1", "cacache": "^15.0.5", "chokidar": "^3.5.1", "dotenv": "^16.0.0", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index 92f57a22e6..c6d8d60298 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.2.3-pre.0", + "version": "1.2.3-pre.1", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.2.3-pre.0", - "@remix-run/server-runtime": "1.2.3-pre.0" + "@remix-run/node": "1.2.3-pre.1", + "@remix-run/server-runtime": "1.2.3-pre.1" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index e98e910ebc..5db69b57fb 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.2.3-pre.0", + "version": "1.2.3-pre.1", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.2.3-pre.0", + "@remix-run/server-runtime": "1.2.3-pre.1", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index 377c70cca6..c278b900bc 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.2.3-pre.0", + "version": "1.2.3-pre.1", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.2.3-pre.0", + "@remix-run/express": "1.2.3-pre.1", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 48ea50b2d8..45934aae11 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.2.3-pre.0", + "version": "1.2.3-pre.1", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From 54cdb2a6de8499098de574ce5e8d3490b8b31d2e Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 28 Feb 2022 14:47:29 -0800 Subject: [PATCH 0262/1690] Version 1.2.3 --- packages/remix-dev/package.json | 4 ++-- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 9c10d9773e..07e114e18d 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.2.3-pre.1", + "version": "1.2.3", "license": "MIT", "repository": { "type": "git", @@ -16,7 +16,7 @@ }, "dependencies": { "@esbuild-plugins/node-modules-polyfill": "^0.1.4", - "@remix-run/server-runtime": "1.2.3-pre.1", + "@remix-run/server-runtime": "1.2.3", "cacache": "^15.0.5", "chokidar": "^3.5.1", "dotenv": "^16.0.0", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index c6d8d60298..7b64b08b77 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.2.3-pre.1", + "version": "1.2.3", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.2.3-pre.1", - "@remix-run/server-runtime": "1.2.3-pre.1" + "@remix-run/node": "1.2.3", + "@remix-run/server-runtime": "1.2.3" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 5db69b57fb..ca5c74f535 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.2.3-pre.1", + "version": "1.2.3", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.2.3-pre.1", + "@remix-run/server-runtime": "1.2.3", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index c278b900bc..cfd63da9c2 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.2.3-pre.1", + "version": "1.2.3", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.2.3-pre.1", + "@remix-run/express": "1.2.3", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 45934aae11..08023e0b0f 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.2.3-pre.1", + "version": "1.2.3", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From c4bfec1348b1e8b72ca72eb56857fb0ec91474ec Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 28 Feb 2022 15:50:28 -0800 Subject: [PATCH 0263/1690] fix: Update React Router + History deps --- packages/remix-server-runtime/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 45934aae11..645bb3f55e 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -17,7 +17,7 @@ "@types/cookie": "^0.4.0", "cookie": "^0.4.1", "jsesc": "^3.0.1", - "react-router-dom": "^6.2.1", + "react-router-dom": "^6.2.2", "set-cookie-parser": "^2.4.8", "source-map": "^0.7.3" }, From 6ceb95a77f2e3ae72ad54594f04054aa778130a8 Mon Sep 17 00:00:00 2001 From: Chance Strickland Date: Mon, 28 Feb 2022 15:57:16 -0800 Subject: [PATCH 0264/1690] Version 1.2.3 --- packages/remix-dev/package.json | 4 ++-- packages/remix-express/package.json | 6 +++--- packages/remix-node/package.json | 4 ++-- packages/remix-serve/package.json | 4 ++-- packages/remix-server-runtime/package.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 9c10d9773e..07e114e18d 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/dev", "description": "Dev tools and CLI for Remix", - "version": "1.2.3-pre.1", + "version": "1.2.3", "license": "MIT", "repository": { "type": "git", @@ -16,7 +16,7 @@ }, "dependencies": { "@esbuild-plugins/node-modules-polyfill": "^0.1.4", - "@remix-run/server-runtime": "1.2.3-pre.1", + "@remix-run/server-runtime": "1.2.3", "cacache": "^15.0.5", "chokidar": "^3.5.1", "dotenv": "^16.0.0", diff --git a/packages/remix-express/package.json b/packages/remix-express/package.json index c6d8d60298..7b64b08b77 100644 --- a/packages/remix-express/package.json +++ b/packages/remix-express/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/express", "description": "Express server request handler for Remix", - "version": "1.2.3-pre.1", + "version": "1.2.3", "license": "MIT", "repository": { "type": "git", @@ -12,8 +12,8 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/node": "1.2.3-pre.1", - "@remix-run/server-runtime": "1.2.3-pre.1" + "@remix-run/node": "1.2.3", + "@remix-run/server-runtime": "1.2.3" }, "peerDependencies": { "express": "^4.17.1" diff --git a/packages/remix-node/package.json b/packages/remix-node/package.json index 5db69b57fb..ca5c74f535 100644 --- a/packages/remix-node/package.json +++ b/packages/remix-node/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/node", "description": "Node.js platform abstractions for Remix", - "version": "1.2.3-pre.1", + "version": "1.2.3", "license": "MIT", "repository": { "type": "git", @@ -12,7 +12,7 @@ "url": "https://github.com/remix-run/remix/issues" }, "dependencies": { - "@remix-run/server-runtime": "1.2.3-pre.1", + "@remix-run/server-runtime": "1.2.3", "@types/busboy": "^0.3.1", "@types/node-fetch": "^2.5.12", "@web-std/file": "^3.0.0", diff --git a/packages/remix-serve/package.json b/packages/remix-serve/package.json index c278b900bc..cfd63da9c2 100644 --- a/packages/remix-serve/package.json +++ b/packages/remix-serve/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/serve", "description": "Production application server for Remix", - "version": "1.2.3-pre.1", + "version": "1.2.3", "license": "MIT", "repository": { "type": "git", @@ -15,7 +15,7 @@ "remix-serve": "cli.js" }, "dependencies": { - "@remix-run/express": "1.2.3-pre.1", + "@remix-run/express": "1.2.3", "compression": "^1.7.4", "express": "^4.17.1", "morgan": "^1.10.0" diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json index 645bb3f55e..ba5419f709 100644 --- a/packages/remix-server-runtime/package.json +++ b/packages/remix-server-runtime/package.json @@ -1,7 +1,7 @@ { "name": "@remix-run/server-runtime", "description": "Server runtime for Remix", - "version": "1.2.3-pre.1", + "version": "1.2.3", "license": "MIT", "main": "./index.js", "module": "./esm/index.js", From 9ae078e3af84835b375d9ac2ccf815016c276fcc Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 3 Mar 2022 11:47:09 -0800 Subject: [PATCH 0265/1690] Name all magic exports files the same Also, tweak some build variable names and remove unneeded dep from remix package. --- packages/remix-dev/setup.ts | 12 +++++++----- .../magicExports/{platform.ts => remix.ts} | 3 +-- .../magicExports/{server.ts => remix.ts} | 3 +-- 3 files changed, 9 insertions(+), 9 deletions(-) rename packages/remix-node/magicExports/{platform.ts => remix.ts} (72%) rename packages/remix-server-runtime/magicExports/{server.ts => remix.ts} (90%) diff --git a/packages/remix-dev/setup.ts b/packages/remix-dev/setup.ts index 31306eb182..f30d0a90db 100644 --- a/packages/remix-dev/setup.ts +++ b/packages/remix-dev/setup.ts @@ -33,14 +33,16 @@ export async function setupRemix(platform: SetupPlatform): Promise { } } - let platformPkgJsonFile = resolvePackageJsonFile(`@remix-run/${platform}`); - let serverPkgJsonFile = resolvePackageJsonFile(`@remix-run/server-runtime`); - let clientPkgJsonFile = resolvePackageJsonFile(`@remix-run/react`); - // Update remix/package.json dependencies let remixDeps = {}; + + let platformPkgJsonFile = resolvePackageJsonFile(`@remix-run/${platform}`); await assignDependency(remixDeps, platformPkgJsonFile); + + let serverPkgJsonFile = resolvePackageJsonFile(`@remix-run/server-runtime`); await assignDependency(remixDeps, serverPkgJsonFile); + + let clientPkgJsonFile = resolvePackageJsonFile(`@remix-run/react`); await assignDependency(remixDeps, clientPkgJsonFile); let remixPkgJson = await fse.readJSON(remixPkgJsonFile); @@ -79,8 +81,8 @@ export async function setupRemix(platform: SetupPlatform): Promise { ".js" ); - await fse.writeFile(path.join(remixPkgDir, "index.js"), magicCJS); await fse.writeFile(path.join(remixPkgDir, "index.d.ts"), magicTypes); + await fse.writeFile(path.join(remixPkgDir, "index.js"), magicCJS); await fse.writeFile(path.join(remixPkgDir, "esm/index.js"), magicESM); } diff --git a/packages/remix-node/magicExports/platform.ts b/packages/remix-node/magicExports/remix.ts similarity index 72% rename from packages/remix-node/magicExports/platform.ts rename to packages/remix-node/magicExports/remix.ts index 7d3c27fc3a..01a70161bb 100644 --- a/packages/remix-node/magicExports/platform.ts +++ b/packages/remix-node/magicExports/remix.ts @@ -1,5 +1,4 @@ -// This file lists all exports from this package that are available to `import -// "remix"`. +// Re-export everything from this package that is available in `remix`. export { createFileSessionStorage, diff --git a/packages/remix-server-runtime/magicExports/server.ts b/packages/remix-server-runtime/magicExports/remix.ts similarity index 90% rename from packages/remix-server-runtime/magicExports/server.ts rename to packages/remix-server-runtime/magicExports/remix.ts index 8a06a1d578..a7f7ac1346 100644 --- a/packages/remix-server-runtime/magicExports/server.ts +++ b/packages/remix-server-runtime/magicExports/remix.ts @@ -1,5 +1,4 @@ -// This file lists all exports from this package that are available to `import -// "remix"`. +// Re-export everything from this package that is available in `remix`. export type { ServerBuild, From 8be3612c69cbbc778c3f912f90da268bf01162ad Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Fri, 11 Mar 2022 02:19:04 +0700 Subject: [PATCH 0266/1690] test: added fixture test from Logan for fetcher resource routes (#2269) * test: added fixture test from Logan There doesn't appear to be anything that needs to be done here. * remove stdio --- integration/fetcher-test.ts | 96 ++++++++++++++++++++++++++ integration/helpers/create-fixture.tsx | 16 ++++- 2 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 integration/fetcher-test.ts diff --git a/integration/fetcher-test.ts b/integration/fetcher-test.ts new file mode 100644 index 0000000000..1063bad8c8 --- /dev/null +++ b/integration/fetcher-test.ts @@ -0,0 +1,96 @@ +import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; +import type { Fixture, AppFixture } from "./helpers/create-fixture"; + +describe("useFetcher", () => { + let fixture: Fixture; + let app: AppFixture; + + let CHEESESTEAK = "CHEESESTEAK"; + let LUNCH = "LUNCH"; + + beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/routes/resource-route.jsx": js` + export function loader() { + return "${LUNCH}" + } + export function action() { + return "${CHEESESTEAK}" + } + `, + + "app/routes/index.jsx": js` + import { useFetcher } from "remix"; + export default function Index() { + let fetcher = useFetcher(); + return ( + <> + + + + + + +
{fetcher.data}
+ + ) + } + `, + }, + }); + + app = await createAppFixture(fixture); + }); + + afterAll(async () => { + await app.close(); + }); + + test("Form can hit a loader", async () => { + let enableJavaScript = await app.disableJavaScript(); + await app.goto("/"); + await app.clickSubmitButton("/resource-route", { + wait: false, + method: "get", + }); + await app.page.waitForNavigation(); + expect(await app.getHtml("pre")).toMatch(LUNCH); + await enableJavaScript(); + }); + + test("Form can hit an action", async () => { + let enableJavaScript = await app.disableJavaScript(); + await app.goto("/"); + await app.clickSubmitButton("/resource-route", { + wait: false, + method: "post", + }); + await app.page.waitForNavigation(); + expect(await app.getHtml("pre")).toMatch(CHEESESTEAK); + await enableJavaScript(); + }); + + test("load can hit a loader", async () => { + await app.goto("/"); + await app.clickElement("#fetcher-load"); + expect(await app.getHtml("pre")).toMatch(LUNCH); + }); + + test("submit can hit an action", async () => { + await app.goto("/"); + await app.clickElement("#fetcher-submit"); + expect(await app.getHtml("pre")).toMatch(CHEESESTEAK); + }); +}); diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index 0d297633cd..b03985d933 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -216,12 +216,22 @@ export async function createAppFixture(fixture: Fixture) { */ clickSubmitButton: async ( action: string, - options: { wait: boolean } = { wait: true } + options: { wait: boolean; method?: string } = { wait: true } ) => { - let selector = `button[formaction="${action}"]`; + let selector: string; + if (options.method) { + selector = `button[formAction="${action}"][formMethod="${options.method}"]`; + } else { + selector = `button[formAction="${action}"]`; + } + let el = await page.$(selector); if (!el) { - selector = `form[action="${action}"] button[type="submit"]`; + if (options.method) { + selector = `form[action="${action}"] button[type="submit"][formMethod="${options.method}"]`; + } else { + selector = `form[action="${action}"] button[type="submit"]`; + } el = await page.$(selector); if (!el) { throw new Error(`Can't find button for: ${action}`); From 5f5e7526ebea32d2edc04eeb6f325dc9b979b3a7 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Fri, 11 Mar 2022 19:35:11 +0000 Subject: [PATCH 0267/1690] fix(netlify): use Netlify internal functions dir (#2291) --- packages/remix-dev/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index be5e2ad224..c47f392816 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -324,7 +324,7 @@ export async function readConfig( serverBuildPath = "functions/[[path]].js"; break; case "netlify": - serverBuildPath = "netlify/functions/server/index.js"; + serverBuildPath = ".netlify/functions-internal/server.js"; break; case "vercel": serverBuildPath = "api/index.js"; From 8d83460d0a0a0b352c0ba49b1dba0f3ec09a56f7 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Sat, 12 Mar 2022 14:09:36 -0500 Subject: [PATCH 0268/1690] chore: update remix-dev and remix-serve start message (#2301) --- packages/remix-dev/cli/commands.ts | 20 +++++++++++--------- packages/remix-serve/cli.ts | 10 ++++++---- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 0214b62027..08e7f0811f 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -182,16 +182,18 @@ export async function dev(remixRoot: string, modeArg?: string) { try { await watch(config, mode, { onInitialBuild: () => { - let address = Object.values(os.networkInterfaces()) - .flat() - .find((ip) => ip?.family == "IPv4" && !ip.internal)?.address; - - if (!address) { - address = "localhost"; - } - server = app.listen(port, () => { - console.log(`Remix App Server started at http://${address}:${port}`); + let address = Object.values(os.networkInterfaces()) + .flat() + .find((ip) => ip?.family === "IPv4" && !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})` + ); + } }); }, }); diff --git a/packages/remix-serve/cli.ts b/packages/remix-serve/cli.ts index c8b8036377..eacf04d76e 100644 --- a/packages/remix-serve/cli.ts +++ b/packages/remix-serve/cli.ts @@ -18,11 +18,13 @@ let buildPath = path.resolve(process.cwd(), buildPathArg); createApp(buildPath).listen(port, () => { let address = Object.values(os.networkInterfaces()) .flat() - .find((ip) => ip?.family == "IPv4" && !ip.internal)?.address; + .find((ip) => ip?.family === "IPv4" && !ip.internal)?.address; if (!address) { - address = "localhost"; + console.log(`Remix App Server started at http://localhost:${port}`); + } else { + console.log( + `Remix App Server started at http://localhost:${port} (http://${address}:${port})` + ); } - - console.log(`Remix App Server started at http://${address}:${port}`); }); From fc92e37c3e988658c80a62154788a9fa04f68466 Mon Sep 17 00:00:00 2001 From: Logan McAnsh Date: Mon, 14 Mar 2022 18:33:41 -0400 Subject: [PATCH 0269/1690] feat(create-remix): add support for remote repos (#2189) Co-authored-by: Michael Jackson --- integration/helpers/create-fixture.tsx | 30 +- integration/helpers/setupAfterEnv.ts | 1 + packages/remix-dev/__tests__/cli-test.ts | 343 +++++++++++++- .../remix-dev/__tests__/fixtures/arc.tar.gz | Bin 0 -> 38400 bytes .../fixtures/failing-remix-init.tar.gz | Bin 0 -> 79360 bytes .../fixtures/successful-remix-init.tar.gz | Bin 0 -> 87040 bytes packages/remix-dev/__tests__/fixtures/tar.js | 21 + packages/remix-dev/__tests__/setupAfterEnv.ts | 1 + packages/remix-dev/cli.ts | 362 ++++++++++++--- packages/remix-dev/cli/commands.ts | 74 +++ packages/remix-dev/colors.ts | 16 + packages/remix-dev/create.ts | 423 ++++++++++++++++++ packages/remix-dev/index.ts | 2 + packages/remix-dev/package.json | 16 +- packages/remix-dev/tsconfig.json | 1 + 15 files changed, 1182 insertions(+), 108 deletions(-) create mode 100644 integration/helpers/setupAfterEnv.ts create mode 100644 packages/remix-dev/__tests__/fixtures/arc.tar.gz create mode 100644 packages/remix-dev/__tests__/fixtures/failing-remix-init.tar.gz create mode 100644 packages/remix-dev/__tests__/fixtures/successful-remix-init.tar.gz create mode 100644 packages/remix-dev/__tests__/fixtures/tar.js create mode 100644 packages/remix-dev/__tests__/setupAfterEnv.ts create mode 100644 packages/remix-dev/colors.ts create mode 100644 packages/remix-dev/create.ts diff --git a/integration/helpers/create-fixture.tsx b/integration/helpers/create-fixture.tsx index b03985d933..589e767b1a 100644 --- a/integration/helpers/create-fixture.tsx +++ b/integration/helpers/create-fixture.tsx @@ -9,27 +9,26 @@ import cheerio from "cheerio"; import prettier from "prettier"; import getPort from "get-port"; -import { createRequestHandler } from "../../packages/remix-server-runtime"; -import { createApp } from "../../packages/create-remix"; -import { createRequestHandler as createExpressHandler } from "../../packages/remix-express"; import type { ServerBuild, ServerPlatform, } from "../../packages/remix-server-runtime"; -import type { CreateAppArgs } from "../../packages/create-remix"; +import { createRequestHandler } from "../../packages/remix-server-runtime"; +import { createApp } from "../../packages/remix-dev"; +import { createRequestHandler as createExpressHandler } from "../../packages/remix-express"; import { TMP_DIR } from "./global-setup"; const REMIX_SOURCE_BUILD_DIR = path.join(process.cwd(), "build"); interface FixtureInit { files: { [filename: string]: string }; - server?: CreateAppArgs["server"]; + template?: string; } export type Fixture = Awaited>; export type AppFixture = Awaited>; -export let js = String.raw; +export const js = String.raw; export async function createFixture(init: FixtureInit) { let projectDir = await createFixtureProject(init); @@ -172,7 +171,7 @@ export async function createAppFixture(fixture: Fixture) { /** * Finds a link on the page with a matching href, clicks it, and waits for - * the network to be idle before contininuing. + * the network to be idle before continuing. * * @param href The href of the link you want to click * @param options `{ wait }` waits for the network to be idle before moving on @@ -209,7 +208,7 @@ export async function createAppFixture(fixture: Fixture) { /** * Finds the first submit button with `formAction` that matches the * `action` supplied, clicks it, and optionally waits for the network to - * be idle before contininuing. + * be idle before continuing. * * @param action The formAction of the button you want to click * @param options `{ wait }` waits for the network to be idle before moving on @@ -347,15 +346,20 @@ export async function createAppFixture(fixture: Fixture) { //////////////////////////////////////////////////////////////////////////////// export async function createFixtureProject(init: FixtureInit): Promise { + let appTemplate = path.join( + process.cwd(), + "templates", + init.template ? init.template : "remix" + ); let projectDir = path.join(TMP_DIR, Math.random().toString(32).slice(2)); await createApp({ - install: false, - lang: "js", - server: init.server || "remix", + appTemplate, projectDir, - quiet: true, + installDeps: false, + useTypeScript: false, }); + // TODO: init if necessary? await Promise.all([ writeTestFiles(init, projectDir), installRemix(projectDir), @@ -403,7 +407,7 @@ async function writeTestFiles(init: FixtureInit, dir: string) { * * I found some github issues that says that `modulePathIgnorePatterns` should * help, so I added it to our `jest.config.js`, but it doesn't seem to help, so - * I bruteforced it here. + * I brute-forced it here. */ async function renamePkgJsonApp(dir: string) { let pkgPath = path.join(dir, "package.json"); diff --git a/integration/helpers/setupAfterEnv.ts b/integration/helpers/setupAfterEnv.ts new file mode 100644 index 0000000000..b5062b8542 --- /dev/null +++ b/integration/helpers/setupAfterEnv.ts @@ -0,0 +1 @@ +jest.setTimeout(10000); diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts index b914a6a84b..fa9aad7f60 100644 --- a/packages/remix-dev/__tests__/cli-test.ts +++ b/packages/remix-dev/__tests__/cli-test.ts @@ -2,13 +2,14 @@ import childProcess from "child_process"; import fs from "fs"; import path from "path"; import util from "util"; +import { pathToFileURL } from "url"; import semver from "semver"; const execFile = util.promisify(childProcess.execFile); const remix = path.resolve( __dirname, - "../../../build/node_modules/@remix-run/dev/cli" + "../../../build/node_modules/@remix-run/dev/cli.js" ); describe("remix cli", () => { @@ -20,36 +21,94 @@ describe("remix cli", () => { describe("the --help flag", () => { it("prints help info", async () => { - let { stdout } = await execFile("node", [remix, "--help"]); + let { stdout } = await execFile("node", [remix, "--help"], { + env: { + ...process.env, + NO_COLOR: "1", + }, + }); expect(stdout).toMatchInlineSnapshot(` " - Usage - $ remix build [remixRoot] - $ remix dev [remixRoot] + R E M I X + + Usage: + $ remix create --template