diff --git a/contributors.yml b/contributors.yml index 4c632d504ae..8312616ea9b 100644 --- a/contributors.yml +++ b/contributors.yml @@ -1,3 +1,4 @@ +- aaronpowell - aaronpowell96 - aaronshaf - abereghici diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index aa0cb66ea6a..0239d1141ee 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -18,6 +18,7 @@ npx create-remix@latest Fly.io Netlify Vercel + Azure Static Web Apps ``` Each target has unique file structures, configuration files, cli commands that need to be run, server environment variables to be set etc. Because of this, it's important to read the README.md to deploy the app. It's got all of the steps you need to take to get your app live within minutes. diff --git a/docs/other-api/adapter.md b/docs/other-api/adapter.md index 0eabb3d20b1..8b848a7224f 100644 --- a/docs/other-api/adapter.md +++ b/docs/other-api/adapter.md @@ -8,6 +8,7 @@ order: 2 Idiomatic Remix apps can generally be deployed anywhere because Remix adapt's the server's request/response to the [Web Fetch API][web-fetch-api]. It does this through adapters. We maintain a few adapters: - `@remix-run/architect` +- `@remix-run/azure-functions` - `@remix-run/cloudflare-pages` - `@remix-run/cloudflare-workers` - `@remix-run/express` diff --git a/jest.config.js b/jest.config.js index 11f98c692f4..8f840a7e1f9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,10 +14,11 @@ module.exports = { "packages/remix-node", "packages/remix-react", "packages/remix-server-runtime", + "packages/remix-azure-functions", ], watchPlugins: [ require.resolve("jest-watch-select-projects"), require.resolve("jest-watch-typeahead/filename"), require.resolve("jest-watch-typeahead/testname"), ], -}; +}; \ No newline at end of file diff --git a/package.json b/package.json index 876b692f39a..faf83d479c3 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "packages/create-remix", "packages/remix", "packages/remix-architect", + "packages/remix-azure-functions", "packages/remix-cloudflare", "packages/remix-cloudflare-pages", "packages/remix-cloudflare-workers", diff --git a/packages/remix-azure-functions/README.md b/packages/remix-azure-functions/README.md new file mode 100644 index 00000000000..801448197b5 --- /dev/null +++ b/packages/remix-azure-functions/README.md @@ -0,0 +1,17 @@ +# 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)! + +## About + +This package can be used to host Remix within an [Azure Function](https://docs.microsoft.com/azure/azure-functions/) or in [Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps) (using the built-in Azure Functions support). diff --git a/packages/remix-azure-functions/__tests__/server-test.ts b/packages/remix-azure-functions/__tests__/server-test.ts new file mode 100644 index 00000000000..54dab2bbada --- /dev/null +++ b/packages/remix-azure-functions/__tests__/server-test.ts @@ -0,0 +1,269 @@ +import type { Context, HttpRequest } from "@azure/functions"; +import { + createRequestHandler as createRemixRequestHandler, +} from "@remix-run/node"; + +import { + createRemixHeaders, + createRemixRequest, + 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/node", () => { + let original = jest.requireActual("@remix-run/node"); + return { + ...original, + createRequestHandler: jest.fn(), + }; +}); + +let mockedCreateRequestHandler = + createRemixRequestHandler as jest.MockedFunction< + typeof createRemixRequestHandler + >; + +describe("azure createRequestHandler", () => { + let context: Context; + + beforeEach(() => { + context = { log: jest.fn() } as unknown as Context; + }); + + 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 mockedRequest: HttpRequest = { + method: "GET", + url: "/foo/bar", + rawBody: "", + headers: { + "x-ms-original-url": "http://localhost:3000/foo/bar", + }, + params: {}, + query: {}, + body: "", + }; + + let res = await createRequestHandler({ build: undefined })( + context, + mockedRequest + ); + + expect(res.status).toBe(200); + expect(res.body).toBe("URL: /foo/bar"); + }); + + it("handles status codes", async () => { + mockedCreateRequestHandler.mockImplementation(() => async () => { + return new Response (null, { status: 204 }); + }); + + let mockedRequest: HttpRequest = { + method: "GET", + url: "/foo/bar", + rawBody: "", + headers: { + "x-ms-original-url": "http://localhost:3000/foo/bar", + }, + params: {}, + query: {}, + body: "", + }; + + let res = await createRequestHandler({ build: undefined })( + context, + mockedRequest + ); + + expect(res.status).toBe(204); + }); + + it("sets headers", async () => { + mockedCreateRequestHandler.mockImplementation(() => async (req) => { + let 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" + ); + for(let [key, value] of req.headers.entries()) { + headers.set(key, value); + } + return new Response (null, { headers }); + }); + + let mockedRequest: HttpRequest = { + method: "GET", + url: "/foo/bar", + rawBody: "", + headers: { + "x-ms-original-url": "http://localhost:3000/foo/bar", + }, + params: {}, + query: {}, + body: "", + }; + + let res = await createRequestHandler({ build: undefined })( + context, + mockedRequest + ); + + expect(res.headers["x-ms-original-url"]).toEqual(["http://localhost:3000/foo/bar"]); + expect(res.headers["X-Time-Of-Year"]).toEqual(["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("azure createRemixHeaders", () => { + describe("creates fetch headers from azure 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", + ], + }, + } + `); + }); + }); +}); + +describe("azure createRemixRequest", () => { + it("creates a request with the correct headers", async () => { + let request: HttpRequest = { + method: "GET", + url: "/foo/bar", + rawBody: "", + headers: { + "x-ms-original-url": "http://localhost:3000/foo/bar", + }, + params: {}, + query: {}, + body: "", + }; + + expect(createRemixRequest(request)).toMatchInlineSnapshot(` + NodeRequest { + "abortController": undefined, + "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 { + "x-ms-original-url": Array [ + "http://localhost:3000/foo/bar", + ], + }, + }, + "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-azure-functions/__tests__/setup.ts b/packages/remix-azure-functions/__tests__/setup.ts new file mode 100644 index 00000000000..917305ac938 --- /dev/null +++ b/packages/remix-azure-functions/__tests__/setup.ts @@ -0,0 +1,2 @@ +import { installGlobals } from "@remix-run/node"; +installGlobals(); diff --git a/packages/remix-azure-functions/globals.ts b/packages/remix-azure-functions/globals.ts new file mode 100644 index 00000000000..917305ac938 --- /dev/null +++ b/packages/remix-azure-functions/globals.ts @@ -0,0 +1,2 @@ +import { installGlobals } from "@remix-run/node"; +installGlobals(); diff --git a/packages/remix-azure-functions/index.ts b/packages/remix-azure-functions/index.ts new file mode 100644 index 00000000000..fb640bf4799 --- /dev/null +++ b/packages/remix-azure-functions/index.ts @@ -0,0 +1,4 @@ +import "./globals"; + +export type { GetLoadContextFunction, RequestHandler } from "./server"; +export { createRequestHandler } from "./server"; diff --git a/packages/remix-azure-functions/jest.config.js b/packages/remix-azure-functions/jest.config.js new file mode 100644 index 00000000000..6d55c862313 --- /dev/null +++ b/packages/remix-azure-functions/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + ...require("../../jest/jest.config.shared"), + displayName: "azure-functions", +}; diff --git a/packages/remix-azure-functions/magicExports/remix.ts b/packages/remix-azure-functions/magicExports/remix.ts new file mode 100644 index 00000000000..9fa091084fc --- /dev/null +++ b/packages/remix-azure-functions/magicExports/remix.ts @@ -0,0 +1,9 @@ +/* eslint-disable import/no-extraneous-dependencies */ + +// Re-export everything from this package that is available in `remix`. + +export { + RequestHandler, + createRequestHandler, + GetLoadContextFunction, +} from "@remix-run/azure-functions"; diff --git a/packages/remix-azure-functions/package.json b/packages/remix-azure-functions/package.json new file mode 100644 index 00000000000..1f0168a5612 --- /dev/null +++ b/packages/remix-azure-functions/package.json @@ -0,0 +1,29 @@ +{ + "name": "@remix-run/azure-functions", + "version": "1.7.2", + "description": "Azure Functions server request handler for Remix", + "bugs": { + "url": "https://github.com/remix-run/remix/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/remix-run/remix", + "directory": "packages/remix-azure-functions" + }, + "license": "MIT", + "dependencies": { + "@remix-run/node": "1.7.2" + }, + "devDependencies": { + "@azure/functions": "^3.2.0", + "@types/supertest": "^2.0.12", + "node-mocks-http": "^1.11.0", + "supertest": "^6.2.4" + }, + "peerDependencies": { + "@azure/functions": "^3.0.0" + }, + "engines": { + "node": ">=14" + } +} diff --git a/packages/remix-azure-functions/rollup.config.js b/packages/remix-azure-functions/rollup.config.js new file mode 100644 index 00000000000..a7e5bd46427 --- /dev/null +++ b/packages/remix-azure-functions/rollup.config.js @@ -0,0 +1,6 @@ +const { getAdapterConfig } = require("../../rollup.utils"); + +/** @returns {import("rollup").RollupOptions[]} */ +module.exports = function rollup() { + return [getAdapterConfig("azure-functions")]; +}; diff --git a/packages/remix-azure-functions/server.ts b/packages/remix-azure-functions/server.ts new file mode 100644 index 00000000000..8053796b222 --- /dev/null +++ b/packages/remix-azure-functions/server.ts @@ -0,0 +1,85 @@ +import type { + AzureFunction, + Context, + HttpRequest, + HttpRequestHeaders, +} from "@azure/functions"; +import { + createRequestHandler as createRemixRequestHandler, + Headers as NodeHeaders, + Request as NodeRequest, +} from "@remix-run/node"; +import type { + AppLoadContext, + ServerBuild, + Response as NodeResponse, + RequestInit as NodeRequestInit, +} 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 type GetLoadContextFunction = (req: HttpRequest) => AppLoadContext; + +export type RequestHandler = AzureFunction; + +export function createRequestHandler({ + build, + getLoadContext, + mode = process.env.NODE_ENV, +}: { + build: ServerBuild; + getLoadContext?: GetLoadContextFunction; + mode?: string; +}): RequestHandler { + let handleRequest = createRemixRequestHandler(build, mode); + + return async (_context: Context, req: HttpRequest) => { + let request = createRemixRequest(req); + let loadContext = getLoadContext?.(req); + + let response = (await handleRequest( + request as unknown as Request, + loadContext + )) as unknown as NodeResponse; + + return { + status: response.status, + headers: response.headers.raw(), + body: await response.text(), + }; + }; +} + +export function createRemixHeaders( + requestHeaders: HttpRequestHeaders +): NodeHeaders { + let headers = new NodeHeaders(); + + for (let [key, value] of Object.entries(requestHeaders)) { + if (!value) continue; + headers.set(key, value); + } + + return headers; +} + +export function createRemixRequest(req: HttpRequest): NodeRequest { + let url = req.headers["x-ms-original-url"]!; + + let init: NodeRequestInit = { + method: req.method || "GET", + headers: createRemixHeaders(req.headers), + }; + + if (req.body && !["HEAD", "GET"].includes(req.method)) { + init.body = req.body; + } + + return new NodeRequest(url, init); +} diff --git a/packages/remix-azure-functions/tsconfig.json b/packages/remix-azure-functions/tsconfig.json new file mode 100644 index 00000000000..b725b116c12 --- /dev/null +++ b/packages/remix-azure-functions/tsconfig.json @@ -0,0 +1,16 @@ +{ + "exclude": ["dist", "__tests__"], + "compilerOptions": { + "lib": ["ES2019", "DOM.Iterable"], + "target": "ES2019", + + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "strict": true, + "declaration": true, + "emitDeclarationOnly": true, + "rootDir": ".", + "outDir": "../../build/node_modules/@remix-run/azure/dist", + "composite": true + } +} diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts index 130956de70e..3bb0a181989 100644 --- a/packages/remix-dev/cli/run.ts +++ b/packages/remix-dev/cli/run.ts @@ -129,6 +129,7 @@ const templateChoices = [ { name: "Cloudflare Pages", value: "cloudflare-pages" }, { name: "Cloudflare Workers", value: "cloudflare-workers" }, { name: "Deno", value: "deno" }, + { name: "Azure Static Web Apps", value: "azure-functions" }, ]; const npxInterop = { diff --git a/rollup.utils.js b/rollup.utils.js index 183b13b1447..66c479fa2b1 100644 --- a/rollup.utils.js +++ b/rollup.utils.js @@ -419,7 +419,7 @@ module.exports = { /** * @typedef {Record} MagicExports - * @typedef {"architect" | "cloudflare-pages" | "cloudflare-workers" | "express" | "netlify" | "vercel"} RemixAdapter + * @typedef {"architect" | "azure-functions" | "cloudflare-pages" | "cloudflare-workers" | "express" | "netlify" | "vercel"} RemixAdapter * @typedef {"cloudflare" | "node" | "deno"} RemixRuntime * @typedef {`@remix-run/${RemixAdapter | RemixRuntime | "dev" | "eslint-config" | "react" | "serve" | "server-runtime"}`} ScopedRemixPackage * @typedef {"create-remix" | "remix" | ScopedRemixPackage} RemixPackage diff --git a/templates/azure-functions/.devcontainer/Dockerfile b/templates/azure-functions/.devcontainer/Dockerfile new file mode 100644 index 00000000000..3cfe4a8308a --- /dev/null +++ b/templates/azure-functions/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +# Find the Dockerfile at this URL +# https://github.com/Azure/azure-functions-docker/blob/dev/host/4/bullseye/amd64/python/python39/python39-core-tools.Dockerfile +FROM mcr.microsoft.com/azure-functions/python:4-python3.9-core-tools + +# Copy library scripts to execute +COPY library-scripts/*.sh library-scripts/*.env /tmp/library-scripts/ + +# Install Node.js, Azure Static Web Apps CLI and Azure Functions Core Tools +ARG NODE_VERSION="16" +ARG CORE_TOOLS_VERSION="4" +ENV NVM_DIR="/usr/local/share/nvm" \ + NVM_SYMLINK_CURRENT=true \ + PATH="${NVM_DIR}/current/bin:${PATH}" +RUN bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "${NODE_VERSION}" "${USERNAME}" \ + && su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" \ + && su vscode -c "umask 0002 && npm install --cache /tmp/empty-cache -g @azure/static-web-apps-cli" \ + && if [ $CORE_TOOLS_VERSION != "4" ]; then apt-get remove -y azure-functions-core-tools-4 && apt-get update && apt-get install -y "azure-functions-core-tools-${CORE_TOOLS_VERSION}"; fi \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 \ No newline at end of file diff --git a/templates/azure-functions/.devcontainer/devcontainer.json b/templates/azure-functions/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..eecdcf5fab5 --- /dev/null +++ b/templates/azure-functions/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.158.0/containers/azure-static-web-apps +{ + "name": "Remix for Azure Static Web App", + "dockerFile": "Dockerfile", + "forwardPorts": [7071, 3000, 4280], + + // Set *default* container specific settings.json values on container create. + "settings": { + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-azuretools.vscode-azurefunctions", + "ms-azuretools.vscode-azurestaticwebapps", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "editorconfig.editorconfig" + ], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "", + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" + } + \ No newline at end of file diff --git a/templates/azure-functions/.devcontainer/library-scripts/node-debian.sh b/templates/azure-functions/.devcontainer/library-scripts/node-debian.sh new file mode 100644 index 00000000000..5394a8fe5f4 --- /dev/null +++ b/templates/azure-functions/.devcontainer/library-scripts/node-debian.sh @@ -0,0 +1,169 @@ +#!/bin/bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/node.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./node-debian.sh [directory to install nvm] [node version to install (use "none" to skip)] [non-root user] [Update rc files flag] [install node-gyp deps] + +export NVM_DIR=${1:-"/usr/local/share/nvm"} +export NODE_VERSION=${2:-"lts"} +USERNAME=${3:-"automatic"} +UPDATE_RC=${4:-"true"} +INSTALL_TOOLS_FOR_NODE_GYP="${5:-true}" +export NVM_VERSION="0.38.0" + +set -e + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +updaterc() { + if [ "${UPDATE_RC}" = "true" ]; then + echo "Updating /etc/bash.bashrc and /etc/zsh/zshrc..." + if [[ "$(cat /etc/bash.bashrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/bash.bashrc + fi + if [ -f "/etc/zsh/zshrc" ] && [[ "$(cat /etc/zsh/zshrc)" != *"$1"* ]]; then + echo -e "$1" >> /etc/zsh/zshrc + fi + fi +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Install dependencies +check_packages apt-transport-https curl ca-certificates tar gnupg2 dirmngr + +# Install yarn +if type yarn > /dev/null 2>&1; then + echo "Yarn already installed." +else + # Import key safely (new method rather than deprecated apt-key approach) and install + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarn-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list + apt-get update + apt-get -y install --no-install-recommends yarn +fi + +# Adjust node version if required +if [ "${NODE_VERSION}" = "none" ]; then + export NODE_VERSION= +elif [ "${NODE_VERSION}" = "lts" ]; then + export NODE_VERSION="lts/*" +fi + +# Create a symlink to the installed version for use in Dockerfile PATH statements +export NVM_SYMLINK_CURRENT=true + +# Install the specified node version if NVM directory already exists, then exit +if [ -d "${NVM_DIR}" ]; then + echo "NVM already installed." + if [ "${NODE_VERSION}" != "" ]; then + su ${USERNAME} -c ". $NVM_DIR/nvm.sh && nvm install ${NODE_VERSION} && nvm clear-cache" + fi + exit 0 +fi + +# Create nvm group, nvm dir, and set sticky bit +if ! cat /etc/group | grep -e "^nvm:" > /dev/null 2>&1; then + groupadd -r nvm +fi +umask 0002 +usermod -a -G nvm ${USERNAME} +mkdir -p ${NVM_DIR} +chown :nvm ${NVM_DIR} +chmod g+s ${NVM_DIR} +su ${USERNAME} -c "$(cat << EOF + set -e + umask 0002 + # Do not update profile - we'll do this manually + export PROFILE=/dev/null + curl -so- https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh | bash + source ${NVM_DIR}/nvm.sh + if [ "${NODE_VERSION}" != "" ]; then + nvm alias default ${NODE_VERSION} + fi + nvm clear-cache +EOF +)" 2>&1 +# Update rc files +if [ "${UPDATE_RC}" = "true" ]; then +updaterc "$(cat < /dev/null 2>&1; then + to_install="${to_install} make" + fi + if ! type gcc > /dev/null 2>&1; then + to_install="${to_install} gcc" + fi + if ! type g++ > /dev/null 2>&1; then + to_install="${to_install} g++" + fi + if ! type python3 > /dev/null 2>&1; then + to_install="${to_install} python3-minimal" + fi + if [ ! -z "${to_install}" ]; then + apt_get_update_if_needed + apt-get -y install ${to_install} + fi +fi + +echo "Done!" \ No newline at end of file diff --git a/templates/azure-functions/.eslintrc b/templates/azure-functions/.eslintrc new file mode 100644 index 00000000000..71569754629 --- /dev/null +++ b/templates/azure-functions/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"] +} diff --git a/templates/azure-functions/.gitignore b/templates/azure-functions/.gitignore new file mode 100644 index 00000000000..fba2dd83f63 --- /dev/null +++ b/templates/azure-functions/.gitignore @@ -0,0 +1,8 @@ +node_modules + +/.cache +/azure/function/build +/public/build +.env + +local.settings.json diff --git a/templates/azure-functions/.nvmrc b/templates/azure-functions/.nvmrc new file mode 100644 index 00000000000..8351c19397f --- /dev/null +++ b/templates/azure-functions/.nvmrc @@ -0,0 +1 @@ +14 diff --git a/templates/azure-functions/README.md b/templates/azure-functions/README.md new file mode 100644 index 00000000000..1e95d7c49b8 --- /dev/null +++ b/templates/azure-functions/README.md @@ -0,0 +1,25 @@ +# Welcome to Remix for Azure Static Web Apps! + +- [Remix Docs](https://docs.remix.run) +- [Azure Static Web Apps](https://docs.microsoft.com/azure/static-web-apps) + +## Development + +From your terminal: + +```sh +npm run dev +``` + +This starts the [Static Web Apps emulator](https://github.com/Azure/static-web-apps-cli) and the Remix server in the background. + +A [VS Code Devcontainer definition](https://code.visualstudio.com/docs/remote/containers) has been included to setup a local development environment with all the dependencies and recommended VS Code extensions. + +## Deploying + +Prerequisites: + +- [Azure Account](https://portal.azure.com/) +- [GitHub Account](https://github.com/) + +First, you need to follow the [instructions](https://docs.microsoft.com/azure/static-web-apps/get-started-portal?tabs=vanilla-javascript) to create a new project on Azure. This will also setup the GitHub Actions workflow to perform automated deployments of your site. diff --git a/templates/azure-functions/app/entry.client.tsx b/templates/azure-functions/app/entry.client.tsx new file mode 100644 index 00000000000..ffef95f66db --- /dev/null +++ b/templates/azure-functions/app/entry.client.tsx @@ -0,0 +1,4 @@ +import { RemixBrowser } from "@remix-run/react"; +import { hydrateRoot } from "react-dom/client"; + +hydrateRoot(document, ); diff --git a/templates/azure-functions/app/entry.server.tsx b/templates/azure-functions/app/entry.server.tsx new file mode 100644 index 00000000000..f339800aabc --- /dev/null +++ b/templates/azure-functions/app/entry.server.tsx @@ -0,0 +1,48 @@ +import { PassThrough } from "stream"; +import type { EntryContext } from "@remix-run/node"; +import { Response } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext +) { + return new Promise((resolve, reject) => { + let didError = false; + + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady: () => { + const body = new PassThrough(); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(body, { + headers: responseHeaders, + status: didError ? 500 : responseStatusCode, + }) + ); + + pipe(body); + }, + onShellError: (err) => { + reject(err); + }, + onError: (error) => { + didError = true; + + console.error(error); + }, + } + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/templates/azure-functions/app/root.tsx b/templates/azure-functions/app/root.tsx new file mode 100644 index 00000000000..927a0f745df --- /dev/null +++ b/templates/azure-functions/app/root.tsx @@ -0,0 +1,32 @@ +import type { MetaFunction } from "@remix-run/node"; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; + +export const meta: MetaFunction = () => ({ + charset: "utf-8", + title: "New Remix App", + viewport: "width=device-width,initial-scale=1", +}); + +export default function App() { + return ( + + + + + + + + + + + + + ); +} diff --git a/templates/azure-functions/app/routes/index.tsx b/templates/azure-functions/app/routes/index.tsx new file mode 100644 index 00000000000..cbca6124ea7 --- /dev/null +++ b/templates/azure-functions/app/routes/index.tsx @@ -0,0 +1,32 @@ +export default function Index() { + return ( + + ); +} diff --git a/templates/azure-functions/azure/function.json b/templates/azure-functions/azure/function.json new file mode 100644 index 00000000000..7eb6dd4e1ae --- /dev/null +++ b/templates/azure-functions/azure/function.json @@ -0,0 +1,27 @@ +{ + "bindings": [ + { + "authLevel": "anonymous", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "head", + "post", + "put", + "delete", + "connect", + "options", + "trace", + "patch" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ], + "scriptFile": "./function/index.js" +} diff --git a/templates/azure-functions/azure/function/index.js b/templates/azure-functions/azure/function/index.js new file mode 100644 index 00000000000..bb3cbad42d5 --- /dev/null +++ b/templates/azure-functions/azure/function/index.js @@ -0,0 +1,3 @@ +const { createRequestHandler } = require("@remix-run/azure-functions"); + +module.exports = createRequestHandler({ build: require("./build") }); diff --git a/templates/azure-functions/github-actions/azure-static-web-apps.yml b/templates/azure-functions/github-actions/azure-static-web-apps.yml new file mode 100644 index 00000000000..85a4edd6770 --- /dev/null +++ b/templates/azure-functions/github-actions/azure-static-web-apps.yml @@ -0,0 +1,45 @@ +name: Azure Static Web Apps CI/CD + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened, closed] + branches: + - main + +jobs: + build_and_deploy_job: + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') + runs-on: ubuntu-latest + name: Build and Deploy Job + steps: + - uses: actions/checkout@v3 + with: + submodules: true + - name: Build And Deploy + id: builddeploy + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_TOKEN }} + repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) + action: "upload" + ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### + # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig + app_location: "/" # App source code path + api_location: "/" # Api source code path - optional + output_location: "public" # Built app content directory - optional + ###### End of Repository/Build Configurations ###### + + close_pull_request_job: + if: github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: ubuntu-latest + name: Close Pull Request Job + steps: + - name: Close Pull Request + id: closepullrequest + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_TOKEN }} + action: "close" diff --git a/templates/azure-functions/host.json b/templates/azure-functions/host.json new file mode 100644 index 00000000000..9bbc8364f87 --- /dev/null +++ b/templates/azure-functions/host.json @@ -0,0 +1,20 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensions": { + "http": { + "routePrefix": "api" + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[2.*, 3.0.0)" + } +} diff --git a/templates/azure-functions/package.json b/templates/azure-functions/package.json new file mode 100644 index 00000000000..a182f21d2e3 --- /dev/null +++ b/templates/azure-functions/package.json @@ -0,0 +1,26 @@ +{ + "scripts": { + "build": "remix build", + "dev": "swa start ./public --run \"remix dev\" --api-location .", + "postinstall": "func init --worker-runtime node --language typescript" + }, + "dependencies": { + "@remix-run/azure-functions": "*", + "@remix-run/node": "*", + "@remix-run/react": "*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@azure/functions": "^3.2.0", + "@azure/static-web-apps-cli": "^1.0.2", + "@remix-run/dev": "*", + "@remix-run/eslint-config": "*", + "@remix-run/serve": "*", + "@types/react": "^18.0.15", + "@types/react-dom": "^18.0.6", + "azure-functions-core-tools": "^4.0.4785", + "eslint": "^8.23.1", + "typescript": "^4.7.4" + } +} diff --git a/templates/azure-functions/public/favicon.ico b/templates/azure-functions/public/favicon.ico new file mode 100644 index 00000000000..8830cf6821b Binary files /dev/null and b/templates/azure-functions/public/favicon.ico differ diff --git a/templates/azure-functions/public/index.html b/templates/azure-functions/public/index.html new file mode 100644 index 00000000000..2455f5690a3 --- /dev/null +++ b/templates/azure-functions/public/index.html @@ -0,0 +1,15 @@ + + + + + + + Remix + + +

+ This is a placeholder file you should never see, we'll rewrite all + requests to our azure function +

+ + diff --git a/templates/azure-functions/public/staticwebapp.config.json b/templates/azure-functions/public/staticwebapp.config.json new file mode 100644 index 00000000000..e881715ad4a --- /dev/null +++ b/templates/azure-functions/public/staticwebapp.config.json @@ -0,0 +1,20 @@ +{ + "routes": [ + { + "route": "/build/*" + }, + { + "route": "/favicon.ico" + }, + { + "route": "/*", + "rewrite": "/api/azure" + } + ], + "navigationFallback": { + "rewrite": "/api/azure" + }, + "platform": { + "apiRuntime": "node:14" + } +} \ No newline at end of file diff --git a/templates/azure-functions/remix.config.js b/templates/azure-functions/remix.config.js new file mode 100644 index 00000000000..3957ec73b97 --- /dev/null +++ b/templates/azure-functions/remix.config.js @@ -0,0 +1,7 @@ +module.exports = { + appDirectory: "app", + browserBuildDirectory: "public/build", + publicPath: "/build/", + serverBuildDirectory: "azure/function/build", + devServerPort: 8002, +}; diff --git a/templates/azure-functions/tsconfig.json b/templates/azure-functions/tsconfig.json new file mode 100644 index 00000000000..20f8a386a6c --- /dev/null +++ b/templates/azure-functions/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "ES2019", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Remix takes care of building everything in `remix build`. + "noEmit": true + } +} diff --git a/yarn.lock b/yarn.lock index fd112c3ee6f..b9b21beee66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,6 +22,11 @@ run-waterfall "^1.1.7" uid-safe "^2.1.5" +"@azure/functions@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@azure/functions/-/functions-3.2.0.tgz#c73003db8bd93be186824db29e98e00879bf0f91" + integrity sha512-HbE7iORnYcjLzKNf5mIQRJQDTsVxhoXHRWEZ6KWdGh4e7+F9xTloiBicavbSoVmlAYivenIVpryHanVwsQaHUw== + "@babel/code-frame@7.16.7", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": version "7.16.7" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz" @@ -3094,6 +3099,13 @@ dependencies: "@types/superagent" "*" +"@types/supertest@^2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" + integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== + dependencies: + "@types/superagent" "*" + "@types/tar-fs@^2.0.1": version "2.0.1" resolved "https://registry.npmjs.org/@types/tar-fs/-/tar-fs-2.0.1.tgz" @@ -3779,6 +3791,11 @@ arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + asn1@~0.2.3: version "0.2.6" resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" @@ -4753,6 +4770,11 @@ cookiejar@^2.1.2: resolved "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz" integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== +cookiejar@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" + integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz" @@ -5009,6 +5031,13 @@ debug@^3.0.1, debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -5148,6 +5177,14 @@ detect-newline@3.1.0, detect-newline@^3.0.0: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +dezalgo@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ== + dependencies: + asap "^2.0.0" + wrappy "1" + diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz" @@ -6276,7 +6313,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fast-safe-stringify@^2.0.7: +fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -6459,6 +6496,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" @@ -6478,6 +6524,16 @@ formidable@^1.2.2: resolved "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz" integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== +formidable@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff" + integrity sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ== + dependencies: + dezalgo "1.0.3" + hexoid "1.0.0" + once "1.4.0" + qs "6.9.3" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" @@ -6914,6 +6970,11 @@ headers-polyfill@^3.0.4: resolved "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-3.0.7.tgz" integrity sha512-JoLCAdCEab58+2/yEmSnOlficyHFpIl0XJqwu3l+Unkm1gXpFUYsThz6Yha3D6tNhocWkCPfyW0YVIGWFqTi7w== +hexoid@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + history@^5.2.0, history@^5.3.0: version "5.3.0" resolved "https://registry.npmjs.org/history/-/history-5.3.0.tgz" @@ -9334,6 +9395,11 @@ mime@1.6.0, mime@^1.3.4: resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mime@3.0.0, mime@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz" @@ -9596,7 +9662,7 @@ node-int64@^0.4.0: resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-mocks-http@^1.10.1: +node-mocks-http@^1.10.1, node-mocks-http@^1.11.0: version "1.11.0" resolved "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.11.0.tgz" integrity sha512-jS/WzSOcKbOeGrcgKbenZeNhxUNnP36Yw11+hL4TTxQXErGfqYZ+MaYNNvhaTiGIJlzNSqgQkk9j8dSu1YWSuw== @@ -9776,7 +9842,7 @@ on-headers@~1.0.2: resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@1.4.0, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -10441,11 +10507,23 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qs@6.9.3: + version "6.9.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" + integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== + qs@6.9.7, qs@^6.9.4: version "6.9.7" resolved "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz" integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== +qs@^6.10.3: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + qs@~6.5.2: version "6.5.3" resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz" @@ -11140,7 +11218,7 @@ set-blocking@^2.0.0: set-cookie-parser@^2.4.6, set-cookie-parser@^2.4.8: version "2.4.8" - resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.8.tgz#d0da0ed388bc8f24e706a391f9c9e252a13c58b2" integrity sha512-edRH8mBKEWNVIVMKejNnuJxleqYE/ZSdcT8/Nem9/mmosx12pctd80s2Oy00KNZzrogMZS5mauK2/ymL1bvlvg== set-getter@^0.1.0: @@ -11716,14 +11794,39 @@ superagent@^6.1.0: readable-stream "^3.6.0" semver "^7.3.2" +superagent@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.0.0.tgz#2ea4587df4b81ef023ec01ebc6e1bcb9e2344cb6" + integrity sha512-iudipXEel+SzlP9y29UBWGDjB+Zzag+eeA1iLosaR2YHBRr1Q1kC29iBrF2zIVD9fqVbpZnXkN/VJmwFMVyNWg== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.0.1" + methods "^1.1.2" + mime "2.6.0" + qs "^6.10.3" + readable-stream "^3.6.0" + semver "^7.3.7" + supertest@^6.1.5: - version "6.1.6" - resolved "https://registry.npmjs.org/supertest/-/supertest-6.1.6.tgz" - integrity sha512-0hACYGNJ8OHRg8CRITeZOdbjur7NLuNs0mBjVhdpxi7hP6t3QIbOzLON5RTUmZcy2I9riuII3+Pr2C7yztrIIg== + version "6.1.5" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.1.5.tgz#b011c8465281b30c64e9d4be4cb3808b91cd1ec0" + integrity sha512-Is3pFB2TxSFPohDS2tIM8h2JOMvUQwbJ9TvTfsWAm89ZZv1CF4VTLAsQz+5+5S1wOgaMqFqSpFriU15L3e2AXQ== dependencies: methods "^1.1.2" superagent "^6.1.0" +supertest@^6.2.4: + version "6.2.4" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.2.4.tgz#3dcebe42f7fd6f28dd7ac74c6cba881f7101b2f0" + integrity sha512-M8xVnCNv+q2T2WXVzxDECvL2695Uv2uUj2O0utxsld/HRyJvOU8W9f1gvsYxSNU4wmIe0/L/ItnpU4iKq0emDA== + dependencies: + methods "^1.1.2" + superagent "^8.0.0" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz"