diff --git a/.eslintignore b/.eslintignore index 7c825c06907..efb6260f60e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,11 @@ **/tests/__snapshots/ **/node_modules/ !.eslintrc.js -/packages/remix-deno/deno.d.ts +.tmp +/playground +**/__tests__/fixtures + +# deno +integration/helpers/deno-template +packages/remix-deno +templates/deno diff --git a/.eslintrc.js b/.eslintrc.js index 8f5b18f70ba..a7c8a1014fa 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,70 +1,8 @@ module.exports = { root: true, extends: [ - require.resolve("./packages/remix-eslint-config/index.js"), - require.resolve("./packages/remix-eslint-config/jest-testing-library.js"), + require.resolve("./packages/remix-eslint-config/internal.js"), "plugin:markdown/recommended", ], - overrides: [ - { - // all ```jsx & ```tsx code blocks in .md files - files: ["**/*.md/*.js", "**/*.md/*.jsx", "**/*.md/*.ts", "**/*.md/*.tsx"], - rules: { - "no-unreachable": "off", - "jsx-a11y/alt-text": "off", - "jsx-a11y/anchor-has-content": "off", - "react/jsx-no-comment-textnodes": "off", - "react/jsx-no-undef": "off", - }, - }, - { - // all ```ts & ```tsx code blocks in .md files - files: ["**/*.md/*.ts", "**/*.md/*.tsx"], - rules: { - "@typescript-eslint/no-unused-expressions": "off", - "@typescript-eslint/no-unused-vars": "off", - }, - }, - { - files: [ - "packages/create-remix/templates/cloudflare-workers/**/*.js", - "packages/remix-cloudflare-workers/**/*.ts", - ], - rules: { - "no-restricted-globals": "off", - }, - }, - { - files: ["fixtures/gists-app/jest/**/*.js"], - env: { - "jest/globals": true, - }, - }, - { - files: ["examples/**/*.js", "examples/**/*.jsx"], - rules: { - "no-unused-vars": "off", - }, - }, - { - files: ["examples/**/*.ts", "examples/**/*.tsx"], - rules: { - "@typescript-eslint/no-unused-vars": "off", - }, - }, - ], - rules: { - "@typescript-eslint/consistent-type-imports": "error", - "import/order": [ - "error", - { - "newlines-between": "always", - groups: [ - ["builtin", "external", "internal"], - ["parent", "sibling", "index"], - ], - }, - ], - "jest/no-disabled-tests": "off", - }, + plugins: ["markdown"], }; diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 95541803a1d..917f9b5260b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,7 @@ name: ๐Ÿ› Bug Report description: Something is wrong with Remix. labels: - - bug + - "bug:unverified" body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5942a64907f..23fc00082af 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -10,3 +10,12 @@ contact_links: about: We appreciate you taking the time to improve Remix with your ideas, but we use the Discussions for this instead of the issues tab ๐Ÿ™‚. + - name: ๐Ÿ’ฌ Remix Discord Channel + url: https://rmx.as/discord + about: Interact with other people using Remix ๐Ÿ“€ + - name: ๐Ÿ’ฌ New Updates (Twitter) + url: https://twitter.com/remix_run + about: Stay up to date with Remix news on twitter + - name: ๐Ÿฟ Remix YouTube Channel + url: https://rmx.as/youtube + about: Are you a tech lead or wanting to learn more about Remix in depth? Checkout the Remix YouTube Channel 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: + + 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..463b5a69844 --- /dev/null +++ b/.github/workflows/deployments.yml @@ -0,0 +1,226 @@ +name: ๐Ÿš€ Deployment Tests + +on: + repository_dispatch: + types: [release] + +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" + + # 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 + uses: bahmutov/npm-install@v1 + with: + working-directory: ./scripts/deployment-test + useLockFile: false + + - 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" + + # 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 + uses: bahmutov/npm-install@v1 + with: + working-directory: ./scripts/deployment-test + useLockFile: false + + - 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" + + # 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 + uses: bahmutov/npm-install@v1 + with: + working-directory: ./scripts/deployment-test + useLockFile: false + + - 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 }} + + 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" + + # 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 + uses: bahmutov/npm-install@v1 + with: + working-directory: ./scripts/deployment-test + useLockFile: false + + - 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" + + # 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 + uses: bahmutov/npm-install@v1 + with: + working-directory: ./scripts/deployment-test + useLockFile: false + + - 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" + + # 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 + uses: bahmutov/npm-install@v1 + with: + working-directory: ./scripts/deployment-test + useLockFile: false + + - 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 d7614290388..cfbd55a03ed 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -1,4 +1,4 @@ -name: Format +name: ๐Ÿ‘” Format on: push: @@ -12,26 +12,29 @@ 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 + # even though this is called "npm-install" it does use yarn to install + # because we have a yarn.lock and caches efficiently. + uses: bahmutov/npm-install@v1 - - name: Format + - name: ๐Ÿ‘” Format run: npm run format --if-present - - name: Commit + - 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..b2d4b0d66ac --- /dev/null +++ b/.github/workflows/nightly.yml @@ -0,0 +1,102 @@ +name: ๐ŸŒ’ Nightly Release + +on: + schedule: + - cron: "0 7 * * *" # every day at 12AM PST + +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 + 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 + # even though this is called "npm-install" it does use yarn to install + # because we have a yarn.lock and caches efficiently. + uses: bahmutov/npm-install@v1 + + - name: โคด๏ธ Update Version + id: version + run: | + git config --local user.email "hello@remix.run" + git config --local user.name "Remix Run Bot" + + SHA=$(git rev-parse HEAD) + SHORT_SHA=${SHA::7} + 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 + + - name: ๐Ÿ— Build + run: yarn build + + - name: ๐Ÿท Push Tag + run: git push origin --tags + + - 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 + + - name: ๐Ÿฑ Create GitHub release + uses: actions/create-release@v1 + with: + draft: false + prerelease: true + release_name: v${{ steps.version.outputs.NEXT_VERSION }} + tag_name: v${{ steps.version.outputs.NEXT_VERSION }} + env: + # need this token in order to have it trigger the comment and deployment test workflows + GITHUB_TOKEN: ${{ secrets.NIGHTLY_PAT }} + + comment: + needs: [nightly] + name: ๐Ÿ›ด Kick off comment and deployment test workflows + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + steps: + - uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.NIGHTLY_PAT }} + event-type: release + client-payload: '{ "ref": "refs/tags/v${{ needs.nightly.outputs.NEXT_VERSION }}", "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/release-comments.yml b/.github/workflows/release-comments.yml new file mode 100644 index 00000000000..d918d4cccd3 --- /dev/null +++ b/.github/workflows/release-comments.yml @@ -0,0 +1,32 @@ +name: ๐Ÿ“ Comment on Release + +on: + repository_dispatch: + types: [release] + +jobs: + comment: + name: Comment on Release + if: github.repository == 'remix-run/remix' + runs-on: ubuntu-latest + steps: + - 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 + # even though this is called "npm-install" it does use yarn to install + # because we have a yarn.lock and caches efficiently. + uses: bahmutov/npm-install@v1 + + - name: ๐Ÿ“ Comment on issues + run: node ./scripts/release/comment.mjs + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ github.token }} + VERSION: ${{ github.event.client_payload.ref }} diff --git a/.github/workflows/release-private.yml b/.github/workflows/release-private.yml index 70d14d75a6c..91e2e6eddc5 100644 --- a/.github/workflows/release-private.yml +++ b/.github/workflows/release-private.yml @@ -1,4 +1,4 @@ -name: release-private +name: ๐Ÿ‘Ÿ Release Private on: release: types: [published] @@ -9,37 +9,30 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 - - name: Setup node + - name: โŽ” Setup node uses: actions/setup-node@v3 with: - node-version: "${{ steps.nvmrc.outputs.version }}" + node-version-file: ".nvmrc" + cache: "yarn" - - run: echo "::set-output name=dir::$(yarn cache dir)" - id: yarn-cache + - name: ๐Ÿ“ฅ Install deps + # even though this is called "npm-install" it does use yarn to install + # because we have a yarn.lock and caches efficiently. + uses: bahmutov/npm-install@v1 - - 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 + - name: ๐Ÿ— Build run: yarn build - - name: Setup npm auth + - 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 + - name: ๐Ÿš€ Publish run: npm run publish:private diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 79083aec7d7..d79cb61885b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,242 +1,57 @@ -name: release +name: ๐Ÿ•Š Release on: release: types: [published] jobs: - build: - if: github.repository == 'remix-run/remix' + manual: + name: ๐Ÿ“ Manual 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 + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 - - name: Setup node + - 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- + node-version-file: ".nvmrc" + cache: "yarn" - - name: Install dependencies - run: yarn --frozen-lockfile + - name: ๐Ÿ“ฅ Install deps + # even though this is called "npm-install" it does use yarn to install + # because we have a yarn.lock and caches efficiently. + uses: bahmutov/npm-install@v1 - - name: Build + - 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 + - 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] + comment: + needs: [manual] + name: ๐Ÿ›ด Kick off comment and deployment test workflows 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 + - uses: peter-evans/repository-dispatch@v2 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 - env: - VERCEL_TOKEN: ${{ secrets.TEST_VERCEL_TOKEN }} - VERCEL_ORG_ID: ${{ secrets.TEST_VERCEL_USER_ID }} + token: ${{ secrets.NIGHTLY_PAT }} + event-type: release + client-payload: '{ "ref": "${{ github.ref }}", "version": "${{ github.ref_name }}" }' diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml new file mode 100644 index 00000000000..d0f5c1fae61 --- /dev/null +++ b/.github/workflows/stacks.yml @@ -0,0 +1,248 @@ +name: ๐Ÿฅž Remix Stacks Test + +on: + repository_dispatch: + types: [release] + +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 ${{ github.event.client_payload.version }} + run: | + npx -y create-remix@${{ github.event.client_payload.version }} ${{ matrix.stack.name }} --template ${{ matrix.stack.repo }} --typescript --no-install + + - name: ๐Ÿ“ฅ Download deps + uses: bahmutov/npm-install@v1 + with: + working-directory: ${{ matrix.stack.name }} + useLockFile: false + + - 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: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + + - 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: ๐Ÿ“ฅ Download deps + uses: bahmutov/npm-install@v1 + with: + 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: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + + - 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: ๐Ÿ“ฅ Download deps + uses: bahmutov/npm-install@v1 + with: + 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: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + + - 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: ๐Ÿ“ฅ Download deps + uses: bahmutov/npm-install@v1 + with: + 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: โŽ” Setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + + - 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: ๐Ÿ“ฅ Download deps + uses: bahmutov/npm-install@v1 + with: + 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@v3 + 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 e16e092b8f6..682f6dfe753 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,38 +1,174 @@ -name: test +name: ๐Ÿงช Test on: push: branches: - main - dev - - release/* + - release-* tags-ignore: - v* paths-ignore: - "docs/**" + - "scripts/**" - "**/README.md" - pull_request: {} + pull_request: + paths-ignore: + - "docs/**" + - "**/*.md" jobs: build: + name: โš™๏ธ Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: ๐Ÿ›‘ Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.9.1 - - run: echo "::set-output name=version::$(cat .nvmrc)" - id: nvmrc + - name: โฌ‡๏ธ Checkout repo + uses: actions/checkout@v3 - - name: Setup node + - name: โŽ” Setup node uses: actions/setup-node@v3 with: - node-version: "${{ steps.nvmrc.outputs.version }}" + node-version-file: ".nvmrc" cache: "yarn" - - name: Install dependencies - run: yarn --frozen-lockfile + - name: ๐Ÿ“ฅ Install deps + # even though this is called "npm-install" it does use yarn to install + # because we have a yarn.lock and caches efficiently. + uses: bahmutov/npm-install@v1 - - name: Build + - name: ๐Ÿ— Build run: yarn build - - name: Test - run: yarn test + 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 + # even though this is called "npm-install" it does use yarn to install + # because we have a yarn.lock and caches efficiently. + uses: bahmutov/npm-install@v1 + + - name: ๐Ÿ”ฌ Lint + run: yarn lint + + test: + name: "๐Ÿงช Test: (OS: ${{ matrix.os }} Node: ${{ matrix.node }})" + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + # - macos-latest + - windows-latest + node: + - 14 + - 16 + - 18 + 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 + # even though this is called "npm-install" it does use yarn to install + # because we have a yarn.lock and caches efficiently. + uses: bahmutov/npm-install@v1 + + - name: ๐Ÿงช Run Primary Tests + run: "yarn test:primary" + + integration: + name: "๐Ÿ‘€ Integration Test: (OS: ${{ matrix.os }} Node: ${{ matrix.node }})" + strategy: + fail-fast: false + matrix: + node: + - 14 + - 16 + - 18 + 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 + # even though this is called "npm-install" it does use yarn to install + # because we have a yarn.lock and caches efficiently. + uses: bahmutov/npm-install@v1 + + # 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/website.yml b/.github/workflows/website.yml index 087c550d4fa..51c5103579b 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -1,4 +1,4 @@ -name: website +name: ๐ŸŒ Website on: schedule: @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Refresh the docs + - name: ๐Ÿ”„ Refresh the docs uses: fjogeleit/http-request-action@v1.9.0 with: url: "${{ secrets.DOCS_REFRESH_URL }}?ref=${{ github.ref }}" diff --git a/.gitignore b/.gitignore index dfe1bd9f3d1..1ded76d7f95 100644 --- a/.gitignore +++ b/.gitignore @@ -9,10 +9,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..8444a962762 --- /dev/null +++ b/.vscode/deno_resolve_npm_imports.json @@ -0,0 +1,13 @@ +{ + "// 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": { + "mime": "https://esm.sh/mime@3.0.0", + "@remix-run/server-runtime": "https://esm.sh/@remix-run/server-runtime@1.4.3" + } +} 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 6164f271d7c..633359fa823 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- --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- --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 @@ -97,3 +103,113 @@ git commit -m "fix: squashed a super gnarly bug" yarn run version patch yarn run publish ``` + +## Local Development Tips and Tricks + +### Environment Variables + +This repository supports handful of environment variables to streamline the local development/testing process. + +**`REMIX_DEBUG`** + +By default, the Remix `rollup` build will strip any `console.debug` calls to avoid cluttering up the console during application usage. These `console.debug` statements can be preserved by setting `REMIX_DEBUG=true` during your local build. + +```sh +REMIX_DEBUG=true yarn watch +``` + +**`REMIX_LOCAL_DEV_OUTPUT_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. + +```sh +# Tab 1 - create an run a local remix application +npx create-remix +cd my-remix-app +npm run dev + +# Tab 2 - remix repository +REMIX_LOCAL_DEV_OUTPUT_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 ๐ŸŽ‰. + +### Transition Manager Flows + +The transition manager is a complex and heavily async bit of logic that is foundational to Remix's ability to manage data loading, submission, error handling, and interruptions. Due to the user-driven nature of interruptions we don't quite believe it can be modeled as a finite state machine, however we have modeled some of the happy path flows below for clarity. + +#### Transitions + +_Note: This does not depict error or interruption flows_ + +```mermaid +graph LR + %% transition + idle -->|link clicked| loading/normalLoad + idle -->|form method=get| submitting/loaderSubmission + idle -->|form method=post| submitting/actionSubmission + idle -->|fetcher action redirects| loading/fetchActionRedirect + + subgraph "<Link> transition" + loading/normalLoad -->|loader redirected| loading/normalRedirect + loading/normalRedirect --> loading/normalRedirect + end + loading/normalLoad -->|loaders completed| idle + loading/normalRedirect -->|loaders completed| idle + + subgraph "<Form method=get>" + submitting/loaderSubmission -->|loader redirected| loading/loaderSubmissionRedirect + loading/loaderSubmissionRedirect --> loading/loaderSubmissionRedirect + end + submitting/loaderSubmission -->|loaders completed| idle + loading/loaderSubmissionRedirect -->|loaders completed| idle + + subgraph "<Form method=post>" + submitting/actionSubmission -->|action returned| loading/actionReload + submitting/actionSubmission -->|action redirected| loading/actionRedirect + loading/actionReload -->|loader redirected| loading/actionRedirect + loading/actionRedirect --> loading/actionRedirect + end + loading/actionReload -->|loaders completed| idle + loading/actionRedirect -->|loaders completed| idle + + subgraph "Fetcher action redirect" + loading/fetchActionRedirect --> loading/fetchActionRedirect + end + loading/fetchActionRedirect -->|loaders completed| idle +``` + +#### Fetchers + +_Note: This does not depict error or interruption flows, nor the ability to re-use fetchers once they've reached `idle/done`._ + +```mermaid +graph LR + idle/init -->|"load"| loading/normalLoad + idle/init -->|"submit (get)"| submitting/loaderSubmission + idle/init -->|"submit (post)"| submitting/actionSubmission + + subgraph "Normal Fetch" + loading/normalLoad -.->|loader redirected| T1{{transition}} + end + loading/normalLoad -->|loader completed| idle/done + T1{{transition}} -.-> idle/done + + subgraph "Loader Submission" + submitting/loaderSubmission -.->|"loader redirected"| T2{{transition}} + end + submitting/loaderSubmission -->|loader completed| idle/done + T2{{transition}} -.-> idle/done + + subgraph "Action Submission" + submitting/actionSubmission -->|action completed| loading/actionReload + submitting/actionSubmission -->|action redirected| loading/actionRedirect + loading/actionRedirect -.-> T3{{transition}} + loading/actionReload -.-> |loaders redirected| T3{{transition}} + end + T3{{transition}} -.-> idle/done + loading/actionReload --> |loaders completed| idle/done + + classDef transition fill:lightgreen; + class T1,T2,T3 transition; +``` diff --git a/babel.config.js b/babel.config.js index 7609dec382e..2da99242267 100644 --- a/babel.config.js +++ b/babel.config.js @@ -14,5 +14,14 @@ module.exports = { plugins: [ "@babel/plugin-proposal-export-namespace-from", "@babel/plugin-proposal-optional-chaining", + // Strip console.debug calls unless REMIX_DEBUG=true + ...(process.env.REMIX_DEBUG === "true" + ? [] + : [ + [ + "transform-remove-console", + { exclude: ["error", "warn", "log", "info"] }, + ], + ]), ], }; diff --git a/contributors.yml b/contributors.yml index 02b8fca83b6..280d0143a7f 100644 --- a/contributors.yml +++ b/contributors.yml @@ -2,14 +2,23 @@ - aaronshaf - abereghici - abotsi +- accidentaldeveloper +- achinchen +- adicuco - ahbruns - ahmedeldessouki +- aiji42 - airjp73 - airondumael - Alarid - alex-ketch - alexuxui +- alireza-bonab +- alvinthen +- amorriscode - andrelandgraf +- andrewbrey +- AndrewIngram - anishpras - anmolm96 - anmonteiro @@ -21,14 +30,19 @@ - arganaphangquestian - AriGunawan - arvigeus +- arvindell - ascorbic - ashleyryan - ashocean +- athongsavath +- axel-habermaier - BasixKOR - BenMcH +- bmarvinb - bmontalvo - bogas04 - BogdanDevBst +- bolchowka - brophdawg11 - bruno-oliveira - bsharrow @@ -37,11 +51,16 @@ - c43721 - camiaei - CanRau +- ccssmnn - chaance - chenc041 +- chenxsan +- chiangs - christianhg - christophgockel - clarkmitchell +- cliffordfajardo +- cloudy9101 - codymjarrett - confix - coryhouse @@ -49,21 +68,29 @@ - crismali - cysp - damiensedgwick +- dan-gamble - danielweinmann - davecalnan - DavidHollins6 +- davongit - denissb - derekr - developit - dhargitai - dhmacs - dima-takoy +- DNLHC +- dogukanakkaya - dokeet - donavon - Dueen - dunglas +- dwightwatson - dwt47 +- dylanplayer - eastlondoner +- eccentric-j +- EddyVinck - edgesoft - edmundhung - efkann @@ -71,9 +98,13 @@ - emzoumpo - eps1lon - evanwinter +- exegeteio +- F3n67u - fergusmeiklejohn +- fgiuliani - fishel-feng - francisudeji +- frontsideair - fx109138 - gabimor - gautamkrishnar @@ -83,6 +114,7 @@ - Gim3l - Girish21 - gkueny +- gmaliar - gon250 - goncy - gonzoscript @@ -94,6 +126,7 @@ - hardingmatt - helderburato - HenryVogt +- hicksy - himorishige - hkan - Holben888 @@ -103,6 +136,7 @@ - hzhu - IAmLuisJ - ianduvall +- illright - imzshh - isaacrmoreno - ishan-me @@ -111,6 +145,8 @@ - JacobParis - jakewtaylor - jamiebuilds +- janhoogeveen +- Jannis-Morgenstern - jaydiablo - jca41 - jdeniau @@ -123,33 +159,48 @@ - jmasson - jo-ninja - joaosamouco +- jodygeraldo - johannesbraeunig - johnson444 - johnson444 - joms - joshball - jssisodiya +- jstafman - juhanakristian +- JulesBlm - justinnoel - juwiragiye +- jveldridge - jvnm-dev - kalch - kanermichael - karimsan +- kauffmanes +- kbariotis - KenanYusuf - kentcdodds - kevinrambaud - kgregory +- kilian +- kiliman - kimdontdoit +- klauspaiva - knowler +- konradkalemba - kubaprzetakiewicz +- kuldar - kumard3 - lachlanjc +- laughnan - lawrencecchen - leo - leon - levippaul +- LewisArdern +- lifeiscontent - lionotm +- liranm - lpsinger - lswest - lucasdibz @@ -161,20 +212,24 @@ - lukasgerm - m0nica - m5r +- machour - maferland - manosim - mantey-github - manzano78 - manzoorwanijk +- marcisbee - marcomafessolli - marshallwalker - martensonbj - marvinwu - matchai - mathieusteele +- matt-l-w - matthew-burfield - Matthew-Mallimo - MatthewAlbrecht +- matthova - mattmazzola - mattstobbs - mbarto @@ -182,6 +237,9 @@ - medayz - meetbryce - mehulmpt +- memark +- mennopruijssers +- michaeldebetaz - michaeldeboey - michaelfriedman - michaseel @@ -199,6 +257,8 @@ - na2hiro - nareshbhatia - navid-kalaei +- nexxeln +- nicholaschiang - niconiahi - nielsdb97 - ninjaPixel @@ -206,27 +266,37 @@ - nobeeakon - nordiauwu - nurul3101 +- nvh95 - nwalters512 +- octokatherine - omamazainab - oott123 - orballo +- pacexy +- pcattori +- penspinner - phishy - plastic041 - princerajroy - prvnbist - ptitFicus - pyr0gan +- RATIU5 - raulrpearson - real34 - reggie3 +- rlfarman +- roachjc - robindrost - roddds - RomanSavarin +- rossipedia - RossJHagan - RossMcMillan92 - rowinbot - rphlmr - rtabulov +- ruisaraiva19 - Runner-dev - rvlewerissa - ryanflorence @@ -237,20 +307,26 @@ - sdavids - sean-roberts - selfish +- sergiocarneiro - sergiodxa - shumuu - sidkh +- sidv1905 - silvenon +- simonepizzamiglio - simonswiss - sinhalite - sitek94 - skube - sndrem +- sobrinho - squidpunch - stephanerangaya - SufianBabri - supachaidev - tascord +- TheRealAstoo +- therealflyingcoder - thomasheyenbrock - thomasrettig - tjefferson08 @@ -268,11 +344,17 @@ - VictorPeralta - vimutti77 - visormatt +- vkrol - weavdale +- wKovacs64 - wladiston +- XiNiHa - xstevenyung - yauri-io - yesmeck - yomeshgupta +- youbicode +- youngvform - zachdtaylor - zainfathoni +- zhe 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..c2a70a01789 --- /dev/null +++ b/decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md @@ -0,0 +1,117 @@ +--- +title: 0001 - Use npm to manage NPM dependencies for Deno projects +--- + +# 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_ or 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..ae01522d31c --- /dev/null +++ b/decisions/0002-do-not-clone-request.md @@ -0,0 +1,23 @@ +--- +title: 0002 - Do not clone request +--- + +# 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/index.md b/decisions/index.md new file mode 100644 index 00000000000..6e30e1348e2 --- /dev/null +++ b/decisions/index.md @@ -0,0 +1,3 @@ +--- +title: Decisions +--- diff --git a/decisions/template.md b/decisions/template.md new file mode 100644 index 00000000000..2952b659dc3 --- /dev/null +++ b/decisions/template.md @@ -0,0 +1,15 @@ +--- +title: Title +--- + +# 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 bf044020c45..e5ac896216a 100644 --- a/docs/api/conventions.md +++ b/docs/api/conventions.md @@ -5,7 +5,7 @@ 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 when _Remix calls your code_. ## remix.config.js @@ -18,7 +18,7 @@ This file has a few build and development configuration options, but does not ac module.exports = { appDirectory: "app", assetsBuildDirectory: "public/build", - ignoredRouteFiles: [".*"], + ignoredRouteFiles: ["**/.*"], publicPath: "/build/", routes(defineRoutes) { return defineRoutes((route) => { @@ -143,7 +143,7 @@ A list of regex patterns that determined if a module is transpiled and included 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=[11-16] +```ts filename=remix.config.js lines=[10-15] /** * @type {import('@remix-run/dev').AppConfig} */ @@ -152,7 +152,7 @@ module.exports = { assetsBuildDirectory: "public/build", publicPath: "/build/", serverBuildDirectory: "build", - ignoredRouteFiles: [".*"], + ignoredRouteFiles: ["**/.*"], serverDependenciesToBundle: [ /^rehype.*/, /^remark.*/, @@ -178,7 +178,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 @@ -259,8 +259,11 @@ For example: `app/routes/blog/$postId.tsx` will match the following URLs: 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: ```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 "@remix-run/cloudflare" export const loader: LoaderFunction = async ({ params, @@ -353,7 +356,7 @@ For example, all of your marketing pages could be in `app/routes/__marketing/*` Be careful, pathless layout routes introduce the possibility of URL conflicts -#### Dot Delimeters +#### Dot Delimiters ```markdown [8] @@ -419,8 +422,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 "@remix-run/cloudflare" export const loader: LoaderFunction = async ({ params, @@ -459,10 +465,10 @@ Typically this module uses `ReactDOM.hydrate` to re-hydrate the markup that was 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(, document); +hydrate(, 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. @@ -478,12 +484,12 @@ You can also export an optional `handleDataRequest` function that will allow you 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 "@remix-run/cloudflare" +import { RemixServer } from "@remix-run/react"; export default function handleRequest( request: Request, @@ -491,7 +497,7 @@ export default function handleRequest( responseHeaders: Headers, remixContext: EntryContext ) { - const markup = ReactDOMServer.renderToString( + const markup = renderToString( ); @@ -540,20 +546,25 @@ export default function SomeRouteComponent() { Watch the ๐Ÿ“ผ Remix Single: Loading data into components -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-run/node"; // or "@remix-run/cloudflare" + export const loader = async () => { - return { ok: true }; + // The `json` function converts a serializable object into a JSON response + // All loaders must return a `Response` object. + return json({ ok: true }); }; ``` ```ts // Typescript -import type { LoaderFunction } from "remix"; +import { json } from "@remix-run/node"; // or "@remix-run/cloudflare" +import type { LoaderFunction } from "@remix-run/node"; // or "@remix-run/cloudflare" export const loader: LoaderFunction = async () => { - return { ok: true }; + return json({ ok: true }); }; ``` @@ -561,13 +572,14 @@ 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 { useLoaderData } from "remix"; +```tsx lines=[1-2,6-8,11] +import { json } from "@remix-run/node"; // or "@remix-run/cloudflare" +import { useLoaderData } from "@remix-run/react"; import { prisma } from "../db"; export const loader = async () => { - return prisma.user.findMany(); + return json(await prisma.user.findMany()); }; export default function Users() { @@ -653,19 +665,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 -export const loader = async () => { - return { 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 () => { @@ -679,10 +681,10 @@ export const loader: LoaderFunction = async () => { }; ``` -Remix provides helpers, like `json`, so you don't have to construct them yourself: +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 "@remix-run/cloudflare" export const loader: LoaderFunction = async () => { const users = await fakeDb.users.findMany(); @@ -690,10 +692,10 @@ export const loader: LoaderFunction = async () => { }; ``` -Between these two examples you can see how `json` just does a little of the work to make your loader a lot cleaner. You usually want to use the `json` helper when you're adding headers or a status code to your response: +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 "@remix-run/cloudflare" export const loader: LoaderFunction = async ({ params, @@ -722,8 +724,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 "@remix-run/cloudflare" +import type { ThrownResponse } from "@remix-run/react"; export type InvoiceNotFoundResponse = ThrownResponse< 404, @@ -740,7 +742,7 @@ export function getInvoice(id, user) { ``` ```ts filename=app/http.ts -import { redirect } from "remix"; +import { redirect } from "@remix-run/node"; // or "@remix-run/cloudflare" import { getSession } from "./session"; @@ -758,8 +760,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"; @@ -787,7 +789,7 @@ export const loader = async ({ request, params }) => { throw json(data, { status: 401 }); } - return invoice; + return json(invoice); }; export default function InvoiceRoute() { @@ -836,13 +838,14 @@ 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 { redirect, Form } from "remix"; +import { json, redirect } from "@remix-run/node"; // or "@remix-run/cloudflare" +import { Form } from "@remix-run/react"; import { fakeGetTodos, fakeCreateTodo } from "~/utils/db"; import { TodoList } from "~/components/TodoList"; export async function loader() { - return fakeGetTodos(); + return json(await fakeGetTodos()); } export async function action({ request }) { @@ -884,13 +887,18 @@ See also: - [`
`][form] - [``][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", @@ -898,7 +906,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 }) { @@ -908,16 +916,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: @@ -966,8 +974,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 "@remix-run/cloudflare" export default function handleRequest( request: Request, @@ -996,7 +1004,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 "@remix-run/cloudflare" export const meta: MetaFunction = () => { return { @@ -1007,32 +1015,43 @@ export const meta: MetaFunction = () => { }; ``` -There are a few special cases like `title` renders a `` tag, `og:style` tags will render `<meta property content>`, the rest render `<meta name={key} content={value}/>`. - -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. +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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta). 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. -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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title). + +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-hatch 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`. Examples: ```tsx -import type { MetaFunction } from "remix"; - -export const meta: MetaFunction = () => { - return { - title: "Josie's Shake Shack", // <title>Josie's Shake Shack - description: "Delicious shakes", // - "og:image": "https://josiesshakeshack.com/logo.jpg", // - }; -}; +import type { MetaFunction } from "@remix-run/node"; // or "@remix-run/cloudflare" + +export const meta: MetaFunction = () => ({ + // Special cases + charset: "utf-8", // + "og:image": "https://josiesshakeshack.com/logo.jpg", // + title: "Josie's Shake Shack", // Josie's Shake Shack + + // name => content + description: "Delicious shakes", // + viewport: "width=device-width,initial-scale=1", // + + // + refresh: { + httpEquiv: "refresh", + content: "3;url=https://www.mozilla.org", + }, // +}); ``` #### Page context in `meta` function @@ -1044,12 +1063,29 @@ export const meta: MetaFunction = () => { - `params` is an object containing route params - `parentsData` is a hashmap of all the data exported by `loader` functions of current route and all of its parents +```tsx +export const meta: MetaFunction = ({ data, params }) => { + if (!data) { + return { + title: "Missing Shake", + description: `There is no shake with the ID of ${params.shakeId}. ๐Ÿ˜ข`, + }; + } + + const { shake } = data as LoaderData; + return { + title: `${shake.name} milkshake`, + description: shake.summary, + }; +}; +``` + ### `links` 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 "@remix-run/cloudflare" export const links: LinksFunction = () => { return [ @@ -1083,7 +1119,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 "@remix-run/cloudflare" import stylesHref from "../styles/something.css"; @@ -1152,7 +1188,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(); @@ -1213,7 +1249,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 = ({ @@ -1252,12 +1288,12 @@ It's common for root loaders to return data that never changes, like environment ```js [10] export const loader = async () => { - return { + return json({ ENV: { CLOUDINARY_ACCT: process.env.CLOUDINARY_ACCT, STRIPE_PUBLIC_KEY: process.env.STRIPE_PUBLIC_KEY, }, - }; + }); }; export const unstable_shouldReload = () => false; @@ -1279,33 +1315,35 @@ 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: -```js [2,7] +```js [2,8] export async function loader({ request, params }) { const url = new URL(request.url); - return exampleDb.activity.findAll({ - where: { - projectId: params.projectId, - name: { - contains: url.searchParams.get("search"), + return json( + await exampleDb.activity.findAll({ + where: { + projectId: params.projectId, + name: { + contains: url.searchParams.get("search"), + }, }, - }, - }); + }) + ); } ``` @@ -1315,7 +1353,7 @@ In this UI, that's wasted bandwidth for the user, your server, and your database ```tsx export async function loader({ params }) { - return fakedb.findProject(params.projectId); + return json(await fakedb.findProject(params.projectId)); } ``` @@ -1323,7 +1361,7 @@ We want this loader to be called only if the project has had an update, so we ca ```tsx export function unstable_shouldReload({ submission }) { - return submission && submission.method !== "GET"; + return !!submission && submission.method !== "GET"; } ``` @@ -1338,7 +1376,7 @@ export function unstable_shouldReload({ params, submission, }) { - return ( + return !!( submission && submission.action === `/projects/${params.projectId}` ); @@ -1358,7 +1396,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 "@remix-run/cloudflare" import styles from "./styles/app.css"; import banner from "./images/banner.jpg"; @@ -1384,6 +1422,7 @@ 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 diff --git a/docs/api/remix.md b/docs/api/remix.md index a983ba04c49..87ac3e38b62 100644 --- a/docs/api/remix.md +++ b/docs/api/remix.md @@ -1,39 +1,97 @@ --- -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/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](https://developer.mozilla.org/en-US/docs/Web/API/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. -```tsx lines=[1,8-9,13] -import { Meta, Links, Scripts, Outlet } from "remix"; +```tsx +import type { + LinksFunction, + MetaFunction, +} from "@remix-run/node"; // or "@remix-run/cloudflare" +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; + +import globalStylesheetUrl from "./global-styles.css"; + +export const links: LinksFunction = () => { + return [{ rel: "stylesheet", href: globalStylesheetUrl }]; +}; + +export const meta: MetaFunction = () => ({ + charset: "utf-8", + title: "My Amazing App", + viewport: "width=device-width,initial-scale=1", +}); export default function App() { return ( - + {/* All meta exports on all routes will go here */} + + {/* All link exports on all routes will go here */} + {/* Child routes go here */} + + {/* 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. */} + ); } ``` -You can pass extra props to `` like `` for hosting your static assets on a different server than your app, or `