diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000000..e5b6d8d6a67 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000000..823547ba4b6 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", + "changelog": ["@remix-run/changelog-github", { "repo": "remix-run/remix" }], + "commit": false, + "fixed": [ + [ + "create-remix", + "remix", + "@remix-run/dev", + "@remix-run/eslint-config", + "@remix-run/react", + "@remix-run/serve", + + "@remix-run/server-runtime", + "@remix-run/cloudflare", + "@remix-run/node", + + "@remix-run/deno", + "@remix-run/cloudflare-pages", + "@remix-run/cloudflare-workers", + "@remix-run/express", + "@remix-run/netlify", + "@remix-run/vercel" + ] + ], + "linked": [], + "access": "public", + "baseBranch": "dev", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/four-ladybugs-wait.md b/.changeset/four-ladybugs-wait.md new file mode 100644 index 00000000000..1e20b75c8e5 --- /dev/null +++ b/.changeset/four-ladybugs-wait.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": patch +--- + +Allow importing `.sql` files as text diff --git a/.changeset/great-pumas-act.md b/.changeset/great-pumas-act.md new file mode 100644 index 00000000000..dbc214cca15 --- /dev/null +++ b/.changeset/great-pumas-act.md @@ -0,0 +1,16 @@ +--- +"@remix-run/netlify": minor +"@remix-run/server-runtime": minor +--- + +Type safety for load context. + +Change `AppLoadContext` to be an interface mapping `string` to `unknown`, allowing users to extend it via: + +```ts +declare module "@remix-run/server-runtime" { + interface AppLoadContext { + // add custom properties here! + } +} +``` diff --git a/.changeset/nasty-cars-care.md b/.changeset/nasty-cars-care.md new file mode 100644 index 00000000000..d89994d6c84 --- /dev/null +++ b/.changeset/nasty-cars-care.md @@ -0,0 +1,5 @@ +--- +"@remix-run/react": patch +--- + +React 18 gets more strict with types, this adds a runtime cast to a string for the `` tag value. diff --git a/.eslintignore b/.eslintignore index e6a23df2c01..559d94f603b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,5 +2,12 @@ **/tests/__snapshots/ **/node_modules/ !.eslintrc.js -templates/deno .tmp +/playground +**/__tests__/fixtures +**/__tests__/**/*/fixtures + +# Deno +integration/helpers/deno-template +packages/remix-deno +templates/deno diff --git a/.eslintrc.js b/.eslintrc.js index a7c8a1014fa..be11e2f9814 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -5,4 +5,12 @@ module.exports = { "plugin:markdown/recommended", ], plugins: ["markdown"], + overrides: [ + { + files: ["rollup.config.js"], + rules: { + "import/no-extraneous-dependencies": 0, + }, + }, + ], }; diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 06763cb2961..29a2b9927ac 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,3 +22,21 @@ Closes: # - [ ] Docs - [ ] Tests + +Testing Strategy: + +<!-- +Please explain how you tested this. For example: + +> This test covers this code: <link to test> + +Or + +> I opened up my windows machine and ran this script: +> +> ``` +> npx create-remix@0.0.0-experimental-7e420ee3 --template remix my-test +> cd my-test +> npm run dev +> ``` +--> diff --git a/.github/workflows/deployment-test.yml b/.github/workflows/deployment-test.yml deleted file mode 100644 index bceca397bbe..00000000000 --- a/.github/workflows/deployment-test.yml +++ /dev/null @@ -1,196 +0,0 @@ -name: Deployment Test - -on: - - workflow_dispatch - -jobs: - arc_deploy: - name: Architect Deploy - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Deploy to Arc - run: node ./arc.mjs - working-directory: ./scripts/deployment-test - env: - CI: true - AWS_ACCESS_KEY_ID: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }} - - cf_pages_deploy: - name: "CF Pages Deploy" - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Deploy to Cloudflare Pages - run: node ./cf-pages.mjs - working-directory: ./scripts/deployment-test - env: - CF_ACCOUNT_ID: ${{ secrets.TEST_CF_ACCOUNT_ID }} - CF_GLOBAL_API_KEY: ${{ secrets.TEST_CF_GLOBAL_API_KEY }} - CF_EMAIL: ${{ secrets.TEST_CF_EMAIL }} - GITHUB_TOKEN: ${{ secrets.TEST_CF_GITHUB_TOKEN }} - - cf_workers_deploy: - name: "CF Workers Deploy" - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Deploy to Cloudflare Workers - run: node ./cf-workers.mjs - working-directory: ./scripts/deployment-test - env: - CF_ACCOUNT_ID: ${{ secrets.TEST_CF_ACCOUNT_ID }} - CF_API_TOKEN: ${{ secrets.TEST_CF_API_TOKEN }} - - fly_deploy: - name: "Fly Deploy" - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Install the Fly CLI - working-directory: ./scripts/deployment-test - run: curl -L https://fly.io/install.sh | FLYCTL_INSTALL=/usr/local sh - - - name: Deploy to Fly - run: node ./fly.mjs - working-directory: ./scripts/deployment-test - env: - FLY_API_TOKEN: ${{ secrets.TEST_FLY_TOKEN }} - - netlify_deploy: - name: "Netlify Deploy" - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Deploy to Netlify - run: node ./netlify.mjs - working-directory: ./scripts/deployment-test - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.TEST_NETLIFY_TOKEN }} - - vercel_deploy: - name: "Vercel Deploy" - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Deploy to Vercel - run: node ./vercel.mjs - working-directory: ./scripts/deployment-test - env: - VERCEL_TOKEN: ${{ secrets.TEST_VERCEL_TOKEN }} - VERCEL_ORG_ID: ${{ secrets.TEST_VERCEL_USER_ID }} diff --git a/.github/workflows/deployments.yml b/.github/workflows/deployments.yml new file mode 100644 index 00000000000..1f55c245d62 --- /dev/null +++ b/.github/workflows/deployments.yml @@ -0,0 +1,295 @@ +name: ๐Ÿš€ Deployment Tests + +on: + workflow_call: + secrets: + TEST_AWS_ACCESS_KEY_ID: + required: true + TEST_AWS_SECRET_ACCESS_KEY: + required: true + TEST_CF_ACCOUNT_ID: + required: true + TEST_CF_GLOBAL_API_KEY: + required: true + TEST_CF_EMAIL: + required: true + TEST_CF_PAGES_API_TOKEN: + required: true + TEST_CF_API_TOKEN: + required: true + TEST_DENO_DEPLOY_TOKEN: + required: true + TEST_FLY_TOKEN: + required: true + TEST_NETLIFY_TOKEN: + required: true + TEST_VERCEL_TOKEN: + required: true + TEST_VERCEL_USER_ID: + required: true + +jobs: + arc_deploy: + name: Architect Deploy + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: npm + cache-dependency-path: ./scripts/deployment-test/package.json # no lockfile, key caching off package.json + + # some deployment targets require the latest version of npm + # TODO: remove this eventually when the default version we get + # is "latest" enough. + - name: ๐Ÿ“ฆ Install latest version of npm + run: npm install -g npm@latest + working-directory: ./scripts/deployment-test + + - name: ๐Ÿ“ฅ Install deployment-test deps + run: npm install + working-directory: ./scripts/deployment-test + + - name: ๐Ÿš€ Deploy to Arc + run: node ./arc.mjs + working-directory: ./scripts/deployment-test + env: + CI: true + AWS_ACCESS_KEY_ID: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }} + + cf_pages_deploy: + name: "CF Pages Deploy" + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: npm + cache-dependency-path: ./scripts/deployment-test/package.json # no lockfile, key caching off package.json + + # some deployment targets require the latest version of npm + # TODO: remove this eventually when the default version we get + # is "latest" enough. + - name: ๐Ÿ“ฆ Install latest version of npm + run: npm install -g npm@latest + working-directory: ./scripts/deployment-test + + - name: ๐Ÿ“ฅ Install deployment-test deps + run: npm install + working-directory: ./scripts/deployment-test + + - name: ๐Ÿš€ Deploy to Cloudflare Pages + run: node ./cf-pages.mjs + working-directory: ./scripts/deployment-test + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.TEST_CF_ACCOUNT_ID }} + CLOUDFLARE_GLOBAL_API_KEY: ${{ secrets.TEST_CF_GLOBAL_API_KEY }} + CLOUDFLARE_EMAIL: ${{ secrets.TEST_CF_EMAIL }} + CLOUDFLARE_API_TOKEN: ${{ secrets.TEST_CF_PAGES_API_TOKEN }} + + cf_workers_deploy: + name: "CF Workers Deploy" + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: npm + cache-dependency-path: ./scripts/deployment-test/package.json # no lockfile, key caching off package.json + + # some deployment targets require the latest version of npm + # TODO: remove this eventually when the default version we get + # is "latest" enough. + - name: ๐Ÿ“ฆ Install latest version of npm + run: npm install -g npm@latest + working-directory: ./scripts/deployment-test + + - name: ๐Ÿ“ฅ Install deployment-test deps + run: npm install + working-directory: ./scripts/deployment-test + + - name: ๐Ÿš€ Deploy to Cloudflare Workers + run: node ./cf-workers.mjs + working-directory: ./scripts/deployment-test + env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.TEST_CF_ACCOUNT_ID }} + CLOUDFLARE_API_TOKEN: ${{ secrets.TEST_CF_API_TOKEN }} + CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }} + CLOUDFLARE_GLOBAL_API_KEY: ${{ secrets.CLOUDFLARE_GLOBAL_API_KEY }} + + # "deploy deploy" is not a typo, we are deploying to Deno Deploy + deno_deploy_deploy: + name: "Deno Deploy Deploy" + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: npm + cache-dependency-path: ./scripts/deployment-test/package.json # no lockfile, key caching off package.json + + # some deployment targets require the latest version of npm + # TODO: remove this eventually when the default version we get + # is "latest" enough. + - name: ๐Ÿ“ฆ Install latest version of npm + run: npm install -g npm@latest + working-directory: ./scripts/deployment-test + + - name: ๐Ÿ“ฅ Install deployment-test deps + run: npm install + working-directory: ./scripts/deployment-test + + - name: ๐Ÿฆ• Install Deno + uses: denoland/setup-deno@v1 + with: + deno-version: vx.x.x + - name: ๐Ÿฆ• Deno Deploy CLI + run: deno install --allow-read --allow-write --allow-env --allow-net --allow-run --no-check -r -f https://deno.land/x/deploy/deployctl.ts + + - name: ๐Ÿš€ Deploy to Deno Deploy + run: node ./deno-deploy.mjs + working-directory: ./scripts/deployment-test + env: + DENO_DEPLOY_TOKEN: ${{ secrets.TEST_DENO_DEPLOY_TOKEN }} + + fly_deploy: + name: "Fly Deploy" + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: npm + cache-dependency-path: ./scripts/deployment-test/package.json # no lockfile, key caching off package.json + + # some deployment targets require the latest version of npm + # TODO: remove this eventually when the default version we get + # is "latest" enough. + - name: ๐Ÿ“ฆ Install latest version of npm + run: npm install -g npm@latest + working-directory: ./scripts/deployment-test + + - name: ๐Ÿ“ฅ Install deployment-test deps + run: npm install + working-directory: ./scripts/deployment-test + + - name: ๐ŸŽˆ Install the Fly CLI + run: curl -L https://fly.io/install.sh | FLYCTL_INSTALL=/usr/local sh + + - name: ๐Ÿš€ Deploy to Fly + run: node ./fly.mjs + working-directory: ./scripts/deployment-test + env: + FLY_API_TOKEN: ${{ secrets.TEST_FLY_TOKEN }} + + netlify_deploy: + name: "Netlify Deploy" + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: npm + cache-dependency-path: ./scripts/deployment-test/package.json # no lockfile, key caching off package.json + + # some deployment targets require the latest version of npm + # TODO: remove this eventually when the default version we get + # is "latest" enough. + - name: ๐Ÿ“ฆ Install latest version of npm + run: npm install -g npm@latest + working-directory: ./scripts/deployment-test + + - name: ๐Ÿ“ฅ Install deployment-test deps + run: npm install + working-directory: ./scripts/deployment-test + + - name: ๐Ÿš€ Deploy to Netlify + run: node ./netlify.mjs + working-directory: ./scripts/deployment-test + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.TEST_NETLIFY_TOKEN }} + + vercel_deploy: + name: "Vercel Deploy" + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: npm + cache-dependency-path: ./scripts/deployment-test/package.json # no lockfile, key caching off package.json + + # some deployment targets require the latest version of npm + # TODO: remove this eventually when the default version we get + # is "latest" enough. + - name: ๐Ÿ“ฆ Install latest version of npm + run: npm install -g npm@latest + working-directory: ./scripts/deployment-test + + - name: ๐Ÿ“ฅ Install deployment-test deps + run: npm install + working-directory: ./scripts/deployment-test + + - name: ๐Ÿš€ Deploy to Vercel + run: node ./vercel.mjs + working-directory: ./scripts/deployment-test + env: + VERCEL_TOKEN: ${{ secrets.TEST_VERCEL_TOKEN }} + VERCEL_ORG_ID: ${{ secrets.TEST_VERCEL_USER_ID }} diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index a22036568b7..8a1d94fe917 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -1,4 +1,4 @@ -name: Format +name: ๐Ÿ‘” Format on: push: @@ -12,29 +12,38 @@ jobs: runs-on: ubuntu-latest steps: - - name: Cancel Previous Runs + - name: ๐Ÿ›‘ Cancel Previous Runs uses: styfle/cancel-workflow-action@0.9.1 - - name: Checkout Repository + - name: โฌ‡๏ธ Checkout repo uses: actions/checkout@v3 with: token: ${{ secrets.FORMAT_PAT }} - - name: Use Node.js + - name: โŽ” Setup node uses: actions/setup-node@v3 with: - node-version: 14 + node-version-file: ".nvmrc" + cache: "yarn" - - name: Install dependencies - run: yarn install --frozen-lockfile + - name: ๐Ÿ“ฅ Install deps + run: yarn --frozen-lockfile - - name: Update Example and Template names - run: node scripts/patchup-example-names.mjs + - name: ๐Ÿ”— Convert Docs links to references + run: node scripts/markdown-references.mjs - - name: Format - run: npm run format --if-present + - name: ๐Ÿ‘” Format + run: yarn format - - name: Commit + - name: ๐Ÿฆ• Install Deno + uses: denoland/setup-deno@v1 + with: + deno-version: vx.x.x + + - name: ๐Ÿ‘” Format Deno files + run: yarn format:deno + + - name: ๐Ÿ’ช Commit run: | git config --local user.email "hello@remix.run" git config --local user.name "Remix Run Bot" diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 00000000000..41e3303c9ef --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,131 @@ +name: ๐ŸŒ’ Nightly Release + +on: + schedule: + - cron: "0 7 * * *" # every day at 12AM PST + +env: + CI: true + +jobs: + # HEADS UP! this "nightly" job will only ever run on the `main` branch due to it being a cron job, + # and the last commit on main will be what github shows as the trigger + # however in the checkout below we specify the `dev` branch, so all the scripts + # in this job will be ran from that, confusing i know, so in some cases we'll need to create + # multiple PRs when modifying nightly release processes + nightly: + name: ๐ŸŒ’ Nightly Release + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + outputs: + # allows this to be used in the `comment` job below - will be undefined if there's no release necessary + NEXT_VERSION: ${{ steps.version.outputs.NEXT_VERSION }} + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + with: + ref: dev + # checkout using a custom token so that we can push later on + token: ${{ secrets.NIGHTLY_PAT }} + fetch-depth: 0 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: "yarn" + + - name: ๐Ÿ“ฅ Install deps + run: yarn --frozen-lockfile + + - name: โคด๏ธ Update Version if needed + id: version + run: | + # get latest commit sha + SHA=$(git rev-parse HEAD) + SHORT_SHA=${SHA::7} + + # get latest nightly tag + LATEST_NIGHTLY_TAG=$(git tag -l v0.0.0-nightly-\* --sort=-taggerdate | head -n 1) + + # check if last commit to dev starts would be the nightly tag we're about to create (minus the date) + # if it is, we'll skip the nightly creation + # if not, we'll create a new nightly tag + if [[ ${LATEST_NIGHTLY_TAG} == v0.0.0-nightly-${SHORT_SHA}-* ]]; then + echo "๐Ÿ›‘ Latest nightly tag is the same as the latest commit sha, skipping nightly release" + else + git config --local user.email "hello@remix.run" + git config --local user.name "Remix Run Bot" + + DATE=$(date '+%Y%m%d') + NEXT_VERSION=0.0.0-nightly-${SHORT_SHA}-${DATE} + echo ::set-output name=NEXT_VERSION::${NEXT_VERSION} + + git checkout -b nightly/${NEXT_VERSION} + + if [ -z "$(git status --porcelain)" ]; then + echo "โœจ" + else + echo "dirty working directory..." + git add . + git commit -m "dirty working directory..." + fi + + yarn run version ${NEXT_VERSION} --skip-prompt + fi + + - name: ๐Ÿ— Build + if: steps.version.outputs.NEXT_VERSION + run: yarn build + + - name: ๐Ÿท Push Tag + if: steps.version.outputs.NEXT_VERSION + run: git push origin --tags + + - name: ๐Ÿ” Setup npm auth + if: steps.version.outputs.NEXT_VERSION + run: | + echo "registry=https://registry.npmjs.org" >> ~/.npmrc + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc + + - name: ๐Ÿš€ Publish + if: steps.version.outputs.NEXT_VERSION + run: npm run publish + + comment: + needs: [nightly] + name: ๐Ÿ“ Comment on related issues and pull requests + if: github.repository == 'remix-run/remix' && needs.nightly.outputs.NEXT_VERSION + uses: remix-run/remix/.github/workflows/release-comments.yml@main + with: + ref: "refs/tags/v${{ needs.nightly.outputs.NEXT_VERSION }}" + + deployments: + needs: [nightly] + name: ๐Ÿš€ Deployment Tests + if: github.repository == 'remix-run/remix' && needs.nightly.outputs.NEXT_VERSION + uses: remix-run/remix/.github/workflows/deployments.yml@main + secrets: + TEST_AWS_ACCESS_KEY_ID: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }} + TEST_AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }} + TEST_CF_ACCOUNT_ID: ${{ secrets.TEST_CF_ACCOUNT_ID }} + TEST_CF_GLOBAL_API_KEY: ${{ secrets.TEST_CF_GLOBAL_API_KEY }} + TEST_CF_EMAIL: ${{ secrets.TEST_CF_EMAIL }} + TEST_CF_PAGES_API_TOKEN: ${{ secrets.TEST_CF_PAGES_API_TOKEN }} + TEST_CF_API_TOKEN: ${{ secrets.TEST_CF_API_TOKEN }} + TEST_DENO_DEPLOY_TOKEN: ${{ secrets.TEST_DENO_DEPLOY_TOKEN }} + TEST_FLY_TOKEN: ${{ secrets.TEST_FLY_TOKEN }} + TEST_NETLIFY_TOKEN: ${{ secrets.TEST_NETLIFY_TOKEN }} + TEST_VERCEL_TOKEN: ${{ secrets.TEST_VERCEL_TOKEN }} + TEST_VERCEL_USER_ID: ${{ secrets.TEST_VERCEL_USER_ID }} + + stacks: + needs: [nightly] + name: ๐Ÿฅž Remix Stacks Test + if: github.repository == 'remix-run/remix' && needs.nightly.outputs.NEXT_VERSION + uses: remix-run/remix/.github/workflows/stacks.yml@main + with: + version: "${{ needs.nightly.outputs.NEXT_VERSION }}" diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml index 1d883ee4b0b..37f89777074 100644 --- a/.github/workflows/no-response.yml +++ b/.github/workflows/no-response.yml @@ -1,4 +1,4 @@ -name: No Response +name: ๐Ÿฅบ No Response on: issue_comment: @@ -7,19 +7,31 @@ on: # Schedule for five minutes after the hour, every hour - cron: "5 * * * *" +permissions: + issues: write + pull-requests: write + jobs: noResponse: if: github.repository == 'remix-run/remix' runs-on: ubuntu-latest steps: - - uses: lee-dohm/no-response@v0.5.0 + - name: ๐Ÿฅบ Handle Ghosting + uses: actions/stale@v5 with: - closeComment: > + close-issue-message: > This issue has been automatically closed because we haven't received a response from the original author ๐Ÿ™ˆ. This automation helps keep the issue tracker clean from issues that are unactionable. Please reach out if you have more information for us! ๐Ÿ™‚ - daysUntilClose: 10 - responseRequiredLabel: needs-response - token: ${{ github.token }} + close-pr-message: > + This PR has been automatically closed because we haven't received a + response from the original author ๐Ÿ™ˆ. This automation helps keep the issue + tracker clean from PRs that are unactionable. Please reach out if you + have more information for us! ๐Ÿ™‚ + days-before-close: 10 + # don't automatically mark issues/PRs as stale + days-before-stale: -1 + any-of-labels: needs-response + labels-to-remove-when-unstale: needs-response diff --git a/.github/workflows/postrelease.yml b/.github/workflows/postrelease.yml new file mode 100644 index 00000000000..d0b00b4423b --- /dev/null +++ b/.github/workflows/postrelease.yml @@ -0,0 +1,45 @@ +name: ๐Ÿ•Š Release + +on: + push: + tags: + # only run on `remix` tags + - "remix@*" + +jobs: + comment: + needs: [release] + name: ๐Ÿ“ Comment on related issues and pull requests + if: github.repository == 'remix-run/remix' + uses: remix-run/remix/.github/workflows/release-comments.yml@main + with: + ref: ${{ github.ref }} + # this should match the above tag to watch including the trailing "@" + packageVersionToFollow: "remix@" + + deployments: + needs: [release] + name: ๐Ÿš€ Deployment Tests + if: github.repository == 'remix-run/remix' + uses: remix-run/remix/.github/workflows/deployments.yml@main + secrets: + TEST_AWS_ACCESS_KEY_ID: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }} + TEST_AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }} + TEST_CF_ACCOUNT_ID: ${{ secrets.TEST_CF_ACCOUNT_ID }} + TEST_CF_GLOBAL_API_KEY: ${{ secrets.TEST_CF_GLOBAL_API_KEY }} + TEST_CF_EMAIL: ${{ secrets.TEST_CF_EMAIL }} + TEST_CF_PAGES_API_TOKEN: ${{ secrets.TEST_CF_PAGES_API_TOKEN }} + TEST_CF_API_TOKEN: ${{ secrets.TEST_CF_API_TOKEN }} + TEST_DENO_DEPLOY_TOKEN: ${{ secrets.TEST_DENO_DEPLOY_TOKEN }} + TEST_FLY_TOKEN: ${{ secrets.TEST_FLY_TOKEN }} + TEST_NETLIFY_TOKEN: ${{ secrets.TEST_NETLIFY_TOKEN }} + TEST_VERCEL_TOKEN: ${{ secrets.TEST_VERCEL_TOKEN }} + TEST_VERCEL_USER_ID: ${{ secrets.TEST_VERCEL_USER_ID }} + + stacks: + needs: [release] + name: ๐Ÿฅž Remix Stacks Test + if: github.repository == 'remix-run/remix' + uses: remix-run/remix/.github/workflows/stacks.yml@main + with: + version: ${{ github.ref_name }} diff --git a/.github/workflows/release-comments.yml b/.github/workflows/release-comments.yml new file mode 100644 index 00000000000..159a8d2f062 --- /dev/null +++ b/.github/workflows/release-comments.yml @@ -0,0 +1,44 @@ +name: ๐Ÿ“ Comment on Release + +on: + workflow_call: + inputs: + ref: + required: true + type: string + packageVersionToFollow: + required: false + type: string + +jobs: + comment: + name: Comment on Release + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref }} + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: "npm" + cache-dependency-path: scripts/release/package-lock.json + + - name: ๐Ÿ“ฅ Install deps + run: npm ci + working-directory: ./scripts/release + + - name: ๐Ÿ“ Comment on issues + working-directory: ./scripts/release + run: node -r esbuild-register ./comment.ts + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ github.token }} + VERSION: ${{ inputs.ref }} + DEFAULT_BRANCH: "main" + NIGHTLY_BRANCH: "dev" + PACKAGE_VERSION_TO_FOLLOW: ${{ inputs.packageToWatch }} diff --git a/.github/workflows/release-experimental.yml b/.github/workflows/release-experimental.yml new file mode 100644 index 00000000000..63d048f411b --- /dev/null +++ b/.github/workflows/release-experimental.yml @@ -0,0 +1,67 @@ +# Experimental releases are handled a bit differently than standard releases. +# Experimental releases can be branched from anywhere as they are not intended +# for general use, and all packages will be versioned and published with the +# same hash for testing. +# +# This workflow will run when a GitHub release is created from experimental +# version tag. Unlike standard releases created via Changesets, only one tag +# should be created for all packages. +# +# To create a release: +# - Create a new branch for the release: git checkout -b `release-experimental` +# - IMPORTANT: You should always create a new branch so that the version +# changes don't accidentally get merged into `dev` or `main`. The branch +# name must follow the convention of `release-experimental` or +# `release-experimental-[feature]`. +# - Make whatever changes you need and commit them: +# - `git add . && git commit "experimental changes!"` +# - Update version numbers and create a release tag: +# - `yarn run version:experimental` +# - Push to GitHub: +# - `git push origin --follow-tags` +# - Create a new release for the tag on GitHub to trigger the CI workflow that +# will publish the release to npm + +name: ๐Ÿงช Release (experimental) +on: + push: + tags: + - "v0.0.0-experimental*" + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +env: + CI: true + +jobs: + release: + name: ๐Ÿง‘โ€๐Ÿ”ฌ Experimental Release + if: | + github.repository == 'remix-run/remix' && + contains(github.ref, 'experimental') + runs-on: ubuntu-latest + steps: + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: "yarn" + + - name: ๐Ÿ“ฅ Install deps + run: yarn --frozen-lockfile + + - name: ๐Ÿ— Build + run: yarn build + + - name: ๐Ÿ” Setup npm auth + run: | + echo "registry=https://registry.npmjs.org" >> ~/.npmrc + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc + + - name: ๐Ÿš€ Publish + run: npm run publish diff --git a/.github/workflows/release-private.yml b/.github/workflows/release-private.yml deleted file mode 100644 index 70d14d75a6c..00000000000 --- a/.github/workflows/release-private.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: release-private -on: - release: - types: [published] - -jobs: - build: - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - - - run: echo "::set-output name=dir::$(yarn cache dir)" - id: yarn-cache - - - name: Restore dependency cache - uses: actions/cache@v2 - with: - path: "${{ steps.yarn-cache.outputs.dir }}" - key: ${{ runner.os }}-yarn-cache-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn-cache- - - - name: Install dependencies - run: yarn --frozen-lockfile - - - name: Build - run: yarn build - - - name: Setup npm auth - run: | - echo "@remix-run:registry=https://npm.pkg.github.com" >> ~/.npmrc - echo "//npm.pkg.github.com/:_authToken=${{ secrets.GH_PACKAGES_PUBLISH_TOKEN }}" >> ~/.npmrc - - - name: Publish - run: npm run publish:private diff --git a/.github/workflows/release-test.yml b/.github/workflows/release-test.yml new file mode 100644 index 00000000000..2d2f7dc4df5 --- /dev/null +++ b/.github/workflows/release-test.yml @@ -0,0 +1,19 @@ +name: ๐Ÿงช Test + +on: + push: + branches: + - release-* + tags-ignore: + - v* + paths-ignore: + - "docs/**" + - "scripts/**" + - "**/README.md" + +jobs: + test: + if: github.repository == 'remix-run/remix' + uses: remix-run/remix/.github/workflows/reusable-test.yml@main + with: + node_version: "[14, 16, 18]" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79083aec7d7..83a974ee05e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,242 +1,62 @@ -name: release - +name: ๐Ÿฆ‹ Changesets Release on: - release: - types: [published] + push: + branches: + - release + - "release-*" + - "!release-experimental" + - "!release-experimental-*" + - "!release-manual" + - "!release-manual-*" jobs: - build: - if: github.repository == 'remix-run/remix' + release: + name: ๐Ÿฆ‹ Changesets Release + # we need to check for `nightly` refs and skip them as we dont want to + # double publish a version as it would fail. unfortantely even using curl + # and a `repository_dispatch` trigger, actions still aren't ran if a version + # is published using the default secrets.GITHUB_TOKEN. + if: | + github.repository == 'remix-run/remix' && + !contains(github.ref, 'nightly') runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 - - name: Setup node - uses: actions/setup-node@v3 + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 with: - node-version: "${{ steps.nvmrc.outputs.version }}" - - - run: echo "::set-output name=dir::$(yarn cache dir)" - id: yarn-cache + fetch-depth: 0 - - name: Restore dependency cache - uses: actions/cache@v2 + - name: โŽ” Setup node + uses: actions/setup-node@v3 with: - path: "${{ steps.yarn-cache.outputs.dir }}" - key: ${{ runner.os }}-yarn-cache-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn-cache- + node-version-file: ".nvmrc" + cache: "yarn" - - name: Install dependencies + - name: ๐Ÿ“ฅ Install deps run: yarn --frozen-lockfile - - name: Build - run: yarn build - - - name: Setup npm auth + - name: ๐Ÿ” Setup npm auth run: | echo "registry=https://registry.npmjs.org" >> ~/.npmrc echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc - - name: Publish - run: npm run publish - - arc_deploy: - name: Architect Deploy - needs: [build] - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Deploy to Arc - run: node ./arc.mjs - working-directory: ./scripts/deployment-test - env: - CI: true - AWS_ACCESS_KEY_ID: ${{ secrets.TEST_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.TEST_AWS_SECRET_ACCESS_KEY }} - - cf_pages_deploy: - name: "CF Pages Deploy" - needs: [build] - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Deploy to Cloudflare Pages - run: node ./cf-pages.mjs - working-directory: ./scripts/deployment-test - env: - CF_ACCOUNT_ID: ${{ secrets.TEST_CF_ACCOUNT_ID }} - CF_GLOBAL_API_KEY: ${{ secrets.TEST_CF_GLOBAL_API_KEY }} - CF_EMAIL: ${{ secrets.TEST_CF_EMAIL }} - GITHUB_TOKEN: ${{ secrets.TEST_CF_GITHUB_TOKEN }} - - cf_workers_deploy: - name: "CF Workers Deploy" - needs: [build] - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Deploy to Cloudflare Workers - run: node ./cf-workers.mjs - working-directory: ./scripts/deployment-test - env: - CF_ACCOUNT_ID: ${{ secrets.TEST_CF_ACCOUNT_ID }} - CF_API_TOKEN: ${{ secrets.TEST_CF_API_TOKEN }} - - fly_deploy: - name: "Fly Deploy" - needs: [build] - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Install the Fly CLI - run: curl -L https://fly.io/install.sh | FLYCTL_INSTALL=/usr/local sh - - - name: Deploy to Fly - run: node ./fly.mjs - working-directory: ./scripts/deployment-test - env: - FLY_API_TOKEN: ${{ secrets.TEST_FLY_TOKEN }} - - netlify_deploy: - name: "Netlify Deploy" - needs: [build] - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Deploy to Netlify - run: node ./netlify.mjs - working-directory: ./scripts/deployment-test - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.TEST_NETLIFY_TOKEN }} - - vercel_deploy: - name: "Vercel Deploy" - needs: [build] - if: github.repository == 'remix-run/remix' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "npm" - - - name: Install dependencies - run: npm install - working-directory: ./scripts/deployment-test - - - name: Install latest version of npm - run: npm install -g npm@latest - working-directory: ./scripts/deployment-test - - - name: Deploy to Vercel - run: node ./vercel.mjs - working-directory: ./scripts/deployment-test + # This action has two responsibilities. The first time the workflow runs + # (initial push to a `release-*` branch) it will create a new branch and + # then open a PR with the related changes for the new version. After the + # PR is merged, the workflow will run again and this action will build + + # publish to npm. + - name: ๐Ÿš€ PR / Publish + id: changesets + uses: changesets/action@v1 + with: + version: yarn run changeset:version + commit: "chore: Update version for release" + title: "chore: Update version for release" + publish: yarn run changeset:release + createGithubReleases: false env: - VERCEL_TOKEN: ${{ secrets.TEST_VERCEL_TOKEN }} - VERCEL_ORG_ID: ${{ secrets.TEST_VERCEL_USER_ID }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml new file mode 100644 index 00000000000..835b13a38d4 --- /dev/null +++ b/.github/workflows/reusable-test.yml @@ -0,0 +1,164 @@ +name: ๐Ÿงช Test + +on: + workflow_call: + inputs: + node_version: + required: true + # this is limited to string | boolean | number (https://github.community/t/can-action-inputs-be-arrays/16457) + # but we want to pass an array (node_version: "[14, 16, 18]"), + # so we'll need to manually stringify it for now + type: string + +env: + CI: true + +jobs: + build: + name: โš™๏ธ Build + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: "yarn" + + - name: ๐Ÿ“ฅ Install deps + run: yarn --frozen-lockfile + + - name: ๐Ÿ— Build + run: yarn build + + lint: + name: โฌฃ Lint + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version-file: ".nvmrc" + cache: "yarn" + + - name: ๐Ÿ“ฅ Install deps + run: yarn --frozen-lockfile + + - name: ๐Ÿ”ฌ Lint + run: yarn lint + + - name: ๐Ÿฆ• Install Deno + uses: denoland/setup-deno@v1 + with: + deno-version: vx.x.x + + - name: ๐Ÿ”ฌ Lint deno files + run: yarn lint:deno + + test: + name: "๐Ÿงช Test: (OS: ${{ matrix.os }} Node: ${{ matrix.node }})" + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + # - macos-latest + - windows-latest + node: ${{ fromJSON(inputs.node_version) }} + runs-on: ${{ matrix.os }} + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + + - name: โŽ” Setup node ${{ matrix.node }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: "yarn" + + - name: ๐Ÿ“ฅ Install deps + run: yarn --frozen-lockfile + + - name: ๐Ÿงช Run Primary Tests + run: "yarn test:primary" + + integration: + name: "๐Ÿ‘€ Integration Test: (OS: ${{ matrix.os }} Node: ${{ matrix.node }})" + strategy: + fail-fast: false + matrix: + node: ${{ fromJSON(inputs.node_version) }} + os: + - ubuntu-latest + # - macos-latest + - windows-latest + include: + - os: ubuntu-latest + playwright_binary_path: ~/.cache/ms-playwright + # - os: macos-latest + # playwright_binary_path: ~/Library/Caches/ms-playwright + - os: windows-latest + playwright_binary_path: '~\\AppData\\Local\\ms-playwright' + + runs-on: ${{ matrix.os }} + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 + + - name: โŽ” Setup node ${{ matrix.node }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + cache: "yarn" + + - name: ๐Ÿ“ฅ Install deps + run: yarn --frozen-lockfile + + # playwright recommends if you cache the binaries to keep it tied to the version of playwright you are using. + # https://playwright.dev/docs/ci#caching-browsers + - name: ๐Ÿ•ต๏ธโ€โ™‚๏ธ Get current Playwright version + id: playwright-version + shell: bash + run: | + playwright_version=$(npm info @playwright/test version) + echo "::set-output name=version::${playwright_version}" + + - name: ๐Ÿค– Cache Playwright binaries + uses: actions/cache@v3 + id: playwright-cache + with: + path: ${{ matrix.playwright_binary_path }} + key: ${{ runner.os }}-${{ runner.arch }}-cache-playwright-${{ steps.playwright-version.outputs.version }} + + - name: ๐Ÿ–จ๏ธ Playwright info + shell: bash + run: | + echo "OS: ${{ matrix.os }}" + echo "Playwright version: ${{ steps.playwright-version.outputs.version }}" + echo "Playwright install dir: ${{ matrix.playwright_binary_path }}" + echo "Cache key: ${{ runner.os }}-${{ runner.arch }}-cache-playwright-${{ steps.playwright-version.outputs.version }}" + echo "Cache hit: ${{ steps.playwright-cache.outputs.cache-hit == 'true' }}" + + - name: ๐Ÿ“ฅ Install Playwright + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps + + - name: ๐Ÿ‘€ Run Integration Tests + run: "yarn test:integration" diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml new file mode 100644 index 00000000000..f2053540071 --- /dev/null +++ b/.github/workflows/stacks.yml @@ -0,0 +1,259 @@ +name: ๐Ÿฅž Remix Stacks Test + +on: + workflow_call: + inputs: + version: + required: true + type: string + +jobs: + setup: + name: Remix Stacks Test + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + strategy: + matrix: + stack: + - repo: "remix-run/indie-stack" + name: "indie" + - repo: "remix-run/blues-stack" + name: "blues" + - repo: "remix-run/grunge-stack" + name: "grunge" + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: โš’๏ธ Create new ${{ matrix.stack.name }} app with ${{ inputs.version }} + run: | + npx -y create-remix@${{ inputs.version }} ${{ matrix.stack.name }} --template ${{ matrix.stack.repo }} --typescript --no-install + + - name: โŽ” Setup dependency caching + uses: actions/setup-node@v3 + with: + cache: npm + cache-dependency-path: ${{ matrix.stack.name }}/package.json + + - name: ๐Ÿ“ฅ Install deps + run: npm install + working-directory: ${{ matrix.stack.name }} + + - name: Run `remix init` + run: | + cd ${{ matrix.stack.name }} + npx remix init + + - name: ๐Ÿ„ Copy test env vars + run: | + cd ${{ matrix.stack.name }} + cp .env.example .env + + - name: ๐Ÿ“ Zip artifact + run: zip ${{ matrix.stack.name }}.zip ./${{ matrix.stack.name }} -r -x "**/node_modules/*" + + - name: ๐Ÿ—„๏ธ Archive ${{ matrix.stack.name }} + uses: actions/upload-artifact@v3 + with: + name: ${{ matrix.stack.name }}-archive + path: ${{ matrix.stack.name }}.zip + + lint: + name: โฌฃ ESLint + if: github.repository == 'remix-run/remix' + needs: [setup] + runs-on: ubuntu-latest + strategy: + matrix: + stack: + - repo: "remix-run/indie-stack" + name: "indie" + - repo: "remix-run/blues-stack" + name: "blues" + - repo: "remix-run/grunge-stack" + name: "grunge" + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: ๐Ÿ—„๏ธ Restore ${{ matrix.stack.name }} + uses: actions/download-artifact@v3 + with: + name: ${{ matrix.stack.name }}-archive + + - name: ๐Ÿ“ Unzip artifact + run: unzip ${{ matrix.stack.name }}.zip + + - name: โŽ” Setup node and dependency caching + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: npm + cache-dependency-path: ${{ matrix.stack.name }}/package.json + + - name: ๐Ÿ“ฅ Install deps + run: npm install + working-directory: ${{ matrix.stack.name }} + + - name: ๐Ÿ”ฌ Lint + run: | + cd ${{ matrix.stack.name }} + npm run lint + + typecheck: + name: สฆ TypeScript + needs: [setup] + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + strategy: + matrix: + stack: + - repo: "remix-run/indie-stack" + name: "indie" + - repo: "remix-run/blues-stack" + name: "blues" + - repo: "remix-run/grunge-stack" + name: "grunge" + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: ๐Ÿ—„๏ธ Restore ${{ matrix.stack.name }} + uses: actions/download-artifact@v3 + with: + name: ${{ matrix.stack.name }}-archive + + - name: ๐Ÿ“ Unzip artifact + run: unzip ${{ matrix.stack.name }}.zip + + - name: โŽ” Setup node and dependency caching + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: npm + cache-dependency-path: ${{ matrix.stack.name }}/package.json + + - name: ๐Ÿ“ฅ Install deps + run: npm install + working-directory: ${{ matrix.stack.name }} + + - name: ๐Ÿ”Ž Type check + run: | + cd ${{ matrix.stack.name }} + npm run typecheck --if-present + + vitest: + name: โšก Vitest + if: github.repository == 'remix-run/remix' + needs: [setup] + runs-on: ubuntu-latest + strategy: + matrix: + stack: + - repo: "remix-run/indie-stack" + name: "indie" + - repo: "remix-run/blues-stack" + name: "blues" + - repo: "remix-run/grunge-stack" + name: "grunge" + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: ๐Ÿ—„๏ธ Restore ${{ matrix.stack.name }} + uses: actions/download-artifact@v3 + with: + name: ${{ matrix.stack.name }}-archive + + - name: ๐Ÿ“ Unzip artifact + run: unzip ${{ matrix.stack.name }}.zip + + - name: โŽ” Setup node and dependency caching + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: npm + cache-dependency-path: ${{ matrix.stack.name }}/package.json + + - name: ๐Ÿ“ฅ Install deps + run: npm install + working-directory: ${{ matrix.stack.name }} + + - name: โšก Run vitest + run: | + cd ${{ matrix.stack.name }} + npm run test -- --coverage + + cypress: + name: โšซ๏ธ Cypress + if: github.repository == 'remix-run/remix' + needs: [setup] + runs-on: ubuntu-latest + strategy: + matrix: + stack: + - repo: "remix-run/indie-stack" + name: "indie" + cypress: "npm run start:mocks" + - repo: "remix-run/blues-stack" + name: "blues" + cypress: "npm run start:mocks" + - repo: "remix-run/grunge-stack" + name: "grunge" + cypress: "npm run dev" + steps: + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 + + - name: ๐Ÿ—„๏ธ Restore ${{ matrix.stack.name }} + uses: actions/download-artifact@v3 + with: + name: ${{ matrix.stack.name }}-archive + + - name: ๐Ÿ“ Unzip artifact + run: unzip ${{ matrix.stack.name }}.zip + + - name: โŽ” Setup node and dependency caching + uses: actions/setup-node@v3 + with: + node-version: 16 + cache: npm + cache-dependency-path: ${{ matrix.stack.name }}/package.json + + - name: ๐Ÿ“ฅ Install deps + run: npm install + working-directory: ${{ matrix.stack.name }} + + - name: ๐Ÿณ Docker compose + if: ${{ matrix.stack.name == 'blues' }} + # the sleep is just there to give time for postgres to get started + run: | + cd ${{ matrix.stack.name }} + docker-compose up -d && sleep 3 + env: + DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/postgres" + + - name: ๐Ÿ›  Setup Database + if: ${{ matrix.stack.name != 'grunge' }} + run: | + cd ${{ matrix.stack.name }} + npx prisma migrate reset --force + + - name: โš™๏ธ Build + run: | + cd ${{ matrix.stack.name }} + npm run build + + - name: ๐ŸŒณ Cypress run + uses: cypress-io/github-action@v4 + with: + start: ${{ matrix.stack.cypress }} + wait-on: "http://localhost:8811" + working-directory: ${{ matrix.stack.name }} + env: + PORT: "8811" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db016202966..e0551fc7955 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,51 +1,22 @@ -name: test +name: ๐Ÿงช Test on: push: branches: - main - dev - - release/* - tags-ignore: - - v* paths-ignore: - "docs/**" + - "scripts/**" - "**/README.md" - pull_request: {} + pull_request: + paths-ignore: + - "docs/**" + - "**/*.md" jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: - - ubuntu-latest - # TODO: Fix cross-env issues in Jest before enabling - # - windows-latest - steps: - # https://github.com/actions/virtual-environments/issues/1187 - - name: prep windows network - run: Disable-NetAdapterChecksumOffload -Name * -TcpIPv4 -UdpIPv4 -TcpIPv6 -UdpIPv6 - if: ${{ matrix.settings.host == 'windows-latest' }} - - uses: actions/checkout@v2 - - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc - - - name: Setup node - uses: actions/setup-node@v3 - with: - node-version: "${{ steps.nvmrc.outputs.version }}" - cache: "yarn" - - - name: Install dependencies - run: yarn --frozen-lockfile --network-timeout 1000000 - - - name: Lint - run: yarn lint - - - name: Build - run: yarn build - - - name: Test - run: yarn test + test: + if: github.repository == 'remix-run/remix' + uses: remix-run/remix/.github/workflows/reusable-test.yml@main + with: + node_version: '["latest"]' diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 087c550d4fa..7d2c75af349 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -1,4 +1,4 @@ -name: website +name: ๐ŸŒ Website on: schedule: @@ -16,8 +16,8 @@ jobs: runs-on: ubuntu-latest steps: - - name: Refresh the docs - uses: fjogeleit/http-request-action@v1.9.0 + - name: ๐Ÿ”„ Refresh the docs + uses: fjogeleit/http-request-action@v1 with: url: "${{ secrets.DOCS_REFRESH_URL }}?ref=${{ github.ref }}" method: "POST" diff --git a/.gitignore b/.gitignore index dfe1bd9f3d1..b86c2b12f74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .DS_Store /build/ +/packages/*/dist/ +/packages/*/LICENSE.md node_modules/ yarn-error.log @@ -9,10 +11,19 @@ yarn-error.log /fixtures/test /fixtures/my-remix-app /fixtures/deno-app +/playwright-report +/test-results +/uploads .eslintcache .tmp /scripts/deployment-test/apps +/scripts/deployment-test/package-lock.json +/scripts/deployment-test/yarn.lock /.idea/ +/playground +/scripts/playground/template.local +/scripts/playground/template/build +/scripts/playground/template/package-lock.json diff --git a/.vscode/deno_resolve_npm_imports.json b/.vscode/deno_resolve_npm_imports.json new file mode 100644 index 00000000000..885f45e2763 --- /dev/null +++ b/.vscode/deno_resolve_npm_imports.json @@ -0,0 +1,11 @@ +{ + "// Resolve NPM imports for `packages/remix-deno`.": "", + "// This import map is used solely for the denoland.vscode-deno extension.": "", + "// Remix does not support import maps.": "", + "// Dependency management is done through `npm` and `node_modules/` instead.": "", + "// Deno-only dependencies may be imported via URL imports (without using import maps).": "", + "imports": { + "@remix-run/server-runtime": "https://esm.sh/@remix-run/server-runtime@1.6.4", + "mime": "https://esm.sh/mime@3.0.0" + } +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000000..b91c276711f --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "denoland.vscode-deno", + "esbenp.prettier-vscode" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa6215fdd..885b1c44c63 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,5 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "deno.enablePaths": ["./packages/remix-deno/"], + "deno.importMap": "./.vscode/deno_resolve_npm_imports.json" } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 083cc59eed9..c8455241e2f 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -44,9 +44,13 @@ git checkout dev # create a prerelease tag. yarn release start patch|minor|major -# Once you create the pre-release, you can run tests and even publish a pre-release -# directly to ensure everything works as expected. If there are any issues, fix the bugs and commit directly to the pre-release branch. Once you're done working, you -# can iterate with a new pre-release with the following command: +# At this point you can push to GitHub... +git push origin/release-<version> --follow-tags +# ...then publish the pre-release by creating a release in the GitHub UI. Don't +# forget to check the pre-release checkbox! + +# If there are any issues with the pre-release, fix the bugs and commit directly +# to the release branch. You can iterate with a new pre-release with the following # command, then publish via GitHub the same as before. yarn release bump # Once all tests have passed and the release is ready to be made stable, the following @@ -54,10 +58,12 @@ yarn release bump # and prompt you to push the changes and tags to GitHub yarn release finish git push origin/release-<version> --follow-tags - -# Now you can create the release from GitHub from the new tag and write release notes! ``` +Once the release is finished, you should see tests run in GitHub actions. Assuming there are no issues (you should also run tests locally before pushing) you can trigger publishing by creating a new release in the GitHub UI, this time using the stable release tag. + +After the release process is complete, be sure to merge the release branch back into `dev` and `main` and push both branches to GitHub. + ### `create-remix` All packages are published together except for `create-remix`, which is @@ -112,7 +118,7 @@ By default, the Remix `rollup` build will strip any `console.debug` calls to avo REMIX_DEBUG=true yarn watch ``` -**`REMIX_LOCAL_DEV_OUTPUT_DIRECTORY`** +**`REMIX_LOCAL_BUILD_DIRECTORY`** When developing Remix locally, you often need to go beyond unit/integration tests and test your changes in a local Remix application. The easiest way to do this is to run your local Remix build and use this environment variable to direct `rollup` to write the output files directly into the local Remix application's `node_modules` folder. Then you just need to restart your local Remix application server to pick up the changes. @@ -123,7 +129,7 @@ cd my-remix-app npm run dev # Tab 2 - remix repository -REMIX_LOCAL_DEV_OUTPUT_DIRECTORY=../my-remix-app yarn watch +REMIX_LOCAL_BUILD_DIRECTORY=../my-remix-app yarn watch ``` Now - any time you make changes in the Remix repository, they will be written out to the appropriate locations in `../my-remix-app/node_modules` and you can restart the `npm run dev` command to pick them up ๐ŸŽ‰. diff --git a/contributors.yml b/contributors.yml index b076d38ef28..2c13c3e7e14 100644 --- a/contributors.yml +++ b/contributors.yml @@ -2,34 +2,54 @@ - aaronshaf - abereghici - abotsi +- accidentaldeveloper +- achinchen +- adicuco +- ahabhgk - ahbruns - ahmedeldessouki +- aiji42 - airjp73 - airondumael - Alarid - alex-ketch +- alextes - alexuxui +- alireza-bonab +- alisd23 +- alvinthen +- amorriscode - andrelandgraf +- andrewbrey - AndrewIngram - anishpras - anmolm96 - anmonteiro - AntoninBeaufort - anubra266 +- apeltop - Aprillion - arange - archwebio - arganaphangquestian - AriGunawan - arvigeus +- arvindell - ascorbic - ashleyryan - ashocean +- athongsavath +- awthwathje +- axel-habermaier - BasixKOR - BenMcH +- bgschiller +- binajmen +- bmarvinb - bmontalvo - bogas04 - BogdanDevBst +- bolchowka - brophdawg11 - bruno-oliveira - bsharrow @@ -38,13 +58,21 @@ - c43721 - camiaei - CanRau +- ccssmnn - chaance - chenc041 +- chenxsan +- chiangs +- chipit24 - christianhg +- christophertrudel - christophgockel - clarkmitchell - cliffordfajardo +- cloudy9101 +- cmd-johnson - codymjarrett +- colinhacks - confix - coryhouse - craigglennie @@ -52,38 +80,52 @@ - cysp - damiensedgwick - dan-gamble +- danielfgray - danielweinmann - davecalnan +- davecranwell-vocovo - DavidHollins6 - davongit - denissb - derekr - developit +- dgurns - dhargitai - dhmacs - dima-takoy +- DNLHC +- dogukanakkaya - dokeet - donavon +- Drew-Daniels - Dueen - dunglas +- dvargas92495 - dwightwatson - dwt47 - dylanplayer - eastlondoner +- eccentric-j +- EddyVinck - edgesoft - edmundhung - efkann - eldarshamukhamedov - emzoumpo - eps1lon +- esamattis - evanwinter - exegeteio +- F3n67u +- federicoestevez - fergusmeiklejohn - fgiuliani - fishel-feng - francisudeji +- frontsideair - fx109138 - gabimor +- garand - gautamkrishnar - gavriguy - ghaagsma @@ -91,77 +133,106 @@ - Gim3l - Girish21 - gkueny +- gmaliar - gon250 - goncy - gonzoscript - graham42 - GregBrimble +- GSt4r +- guatedude2 - guerra08 - gunners6518 - hadizz - hardingmatt - helderburato - HenryVogt +- hicksy - himorishige - hkan - Holben888 - hollandThomas -- hollandThomas - Hopsken - hzhu - IAmLuisJ - ianduvall +- illright - imzshh - isaacrmoreno - ishan-me - IshanKBG +- itsMapleLeaf - jacob-ebey - JacobParis - jakewtaylor - jamiebuilds +- janhoogeveen +- Jannis-Morgenstern +- jasonadelia - jaydiablo - jca41 - jdeniau +- JeffBeltran - jenseng - jeremyjfleming - jesse-deboer - jesseflorig - jgarrow +- jiahao-c +- jimniels - jkup - jmasson +- JNaftali - jo-ninja - joaosamouco +- jodygeraldo - johannesbraeunig -- johnson444 +- johnpolacek - johnson444 - joms - joshball +- jrubins - jssisodiya - jstafman - juhanakristian +- JulesBlm +- juliaqiuxy - justinnoel +- justinsalasdev +- justsml - juwiragiye +- jveldridge - jvnm-dev - kalch - kanermichael - karimsan +- kauffmanes +- kbariotis - KenanYusuf - kentcdodds - kevinrambaud - kgregory +- kiancross +- kilian +- kiliman - kimdontdoit - klauspaiva - knowler +- konradkalemba - kubaprzetakiewicz +- kuldar - kumard3 - lachlanjc - laughnan - lawrencecchen +- lensbart - leo - leon - levippaul +- LewisArdern - lifeiscontent - lionotm +- liranm - lpsinger - lswest - lucasdibz @@ -175,6 +246,7 @@ - m5r - machour - maferland +- manan30 - manosim - mantey-github - manzano78 @@ -186,9 +258,11 @@ - marvinwu - matchai - mathieusteele +- matt-l-w - matthew-burfield - Matthew-Mallimo - MatthewAlbrecht +- matthova - mattmazzola - mattstobbs - mbarto @@ -198,6 +272,7 @@ - mehulmpt - memark - mennopruijssers +- michaeldebetaz - michaeldeboey - michaelfriedman - michaseel @@ -212,9 +287,14 @@ - mskoroglu - msutkowski - mtt87 +- mush159 - na2hiro - nareshbhatia - navid-kalaei +- nexxeln +- ni554n +- nicholaschiang +- nickytonline - niconiahi - nielsdb97 - ninjaPixel @@ -222,6 +302,7 @@ - nobeeakon - nordiauwu - nurul3101 +- nvh95 - nwalters512 - octokatherine - omamazainab @@ -229,25 +310,34 @@ - orballo - pacexy - pcattori +- penspinner +- penx - phishy - plastic041 +- plondon - princerajroy - prvnbist - ptitFicus - pyr0gan +- RATIU5 - raulrpearson - real34 +- realjokele - reggie3 - rlfarman +- roachjc - robindrost - roddds - RomanSavarin +- ronnylt +- rossipedia - RossJHagan - RossMcMillan92 - rowinbot -- rossipedia - rphlmr - rtabulov +- ruisaraiva19 +- runmoore - Runner-dev - rvlewerissa - ryanflorence @@ -257,21 +347,28 @@ - schpet - sdavids - sean-roberts +- sebz - selfish +- sergiocarneiro - sergiodxa +- shashankboosi - shumuu - sidkh - sidv1905 - silvenon +- simonepizzamiglio - simonswiss - sinhalite - sitek94 - skube - sndrem +- sobrinho - squidpunch - stephanerangaya - SufianBabri - supachaidev +- Synvox +- tagraves - tascord - TheRealAstoo - therealflyingcoder @@ -280,9 +377,11 @@ - tjefferson08 - tombyrer - toyozaki +- turkerdev - tvanantwerp - twhitbeck - tylerbrostrom +- udasitharani - uhoh-itsmaciek - unhackit - UsamaHameed @@ -291,14 +390,24 @@ - veritem - VictorPeralta - vimutti77 +- vincecao - visormatt +- vkrol +- vlindhol +- vmosyaykin +- vorcigernix - weavdale +- wKovacs64 - wladiston +- XiNiHa - xstevenyung - yauri-io - yesmeck - yomeshgupta +- youbicode - youngvform - zachdtaylor - zainfathoni +- zhe - kayac-chang + diff --git a/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md b/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md new file mode 100644 index 00000000000..46853296f58 --- /dev/null +++ b/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md @@ -0,0 +1,113 @@ +# Use `npm` to manage NPM dependencies for Deno projects + +Date: 2022-05-10 + +Status: accepted + +## Context + +Deno has three ways to manage dependencies: + +1. Inlined URL imports: `import {...} from "https://deno.land/x/blah"` +2. [deps.ts](https://deno.land/manual/examples/manage_dependencies) +3. [Import maps](https://deno.land/manual/linking_to_external_code/import_maps) + +Additionally, NPM packages can be accessed as Deno modules via [Deno-friendly CDNs](https://deno.land/manual/node/cdns#deno-friendly-cdns) like https://esm.sh . + +Remix has some requirements around dependencies: + +- Remix treeshakes dependencies that are free of side-effects. +- Remix sets the environment (dev/prod/test) across all code, including dependencies, at runtime via the `NODE_ENV` environment variable. +- Remix depends on some NPM packages that should be specified as peer dependencies (notably, `react` and `react-dom`). + +### Treeshaking + +To optimize bundle size, Remix [treeshakes](https://esbuild.github.io/api/#tree-shaking) your app's code and dependencies. +This also helps to separate browser code and server code. + +Under the hood, the Remix compiler uses [esbuild](https://esbuild.github.io). +Like other bundlers, `esbuild` uses [`sideEffects` in `package.json` to determine when it is safe to eliminate unused imports](https://esbuild.github.io/api/#conditionally-injecting-a-file). + +Unfortunately, URL imports do not have a standard mechanism for marking packages as side-effect free. + +### Setting dev/prod/test environment + +Deno-friendly CDNs set the environment via a query parameter (e.g. `?dev`), not via an environment variable. +That means changing environment requires changing the URL import in the source code. +While you could use multiple import maps (`dev.json`, `prod.json`, etc...) to workaround this, import maps have other limitations: + +- standard tooling for managing import maps is not available +- import maps are not composeable, so any dependencies that use import maps must be manually accounted for + +### Specifying peer dependencies + +Even if import maps were perfected, CDNs compile each dependency in isolation. +That means that specifying peer dependencies becomes tedious and error-prone as the user needs to: + +- determine which dependencies themselves depend on `react` (or other similar peer dependency), even if indirectly. +- manually figure out which `react` version works across _all_ of these dependencies +- set that version for `react` as a query parameter in _all_ of the URLs for the identified dependencies + +If any dependencies change (added, removed, version change), +the user must repeat all of these steps again. + +## Decision + +### Use `npm` to manage NPM dependencies for Deno + +Do not use Deno-friendly CDNs for NPM dependencies in Remix projects using Deno. + +Use `npm` and `node_modules/` to manage NPM dependencies like `react` for Remix projects, even when using Deno with Remix. + +Deno module dependencies (e.g. from `https://deno.land`) can still be managed via URL imports. + +### Allow URL imports + +Remix will preserve any URL imports in the built bundles as external dependencies, +letting your browser runtime and server runtime handle them accordingly. +That means that you may: + +- use URL imports for the browser +- use URL imports for the server, if your server runtime supports it + +For example, Node will throw errors for URL imports, while Deno will resolve URL imports as normal. + +### Do not support import maps + +Remix will not yet support import maps. + +## Consequences + +- URL imports will not be treeshaken +- Users can specify environment via the `NODE_ENV` environment variable at runtime. +- Users won't have to do error-prone, manual dependency resolution. + +### VS Code type hints + +Users may configure an import map for the [Deno extension for VS Code](denoland.vscode-deno) to enable type hints for NPM-managed dependencies within their Deno editor: + +`.vscode/resolve_npm_imports_in_deno.json` + +```json +{ + "// This import map is used solely for the denoland.vscode-deno extension.": "", + "// Remix does not support import maps.": "", + "// Dependency management is done through `npm` and `node_modules/` instead.": "", + "// Deno-only dependencies may be imported via URL imports (without using import maps).": "", + + "imports": { + "react": "https://esm.sh/react@18.0.0", + "react-dom": "https://esm.sh/react-dom@18.0.0", + "react-dom/server": "https://esm.sh/react-dom@18.0.0/server" + } +} +``` + +`.vscode/settings.json` + +```json +{ + "deno.enable": true, + "deno.importMap": "./.vscode/resolve_npm_imports_in_deno.json" +} +``` diff --git a/decisions/0002-do-not-clone-request.md b/decisions/0002-do-not-clone-request.md new file mode 100644 index 00000000000..30f599f7f0f --- /dev/null +++ b/decisions/0002-do-not-clone-request.md @@ -0,0 +1,19 @@ +# Do not clone request + +Date: 2022-05-13 + +Status: accepted + +## Context + +To allow multiple loaders / actions to read the body of a request, we have been cloning the request before forwarding it to user-code. This is not the best thing to do as some runtimes will begin buffering the body to allow for multiple consumers. It also goes against "the platform" that states a request body should only be consumed once. + +## Decision + +Do not clone requests before they are passed to user-code (actions, handleDocumentRequest, handleDataRequest), and remove body from request passed to loaders. Loaders should be thought of as a "GET" / "HEAD" request handler. These request methods are not allowed to have a body, therefore you should not be reading it in your Remix loader function. + +## Consequences + +Loaders always receive a null body for the request. + +If you are reading the request body in both an action and handleDocumentRequest or handleDataRequest this will now fail as the body will have already been read. If you wish to continue reading the request body in multiple places for a single request against recommendations, consider using `.clone()` before reading it; just know this comes with tradeoffs. diff --git a/decisions/0003-infer-types-for-useloaderdata-and-useactiondata-from-loader-and-action-via-generics.md b/decisions/0003-infer-types-for-useloaderdata-and-useactiondata-from-loader-and-action-via-generics.md new file mode 100644 index 00000000000..23377f74a87 --- /dev/null +++ b/decisions/0003-infer-types-for-useloaderdata-and-useactiondata-from-loader-and-action-via-generics.md @@ -0,0 +1,230 @@ +# Infer types for `useLoaderData` and `useActionData` from `loader` and `action` via generics + +Date: 2022-07-11 + +Status: accepted + +## Context + +Goal: End-to-end type safety for `useLoaderData` and `useActionData` with great Developer Experience (DX) + +Related discussions: + +- [remix-run/remix#1254](https://github.com/remix-run/remix/pull/1254) +- [remix-run/remix#3276](https://github.com/remix-run/remix/pull/3276) + +--- + +In Remix v1.6.4, types for both `useLoaderData` and `useActionData` are parameterized with a generic: + +```tsx +type MyLoaderData = { + /* ... */ +}; +type MyActionData = { + /* ... */ +}; + +export default function Route() { + let loaderData = useLoaderData<MyLoaderData>(); + let actionData = useActionData<MyActionData>(); + return <div>{/* ... */}</div>; +} +``` + +For end-to-end type safety, it is then the user's responsability to make sure that `loader` and `action` also use the same type in the `json` generic: + +```ts +export const loader: LoaderFunction = () => { + return json<MyLoaderData>({ + /* ... */ + }); +}; + +export const action: ActionFunction = () => { + return json<MyActionData>({ + /* ... */ + }); +}; +``` + +### Diving into `useLoaderData`'s and `useActionData`'s generics + +Tracing through the `@remix-run/react` source code (v1.6.4), you'll find that `useLoaderData` returns an `any` type that is implicitly type cast to whatever type gets passed into the `useLoaderData` generic: + +```ts +// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/components.tsx#L1370 +export function useLoaderData<T = AppData>(): T { + return useRemixRouteContext().data; // +} + +// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/components.tsx#L73 +function useRemixRouteContext(): RemixRouteContextType { + /* ... */ +} + +// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/components.tsx#L56 +interface RemixRouteContextType { + data: AppData; + id: string; +} + +// https://github.com/remix-run/remix/blob/v1.6.4/packages/remix-react/data.ts#L4 +export type AppData = any; +``` + +Boiling this down, the code looks like: + +```ts +let data: any; + +// somewhere else, `loader` gets called an sets `data` to some value + +function useLoaderData<T>(): T { + return data; // <-- Typescript casts this `any` to `T` +} +``` + +`useLoaderData` isn't basing its return type on how `data` was set (i.e. the return value of `loader`) nor is it validating the data. +It's just blindly casting `data` to whatever the user passed in for the generic `T`. + +### Issues with current approach + +The developer experience is subpar. +Users are required to write redundant code for the data types that could have been inferred from the arguments to `json`. +Changes to the data shape require changing _both_ the declared `type` or `interface` as well as the argument to `json`. + +Additionally, the current approach encourages users to pass the same type to `json` with the `loader` and to `useLoaderData`, but **this is a footgun**! +`json` can accept data types like `Date` that are JSON serializable, but `useLoaderData` will return the _serialized_ type: + +```ts +type MyLoaderData = { + birthday: Date; +}; + +export const loader: LoaderFunction = () => { + return json<MyLoaderData>({ birthday: new Date("February 15, 1992") }); +}; + +export default function Route() { + let { birthday } = useLoaderData<MyLoaderData>(); + // ^ `useLoaderData` tricks Typescript into thinking this is a `Date`, when in fact its a `string`! +} +``` + +Again, the same goes for `useActionData`. + +### Solution criteria + +- Return type of `useLoaderData` and `useActionData` should somehow be inferred from `loader` and `action`, not blindly type cast +- Return type of `loader` and `action` should be inferred + - Necessarily, return type of `json` should be inferred from its input +- No module side-effects (so higher-order functions like `makeLoader` is definitely a no). +- `json` should allow everything that `JSON.stringify` allows. +- `json` should allow only what `JSON.stringify` allows. +- `useLoaderData` should not return anything that `JSON.parse` can't return. + +### Key insight: `loader` and `action` are an _implicit_ inputs + +While there's been interest in inferring the types for `useLoaderData` based on `loader`, there was [hesitance to use a Typescript generic to do so](https://github.com/remix-run/remix/pull/3276#issuecomment-1164764821). +Typescript generics are apt for specifying or inferring types for _inputs_, not for blindly type casting output types. + +A key factor in the decision was identifying that `loader` and `action` are _implicit_ inputs of `useLoaderData` and `useActionData`. + +In other words, if `loader` and `useLoaderData` were guaranteed to run in the same process (and not cross the network), then we could write `useLoaderData(loader)`, specifying `loader` as an explicit input for `useLoaderData`. + +```ts +// _conceptually_ `loader` is an input for `useLoaderData` +function useLoaderData<Loader extends LoaderFunction>(loader: Loader) { + /*...*/ +} +``` + +Though `loader` and `useLoaderData` exist together in the same file at development-time, `loader` does not exist at runtime in the browser. +Without the `loader` argument to infer types from, `useLoaderData` needs a way to learn about `loader`'s type at compile-time. + +Additionally, `loader` and `useLoaderData` are both managed by Remix across the network. +While its true that Remix doesn't "own" the network in the strictest sense, having `useLoaderData` return data that does not correspond to its `loader` is an exceedingly rare edge-case. + +Same goes for `useActionData`. + +--- + +A similar case is how [Prisma](https://www.prisma.io/) infers types from database schemas available at runtime, even though there are (exceedingly rare) edge-cases where that database schema _could_ be mutated after compile-time but before run-time. + +## Decision + +Explicitly provide type of the implicit `loader` input for `useLoaderData` and then infer the return type for `useLoaderData`. +Do the same for `action` and `useActionData`. + +```ts +export const loader = async (args: LoaderArgs) => { + // ... + return json(/*...*/); +}; + +export default function Route() { + let data = useLoaderData<typeof loader>(); + // ... +} +``` + +Additionally, the inferred return type for `useLoaderData` will only include serializable (JSON) types. + +### Return `unknown` when generic is omitted + +Omitting the generic for `useLoaderData` or `useActionData` results in `any` being returned. +This hides potential type errors from the user. +Instead, we'll change the return type to `unknown`. + +```ts +type MyLoaderData = { + /*...*/ +}; + +export default function Route() { + let data = useLoaderData(); + // ^? unknown +} +``` + +Note: Since this would be a breaking change, changing the return type to `unknown` will be slated for v2. + +### Deprecate non-inferred types via generics + +Passing in a non-inferred type for `useLoaderData` is hiding an unsafe type cast. +Using the `useLoaderData` in this way will be deprecated in favor of an explicit type cast that clearly communicates the assumptions being made: + +```ts +type MyLoaderData = { + /*...*/ +}; + +export default function Route() { + let dataGeneric = useLoaderData<MyLoaderData>(); // <-- will be deprecated + let dataCast = useLoaderData() as MyLoaderData; // <- use this instead +} +``` + +## Consequences + +- Users can continue to provide non-inferred types by type casting the result of `useLoaderData` or `useActionData` +- Users can opt-in to inferred types by using `typeof loader` or `typeof action` at the generic for `useLoaderData` or `useActionData`. +- Return types for `loader` and `action` will be the sources-of-truth for the types inferred for `useLoaderData` and `useActionData`. +- Users do not need to write redundant code to align types across the network +- Return type of `useLoaderData` and `useActionData` will correspond to the JSON _serialized_ types from `json` calls in `loader` and `action`, eliminating a class of errors. +- `LoaderFunction` and `ActionFunction` should not be used when opting into type inference as they override the inferred return types.[^1] + +๐Ÿšจ Users who opt-in to inferred types **MUST** return a `TypedResponse` from `json` and **MUST NOT** return a bare object: + +```ts +const loader = () => { + // NO + return { hello: "world" }; + + // YES + return json({ hello: "world" }); +}; +``` + +[^1]: The proposed `satisfies` operator for Typescript would let `LoaderFunction` and `ActionFunction` enforce function types while preserving the narrower inferred return type: https://github.com/microsoft/TypeScript/issues/47920 diff --git a/decisions/template.md b/decisions/template.md new file mode 100644 index 00000000000..105960da21c --- /dev/null +++ b/decisions/template.md @@ -0,0 +1,11 @@ +# Title + +Date: YYYY-MM-DD + +Status: proposed | rejected | accepted | deprecated | โ€ฆ | superseded by [0005](0005-example.md) + +## Context + +## Decision + +## Consequences diff --git a/docs/api/conventions.md b/docs/api/conventions.md index 4b430aeb8ef..8a1a48b24a6 100644 --- a/docs/api/conventions.md +++ b/docs/api/conventions.md @@ -5,20 +5,18 @@ order: 1 # Conventions -A lot of Remix APIs aren't imported from the `"remix"` package, but are instead conventions and exports from _your_ application modules. When you `import from "remix"`, _you are calling Remix_, but these APIs are when _Remix calls your code_. +A lot of Remix APIs aren't imported from the `"@remix-run/*"` packages, but are instead conventions and exports from _your_ application modules. When you `import from "@remix-run/*"`, _you are calling Remix_, but these APIs are consumed when _Remix calls your code_. ## remix.config.js This file has a few build and development configuration options, but does not actually run on your server. ```tsx filename=remix.config.js -/** - * @type {import('@remix-run/dev').AppConfig} - */ +/** @type {import('@remix-run/dev').AppConfig} */ module.exports = { appDirectory: "app", assetsBuildDirectory: "public/build", - ignoredRouteFiles: [".*"], + ignoredRouteFiles: ["**/.*"], publicPath: "/build/", routes(defineRoutes) { return defineRoutes((route) => { @@ -66,7 +64,7 @@ The port number to use for the dev websocket server. Defaults to 8002. This is an array of globs (via [minimatch][minimatch]) that Remix will match to files while reading your `app/routes` directory. If a file matches, it will be -ignored rather that treated like a route module. This is useful for ignoring +ignored rather than treated like a route module. This is useful for ignoring dotfiles (like `.DS_Store` files) or CSS/test files you wish to colocate. ### publicPath @@ -110,7 +108,7 @@ either a `.js` or `.ts` file extension. ### serverBuildDirectory <docs-warning>This option is deprecated and will likely be removed in a future -stable release. Use [`serverBuildPath`](#serverbuildpath) instead.</docs-warning> +stable release. Use [`serverBuildPath`][server-build-path] instead.</docs-warning> The path to the server build, relative to `remix.config.js`. Defaults to "build". This needs to be deployed to your server. @@ -121,7 +119,7 @@ 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 -[`serverBuildTarget`](#serverbuildtarget). +[`serverBuildTarget`][server-build-target]. ### serverBuildTarget @@ -129,30 +127,28 @@ The target of the server build. Defaults to `"node-cjs"`. The `serverBuildTarget` can be one of the following: -- [`"arc"`](https://arc.codes) -- [`"cloudflare-pages"`](https://pages.cloudflare.com/) -- [`"cloudflare-workers"`](https://workers.cloudflare.com/) -- [`"deno"`](https://deno.land/) -- [`"netlify"`](https://www.netlify.com/) -- [`"node-cjs"`](https://nodejs.org/en/) -- [`"vercel"`](https://vercel.com/) +- [`"arc"`][arc] +- [`"cloudflare-pages"`][cloudflare-pages] +- [`"cloudflare-workers"`][cloudflare-workers] +- [`"deno"`][deno] +- [`"netlify"`][netlify] +- [`"node-cjs"`][node-cjs] +- [`"vercel"`][vercel] ### serverDependenciesToBundle -A list of regex 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. +A list of regex patterns that determines if a module is transpiled and included in the server bundle. This can be useful when consuming ESM only packages in a CJS build. For example, the `unified` ecosystem is all ESM-only. Let's also say we're using a `@sindresorhus/slugify` which is ESM-only as well. Here's how you would be able to consume those packages in a CJS app without having to use dynamic imports: -```ts filename=remix.config.js lines=[10-15] -/** - * @type {import('@remix-run/dev').AppConfig} - */ +```ts filename=remix.config.js lines=[8-13] +/** @type {import('@remix-run/dev').AppConfig} */ module.exports = { appDirectory: "app", assetsBuildDirectory: "public/build", publicPath: "/build/", serverBuildDirectory: "build", - ignoredRouteFiles: [".*"], + ignoredRouteFiles: ["**/.*"], serverDependenciesToBundle: [ /^rehype.*/, /^remark.*/, @@ -162,11 +158,21 @@ module.exports = { }; ``` +### watchPaths + +A function for defining custom directories to watch while running [remix dev][remix-dev], in addition to [`appDirectory`][app-directory]. + +```tsx +exports.watchPaths = async () => { + return ["/some/path/*"]; +}; +``` + ## File Name Conventions There are a few conventions that Remix uses you should be aware of. -<docs-info>[Dilum Sanjaya](https://twitter.com/DilumSanjaya) made [an awesome visualization](https://remix-routing-demo.netlify.app/) of how routes in the file system map to the URL in your app that might help you understand these conventions.</docs-info> +<docs-info>[Dilum Sanjaya][dilum-sanjaya] made [an awesome visualization][an-awesome-visualization] of how routes in the file system map to the URL in your app that might help you understand these conventions.</docs-info> ### Special Files @@ -178,7 +184,7 @@ There are a few conventions that Remix uses you should be aware of. Setting up routes in Remix is as simple as creating files in your `app` directory. These are the conventions you should know to understand how routing in Remix works. -Please note that you can use either `.jsx` or `.tsx` file extensions depending on whether or not you use TypeScript. We'll stick with `.tsx` in the examples to avoid duplication (and because we โค๏ธ TypeScript). +Please note that you can use either `.js`, `.jsx` or `.tsx` file extensions depending on whether or not you use TypeScript. We'll stick with `.tsx` in the examples to avoid duplication (and because we โค๏ธ TypeScript). #### Root Layout Route @@ -191,9 +197,9 @@ app/ The file in `app/root.tsx` is your root layout, or "root route" (very sorry for those of you who pronounce those words the same way!). It works just like all other routes: -- You can export a [`loader`](#loader), [`action`](#action), [`meta`](#meta), [`headers`](#headers), or [`links`](#links) function -- You can export an [`ErrorBoundary`](#errorboundary) or [`CatchBoundary`](#catchboundary) -- Your default export is the layout component that renders the rest of your app in an [`<Outlet />`](https://reactrouter.com/docs/en/v6/api#outlet) +- You can export a [`loader`][loader], [`action`][action], [`meta`][meta], [`headers`][headers-2], or [`links`][links] function +- You can export an [`ErrorBoundary`][error-boundary] or [`CatchBoundary`][catch-boundary] +- Your default export is the layout component that renders the rest of your app in an [`<Outlet />`][outlet] #### Basic Routes @@ -256,11 +262,14 @@ For example: `app/routes/blog/$postId.tsx` will match the following URLs: - `/blog/once-upon-a-time` - `/blog/how-to-ride-a-bike` -On each of these pages, the dynamic segment of the URL path is the value of the parameter. There can be multiple parameters active at any time (as in `/dashboard/:client/invoices/:invoiceId` [view example app](https://github.com/remix-run/remix/tree/main/examples/multiple-params)) and all parameters can be accessed within components via [`useParams`](https://reactrouter.com/docs/en/v6/api#useparams) and within loaders/actions via the argument's [`params`](#loader-params) property: +On each of these pages, the dynamic segment of the URL path is the value of the parameter. There can be multiple parameters active at any time (as in `/dashboard/:client/invoices/:invoiceId` [view example app][view-example-app]) and all parameters can be accessed within components via [`useParams`][use-params] and within loaders/actions via the argument's [`params`][params] property: ```tsx filename=app/routes/blog/$postId.tsx -import { useParams } from "remix"; -import type { LoaderFunction, ActionFunction } from "remix"; +import { useParams } from "@remix-run/react"; +import type { + LoaderFunction, + ActionFunction, +} from "@remix-run/node"; // or cloudflare/deno export const loader: LoaderFunction = async ({ params, @@ -282,7 +291,7 @@ export default function PostRoute() { Nested routes can also contain dynamic segments by using the `$` character in the parent's directory name. For example, `app/routes/blog/$postId/edit.tsx` might represent the editor page for blog entries. -See the [routing guide](../guides/routing.md) for more information. +See the [routing guide][routing-guide] for more information. #### Layout Routes @@ -314,7 +323,7 @@ app/ </details> -In the example above, the `blog.tsx` is a "layout route" for everything within the `blog` directory (`blog/index.tsx` and `blog/categories.tsx`). When a route has the same name as its directory (`routes/blog.tsx` and `routes/blog/`), it becomes a layout route for all of the routes inside that directory ("child routes"). Similar to your [root route](#root-layout-route), the parent route should render an `<Outlet />` where the child routes should appear. This is how you can create multiple levels of persistent layout nesting associated with URLs. +In the example above, the `blog.tsx` is a "layout route" for everything within the `blog` directory (`blog/index.tsx` and `blog/categories.tsx`). When a route has the same name as its directory (`routes/blog.tsx` and `routes/blog/`), it becomes a layout route for all of the routes inside that directory ("child routes"). Similar to your [root route][root-route], the parent route should render an `<Outlet />` where the child routes should appear. This is how you can create multiple levels of persistent layout nesting associated with URLs. #### Pathless Layout Routes @@ -353,7 +362,7 @@ For example, all of your marketing pages could be in `app/routes/__marketing/*` <docs-warning>Be careful, pathless layout routes introduce the possibility of URL conflicts</docs-warning> -#### Dot Delimeters +#### Dot Delimiters <!-- prettier-ignore --> ```markdown [8] @@ -419,8 +428,11 @@ Files that are named `$.tsx` are called "splat" (or "catch-all") routes. These r Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key. ```tsx filename=app/routes/$.tsx -import { useParams } from "remix"; -import type { LoaderFunction, ActionFunction } from "remix"; +import { useParams } from "@remix-run/react"; +import type { + LoaderFunction, + ActionFunction, +} from "@remix-run/node"; // or cloudflare/deno export const loader: LoaderFunction = async ({ params, @@ -442,7 +454,7 @@ export default function PostRoute() { ### Escaping special characters -Because some characters have special meaning, you must use our escaping syntax if you want those characters to actually appear in the route. For example, if I wanted to make a [Resource Route](../guides/resource-routes) for a `/sitemap.xml`, I could name the file `app/routes/[sitemap.xml].tsx`. So you simply wrap any part of the filename with brackets and that will escape any special characters. +Because some characters have special meaning, you must use our escaping syntax if you want those characters to actually appear in the route. For example, if I wanted to make a [Resource Route][resource-route] for a `/sitemap.xml`, I could name the file `app/routes/[sitemap.xml].tsx`. So you simply wrap any part of the filename with brackets and that will escape any special characters. <docs-info> Note, you could even do `app/routes/sitemap[.]xml.tsx` if you wanted to only wrap the part that needs to be escaped. It makes no difference. Choose the one you like best. @@ -454,15 +466,15 @@ Because some characters have special meaning, you must use our escaping syntax i Remix uses `app/entry.client.tsx` as the entry point for the browser bundle. This module gives you full control over the "hydrate" step after JavaScript loads into the document. -Typically this module uses `ReactDOM.hydrate` to re-hydrate the markup that was already generated on the server in your [server entry module](#entryservertsx). +Typically this module uses `ReactDOM.hydrate` to re-hydrate the markup that was already generated on the server in your [server entry module][server-entry-module]. Here's a basic example: ```tsx -import ReactDOM from "react-dom"; -import Remix from "@remix-run/react/browser"; +import { hydrate } from "react-dom"; +import { RemixBrowser } from "@remix-run/react"; -ReactDOM.hydrate(<Remix />, document); +hydrate(<RemixBrowser />, document); ``` This is the first piece of code that runs in the browser. As you can see, you have full control here. You can initialize client side libraries, setup things like `window.history.scrollRestoration`, etc. @@ -471,19 +483,19 @@ This is the first piece of code that runs in the browser. As you can see, you ha Remix uses `app/entry.server.tsx` to generate the HTTP response when rendering on the server. The `default` export of this module is a function that lets you create the response, including HTTP status, headers, and HTML, giving you full control over the way the markup is generated and sent to the client. -This module should render the markup for the current page using a `<RemixServer>` element with the `context` and `url` for the current request. This markup will (optionally) be re-hydrated once JavaScript loads in the browser using the [browser entry module](#entryclienttsx). +This module should render the markup for the current page using a `<RemixServer>` element with the `context` and `url` for the current request. This markup will (optionally) be re-hydrated once JavaScript loads in the browser using the [browser entry module][browser-entry-module]. You can also export an optional `handleDataRequest` function that will allow you to modify the response of a data request. These are the requests that do not render HTML, but rather return the loader and action data to the browser once client side hydration has occurred. Here's a basic example: ```tsx -import ReactDOMServer from "react-dom/server"; +import { renderToString } from "react-dom/server"; import type { EntryContext, HandleDataRequestFunction, -} from "remix"; -import { RemixServer } from "remix"; +} from "@remix-run/node"; // or cloudflare/deno +import { RemixServer } from "@remix-run/react"; export default function handleRequest( request: Request, @@ -491,7 +503,7 @@ export default function handleRequest( responseHeaders: Headers, remixContext: EntryContext ) { - const markup = ReactDOMServer.renderToString( + const markup = renderToString( <RemixServer context={remixContext} url={request.url} /> ); @@ -519,7 +531,7 @@ export const handleDataRequest: HandleDataRequestFunction = A route in Remix can be used for many things. Usually theyโ€™re used for the user interface of your app, like a React component with server-side lifecycle hooks. But they can also serve as generic routes for any kind of resource (like dynamic CSS or social images). -It's important to read [Route Module Constraints](../guides/constraints). +It's important to read [Route Module Constraints][route-module-constraints]. ### `default` export @@ -540,10 +552,10 @@ export default function SomeRouteComponent() { <docs-success>Watch the <a href="https://www.youtube.com/playlist?list=PLXoynULbYuEDG2wBFSZ66b85EIspy3fy6">๐Ÿ“ผ Remix Single</a>: <a href="https://www.youtube.com/watch?v=NXqEP_PsPNc&list=PLXoynULbYuEDG2wBFSZ66b85EIspy3fy6">Loading data into components</a></docs-success> -Each route can define a "loader" function that will be called on the server before rendering to provide data to the route. +Each route can define a "loader" function that will be called on the server before rendering to provide data to the route. You may think of this as a "GET" request handler in that you should not be reading the body of the request; that is the job of an [`action`][action]. ```js -import { json } from "remix"; +import { json } from "@remix-run/node"; // or cloudflare/deno export const loader = async () => { // The `json` function converts a serializable object into a JSON response @@ -554,8 +566,8 @@ export const loader = async () => { ```ts // Typescript -import { json } from "remix"; -import type { LoaderFunction } from "remix"; +import { json } from "@remix-run/node"; // or cloudflare/deno +import type { LoaderFunction } from "@remix-run/node"; // or cloudflare/deno export const loader: LoaderFunction = async () => { return json({ ok: true }); @@ -566,8 +578,9 @@ This function is only ever run on the server. On the initial server render it wi Using the database ORM Prisma as an example: -```tsx lines=[1,5-7,10] -import { json, useLoaderData } from "remix"; +```tsx lines=[1-2,6-8,11] +import { json } from "@remix-run/node"; // or cloudflare/deno +import { useLoaderData } from "@remix-run/react"; import { prisma } from "../db"; @@ -658,21 +671,9 @@ export const loader: LoaderFunction = async ({ }; ``` -#### Returning objects - -You can return plain JavaScript objects from your loaders that will be made available to your component by the [`useLoaderData`](./remix#useloaderdata) hook. - -```ts -import { json } from "remix"; - -export const loader = async () => { - return json({ whatever: "you want" }); -}; -``` - #### Returning Response Instances -When you return a plain object, Remix turns it into a [Fetch Response][response]. This means you can return them yourself, too. +You need to return a [Fetch Response][response] from your loader. ```ts export const loader: LoaderFunction = async () => { @@ -689,7 +690,7 @@ export const loader: LoaderFunction = async () => { Using the `json` helper simplifies this so you don't have to construct them yourself, but these two examples are effectively the same! ```tsx -import { json } from "remix"; +import { json } from "@remix-run/node"; // or cloudflare/deno export const loader: LoaderFunction = async () => { const users = await fakeDb.users.findMany(); @@ -700,7 +701,7 @@ export const loader: LoaderFunction = async () => { You can see how `json` just does a little of the work to make your loader a lot cleaner. You can also use the `json` helper to add headers or a status code to your response: ```tsx -import { json } from "remix"; +import { json } from "@remix-run/node"; // or cloudflare/deno export const loader: LoaderFunction = async ({ params, @@ -719,7 +720,7 @@ export const loader: LoaderFunction = async ({ See also: -- [`headers`](#headers) +- [`headers`][headers-2] - [MDN Response Docs][response] #### Throwing Responses in Loaders @@ -729,8 +730,8 @@ Along with returning responses, you can also throw Response objects from your lo Here is a full example showing how you can create utility functions that throw responses to stop code execution in the loader and move over to an alternative UI. ```ts filename=app/db.ts -import { json } from "remix"; -import type { ThrownResponse } from "remix"; +import { json } from "@remix-run/node"; // or cloudflare/deno +import type { ThrownResponse } from "@remix-run/react"; export type InvoiceNotFoundResponse = ThrownResponse< 404, @@ -747,7 +748,7 @@ export function getInvoice(id, user) { ``` ```ts filename=app/http.ts -import { redirect } from "remix"; +import { redirect } from "@remix-run/node"; // or cloudflare/deno import { getSession } from "./session"; @@ -765,8 +766,8 @@ export async function requireUserSession(request) { ``` ```tsx filename=app/routes/invoice/$invoiceId.tsx -import { useCatch, useLoaderData } from "remix"; -import type { ThrownResponse } from "remix"; +import { useCatch, useLoaderData } from "@remix-run/react"; +import type { ThrownResponse } from "@remix-run/react"; import { requireUserSession } from "~/http"; import { getInvoice } from "~/db"; @@ -843,7 +844,8 @@ Actions have the same API as loaders, the only difference is when they are calle This enables you to co-locate everything about a data set in a single route module: the data read, the component that renders the data, and the data writes: ```tsx -import { json, redirect, Form } from "remix"; +import { json, redirect } from "@remix-run/node"; // or cloudflare/deno +import { Form } from "@remix-run/react"; import { fakeGetTodos, fakeCreateTodo } from "~/utils/db"; import { TodoList } from "~/components/TodoList"; @@ -891,13 +893,18 @@ See also: - [`<Form>`][form] - [`<Form action>`][form action] +- [`?index` query param][index query param] ### `headers` Each route can define its own HTTP headers. One of the common headers is the `Cache-Control` header that indicates to browser and CDN caches where and for how long a page is able to be cached. ```tsx -export function headers({ loaderHeaders, parentHeaders }) { +export function headers({ + actionHeaders, + loaderHeaders, + parentHeaders, +}) { return { "X-Stretchy-Pants": "its for fun", "Cache-Control": "max-age=300, s-maxage=3600", @@ -905,7 +912,7 @@ export function headers({ loaderHeaders, parentHeaders }) { } ``` -Usually your data is a better indicator of your cache duration than your route module (data tends to be more dynamic than markup), so the loader's headers are passed in to `headers()` too: +Usually your data is a better indicator of your cache duration than your route module (data tends to be more dynamic than markup), so the `action`'s & `loader`'s headers are passed in to `headers()` too: ```tsx export function headers({ loaderHeaders }) { @@ -915,16 +922,16 @@ export function headers({ loaderHeaders }) { } ``` -Note: `loaderHeaders` is an instance of the [Web Fetch API][headers] `Headers` class. +Note: `actionHeaders` & `loaderHeaders` are an instance of the [Web Fetch API][headers] `Headers` class. Because Remix has nested routes, there's a battle of the headers to be won when nested routes match. In this case, the deepest route wins. Consider these files in the routes directory: ``` โ”œโ”€โ”€ users.tsx โ””โ”€โ”€ users - ย ย  โ”œโ”€โ”€ $userId.tsx - ย ย  โ””โ”€โ”€ $userId - ย ย  ย ย  โ””โ”€โ”€ profile.tsx + โ”œโ”€โ”€ $userId.tsx + โ””โ”€โ”€ $userId + โ””โ”€โ”€ profile.tsx ``` If we are looking at `/users/123/profile` then three routes are rendering: @@ -973,8 +980,8 @@ Note that you can also add headers in your `entry.server` file for things that s ```tsx lines=[16] import { renderToString } from "react-dom/server"; -import { RemixServer } from "remix"; -import type { EntryContext } from "remix"; +import { RemixServer } from "@remix-run/react"; +import type { EntryContext } from "@remix-run/node"; // or cloudflare/deno export default function handleRequest( request: Request, @@ -1003,7 +1010,7 @@ Just keep in mind that doing this will apply to _all_ document requests, but doe The meta export will set meta tags for your html document. We highly recommend setting the title and description on every route besides layout routes (their index route will set the meta). ```tsx -import type { MetaFunction } from "remix"; +import type { MetaFunction } from "@remix-run/node"; // or cloudflare/deno export const meta: MetaFunction = () => { return { @@ -1014,26 +1021,28 @@ export const meta: MetaFunction = () => { }; ``` +<docs-warning>The `meta` function _may_ run on the server (e.g. the initial page load) or the client (e.g. a client navigation), so you cannot access server-specific data like `process.env.NODE_ENV` directly. If you need server-side data in `meta`, get the data in the `loader` and access it via the `meta` function's `data` parameter.</docs-warning> + There are a few special cases (read about those below). In the case of nested routes, the meta tags are merged automatically, so parent routes can add meta tags without the child routes needing to copy them. #### `HtmlMetaDescriptor` -This is an object representation and abstraction of a `<meta {...props}>` element and its attributes. [View the MDN docs for the meta API](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta). +This is an object representation and abstraction of a `<meta {...props}>` element and its attributes. [View the MDN docs for the meta API][view-the-mdn-docs-for-the-meta-api]. The `meta` export from a route should return a single `HtmlMetaDescriptor` object. -Almost every `meta` element takes a `name` and `content` attribute, with the exception of [OpenGraph tags](https://ogp.me/) which use `property` instead of `name`. In either case, the attributes represent a key/value pair for each tag. Each pair in the `HtmlMetaDescriptor` object represents a separate `meta` element, and Remix maps each to the correct attributes for that tag. +Almost every `meta` element takes a `name` and `content` attribute, with the exception of [OpenGraph tags][open-graph-tags] which use `property` instead of `name`. In either case, the attributes represent a key/value pair for each tag. Each pair in the `HtmlMetaDescriptor` object represents a separate `meta` element, and Remix maps each to the correct attributes for that tag. -The `meta` object can also hold a `title` reference which maps to the [HTML `<title>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). +The `meta` object can also hold a `title` reference which maps to the [HTML `<title>` element][html-title-element]. As a convenience, `charset: "utf-8"` will render a `<meta charset="utf-8">`. -As a last option, you can also pass an object of attribute/value pairs as the value. This can be used as an escape-hetch for meta tags like the [`http-equiv` tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv) which uses `http-equiv` instead of `name`. +As a last option, you can also pass an object of attribute/value pairs as the value. This can be used as an escape-hatch for meta tags like the [`http-equiv` tag][http-equiv-tag] which uses `http-equiv` instead of `name`. Examples: ```tsx -import type { MetaFunction } from "remix"; +import type { MetaFunction } from "@remix-run/node"; // or cloudflare/deno export const meta: MetaFunction = () => ({ // Special cases @@ -1041,7 +1050,7 @@ export const meta: MetaFunction = () => ({ "og:image": "https://josiesshakeshack.com/logo.jpg", // <meta property="og:image" content="https://josiesshakeshack.com/logo.jpg"> title: "Josie's Shake Shack", // <title>Josie's Shake Shack - // content => name + // name => content description: "Delicious shakes", // viewport: "width=device-width,initial-scale=1", // @@ -1084,7 +1093,7 @@ export const meta: MetaFunction = ({ data, params }) => { The links function defines which `` elements to add to the page when the user visits a route. ```tsx -import type { LinksFunction } from "remix"; +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno export const links: LinksFunction = () => { return [ @@ -1118,7 +1127,7 @@ The `links` export from a route should return an array of `HtmlLinkDescriptor` o Examples: ```tsx -import type { LinksFunction } from "remix"; +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno import stylesHref from "../styles/something.css"; @@ -1187,7 +1196,7 @@ A Remix `CatchBoundary` component works just like a route component, but instead A `CatchBoundary` component has access to the status code and thrown response data through `useCatch`. ```tsx -import { useCatch } from "remix"; +import { useCatch } from "@remix-run/react"; export function CatchBoundary() { const caught = useCatch(); @@ -1210,7 +1219,7 @@ An `ErrorBoundary` is a React component that renders whenever there is an error **Note:** We use the word "error" to mean an uncaught exception; something you didn't anticipate happening. This is different from other types of "errors" that you are able to recover from easily, for example a 404 error where you can still show something in the user interface to indicate you weren't able to find some data. -A Remix `ErrorBoundary` component works just like normal React [error boundaries](https://reactjs.org/docs/error-boundaries.html), but with a few extra capabilities. When there is an error in your route component, the `ErrorBoundary` will be rendered in its place, nested inside any parent routes. `ErrorBoundary` components also render when there is an error in the `loader` or `action` functions for a route, so all errors for that route may be handled in one spot. +A Remix `ErrorBoundary` component works just like normal React [error boundaries][error-boundaries], but with a few extra capabilities. When there is an error in your route component, the `ErrorBoundary` will be rendered in its place, nested inside any parent routes. `ErrorBoundary` components also render when there is an error in the `loader` or `action` functions for a route, so all errors for that route may be handled in one spot. An `ErrorBoundary` component receives one prop: the `error` that occurred. @@ -1237,7 +1246,7 @@ export const handle = { }; ``` -This is almost always used on conjunction with `useMatches`. To see what kinds of things you can do with it, refer to [`useMatches`](./remix#usematches) for more information. +This is almost always used on conjunction with `useMatches`. To see what kinds of things you can do with it, refer to [`useMatches`][use-matches] for more information. ### unstable_shouldReload @@ -1248,7 +1257,7 @@ This is almost always used on conjunction with `useMatches`. To see what kinds o This function lets apps optimize which routes should be reloaded on some client-side transitions. ```ts -import type { ShouldReloadFunction } from "remix"; +import type { ShouldReloadFunction } from "@remix-run/react"; export const unstable_shouldReload: ShouldReloadFunction = ({ @@ -1314,18 +1323,18 @@ Consider these routes: And lets say the UI looks something like this: ``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Project: Design Revamp โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ Tasks โ”‚ Collabs โ”‚ >ACTIVITY โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ Search: _____________ โ”‚ -โ”‚ โ”‚ -โ”‚ - Ryan added an image โ”‚ -โ”‚ โ”‚ -โ”‚ - Michael commented โ”‚ -โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ++------------------------------+ +| Project: Design Revamp | ++------------------------------+ +| Tasks | Collabs | >ACTIVITY | ++------------------------------+ +| Search: _____________ | +| | +| - Ryan added an image | +| | +| - Michael commented | +| | ++------------------------------+ ``` The `activity.tsx` loader can use the search params to filter the list, so visiting a URL like `/projects/design-revamp/activity?search=image` could filter the list of results. Maybe it looks something like this: @@ -1395,7 +1404,7 @@ Any files inside the `app` folder can be imported into your modules. Remix will: It's most common for stylesheets, but can used for anything. ```tsx filename=app/routes/root.tsx -import type { LinksFunction } from "remix"; +import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno import styles from "./styles/app.css"; import banner from "./images/banner.jpg"; @@ -1421,6 +1430,43 @@ export default function Page() { [urlsearchparams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams [form]: ./remix#form [form action]: ./remix#form-action +[index query param]: ../guides/routing#what-is-the-index-query-param [link tag]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link [minimatch]: https://www.npmjs.com/package/minimatch [handledatarequest]: #entryservertsx +[server-build-path]: #serverbuildpath +[server-build-target]: #serverbuildtarget +[arc]: https://arc.codes +[cloudflare-pages]: https://pages.cloudflare.com +[cloudflare-workers]: https://workers.cloudflare.com +[deno]: https://deno.land +[netlify]: https://www.netlify.com +[node-cjs]: https://nodejs.org/en +[vercel]: https://vercel.com +[dilum-sanjaya]: https://twitter.com/DilumSanjaya +[an-awesome-visualization]: https://remix-routing-demo.netlify.app +[loader]: #loader +[action]: #action +[meta]: #meta +[headers-2]: #headers +[links]: #links +[error-boundary]: #errorboundary +[catch-boundary]: #catchboundary +[outlet]: https://reactrouter.com/docs/en/v6/api#outlet +[view-example-app]: https://github.com/remix-run/remix/tree/main/examples/multiple-params +[use-params]: https://reactrouter.com/docs/en/v6/api#useparams +[params]: #loader-params +[routing-guide]: ../guides/routing +[root-route]: #root-layout-route +[resource-route]: ../guides/resource-routes +[server-entry-module]: #entryservertsx +[browser-entry-module]: #entryclienttsx +[route-module-constraints]: ../guides/constraints +[view-the-mdn-docs-for-the-meta-api]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta +[open-graph-tags]: https://ogp.me +[html-title-element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title +[http-equiv-tag]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#attr-http-equiv +[error-boundaries]: https://reactjs.org/docs/error-boundaries.html +[use-matches]: ./remix#usematches +[remix-dev]: https://remix.run/docs/en/v1/other-api/dev#remix-dev +[app-directory]: #appDirectory diff --git a/docs/api/remix.md b/docs/api/remix.md index 4f4a23e7990..85460607cac 100644 --- a/docs/api/remix.md +++ b/docs/api/remix.md @@ -1,20 +1,40 @@ --- -title: Remix Package +title: Remix Packages order: 2 --- -# Remix Package +# Remix Packages -This package provides all the components, hooks, and [Web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) objects and helpers. +React: `@remix-run/react` + +Server runtimes: + +- `@remix-run/cloudflare` +- `@remix-run/deno` +- `@remix-run/node` + +Server adapters: + +- `@remix-run/architect` +- `@remix-run/cloudflare-pages` +- `@remix-run/cloudflare-workers` +- `@remix-run/express` +- `@remix-run/netlify` +- `@remix-run/vercel` + +These package provides all the components, hooks, and [Web Fetch API][web-fetch-api] objects and helpers. ## Components and Hooks ### ``, ``, ``, ``, `` -These components are to be used once inside of your root route (`root.tsx`). They include everything Remix figured out or built in order for your page to render properly. +These components are to be used once inside your root route (`root.tsx`). They include everything Remix figured out or built in order for your page to render properly. ```tsx -import type { LinksFunction, MetaFunction } from "remix"; +import type { + LinksFunction, + MetaFunction, +} from "@remix-run/node"; // or cloudflare/deno import { Links, LiveReload, @@ -22,7 +42,7 @@ import { Outlet, Scripts, ScrollRestoration, -} from "remix"; +} from "@remix-run/react"; import globalStylesheetUrl from "./global-styles.css"; @@ -51,13 +71,16 @@ export default function App() { {/* Manages scroll position for client-side transitions */} + {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */} {/* Script tags go here */} + {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */} {/* Sets up automatic reload when you change code */} {/* and only does anything during development */} + {/* If you use a nonce-based content security policy for scripts, you must provide the `nonce` prop. Otherwise, omit the nonce prop as shown here. */} @@ -65,9 +88,11 @@ export default function App() { } ``` -You can pass extra props to `` like `` for hosting your static assets on a different server than your app, or `