From e01c63ee9b403bba5e186eebc7cbc89e14895597 Mon Sep 17 00:00:00 2001 From: Rob Cameron Date: Tue, 12 Mar 2024 01:08:17 -0700 Subject: [PATCH 01/34] Adds an existential check in case no query vars are present in dbAuth invocation (#10204) --- .changesets/10204.md | 8 ++++++++ packages/auth-providers/dbAuth/api/src/DbAuthHandler.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changesets/10204.md diff --git a/.changesets/10204.md b/.changesets/10204.md new file mode 100644 index 000000000000..1f9d4fe1cb59 --- /dev/null +++ b/.changesets/10204.md @@ -0,0 +1,8 @@ +- chore(dbAuth): restore behavior of checking whether a search query is present + +Previously dbAuth would check whether or not query string variables were +present at all before invoking the proper function. During a refactor we +updated this code to assume a query would *always* be present. Which it would be +during normal browser behavior. But, we had a complaint from a user who relied +on this optional check in one of their tests. So we're restoring the optional +check here. diff --git a/packages/auth-providers/dbAuth/api/src/DbAuthHandler.ts b/packages/auth-providers/dbAuth/api/src/DbAuthHandler.ts index c276f127548c..d2f3e563ca53 100644 --- a/packages/auth-providers/dbAuth/api/src/DbAuthHandler.ts +++ b/packages/auth-providers/dbAuth/api/src/DbAuthHandler.ts @@ -1455,7 +1455,7 @@ export class DbAuthHandler< // figure out which auth method we're trying to call async _getAuthMethod() { // try getting it from the query string, /.redwood/functions/auth?method=[methodName] - let methodName = this.normalizedRequest.query.method as AuthMethodNames + let methodName = this.normalizedRequest.query?.method as AuthMethodNames if ( !DbAuthHandler.METHODS.includes(methodName) && From c8b17cb82a27f27c28d54d2a811dd2d738f607eb Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Tue, 12 Mar 2024 01:37:21 -0700 Subject: [PATCH 02/34] chore(ci): use tarsync instead of project sync for test project tests (#10205) Want to clean up the code in the action more, but this may be enough changes to do things incrementally. The regular sync strategy we use in CI (`yarn rwfw project:sync`) has never handled dependencies or `yarn install`, and since I just recently had some churn with the Vite 5 upgrade and now the React canary one, I want to see if things could "just work" a little more (while also being more correct). Disabling Yarn's hardened mode should speed up yarn install times. Yarn recommends opting out of it for most cases: > Installs operating under Hardened Mode constraints are significantly slower than usual as they need to perform many network requests that would be skipped otherwise. We don't recommend enabling it by default - if you need it in a specific CI job, toggle it on via an environment variable: See https://yarnpkg.com/blog/release/4.0. --- .../set-up-test-project/setUpTestProject.mjs | 45 +++---------------- .github/workflows/ci.yml | 1 + 2 files changed, 6 insertions(+), 40 deletions(-) diff --git a/.github/actions/set-up-test-project/setUpTestProject.mjs b/.github/actions/set-up-test-project/setUpTestProject.mjs index e8eb2da6a80b..6603ad0ce483 100644 --- a/.github/actions/set-up-test-project/setUpTestProject.mjs +++ b/.github/actions/set-up-test-project/setUpTestProject.mjs @@ -3,16 +3,13 @@ import path from 'node:path' -import cache from '@actions/cache' import core from '@actions/core' import fs from 'fs-extra' import { - createCacheKeys, createExecWithEnvInCwd, - projectCopy, - projectDeps, + execInFramework, REDWOOD_FRAMEWORK_PATH, } from '../actionsLib.mjs' @@ -35,36 +32,13 @@ console.log({ console.log() -const { - dependenciesKey, - distKey -} = await createCacheKeys({ baseKeyPrefix: 'test-project', distKeyPrefix: bundler, canary }) - /** * @returns {Promise} */ async function main() { - const distCacheKey = await cache.restoreCache([TEST_PROJECT_PATH], distKey) - - if (distCacheKey) { - console.log(`Cache restored from key: ${distKey}`) - return - } - - const dependenciesCacheKey = await cache.restoreCache([TEST_PROJECT_PATH], dependenciesKey) - - if (dependenciesCacheKey) { - console.log(`Cache restored from key: ${dependenciesKey}`) - await sharedTasks() - } else { - console.log(`Cache not found for input keys: ${distKey}, ${dependenciesKey}`) - await setUpTestProject({ - canary: true - }) - } - - await cache.saveCache([TEST_PROJECT_PATH], distKey) - console.log(`Cache saved with key: ${distKey}`) + await setUpTestProject({ + canary: true + }) } /** @@ -82,9 +56,7 @@ async function setUpTestProject({ canary }) { console.log() await fs.copy(TEST_PROJECT_FIXTURE_PATH, TEST_PROJECT_PATH) - console.log(`Adding framework dependencies to ${TEST_PROJECT_PATH}`) - await projectDeps(TEST_PROJECT_PATH) - console.log() + await execInFramework('yarn project:tarsync --verbose', { env: { RWJS_CWD: TEST_PROJECT_PATH } }) console.log(`Installing node_modules in ${TEST_PROJECT_PATH}`) await execInProject('yarn install') @@ -96,9 +68,6 @@ async function setUpTestProject({ canary }) { console.log() } - await cache.saveCache([TEST_PROJECT_PATH], dependenciesKey) - console.log(`Cache saved with key: ${dependenciesKey}`) - await sharedTasks() } @@ -108,10 +77,6 @@ const execInProject = createExecWithEnvInCwd(TEST_PROJECT_PATH) * @returns {Promise} */ async function sharedTasks() { - console.log('Copying framework packages to project') - await projectCopy(TEST_PROJECT_PATH) - console.log() - console.log({ bundler }) console.log() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28f92aeea855..16d35e1b7903 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ concurrency: env: NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + YARN_ENABLE_HARDENED_MODE: 0 jobs: detect-changes: From dcd588189e8795aca69b9e1be6ded7f1f6453910 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Tue, 12 Mar 2024 02:29:44 -0700 Subject: [PATCH 03/34] chore(ci): refactor out CLI smoke tests (#10206) Continuation of the CI improvements started in https://github.com/redwoodjs/redwood/pull/10205. Right now we run the CLI smoke tests as a part of the Playwright smoke tests. This means we're running the CLI smoke tests twice as much as we need to since the Playwright smoke tests have a matrix that depends on the bundler (vite or webpack) and it makes CI take longer as a whole since the CLI smoke tests have to wait till the Playwright smoke tests finish to start running. This PR refactors out the CLI smoke tests into their own workflow. --- .github/workflows/ci.yml | 126 ++++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16d35e1b7903..d514b24d9b3c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -339,59 +339,109 @@ jobs: REDWOOD_TEST_PROJECT_PATH: ${{ steps.set-up-test-project.outputs.test-project-path }} REDWOOD_DISABLE_TELEMETRY: 1 + smoke-tests-skip: + needs: detect-changes + if: needs.detect-changes.outputs.onlydocs == 'true' + + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + bundler: [vite, webpack] + + name: πŸ”„ Smoke tests / ${{ matrix.os }} / ${{ matrix.bundler }} / node 20 latest + runs-on: ${{ matrix.os }} + + steps: + - run: echo "Skipped" + + cli-smoke-tests: + needs: check + + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + + name: πŸ”„ CLI smoke tests / ${{ matrix.os }} / node 20 latest + runs-on: ${{ matrix.os }} + + env: + REDWOOD_CI: 1 + REDWOOD_VERBOSE_TELEMETRY: 1 + + steps: + - uses: actions/checkout@v4 + + - name: Enable Corepack + run: corepack enable + + - name: β¬’ Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Enable Corepack + run: corepack enable + + - name: 🐈 Set up yarn cache + uses: ./.github/actions/set-up-yarn-cache + + - name: 🐈 Yarn install + run: yarn install --inline-builds + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: πŸ”¨ Build + run: yarn build + + - name: 🌲 Set up test project + id: set-up-test-project + uses: ./.github/actions/set-up-test-project + env: + REDWOOD_DISABLE_TELEMETRY: 1 + YARN_ENABLE_IMMUTABLE_INSTALLS: false + - name: Run `rw info` - run: | - yarn rw info + run: yarn rw info working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run `rw lint` - run: | - yarn rw lint ./api/src --fix + run: yarn rw lint ./api/src --fix working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "rw test api" - run: | - yarn rw test api --no-watch + run: yarn rw test api --no-watch working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "rw test web" - run: | - yarn rw test web --no-watch + run: yarn rw test web --no-watch working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "rw check" - run: | - yarn rw check + run: yarn rw check working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "rw storybook" - run: | - yarn rw sb --smoke-test + run: yarn rw sb --smoke-test working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "rw exec" - run: | - yarn rw g script testScript && yarn rw exec testScript + run: yarn rw g script testScript && yarn rw exec testScript working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "prisma generate" - run: | - yarn rw prisma generate + run: yarn rw prisma generate working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "rw data-migrate" - run: | - yarn rw dataMigrate up + run: yarn rw data-migrate up working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "data-migrate install" - run: | - yarn rw data-migrate install + run: yarn rw data-migrate install working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "prisma migrate" - run: | - yarn rw prisma migrate dev --name ci-test + run: yarn rw prisma migrate dev --name ci-test working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run `rw deploy --help` @@ -403,51 +453,31 @@ jobs: working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "g page" - run: | - yarn rw g page ciTest + run: yarn rw g page ciTest working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "g sdl" - run: | - yarn rw g sdl userExample + run: yarn rw g sdl userExample working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Run "rw type-check" - run: | - yarn rw type-check + run: yarn rw type-check working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} - name: Throw Error | Run `rw g sdl ` - run: | - yarn rw g sdl DoesNotExist + run: yarn rw g sdl DoesNotExist working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} continue-on-error: true - # We've disabled Replay for now but may add it back. When we do, - # we need to add this to all the smoke tests steps' env: - # - # ``` - # env: - # RECORD_REPLAY_METADATA_TEST_RUN_TITLE: πŸ”„ Smoke tests / ${{ matrix.os }} / node 20 latest - # RECORD_REPLAY_TEST_METRICS: 1 - # ``` - # - # - name: Upload Replays - # if: always() - # uses: replayio/action-upload@v0.5.0 - # with: - # api-key: rwk_cZn4WLe8106j6tC5ygNQxDpxAwCLpFo5oLQftiRN7OP - - smoke-tests-skip: + cli-smoke-tests-skip: needs: detect-changes if: needs.detect-changes.outputs.onlydocs == 'true' strategy: matrix: os: [ubuntu-latest, windows-latest] - bundler: [vite, webpack] - name: πŸ”„ Smoke tests / ${{ matrix.os }} / ${{ matrix.bundler }} / node 20 latest + name: πŸ”„ CLI smoke tests / ${{ matrix.os }} / node 20 latest runs-on: ${{ matrix.os }} steps: From 68657bd4f71f24a0281f208b4e04ce408bf9f970 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Tue, 12 Mar 2024 03:16:31 -0700 Subject: [PATCH 04/34] chore(ci): upgrade cache action (#10208) There's a lot of deprecation warnings for some of our workflows. Working on fixing them here: image --- .github/actions/set-up-yarn-cache/action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/set-up-yarn-cache/action.yml b/.github/actions/set-up-yarn-cache/action.yml index 8bafd1508d4a..b2711fa11e66 100644 --- a/.github/actions/set-up-yarn-cache/action.yml +++ b/.github/actions/set-up-yarn-cache/action.yml @@ -17,7 +17,7 @@ runs: # If the primary key doesn't match, the cache will probably be stale or incomplete, # but still worth restoring for the yarn install step. - name: ♻️ Restore yarn cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.get-yarn-cache-directory.outputs.CACHE_DIRECTORY }} key: yarn-cache-${{ runner.os }}-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} @@ -25,13 +25,13 @@ runs: # We avoid restore-keys for these steps because it's important to just start from scratch for new PRs. - name: ♻️ Restore yarn install state - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: .yarn/install-state.gz key: yarn-install-state-${{ runner.os }}-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} - name: ♻️ Restore node_modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: '**/node_modules' key: yarn-node-modules-${{ runner.os }}-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} From a65b8d3cbfcd7f46cb6c16f6764e933ff9e41174 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Tue, 12 Mar 2024 04:20:06 -0700 Subject: [PATCH 05/34] chore(ci): improve set up yarn cache action (#10209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continuation of https://github.com/redwoodjs/redwood/pull/10205. This action lets `yarn install` steps fly by in CI under certain conditions when associated files (`package.json`, `yarn.lock`β€”basically any file that adding a dependency affects) are unchanged. - I've added `save-always: true` now that we're on `@actions/cache` v4. That means workflows that fail can still cache their dependencies. This is actually really great cause they're the ones that need the caching the most since subsequent iterations are usually code changes or other iterative fixes. - I'm also widening the key coverage on yarn's base cacheβ€”it really doesn't need to be tied to the PR, just the runner. - I've added package.json to the key for install state and node_modules since that's where yarn version is specified via Corepack. Not sure if this actually matters since major upgrades of yarn often change the lockfile format anyway, but may as well be safer --- .github/actions/set-up-yarn-cache/action.yml | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/actions/set-up-yarn-cache/action.yml b/.github/actions/set-up-yarn-cache/action.yml index b2711fa11e66..2ca7ec1e3e5d 100644 --- a/.github/actions/set-up-yarn-cache/action.yml +++ b/.github/actions/set-up-yarn-cache/action.yml @@ -1,37 +1,37 @@ +# See https://github.com/yarnpkg/berry/discussions/2621#discussioncomment-505872. + name: Set up yarn cache -description: | - Sets up caching for yarn install steps. +description: > + Sets up caching for `yarn install` steps. Caches yarn's cache directory, install state, and node_modules. + Caching the cache directory avoids yarn's fetch step and caching node_modules avoids yarn's link step. runs: using: composite steps: - # We try to cache and restore yarn's cache directory and install state to speed up the yarn install step. - # Caching yarn's cache directory avoids its fetch step. - - name: πŸ“ Get yarn cache directory + - name: πŸ“ Get yarn's cache directory id: get-yarn-cache-directory run: echo "CACHE_DIRECTORY=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT shell: bash - # If the primary key doesn't match, the cache will probably be stale or incomplete, - # but still worth restoring for the yarn install step. - - name: ♻️ Restore yarn cache + - name: ♻️ Restore yarn's cache uses: actions/cache@v4 with: path: ${{ steps.get-yarn-cache-directory.outputs.CACHE_DIRECTORY }} - key: yarn-cache-${{ runner.os }}-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} - restore-keys: yarn-cache-${{ runner.os }} + key: yarn-cache-${{ runner.os }} + save-always: true - # We avoid restore-keys for these steps because it's important to just start from scratch for new PRs. - - name: ♻️ Restore yarn install state + - name: ♻️ Restore yarn's install state uses: actions/cache@v4 with: path: .yarn/install-state.gz - key: yarn-install-state-${{ runner.os }}-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} + key: yarn-install-state-${{ runner.os }}-${{ hashFiles('package.json', 'yarn.lock', '.yarnrc.yml') }} + save-always: true - name: ♻️ Restore node_modules uses: actions/cache@v4 with: - path: '**/node_modules' - key: yarn-node-modules-${{ runner.os }}-${{ hashFiles('yarn.lock', '.yarnrc.yml') }} + path: node_modules + key: yarn-node-modules-${{ runner.os }}-${{ hashFiles('package.json', 'yarn.lock', '.yarnrc.yml') }} + save-always: true From bbb5169bd36a29922ab9ea7a16f2eafc9dfaf53d Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Tue, 12 Mar 2024 06:52:47 -0700 Subject: [PATCH 06/34] chore(ci): use tarsync for rsc test projects (#10211) Does the same https://github.com/redwoodjs/redwood/pull/10205 did but for the RSC test projects. --- .github/actions/actionsLib.mjs | 11 ++--------- .../actions/set-up-rsc-project/setUpRscProject.mjs | 12 ++---------- .../actions/set-up-test-project/setUpTestProject.mjs | 4 ---- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/.github/actions/actionsLib.mjs b/.github/actions/actionsLib.mjs index 8aaf223dcfff..8f82656af54d 100644 --- a/.github/actions/actionsLib.mjs +++ b/.github/actions/actionsLib.mjs @@ -144,15 +144,8 @@ export async function setUpRscTestProject( console.log() fs.cpSync(fixturePath, testProjectPath, { recursive: true }) - console.log(`Adding framework dependencies to ${testProjectPath}`) - await projectDeps(testProjectPath) - console.log() - - console.log(`Installing node_modules in ${testProjectPath}`) - await execInProject('yarn install') - - console.log(`Copying over framework files to ${testProjectPath}`) - await execInProject(`node ${rwfwBinPath} project:copy`, { + console.log('Syncing framework') + await execInProject(`node ${rwfwBinPath} project:tarsync --verbose`, { env: { RWFW_PATH: REDWOOD_FRAMEWORK_PATH }, }) console.log() diff --git a/.github/actions/set-up-rsc-project/setUpRscProject.mjs b/.github/actions/set-up-rsc-project/setUpRscProject.mjs index f0c717ec3954..6c2ec2f7cc17 100644 --- a/.github/actions/set-up-rsc-project/setUpRscProject.mjs +++ b/.github/actions/set-up-rsc-project/setUpRscProject.mjs @@ -69,10 +69,6 @@ async function setUpRscProject( REDWOOD_FRAMEWORK_PATH, 'packages/cli/dist/index.js' ) - const rwfwBinPath = path.join( - REDWOOD_FRAMEWORK_PATH, - 'packages/cli/dist/rwfw.js' - ) console.log(`Creating project at ${rscProjectPath}`) console.log() @@ -95,16 +91,12 @@ async function setUpRscProject( await execInProject(`node ${rwBinPath} experimental setup-rsc`) console.log() - console.log(`Copying over framework files to ${rscProjectPath}`) - await execInProject(`node ${rwfwBinPath} project:copy`, { + console.log('Syncing framework') + await execInProject('yarn rwfw project:tarsync --verbose', { env: { RWFW_PATH: REDWOOD_FRAMEWORK_PATH }, }) console.log() - console.log('Installing dependencies') - await execInProject('yarn install') - console.log() - console.log(`Building project in ${rscProjectPath}`) await execInProject(`node ${rwBinPath} build -v`) console.log() diff --git a/.github/actions/set-up-test-project/setUpTestProject.mjs b/.github/actions/set-up-test-project/setUpTestProject.mjs index 6603ad0ce483..160d6b8a457b 100644 --- a/.github/actions/set-up-test-project/setUpTestProject.mjs +++ b/.github/actions/set-up-test-project/setUpTestProject.mjs @@ -58,10 +58,6 @@ async function setUpTestProject({ canary }) { await execInFramework('yarn project:tarsync --verbose', { env: { RWJS_CWD: TEST_PROJECT_PATH } }) - console.log(`Installing node_modules in ${TEST_PROJECT_PATH}`) - await execInProject('yarn install') - console.log() - if (canary) { console.log(`Upgrading project to canary`) await execInProject('yarn rw upgrade -t canary') From 6cbb65ccb824def6a543cd53ff5d38057b470ea2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Mar 2024 08:27:17 -0700 Subject: [PATCH 07/34] fix(deps): update dependency webpack to v5.90.3 (#10207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [webpack](https://togithub.com/webpack/webpack) | [`5.90.0` -> `5.90.3`](https://renovatebot.com/diffs/npm/webpack/5.90.0/5.90.3) | [![age](https://developer.mend.io/api/mc/badges/age/npm/webpack/5.90.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/webpack/5.90.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/webpack/5.90.0/5.90.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/webpack/5.90.0/5.90.3?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes
webpack/webpack (webpack) ### [`v5.90.3`](https://togithub.com/webpack/webpack/releases/tag/v5.90.3) [Compare Source](https://togithub.com/webpack/webpack/compare/v5.90.2...v5.90.3) #### Bug Fixes - don't mangle when destructuring a reexport - types for `Stats.toJson()` and `Stats.toString()` - many internal types - \[CSS] clean up export css local vars #### Perf - simplify and optimize chunk graph creation ### [`v5.90.2`](https://togithub.com/webpack/webpack/releases/tag/v5.90.2) [Compare Source](https://togithub.com/webpack/webpack/compare/v5.90.1...v5.90.2) #### Bug Fixes - use `Math.imul` in `fnv1a32` to avoid loss of precision, directly hash UTF16 values - the `setStatus()` of the HMR module should not return an array, which may cause infinite recursion - `__webpack_exports_info__.xxx.canMangle` shouldn't always same as default - mangle export with destructuring - use new runtime to reconsider skipped connections `activeState` - make dynamic import optional in `try/catch` - improve auto publicPath detection #### Dependencies & Maintenance - improve CI setup and include Node.js@21 ### [`v5.90.1`](https://togithub.com/webpack/webpack/releases/tag/v5.90.1) [Compare Source](https://togithub.com/webpack/webpack/compare/v5.90.0...v5.90.1) #### Bug Fixes - set `unmanagedPaths` in defaults - correct `preOrderIndex` and `postOrderIndex` - add fallback for MIME mismatch error in async wasm loading - browsers versions of ECMA features #### Performance - optimize `compareStringsNumeric` - optimize `numberHash` using 32-bit FNV1a for small ranges, 64-bit for larger - reuse VM context across webpack magic comments
--- ### Configuration πŸ“… **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. β™» **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. πŸ”• **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/redwoodjs/redwood). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- packages/core/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 9f0cacea400e..4b90da4ce756 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -66,7 +66,7 @@ "style-loader": "3.3.3", "typescript": "5.3.3", "url-loader": "4.1.1", - "webpack": "5.90.0", + "webpack": "5.90.3", "webpack-bundle-analyzer": "4.9.1", "webpack-cli": "5.1.4", "webpack-dev-server": "4.15.1", diff --git a/yarn.lock b/yarn.lock index 85b1b3b71f82..07172cfdc2f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8312,7 +8312,7 @@ __metadata: style-loader: "npm:3.3.3" typescript: "npm:5.3.3" url-loader: "npm:4.1.1" - webpack: "npm:5.90.0" + webpack: "npm:5.90.3" webpack-bundle-analyzer: "npm:4.9.1" webpack-cli: "npm:5.1.4" webpack-dev-server: "npm:4.15.1" @@ -33981,9 +33981,9 @@ __metadata: languageName: node linkType: hard -"webpack@npm:5, webpack@npm:5.90.0, webpack@npm:^5": - version: 5.90.0 - resolution: "webpack@npm:5.90.0" +"webpack@npm:5, webpack@npm:5.90.3, webpack@npm:^5": + version: 5.90.3 + resolution: "webpack@npm:5.90.3" dependencies: "@types/eslint-scope": "npm:^3.7.3" "@types/estree": "npm:^1.0.5" @@ -34014,7 +34014,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 10c0/4acec1a719a9c5b890a30a9fb18519e671e55382f2c51120b76a2c1c1c521285b6510327faf79f85a4b11c7a2c5c01e1d2e7bf73e5cddbada1503f4d51a63441 + checksum: 10c0/f737aa871cadbbae89833eb85387f1bf9ee0768f039100a3c8134f2fdcc78c3230ca775c373b1aa467b272f74c6831e119f7a8a1c14dcac97327212be9c93eeb languageName: node linkType: hard From 0b185043b1987599487bf1c5198ec47994db4c5d Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Tue, 12 Mar 2024 16:31:02 +0100 Subject: [PATCH 08/34] RSC: Use throw-on-client package (#10212) --- .../web/package.json | 4 ++-- .../web/src/pages/HomePage/words.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/__fixtures__/test-project-rsc-external-packages-and-cells/web/package.json b/__fixtures__/test-project-rsc-external-packages-and-cells/web/package.json index 9a1bb651ddbd..30d029e8d1a4 100644 --- a/__fixtures__/test-project-rsc-external-packages-and-cells/web/package.json +++ b/__fixtures__/test-project-rsc-external-packages-and-cells/web/package.json @@ -12,14 +12,14 @@ }, "dependencies": { "@apollo/experimental-nextjs-app-support": "0.0.0-commit-b8a73fe", + "@jtoar/throw-on-client": "0.0.1", "@redwoodjs/forms": "7.0.0-canary.1011", "@redwoodjs/router": "7.0.0-canary.1011", "@redwoodjs/web": "7.0.0-canary.1011", "@tobbe.dev/rsc-test": "0.0.5", "client-only": "0.0.1", "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913", - "server-only": "0.0.1" + "react-dom": "0.0.0-experimental-e5205658f-20230913" }, "devDependencies": { "@redwoodjs/vite": "7.0.0-canary.1011", diff --git a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/HomePage/words.ts b/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/HomePage/words.ts index a2840f13a78e..894cb7751865 100644 --- a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/HomePage/words.ts +++ b/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/HomePage/words.ts @@ -1,4 +1,5 @@ -import 'server-only' +// Could also have used `import 'server-only' +import '@jtoar/throw-on-client' const RANDOM_WORDS = [ 'retarders', From 81d19cd5bfe5a4fe1888bcbe73b16ccffc62deb3 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Wed, 13 Mar 2024 11:29:38 +0100 Subject: [PATCH 09/34] RSC: Split transform plugin into client and server (#10215) --- .../vite-plugin-rsc-transform.test.mts | 83 -------- ...ts => vite-plugin-rsc-transform-client.ts} | 140 +------------- .../vite-plugin-rsc-transform-server.ts | 183 ++++++++++++++++++ packages/vite/src/rsc/rscBuildForServer.ts | 6 +- packages/vite/src/rsc/rscWorker.ts | 11 +- 5 files changed, 204 insertions(+), 219 deletions(-) delete mode 100644 packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform.test.mts rename packages/vite/src/plugins/{vite-plugin-rsc-transform.ts => vite-plugin-rsc-transform-client.ts} (62%) create mode 100644 packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts diff --git a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform.test.mts b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform.test.mts deleted file mode 100644 index c5b83872f09a..000000000000 --- a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-transform.test.mts +++ /dev/null @@ -1,83 +0,0 @@ -import * as path from 'node:path' - -import { vol } from 'memfs' - -import { rscTransformPlugin } from '../vite-plugin-rsc-transform.js' -import { afterAll, beforeAll, describe, it, expect, vi } from 'vitest' - -const clientEntryFiles = { - 'rsc-AboutCounter.tsx-0': - '/Users/tobbe/rw-app/web/src/components/Counter/AboutCounter.tsx', - 'rsc-Counter.tsx-1': - '/Users/tobbe/rw-app/web/src/components/Counter/Counter.tsx', - 'rsc-NewUserExample.tsx-2': - '/Users/tobbe/rw-app/web/src/components/UserExample/NewUserExample/NewUserExample.tsx', -} - -vi.mock('fs', async () => ({ default: (await import('memfs')).fs })) - -const RWJS_CWD = process.env.RWJS_CWD - -beforeAll(() => { - process.env.RWJS_CWD = '/Users/tobbe/rw-app/' - vol.fromJSON({ 'redwood.toml': '' }, process.env.RWJS_CWD) -}) - -afterAll(() => { - process.env.RWJS_CWD = RWJS_CWD -}) - -describe('rscTransformPlugin', () => { - it('should insert Symbol.for("react.client.reference")', async () => { - const plugin = rscTransformPlugin(clientEntryFiles) - - if (typeof plugin.transform !== 'function') { - return - } - - // Calling `bind` to please TS - // See https://stackoverflow.com/a/70463512/88106 - const output = await plugin.transform.bind({})( - `"use client"; -import { jsx, jsxs } from "react/jsx-runtime"; -import React from "react"; -import "client-only"; -import styles from "./Counter.module.css"; -import "./Counter.css"; -export const Counter = () => { - const [count, setCount] = React.useState(0); - return /* @__PURE__ */ jsxs("div", { style: { - border: "3px blue dashed", - margin: "1em", - padding: "1em" - }, children: [ - /* @__PURE__ */ jsxs("p", { children: [ - "Count: ", - count - ] }), - /* @__PURE__ */ jsx("button", { onClick: () => setCount((c) => c + 1), children: "Increment" }), - /* @__PURE__ */ jsx("h3", { className: styles.header, children: "This is a client component." }) - ] }); -};`, - '/Users/tobbe/rw-app/web/src/components/Counter/Counter.tsx' - ) - - expect(output).toEqual( - `const CLIENT_REFERENCE = Symbol.for('react.client.reference'); -export const Counter = Object.defineProperties(function() {throw new Error("Attempted to call Counter() from the server but Counter is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.");},{$$typeof: {value: CLIENT_REFERENCE},$$id: {value: "${( - path.sep + - path.join( - 'Users', - 'tobbe', - 'rw-app', - 'web', - 'dist', - 'rsc', - 'assets', - 'rsc-Counter.tsx-1.mjs' - ) - ).replaceAll('\\', '\\\\')}#Counter"}}); -` - ) - }) -}) diff --git a/packages/vite/src/plugins/vite-plugin-rsc-transform.ts b/packages/vite/src/plugins/vite-plugin-rsc-transform-client.ts similarity index 62% rename from packages/vite/src/plugins/vite-plugin-rsc-transform.ts rename to packages/vite/src/plugins/vite-plugin-rsc-transform-client.ts index 996713506d01..8ec9ce1fd885 100644 --- a/packages/vite/src/plugins/vite-plugin-rsc-transform.ts +++ b/packages/vite/src/plugins/vite-plugin-rsc-transform-client.ts @@ -5,15 +5,15 @@ import type { Plugin } from 'vite' import { getPaths } from '@redwoodjs/project-config' -export function rscTransformPlugin( +export function rscTransformUseClientPlugin( clientEntryFiles: Record, ): Plugin { return { - name: 'rsc-transform-plugin', + name: 'rsc-transform-use-client-plugin', transform: async function (code, id) { // Do a quick check for the exact string. If it doesn't exist, don't // bother parsing. - if (!code.includes('use client') && !code.includes('use server')) { + if (!code.includes('use client')) { return code } @@ -57,7 +57,7 @@ export function rscTransformPlugin( } } - if (!useClient && !useServer) { + if (!useClient) { return code } @@ -67,138 +67,18 @@ export function rscTransformPlugin( ) } - let transformedCode: string - - if (useClient) { - transformedCode = await transformClientModule( - code, - body, - id, - clientEntryFiles, - ) - } else { - transformedCode = transformServerModule(code, body, id) - } + const transformedCode = await transformClientModule( + code, + body, + id, + clientEntryFiles, + ) return transformedCode }, } } -function addLocalExportedNames(names: Map, node: any) { - switch (node.type) { - case 'Identifier': - names.set(node.name, node.name) - return - - case 'ObjectPattern': - for (let i = 0; i < node.properties.length; i++) { - addLocalExportedNames(names, node.properties[i]) - } - - return - - case 'ArrayPattern': - for (let i = 0; i < node.elements.length; i++) { - const element = node.elements[i] - if (element) { - addLocalExportedNames(names, element) - } - } - - return - - case 'Property': - addLocalExportedNames(names, node.value) - return - - case 'AssignmentPattern': - addLocalExportedNames(names, node.left) - return - - case 'RestElement': - addLocalExportedNames(names, node.argument) - return - - case 'ParenthesizedExpression': - addLocalExportedNames(names, node.expression) - return - } -} - -function transformServerModule(source: string, body: any, url: string): string { - // If the same local name is exported more than once, we only need one of the names. - const localNames = new Map() - const localTypes = new Map() - - for (let i = 0; i < body.length; i++) { - const node = body[i] - - switch (node.type) { - case 'ExportAllDeclaration': - // If export * is used, the other file needs to explicitly opt into "use server" too. - break - - case 'ExportDefaultDeclaration': - if (node.declaration.type === 'Identifier') { - localNames.set(node.declaration.name, 'default') - } else if (node.declaration.type === 'FunctionDeclaration') { - if (node.declaration.id) { - localNames.set(node.declaration.id.name, 'default') - localTypes.set(node.declaration.id.name, 'function') - } - } - - continue - - case 'ExportNamedDeclaration': - if (node.declaration) { - if (node.declaration.type === 'VariableDeclaration') { - const declarations = node.declaration.declarations - - for (let j = 0; j < declarations.length; j++) { - addLocalExportedNames(localNames, declarations[j].id) - } - } else { - const name = node.declaration.id.name - localNames.set(name, name) - - if (node.declaration.type === 'FunctionDeclaration') { - localTypes.set(name, 'function') - } - } - } - - if (node.specifiers) { - const specifiers = node.specifiers - - for (let j = 0; j < specifiers.length; j++) { - const specifier = specifiers[j] - localNames.set(specifier.local.name, specifier.exported.name) - } - } - - continue - } - } - - let newSrc = source + '\n\n;' - localNames.forEach(function (exported, local) { - if (localTypes.get(local) !== 'function') { - // We first check if the export is a function and if so annotate it. - newSrc += 'if (typeof ' + local + ' === "function") ' - } - - newSrc += 'Object.defineProperties(' + local + ',{' - newSrc += '$$typeof: {value: Symbol.for("react.server.reference")},' - newSrc += '$$id: {value: ' + JSON.stringify(url + '#' + exported) + '},' - newSrc += '$$bound: { value: null }' - newSrc += '});\n' - }) - - return newSrc -} - function addExportNames(names: Array, node: any) { switch (node.type) { case 'Identifier': diff --git a/packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts b/packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts new file mode 100644 index 000000000000..92e683bdb520 --- /dev/null +++ b/packages/vite/src/plugins/vite-plugin-rsc-transform-server.ts @@ -0,0 +1,183 @@ +import * as acorn from 'acorn-loose' +import type { Plugin } from 'vite' + +export function rscTransformUseServerPlugin(): Plugin { + return { + name: 'rsc-transform-use-server-plugin', + transform: async function (code, id) { + // Do a quick check for the exact string. If it doesn't exist, don't + // bother parsing. + if (!code.includes('use server')) { + return code + } + + // TODO (RSC): Bad bad hack. Don't do this. + // At least look for something that's guaranteed to be only present in + // transformed modules + // Ideally don't even try to transform twice + if (code.includes('$$id')) { + // Already transformed + return code + } + + let body + + try { + body = acorn.parse(code, { + ecmaVersion: 2024, + sourceType: 'module', + }).body + } catch (x: any) { + console.error('Error parsing %s %s', id, x.message) + return code + } + + let useClient = false + let useServer = false + + for (let i = 0; i < body.length; i++) { + const node = body[i] + + if (node.type !== 'ExpressionStatement' || !node.directive) { + break + } + + if (node.directive === 'use client') { + useClient = true + } + + if (node.directive === 'use server') { + useServer = true + } + } + + if (!useServer) { + return code + } + + if (useClient && useServer) { + throw new Error( + 'Cannot have both "use client" and "use server" directives in the same file.', + ) + } + + const transformedCode = transformServerModule(body, id, code) + + return transformedCode + }, + } +} + +function addLocalExportedNames(names: Map, node: any) { + switch (node.type) { + case 'Identifier': + names.set(node.name, node.name) + return + + case 'ObjectPattern': + for (let i = 0; i < node.properties.length; i++) { + addLocalExportedNames(names, node.properties[i]) + } + + return + + case 'ArrayPattern': + for (let i = 0; i < node.elements.length; i++) { + const element = node.elements[i] + if (element) { + addLocalExportedNames(names, element) + } + } + + return + + case 'Property': + addLocalExportedNames(names, node.value) + return + + case 'AssignmentPattern': + addLocalExportedNames(names, node.left) + return + + case 'RestElement': + addLocalExportedNames(names, node.argument) + return + + case 'ParenthesizedExpression': + addLocalExportedNames(names, node.expression) + return + } +} + +function transformServerModule(body: any, url: string, code: string): string { + // If the same local name is exported more than once, we only need one of the names. + const localNames = new Map() + const localTypes = new Map() + + for (let i = 0; i < body.length; i++) { + const node = body[i] + + switch (node.type) { + case 'ExportAllDeclaration': + // If export * is used, the other file needs to explicitly opt into "use server" too. + break + + case 'ExportDefaultDeclaration': + if (node.declaration.type === 'Identifier') { + localNames.set(node.declaration.name, 'default') + } else if (node.declaration.type === 'FunctionDeclaration') { + if (node.declaration.id) { + localNames.set(node.declaration.id.name, 'default') + localTypes.set(node.declaration.id.name, 'function') + } + } + + continue + + case 'ExportNamedDeclaration': + if (node.declaration) { + if (node.declaration.type === 'VariableDeclaration') { + const declarations = node.declaration.declarations + + for (let j = 0; j < declarations.length; j++) { + addLocalExportedNames(localNames, declarations[j].id) + } + } else { + const name = node.declaration.id.name + localNames.set(name, name) + + if (node.declaration.type === 'FunctionDeclaration') { + localTypes.set(name, 'function') + } + } + } + + if (node.specifiers) { + const specifiers = node.specifiers + + for (let j = 0; j < specifiers.length; j++) { + const specifier = specifiers[j] + localNames.set(specifier.local.name, specifier.exported.name) + } + } + + continue + } + } + + let newSrc = '"use server"\n' + code + '\n\n' + localNames.forEach(function (exported, local) { + if (localTypes.get(local) !== 'function') { + // We first check if the export is a function and if so annotate it. + newSrc += 'if (typeof ' + local + ' === "function") ' + } + + newSrc += 'Object.defineProperties(' + local + ',{' + newSrc += '$$typeof: {value: Symbol.for("react.server.reference")},' + newSrc += '$$id: {value: ' + JSON.stringify(url + '#' + exported) + '},' + newSrc += '$$bound: { value: null }' + newSrc += '});\n\n' + }) + + return newSrc +} diff --git a/packages/vite/src/rsc/rscBuildForServer.ts b/packages/vite/src/rsc/rscBuildForServer.ts index 3e6c0a633036..053af9819cb7 100644 --- a/packages/vite/src/rsc/rscBuildForServer.ts +++ b/packages/vite/src/rsc/rscBuildForServer.ts @@ -5,7 +5,8 @@ import { build as viteBuild } from 'vite' import { getPaths } from '@redwoodjs/project-config' import { onWarn } from '../lib/onWarn.js' -import { rscTransformPlugin } from '../plugins/vite-plugin-rsc-transform.js' +import { rscTransformUseClientPlugin } from '../plugins/vite-plugin-rsc-transform-client.js' +import { rscTransformUseServerPlugin } from '../plugins/vite-plugin-rsc-transform-server.js' /** * RSC build. Step 3. @@ -83,7 +84,8 @@ export async function rscBuildForServer( // /Users/tobbe/.../rw-app/web/dist/server/assets/rsc0.js // That's why it needs the `clientEntryFiles` data // (It does other things as well, but that's why it needs clientEntryFiles) - rscTransformPlugin(clientEntryFiles), + rscTransformUseClientPlugin(clientEntryFiles), + rscTransformUseServerPlugin(), ], build: { ssr: true, diff --git a/packages/vite/src/rsc/rscWorker.ts b/packages/vite/src/rsc/rscWorker.ts index 95900c2eab74..e7d94490f039 100644 --- a/packages/vite/src/rsc/rscWorker.ts +++ b/packages/vite/src/rsc/rscWorker.ts @@ -22,7 +22,8 @@ import type { defineEntries, GetEntry } from '../entries.js' import { registerFwGlobals } from '../lib/registerGlobals.js' import { StatusError } from '../lib/StatusError.js' import { rscReloadPlugin } from '../plugins/vite-plugin-rsc-reload.js' -import { rscTransformPlugin } from '../plugins/vite-plugin-rsc-transform.js' +import { rscTransformUseClientPlugin } from '../plugins/vite-plugin-rsc-transform-client.js' +import { rscTransformUseServerPlugin } from '../plugins/vite-plugin-rsc-transform-server.js' import type { RenderInput, @@ -115,8 +116,9 @@ const handleRender = async ({ id, input }: MessageReq & { type: 'render' }) => { // server. So we have to register them here again. registerFwGlobals() -// TODO: this was copied from waku; they have a todo to remove it. -// We need this to fix a WebSocket error in dev, `WebSocket server error: Port is already in use`. +// TODO (RSC): this was copied from waku; they have a todo to remove it. +// We need this to fix a WebSocket error in dev, `WebSocket server error: Port +// is already in use`. const dummyServer = new Server() // TODO (RSC): `createServer` is mostly used to create a dev server. Is it OK @@ -135,7 +137,8 @@ const vitePromise = createServer({ const message: MessageRes = { type } parentPort.postMessage(message) }), - rscTransformPlugin({}), + rscTransformUseClientPlugin({}), + rscTransformUseServerPlugin(), ], ssr: { resolve: { From 23a9fac74e19f2be79d1c1dea5246a06b7ea41cf Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Thu, 14 Mar 2024 01:47:48 -0700 Subject: [PATCH 10/34] fix(rsc): only use `web/src/entries.ts` as the `rscBuildAnalyze` entry point (#10218) Right now `rscBuildAnalyze` scans three entry points and ~1600 files for `'use client'` and `'use server'` directives. ```js { entries: "~/redwood-app/web/src/entries.ts", "entry.server": "~/redwood-app/web/src/entry.server.tsx", Document: "~/redwood-app/web/src/Document.tsx", } ``` Checked with @Tobbe and the only entry point we actually need to scan is `web/src/entries.ts`. With this change, we only scan 267 files. --- packages/vite/src/rsc/rscBuildAnalyze.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/vite/src/rsc/rscBuildAnalyze.ts b/packages/vite/src/rsc/rscBuildAnalyze.ts index b84d1c40e95f..88889c56c378 100644 --- a/packages/vite/src/rsc/rscBuildAnalyze.ts +++ b/packages/vite/src/rsc/rscBuildAnalyze.ts @@ -61,19 +61,16 @@ export async function rscBuildAnalyze() { build: { manifest: 'rsc-build-manifest.json', write: false, - ssr: true, + // TODO (RSC): In the future we want to generate the entries file + // automatically. Maybe by using `analyzeRoutes()` + // For the dev server we might need to generate these entries on the + // fly - so we will need something like a plugin or virtual module + // to generate these entries, rather than write to actual file. + // And so, we might as well use on-the-fly generation for regular + // builds too + ssr: rwPaths.web.entries, rollupOptions: { onwarn: onWarn, - input: { - // TODO (RSC): In the future we want to generate the entries file - // automatically. Maybe by using `analyzeRoutes()` - // For the dev server we might need to generate these entries on the - // fly - so we will need something like a plugin or virtual module - // to generate these entries, rather than write to actual file. - // And so, we might as well use on-the-fly generation for regular - // builds too - entries: rwPaths.web.entries, - }, }, }, }) From 2abd2d9134c526ce0c1a79b4d4a8f896a379d798 Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:02:06 +0100 Subject: [PATCH 11/34] RSC: Inline dependencies during rsc build (#10217) See the comment in the code as: Externalize every file apart from node built-ins. We want vite/rollup to inline dependencies in the server bundle. This gets round runtime importing of "server-only". We have to do all imports because we can't rely on "server-only" being the name of the package. This is also actually more efficient because less files. Although, at build time it's likely way less efficient because we have to do so many files. --------- Co-authored-by: Tobbe Lundberg --- packages/vite/src/rsc/rscBuildForServer.ts | 49 +++++++--------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/packages/vite/src/rsc/rscBuildForServer.ts b/packages/vite/src/rsc/rscBuildForServer.ts index 053af9819cb7..e47e0db9f935 100644 --- a/packages/vite/src/rsc/rscBuildForServer.ts +++ b/packages/vite/src/rsc/rscBuildForServer.ts @@ -1,4 +1,4 @@ -import path from 'node:path' +// import path from 'node:path' import { build as viteBuild } from 'vite' @@ -39,42 +39,21 @@ export async function rscBuildForServer( const rscServerBuildOutput = await viteBuild({ envFile: false, ssr: { - // Externalize everything except packages with files that have - // 'use client' in them (which are the files in `clientEntryFiles`) + // Externalize every file apart from node built-ins. We want vite/rollup + // to inline dependencies in the server bundle. This gets round runtime + // importing of "server-only". We have to do all imports because we can't + // rely on "server-only" being the name of the package. This is also + // actually more efficient because less files. Although, at build time + // it's likely way less efficient because we have to do so many files. // Files included in `noExternal` are files we want Vite to analyze - // The values in the array here are compared to npm package names, like - // 'react', 'core-js', @anthropic-ai/sdk', @redwoodjs/vite', etc - // The map function below will return '..' for local files. That's not - // very pretty, but it works. It just won't match anything. - noExternal: Object.values(clientEntryFiles).map((fullPath) => { - // On Windows `fullPath` will be something like - // D:/a/redwood/test-project-rsc-external-packages/node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js - const relativePath = path.relative( - path.join(rwPaths.base, 'node_modules'), - fullPath, - ) - // On Windows `relativePath` will be something like - // @tobbe.dev\rsc-test\dist\rsc-test.es.js - // So `splitPath` will in this case become - // ['@tobbe.dev', 'rsc-test', 'dist', 'rsc-test.es.js'] - const splitPath = relativePath.split(path.sep) - - // Packages without scope. Full package name looks like: package_name - let packageName = splitPath[0] - - // Handle scoped packages. Full package name looks like: - // @org_name/package_name - if (splitPath[0].startsWith('@')) { - // join @org_name with package_name - packageName = path.join(splitPath[0], splitPath[1]) - } - - console.log('noExternal fullPath', fullPath, 'packageName', packageName) - - return packageName - }), + noExternal: /^(?!node:)/, + // Can't inline prisma client + external: ['@prisma/client'], resolve: { - externalConditions: ['react-server'], + // These conditions are used in the plugin pipeline, and only affect non-externalized + // dependencies during the SSR build. Which because of `noExternal: /^(?!node:)/` means + // all dependencies apart from node built-ins. + conditions: ['react-server'], }, }, plugins: [ From 25b24c8c75806048950dc835a92ed5b73db16391 Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:56:15 +0100 Subject: [PATCH 12/34] fix(rsc): Preinit client component css for server components in production (#10210) **Problem** Currently RSC during `yarn rw serve` would not load the CSS of nested client components. In development, things appear to work fine without additional manipulation due to the vite dev server. **Approach** During build, we insert [preinit](https://react.dev/reference/react-dom/preinit) calls into server components that cause the browser to load all CSS that is needed for any client component that the server component will cause to be loaded. **Changes** 1. Update the RSC analyse plugin to construct a map between the component id and the component import ids. 2. Add an additional plugin to the RSC server build step. This plugin analyses the client build manifest to gather all the known CSS assets. It then inserts `preinit` calls to load CSS assets into server components that would require them because they load client components that have associated CSS assets. **Notes** 1. The tests added here are really only ensuring that we do not make a change to the behaviour that we currently have. There's a bit of a limit as to what we can do when we simply pass known values to a test rather than run these in a more E2E style fashion in a fully fledged RSC test project - with known src, dist, and browser rendering. --- packages/vite/package.json | 3 + packages/vite/src/buildRscClientAndServer.ts | 4 +- ...e-plugin-rsc-css-preinit-fixture-values.ts | 723 ++++++++++++++++++ .../vite-plugin-rsc-css-preinit.test.mts | 576 ++++++++++++++ .../src/plugins/vite-plugin-rsc-analyze.ts | 13 + .../plugins/vite-plugin-rsc-css-preinit.ts | 240 ++++++ packages/vite/src/rsc/rscBuildAnalyze.ts | 11 +- packages/vite/src/rsc/rscBuildForServer.ts | 3 + yarn.lock | 3 + 9 files changed, 1574 insertions(+), 2 deletions(-) create mode 100644 packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit-fixture-values.ts create mode 100644 packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit.test.mts create mode 100644 packages/vite/src/plugins/vite-plugin-rsc-css-preinit.ts diff --git a/packages/vite/package.json b/packages/vite/package.json index c60996b38c39..0416f7271790 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -64,6 +64,9 @@ "test:watch": "vitest watch src" }, "dependencies": { + "@babel/generator": "7.23.6", + "@babel/parser": "^7.22.16", + "@babel/traverse": "^7.22.20", "@redwoodjs/babel-config": "workspace:*", "@redwoodjs/internal": "workspace:*", "@redwoodjs/project-config": "workspace:*", diff --git a/packages/vite/src/buildRscClientAndServer.ts b/packages/vite/src/buildRscClientAndServer.ts index e179a482fb80..6c3d96fe2aab 100644 --- a/packages/vite/src/buildRscClientAndServer.ts +++ b/packages/vite/src/buildRscClientAndServer.ts @@ -7,7 +7,8 @@ import { rscBuildRwEnvVars } from './rsc/rscBuildRwEnvVars.js' export const buildRscClientAndServer = async () => { // Analyze all files and generate a list of RSCs and RSFs - const { clientEntryFiles, serverEntryFiles } = await rscBuildAnalyze() + const { clientEntryFiles, serverEntryFiles, componentImportMap } = + await rscBuildAnalyze() // Generate the client bundle const clientBuildOutput = await rscBuildClient(clientEntryFiles) @@ -17,6 +18,7 @@ export const buildRscClientAndServer = async () => { clientEntryFiles, serverEntryFiles, {}, + componentImportMap, ) // Copy CSS assets from server to client diff --git a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit-fixture-values.ts b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit-fixture-values.ts new file mode 100644 index 000000000000..ccee9e4b70db --- /dev/null +++ b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit-fixture-values.ts @@ -0,0 +1,723 @@ +export const clientEntryFiles = { + 'rsc-EmptyUsersCell.tsx-0': + '/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx', + 'rsc-AboutCounter.tsx-1': + '/Users/mojombo/rw-app/web/src/components/Counter/AboutCounter.tsx', + 'rsc-rsc-test.es.js-2': + '/Users/mojombo/rw-app/node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js', + 'rsc-UpdateRandomButton.tsx-3': + '/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/UpdateRandomButton.tsx', + 'rsc-Counter.tsx-4': + '/Users/mojombo/rw-app/web/src/components/Counter/Counter.tsx', + 'rsc-NewEmptyUser.tsx-5': + '/Users/mojombo/rw-app/web/src/components/EmptyUser/NewEmptyUser/NewEmptyUser.tsx', + 'rsc-NewUserExample.tsx-6': + '/Users/mojombo/rw-app/web/src/components/UserExample/NewUserExample/NewUserExample.tsx', + 'rsc-UserExamplesCell.tsx-7': + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExamplesCell/UserExamplesCell.tsx', + 'rsc-CellErrorBoundary.js-8': + '/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/components/cell/CellErrorBoundary.js', + 'rsc-DeepSubCounter.tsx-9': + '/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.tsx', + 'rsc-SubCounter.tsx-10': + '/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.tsx', + 'rsc-link.js-11': + '/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/link.js', + 'rsc-navLink.js-12': + '/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/navLink.js', + 'rsc-UserExamples.tsx-13': + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExamples/UserExamples.tsx', + 'rsc-UserExample.tsx-14': + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExample/UserExample.tsx', + 'rsc-ApolloNextAppProvider.js-15': + '/Users/mojombo/rw-app/node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/ApolloNextAppProvider.js', + 'rsc-hooks.js-16': + '/Users/mojombo/rw-app/node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/hooks.js', + 'rsc-useTransportValue.js-17': + '/Users/mojombo/rw-app/node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/useTransportValue.js', + 'rsc-index.js-18': + '/Users/mojombo/rw-app/node_modules/react-hot-toast/dist/index.js', +} + +export const componentImportMap = new Map([ + [ + '/Users/mojombo/rw-app/web/src/entry.server.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/web/src/App.tsx', + '/Users/mojombo/rw-app/web/src/Document.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/entries.ts', + ['/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/entries.js'], + ], + [ + '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUsersPage/EmptyUsersPage.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/pages/UserExample/UserExamplesPage/UserExamplesPage.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExamplesCell/UserExamplesCell.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/pages/UserExample/NewUserExamplePage/NewUserExamplePage.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/web/src/components/UserExample/NewUserExample/NewUserExample.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/pages/UserExample/UserExamplePage/UserExamplePage.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExampleServerCell/UserExampleServerCell.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/web/src/components/EmptyUser/NewEmptyUser/NewEmptyUser.tsx', + ], + ], + ['/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage.css', []], + ['/Users/mojombo/rw-app/web/src/index.css', []], + ['/Users/mojombo/rw-app/web/src/scaffold.css', []], + ['/Users/mojombo/rw-app/web/src/pages/MultiCellPage/MultiCellPage.css', []], + [ + '/Users/mojombo/rw-app/web/src/pages/FatalErrorPage/FatalErrorPage.tsx', + ['react/jsx-runtime'], + ], + [ + '/Users/mojombo/rw-app/web/src/components/Counter/AboutCounter.tsx', + [ + 'react/jsx-runtime', + 'react', + '/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.tsx', + '/Users/mojombo/rw-app/web/src/components/Counter/Counter.module.css', + '/Users/mojombo/rw-app/web/src/components/Counter/Counter.css', + ], + ], + ['/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.css', []], + [ + '/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/UpdateRandomButton.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/actions.ts', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/pages/HomePage/actions.ts', + ['/Users/mojombo/rw-app/web/src/pages/HomePage/words.ts'], + ], + [ + '/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/actions.ts', + [], + ], + [ + '/Users/mojombo/rw-app/web/src/pages/NotFoundPage/NotFoundPage.tsx', + ['react/jsx-runtime'], + ], + [ + '/Users/mojombo/rw-app/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx', + ['react/jsx-runtime'], + ], + [ + '/Users/mojombo/rw-app/web/src/components/Counter/Counter.tsx', + [ + 'react/jsx-runtime', + 'react', + '/Users/mojombo/rw-app/node_modules/client-only/index.js', + '/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.tsx', + '/Users/mojombo/rw-app/web/src/components/Counter/Counter.module.css', + '/Users/mojombo/rw-app/web/src/components/Counter/Counter.css', + ], + ], + ['/Users/mojombo/rw-app/web/src/components/Counter/Counter.css', []], + [ + '/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/RandomNumberServerCell.css', + [], + ], + ['/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.module.css', []], + ['/Users/mojombo/rw-app/web/src/components/Counter/Counter.module.css', []], + [ + '/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.tsx', + [ + 'react/jsx-runtime', + 'react', + '/Users/mojombo/rw-app/node_modules/client-only/index.js', + '/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.module.css', + '/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.css', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.tsx', + [ + 'react/jsx-runtime', + 'react', + '/Users/mojombo/rw-app/node_modules/client-only/index.js', + '/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.tsx', + '/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.module.css', + '/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.css', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/layouts/NavigationLayout/NavigationLayout.css', + [], + ], + [ + '/Users/mojombo/rw-app/web/src/pages/HomePage/words.ts', + ['/Users/mojombo/rw-app/node_modules/server-only/index.js'], + ], + [ + '/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.module.css', + [], + ], + [ + '/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.css', + [], + ], + [ + '/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.module.css', + [], + ], + ['/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.css', []], + [ + '/Users/mojombo/rw-app/web/src/lib/formatters.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/humanize-string/index.js', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/RandomNumberServerCell.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/components/cell/createServerCell.js', + '/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/actions.ts', + '/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/RandomNumberServerCell.css', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExampleServerCell/UserExampleServerCell.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/components/cell/createServerCell.js', + '/Users/mojombo/rw-app/api/src/lib/db.ts', + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExample/UserExample.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUserForm/EmptyUserForm.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/forms/dist/index.js', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExampleForm/UserExampleForm.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/forms/dist/index.js', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/assets.js', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/rwRscGlobal.js', + '/Users/mojombo/rw-app/web/src/components/Counter/AboutCounter.tsx', + '/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage.css', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/pages/MultiCellPage/MultiCellPage.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/assets.js', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/rwRscGlobal.js', + '/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/actions.ts', + '/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/RandomNumberServerCell.tsx', + '/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/UpdateRandomButton.tsx', + '/Users/mojombo/rw-app/web/src/pages/MultiCellPage/MultiCellPage.css', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/assets.js', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/rwRscGlobal.js', + '/Users/mojombo/rw-app/web/src/components/Counter/Counter.tsx', + '/Users/mojombo/rw-app/web/src/pages/HomePage/actions.ts', + '/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.module.css', + '/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.css', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/Document.tsx', + [ + 'react/jsx-runtime', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/App.tsx', + [ + 'react/jsx-runtime', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/apollo/suspense.js', + '/Users/mojombo/rw-app/web/src/pages/FatalErrorPage/FatalErrorPage.tsx', + '/Users/mojombo/rw-app/web/src/Routes.tsx', + '/Users/mojombo/rw-app/web/src/index.css', + '/Users/mojombo/rw-app/web/src/scaffold.css', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/Routes.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/client.js', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import', + '/Users/mojombo/rw-app/web/src/layouts/NavigationLayout/NavigationLayout.tsx', + '/Users/mojombo/rw-app/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx', + '/Users/mojombo/rw-app/web/src/pages/NotFoundPage/NotFoundPage.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx', + [ + 'react/jsx-runtime', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import', + '/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import', + '/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsers/EmptyUsers.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/EmptyUser/NewEmptyUser/NewEmptyUser.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js', + '/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUserForm/EmptyUserForm.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/UserExample/NewUserExample/NewUserExample.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js', + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExampleForm/UserExampleForm.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExamplesCell/UserExamplesCell.tsx', + [ + 'react/jsx-runtime', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import', + '/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import', + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExamples/UserExamples.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/layouts/NavigationLayout/NavigationLayout.tsx', + [ + 'react/jsx-runtime', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import', + '/Users/mojombo/rw-app/web/src/layouts/NavigationLayout/NavigationLayout.css', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsers/EmptyUsers.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js', + '/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx', + '/Users/mojombo/rw-app/web/src/lib/formatters.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExamples/UserExamples.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/components/GraphQLHooksProvider.js?commonjs-es-import', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js', + '/Users/mojombo/rw-app/web/src/lib/formatters.tsx', + ], + ], + [ + '/Users/mojombo/rw-app/web/src/components/UserExample/UserExample/UserExample.tsx', + [ + 'react/jsx-runtime', + '/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import', + '\u0000/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import', + '/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js', + '/Users/mojombo/rw-app/web/src/lib/formatters.tsx', + ], + ], +]) + +export const clientBuildManifest = { + '../../node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/ApolloNextAppProvider.js?commonjs-entry': + { + file: 'assets/rsc-ApolloNextAppProvider.js-15-BjjNQa7m.mjs', + src: '../../node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/ApolloNextAppProvider.js?commonjs-entry', + isEntry: true, + imports: ['_ApolloNextAppProvider-5bPKKKc8.mjs'], + }, + '../../node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/hooks.js?commonjs-entry': + { + file: 'assets/rsc-hooks.js-16-B-wbhCo5.mjs', + src: '../../node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/hooks.js?commonjs-entry', + isEntry: true, + imports: [ + '_index-g0M7Bzdc.mjs', + '../../node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/useTransportValue.js?commonjs-entry', + '_RehydrationContext-Dl2W9Kr7.mjs', + ], + }, + '../../node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/useTransportValue.js?commonjs-entry': + { + file: 'assets/rsc-useTransportValue.js-17-DPe60dHv.mjs', + src: '../../node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/useTransportValue.js?commonjs-entry', + isEntry: true, + imports: ['_index-XIvlupAM.mjs', '_RehydrationContext-Dl2W9Kr7.mjs'], + }, + '../../node_modules/@redwoodjs/router/dist/link.js?commonjs-entry': { + file: 'assets/rsc-link.js-11-LeguEpQ6.mjs', + src: '../../node_modules/@redwoodjs/router/dist/link.js?commonjs-entry', + isEntry: true, + imports: ['_link-AMDPp6FV.mjs'], + }, + '../../node_modules/@redwoodjs/router/dist/navLink.js?commonjs-entry': { + file: 'assets/rsc-navLink.js-12-D-x4cO0F.mjs', + src: '../../node_modules/@redwoodjs/router/dist/navLink.js?commonjs-entry', + isEntry: true, + imports: ['_navLink-DO_92T9r.mjs'], + }, + '../../node_modules/@redwoodjs/web/dist/components/cell/CellErrorBoundary.js?commonjs-entry': + { + file: 'assets/rsc-CellErrorBoundary.js-8-CwwdOVwg.mjs', + src: '../../node_modules/@redwoodjs/web/dist/components/cell/CellErrorBoundary.js?commonjs-entry', + isEntry: true, + imports: ['_CellErrorBoundary-DMFDzi5M.mjs'], + }, + '../../node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js': { + file: 'assets/rsc-rsc-test.es.js-2-DlPy-QDf.mjs', + src: '../../node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js', + isEntry: true, + imports: ['_jsx-runtime-CumG5p_V.mjs', '_index-XIvlupAM.mjs'], + }, + '../../node_modules/react-hot-toast/dist/index.js?commonjs-entry': { + file: 'assets/rsc-index.js-18-DAWxXOEp.mjs', + src: '../../node_modules/react-hot-toast/dist/index.js?commonjs-entry', + isEntry: true, + imports: ['_index-tlgoshdH.mjs'], + }, + '_ApolloNextAppProvider-5bPKKKc8.mjs': { + file: 'assets/ApolloNextAppProvider-5bPKKKc8.mjs', + imports: [ + '_index-XIvlupAM.mjs', + '_index-g0M7Bzdc.mjs', + '_RehydrationContext-Dl2W9Kr7.mjs', + ], + }, + '_CellErrorBoundary-DMFDzi5M.mjs': { + file: 'assets/CellErrorBoundary-DMFDzi5M.mjs', + imports: ['_index-XIvlupAM.mjs', '_interopRequireDefault-eg4KyS4X.mjs'], + }, + '_Counter-!~{00n}~.mjs': { + file: 'assets/Counter-BZpJq_HD.css', + src: '_Counter-!~{00n}~.mjs', + }, + '_Counter-Bq0ieMbL.mjs': { + file: 'assets/Counter-Bq0ieMbL.mjs', + css: ['assets/Counter-BZpJq_HD.css'], + }, + '_RehydrationContext-Dl2W9Kr7.mjs': { + file: 'assets/RehydrationContext-Dl2W9Kr7.mjs', + imports: [ + '_index-XIvlupAM.mjs', + '_index-g0M7Bzdc.mjs', + '_index-CCoFRA3G.mjs', + ], + }, + '_formatters-CUUSZ_T1.mjs': { + file: 'assets/formatters-CUUSZ_T1.mjs', + imports: ['_jsx-runtime-CumG5p_V.mjs', '_index-Bweuhc7G.mjs'], + }, + '_index--S8VRXEP.mjs': { + file: 'assets/index--S8VRXEP.mjs', + imports: ['_index-g0M7Bzdc.mjs'], + }, + '_index-Bd_2BODu.mjs': { + file: 'assets/index-Bd_2BODu.mjs', + imports: [ + '_interopRequireDefault-eg4KyS4X.mjs', + '_starts-with-4Ylsn4Ru.mjs', + '_index-XIvlupAM.mjs', + '_navLink-DO_92T9r.mjs', + '_jsx-runtime-Bx74Uukx.mjs', + ], + }, + '_index-Bweuhc7G.mjs': { + file: 'assets/index-Bweuhc7G.mjs', + }, + '_index-CCoFRA3G.mjs': { + file: 'assets/index-CCoFRA3G.mjs', + imports: [ + '_interopRequireDefault-eg4KyS4X.mjs', + '_index-XIvlupAM.mjs', + '_jsx-runtime-Bx74Uukx.mjs', + ], + }, + '_index-CaDi1HgM.mjs': { + file: 'assets/index-CaDi1HgM.mjs', + imports: [ + '_interopRequireDefault-eg4KyS4X.mjs', + '_starts-with-4Ylsn4Ru.mjs', + '_index-tlgoshdH.mjs', + ], + }, + '_index-CdhfsYOK.mjs': { + file: 'assets/index-CdhfsYOK.mjs', + imports: [ + '_interopRequireDefault-eg4KyS4X.mjs', + '_starts-with-4Ylsn4Ru.mjs', + '_link-AMDPp6FV.mjs', + '_navLink-DO_92T9r.mjs', + '_index-XIvlupAM.mjs', + '_jsx-runtime-Bx74Uukx.mjs', + '_values-COtCHOJX.mjs', + ], + }, + '_index-XIvlupAM.mjs': { + file: 'assets/index-XIvlupAM.mjs', + }, + '_index-g0M7Bzdc.mjs': { + file: 'assets/index-g0M7Bzdc.mjs', + imports: [ + '_interopRequireDefault-eg4KyS4X.mjs', + '_starts-with-4Ylsn4Ru.mjs', + '_values-COtCHOJX.mjs', + '_index-XIvlupAM.mjs', + '_jsx-runtime-Bx74Uukx.mjs', + '_index-CCoFRA3G.mjs', + '_CellErrorBoundary-DMFDzi5M.mjs', + ], + }, + '_index-tlgoshdH.mjs': { + file: 'assets/index-tlgoshdH.mjs', + imports: ['_index-XIvlupAM.mjs'], + }, + '_interopRequireDefault-eg4KyS4X.mjs': { + file: 'assets/interopRequireDefault-eg4KyS4X.mjs', + imports: ['_index-XIvlupAM.mjs'], + }, + '_jsx-runtime-Bx74Uukx.mjs': { + file: 'assets/jsx-runtime-Bx74Uukx.mjs', + imports: ['_index-XIvlupAM.mjs'], + }, + '_jsx-runtime-CumG5p_V.mjs': { + file: 'assets/jsx-runtime-CumG5p_V.mjs', + imports: ['_jsx-runtime-Bx74Uukx.mjs'], + }, + '_link-AMDPp6FV.mjs': { + file: 'assets/link-AMDPp6FV.mjs', + imports: [ + '_index-XIvlupAM.mjs', + '_interopRequireDefault-eg4KyS4X.mjs', + '_values-COtCHOJX.mjs', + '_jsx-runtime-Bx74Uukx.mjs', + ], + }, + '_navLink-DO_92T9r.mjs': { + file: 'assets/navLink-DO_92T9r.mjs', + imports: [ + '_index-XIvlupAM.mjs', + '_interopRequireDefault-eg4KyS4X.mjs', + '_starts-with-4Ylsn4Ru.mjs', + '_link-AMDPp6FV.mjs', + '_values-COtCHOJX.mjs', + '_jsx-runtime-Bx74Uukx.mjs', + ], + }, + '_starts-with-4Ylsn4Ru.mjs': { + file: 'assets/starts-with-4Ylsn4Ru.mjs', + imports: [ + '_interopRequireDefault-eg4KyS4X.mjs', + '_values-COtCHOJX.mjs', + '_index-XIvlupAM.mjs', + '_jsx-runtime-Bx74Uukx.mjs', + ], + }, + '_values-COtCHOJX.mjs': { + file: 'assets/values-COtCHOJX.mjs', + imports: ['_interopRequireDefault-eg4KyS4X.mjs'], + }, + 'components/Counter/AboutCounter.tsx': { + file: 'assets/rsc-AboutCounter.tsx-1-D7GRRRfU.mjs', + src: 'components/Counter/AboutCounter.tsx', + isEntry: true, + imports: [ + '_jsx-runtime-CumG5p_V.mjs', + '_index-XIvlupAM.mjs', + 'components/DeepSubCounter/DeepSubCounter.tsx', + '_Counter-Bq0ieMbL.mjs', + ], + }, + 'components/Counter/Counter.tsx': { + file: 'assets/rsc-Counter.tsx-4-BozfkEJL.mjs', + src: 'components/Counter/Counter.tsx', + isEntry: true, + imports: [ + '_jsx-runtime-CumG5p_V.mjs', + '_index-XIvlupAM.mjs', + 'components/SubCounter/SubCounter.tsx', + '_Counter-Bq0ieMbL.mjs', + ], + }, + 'components/DeepSubCounter/DeepSubCounter.tsx': { + file: 'assets/rsc-DeepSubCounter.tsx-9-jD7pNfn4.mjs', + src: 'components/DeepSubCounter/DeepSubCounter.tsx', + isEntry: true, + imports: ['_jsx-runtime-CumG5p_V.mjs', '_index-XIvlupAM.mjs'], + css: ['assets/rsc-DeepSubCounter-DqMovEyK.css'], + }, + 'components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx': { + file: 'assets/rsc-EmptyUsersCell.tsx-0-B-L_MnYe.mjs', + src: 'components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx', + isEntry: true, + imports: [ + '_jsx-runtime-CumG5p_V.mjs', + '_index--S8VRXEP.mjs', + '_index-CCoFRA3G.mjs', + '_index-CdhfsYOK.mjs', + '_index-CaDi1HgM.mjs', + '_formatters-CUUSZ_T1.mjs', + ], + }, + 'components/EmptyUser/NewEmptyUser/NewEmptyUser.tsx': { + file: 'assets/rsc-NewEmptyUser.tsx-5-BLZ1UKnU.mjs', + src: 'components/EmptyUser/NewEmptyUser/NewEmptyUser.tsx', + isEntry: true, + imports: [ + '_jsx-runtime-CumG5p_V.mjs', + '_index-CCoFRA3G.mjs', + '_index-CdhfsYOK.mjs', + '_index--S8VRXEP.mjs', + '_index-CaDi1HgM.mjs', + '_index-Bd_2BODu.mjs', + ], + }, + 'components/RandomNumberServerCell/UpdateRandomButton.tsx': { + file: 'assets/rsc-UpdateRandomButton.tsx-3-C9DK-uNf.mjs', + src: 'components/RandomNumberServerCell/UpdateRandomButton.tsx', + isEntry: true, + imports: ['_jsx-runtime-CumG5p_V.mjs'], + }, + 'components/SubCounter/SubCounter.tsx': { + file: 'assets/rsc-SubCounter.tsx-10-B8AM92Qq.mjs', + src: 'components/SubCounter/SubCounter.tsx', + isEntry: true, + imports: [ + '_jsx-runtime-CumG5p_V.mjs', + '_index-XIvlupAM.mjs', + 'components/DeepSubCounter/DeepSubCounter.tsx', + ], + css: ['assets/rsc-SubCounter-Bc4odF6o.css'], + }, + 'components/UserExample/NewUserExample/NewUserExample.tsx': { + file: 'assets/rsc-NewUserExample.tsx-6-D27m-VY-.mjs', + src: 'components/UserExample/NewUserExample/NewUserExample.tsx', + isEntry: true, + imports: [ + '_jsx-runtime-CumG5p_V.mjs', + '_index-CCoFRA3G.mjs', + '_index-CdhfsYOK.mjs', + '_index--S8VRXEP.mjs', + '_index-CaDi1HgM.mjs', + '_index-Bd_2BODu.mjs', + ], + }, + 'components/UserExample/UserExample/UserExample.tsx': { + file: 'assets/rsc-UserExample.tsx-14-CiDZqyWK.mjs', + src: 'components/UserExample/UserExample/UserExample.tsx', + isEntry: true, + imports: [ + '_jsx-runtime-CumG5p_V.mjs', + '_index-CCoFRA3G.mjs', + '_index-CdhfsYOK.mjs', + '_index--S8VRXEP.mjs', + '_index-CaDi1HgM.mjs', + '_index-Bweuhc7G.mjs', + ], + }, + 'components/UserExample/UserExamples/UserExamples.tsx': { + file: 'assets/rsc-UserExamples.tsx-13-B55kMTY0.mjs', + src: 'components/UserExample/UserExamples/UserExamples.tsx', + isEntry: true, + imports: [ + '_jsx-runtime-CumG5p_V.mjs', + '_index-CCoFRA3G.mjs', + '_index-CdhfsYOK.mjs', + '_index-CaDi1HgM.mjs', + '_formatters-CUUSZ_T1.mjs', + ], + }, + 'components/UserExample/UserExamplesCell/UserExamplesCell.tsx': { + file: 'assets/rsc-UserExamplesCell.tsx-7-DlCGhOAY.mjs', + src: 'components/UserExample/UserExamplesCell/UserExamplesCell.tsx', + isEntry: true, + imports: [ + '_jsx-runtime-CumG5p_V.mjs', + '_index--S8VRXEP.mjs', + '_index-CCoFRA3G.mjs', + '_index-CdhfsYOK.mjs', + 'components/UserExample/UserExamples/UserExamples.tsx', + ], + }, + 'entry.client.tsx': { + file: 'assets/rwjs-client-entry-B1o165l4.mjs', + src: 'entry.client.tsx', + isEntry: true, + imports: [ + '_jsx-runtime-CumG5p_V.mjs', + '_index-g0M7Bzdc.mjs', + '_index--S8VRXEP.mjs', + '_interopRequireDefault-eg4KyS4X.mjs', + '_starts-with-4Ylsn4Ru.mjs', + '_values-COtCHOJX.mjs', + '_index-XIvlupAM.mjs', + '_ApolloNextAppProvider-5bPKKKc8.mjs', + '_RehydrationContext-Dl2W9Kr7.mjs', + '../../node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/hooks.js?commonjs-entry', + '_index-CCoFRA3G.mjs', + '_jsx-runtime-Bx74Uukx.mjs', + '_index-CdhfsYOK.mjs', + ], + css: ['assets/rwjs-client-entry-79N3uomO.css'], + }, +} diff --git a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit.test.mts b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit.test.mts new file mode 100644 index 000000000000..8a8ce90a679d --- /dev/null +++ b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit.test.mts @@ -0,0 +1,576 @@ +import path from 'node:path' +import { vol } from 'memfs' + +import { generateCssMapping, rscCssPreinitPlugin, generateServerComponentClientComponentMapping, splitClientAndServerComponents } from '../vite-plugin-rsc-css-preinit' +import { afterAll, beforeAll, describe, it, expect, vi } from 'vitest' + +import { + clientBuildManifest, + clientEntryFiles, + componentImportMap, +} from './vite-plugin-rsc-css-preinit-fixture-values' + +vi.mock('fs', async () => ({ default: (await import('memfs')).fs })) + +const RWJS_CWD = process.env.RWJS_CWD + +let consoleLogSpy +beforeAll(() => { + process.env.RWJS_CWD = '/Users/mojombo/rw-app/' + vol.fromJSON({ + 'redwood.toml': '', + [path.join('web', 'dist', 'client', 'client-build-manifest.json')]: JSON.stringify(clientBuildManifest), + }, process.env.RWJS_CWD) + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) +}) + +afterAll(() => { + process.env.RWJS_CWD = RWJS_CWD + consoleLogSpy.mockRestore() +}) + +describe.skip('rscCssPreinitPlugin', () => { + it('should insert preinits for all nested client components', async () => { + const plugin = rscCssPreinitPlugin(clientEntryFiles, componentImportMap) + + if (typeof plugin.transform !== 'function') { + return + } + + // Calling `bind` to please TS + // See https://stackoverflow.com/a/70463512/88106 + const output = await plugin.transform.bind({})( + `import { jsx, jsxs } from "react/jsx-runtime"; + import { RscForm } from "@tobbe.dev/rsc-test"; + import { Assets } from "@redwoodjs/vite/assets"; + import { ProdRwRscServerGlobal } from "@redwoodjs/vite/rwRscGlobal"; + import { Counter } from "../../components/Counter/Counter"; + import { onSend } from "./actions"; + import styles from "./HomePage.module.css"; + import "./HomePage.css"; + globalThis.rwRscGlobal = new ProdRwRscServerGlobal(); + const HomePage = ({ + name = "Anonymous" + }) => { + return /* @__PURE__ */ jsxs("div", { className: "home-page", children: [ + /* @__PURE__ */ jsx(Assets, {}), + /* @__PURE__ */ jsxs("div", { style: { + border: "3px red dashed", + margin: "1em", + padding: "1em" + }, children: [ + /* @__PURE__ */ jsxs("h1", { className: styles.title, children: [ + "Hello ", + name, + "!!" + ] }), + /* @__PURE__ */ jsx(RscForm, { onSend }), + /* @__PURE__ */ jsx(Counter, {}) + ] }) + ] }); + }; + export default HomePage;`, + path.join(process.env.RWJS_CWD!, 'web', 'src', 'pages', 'HomePage', 'HomePage.tsx') + ) + + // You will see that this snapshot contains: + // - an import for the 'preinit' function from 'react-dom' + // - three 'preinit' calls within the HomePage function: + // - one for the Counter component which is a direct child of the HomePage + // - one for the SubCounter component which is a child of the Counter component + // - one for the DeepSubCounter component which is a child of the SubCounter component + expect(output).toMatchInlineSnapshot(` + "import { preinit } from "react-dom"; + import { jsx, jsxs } from "react/jsx-runtime"; + import { RscForm } from "@tobbe.dev/rsc-test"; + import { Assets } from "@redwoodjs/vite/assets"; + import { ProdRwRscServerGlobal } from "@redwoodjs/vite/rwRscGlobal"; + import { Counter } from "../../components/Counter/Counter"; + import { onSend } from "./actions"; + import styles from "./HomePage.module.css"; + import "./HomePage.css"; + globalThis.rwRscGlobal = new ProdRwRscServerGlobal(); + const HomePage = ({ + name = "Anonymous" + }) => { + preinit("assets/Counter-BZpJq_HD.css", { + as: "style" + }); + preinit("assets/rsc-DeepSubCounter-DqMovEyK.css", { + as: "style" + }); + preinit("assets/rsc-SubCounter-Bc4odF6o.css", { + as: "style" + }); + return /* @__PURE__ */jsxs("div", { + className: "home-page", + children: [/* @__PURE__ */jsx(Assets, {}), /* @__PURE__ */jsxs("div", { + style: { + border: "3px red dashed", + margin: "1em", + padding: "1em" + }, + children: [/* @__PURE__ */jsxs("h1", { + className: styles.title, + children: ["Hello ", name, "!!"] + }), /* @__PURE__ */jsx(RscForm, { + onSend + }), /* @__PURE__ */jsx(Counter, {})] + })] + }); + }; + export default HomePage;" + `) + + // We print a log to help with debugging + expect(consoleLogSpy).toHaveBeenCalledWith( + "css-preinit:", + "pages/HomePage/HomePage.tsx", + "x3", + "(assets/rsc-SubCounter-Bc4odF6o.css, assets/rsc-DeepSubCounter-DqMovEyK.css, assets/Counter-BZpJq_HD.css)", + ) + }) + + it('correctly generates css mapping', () => { + const mapping = generateCssMapping(clientBuildManifest) + expect(mapping).toMatchInlineSnapshot(` + Map { + "../../node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/ApolloNextAppProvider.js?commonjs-entry" => [], + "../../node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/hooks.js?commonjs-entry" => [], + "../../node_modules/@apollo/experimental-nextjs-app-support/dist/ssr/useTransportValue.js?commonjs-entry" => [], + "../../node_modules/@redwoodjs/router/dist/link.js?commonjs-entry" => [], + "../../node_modules/@redwoodjs/router/dist/navLink.js?commonjs-entry" => [], + "../../node_modules/@redwoodjs/web/dist/components/cell/CellErrorBoundary.js?commonjs-entry" => [], + "../../node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js" => [], + "../../node_modules/react-hot-toast/dist/index.js?commonjs-entry" => [], + "_ApolloNextAppProvider-5bPKKKc8.mjs" => [], + "_CellErrorBoundary-DMFDzi5M.mjs" => [], + "_Counter-!~{00n}~.mjs" => [], + "_Counter-Bq0ieMbL.mjs" => [ + "assets/Counter-BZpJq_HD.css", + ], + "_RehydrationContext-Dl2W9Kr7.mjs" => [], + "_formatters-CUUSZ_T1.mjs" => [], + "_index--S8VRXEP.mjs" => [], + "_index-Bd_2BODu.mjs" => [], + "_index-Bweuhc7G.mjs" => [], + "_index-CCoFRA3G.mjs" => [], + "_index-CaDi1HgM.mjs" => [], + "_index-CdhfsYOK.mjs" => [], + "_index-XIvlupAM.mjs" => [], + "_index-g0M7Bzdc.mjs" => [], + "_index-tlgoshdH.mjs" => [], + "_interopRequireDefault-eg4KyS4X.mjs" => [], + "_jsx-runtime-Bx74Uukx.mjs" => [], + "_jsx-runtime-CumG5p_V.mjs" => [], + "_link-AMDPp6FV.mjs" => [], + "_navLink-DO_92T9r.mjs" => [], + "_starts-with-4Ylsn4Ru.mjs" => [], + "_values-COtCHOJX.mjs" => [], + "components/Counter/AboutCounter.tsx" => [ + "assets/rsc-DeepSubCounter-DqMovEyK.css", + "assets/Counter-BZpJq_HD.css", + ], + "components/Counter/Counter.tsx" => [ + "assets/rsc-SubCounter-Bc4odF6o.css", + "assets/rsc-DeepSubCounter-DqMovEyK.css", + "assets/Counter-BZpJq_HD.css", + ], + "components/DeepSubCounter/DeepSubCounter.tsx" => [ + "assets/rsc-DeepSubCounter-DqMovEyK.css", + ], + "components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx" => [], + "components/EmptyUser/NewEmptyUser/NewEmptyUser.tsx" => [], + "components/RandomNumberServerCell/UpdateRandomButton.tsx" => [], + "components/SubCounter/SubCounter.tsx" => [ + "assets/rsc-SubCounter-Bc4odF6o.css", + "assets/rsc-DeepSubCounter-DqMovEyK.css", + ], + "components/UserExample/NewUserExample/NewUserExample.tsx" => [], + "components/UserExample/UserExample/UserExample.tsx" => [], + "components/UserExample/UserExamples/UserExamples.tsx" => [], + "components/UserExample/UserExamplesCell/UserExamplesCell.tsx" => [], + "entry.client.tsx" => [ + "assets/rwjs-client-entry-79N3uomO.css", + ], + } + `) + }) + + it('correctly splits client and server components', () => { + const { serverComponentImports, clientComponentImports } = + splitClientAndServerComponents(clientEntryFiles, componentImportMap) + + expect(serverComponentImports).toMatchInlineSnapshot(` + Map { + "/Users/mojombo/rw-app/web/src/entry.server.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/web/src/App.tsx", + "/Users/mojombo/rw-app/web/src/Document.tsx", + ], + "/Users/mojombo/rw-app/web/src/entries.ts" => [ + "/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/entries.js", + ], + "/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUsersPage/EmptyUsersPage.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx", + ], + "/Users/mojombo/rw-app/web/src/pages/UserExample/UserExamplesPage/UserExamplesPage.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExamplesCell/UserExamplesCell.tsx", + ], + "/Users/mojombo/rw-app/web/src/pages/UserExample/NewUserExamplePage/NewUserExamplePage.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/web/src/components/UserExample/NewUserExample/NewUserExample.tsx", + ], + "/Users/mojombo/rw-app/web/src/pages/UserExample/UserExamplePage/UserExamplePage.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExampleServerCell/UserExampleServerCell.tsx", + ], + "/Users/mojombo/rw-app/web/src/pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/web/src/components/EmptyUser/NewEmptyUser/NewEmptyUser.tsx", + ], + "/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage.css" => [], + "/Users/mojombo/rw-app/web/src/index.css" => [], + "/Users/mojombo/rw-app/web/src/scaffold.css" => [], + "/Users/mojombo/rw-app/web/src/pages/MultiCellPage/MultiCellPage.css" => [], + "/Users/mojombo/rw-app/web/src/pages/FatalErrorPage/FatalErrorPage.tsx" => [ + "react/jsx-runtime", + ], + "/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.css" => [], + "/Users/mojombo/rw-app/web/src/pages/HomePage/actions.ts" => [ + "/Users/mojombo/rw-app/web/src/pages/HomePage/words.ts", + ], + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/actions.ts" => [], + "/Users/mojombo/rw-app/web/src/pages/NotFoundPage/NotFoundPage.tsx" => [ + "react/jsx-runtime", + ], + "/Users/mojombo/rw-app/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx" => [ + "react/jsx-runtime", + ], + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.css" => [], + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/RandomNumberServerCell.css" => [], + "/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.module.css" => [], + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.module.css" => [], + "/Users/mojombo/rw-app/web/src/layouts/NavigationLayout/NavigationLayout.css" => [], + "/Users/mojombo/rw-app/web/src/pages/HomePage/words.ts" => [ + "/Users/mojombo/rw-app/node_modules/server-only/index.js", + ], + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.module.css" => [], + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.css" => [], + "/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.module.css" => [], + "/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.css" => [], + "/Users/mojombo/rw-app/web/src/lib/formatters.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/humanize-string/index.js", + ], + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/RandomNumberServerCell.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/components/cell/createServerCell.js", + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/actions.ts", + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/RandomNumberServerCell.css", + ], + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExampleServerCell/UserExampleServerCell.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/components/cell/createServerCell.js", + "/Users/mojombo/rw-app/api/src/lib/db.ts", + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExample/UserExample.tsx", + ], + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUserForm/EmptyUserForm.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/forms/dist/index.js", + ], + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExampleForm/UserExampleForm.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/forms/dist/index.js", + ], + "/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/assets.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/rwRscGlobal.js", + "/Users/mojombo/rw-app/web/src/components/Counter/AboutCounter.tsx", + "/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage.css", + ], + "/Users/mojombo/rw-app/web/src/pages/MultiCellPage/MultiCellPage.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/assets.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/rwRscGlobal.js", + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/actions.ts", + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/RandomNumberServerCell.tsx", + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/UpdateRandomButton.tsx", + "/Users/mojombo/rw-app/web/src/pages/MultiCellPage/MultiCellPage.css", + ], + "/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/assets.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/fully-react/rwRscGlobal.js", + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.tsx", + "/Users/mojombo/rw-app/web/src/pages/HomePage/actions.ts", + "/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.module.css", + "/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.css", + ], + "/Users/mojombo/rw-app/web/src/Document.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + ], + "/Users/mojombo/rw-app/web/src/App.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/apollo/suspense.js", + "/Users/mojombo/rw-app/web/src/pages/FatalErrorPage/FatalErrorPage.tsx", + "/Users/mojombo/rw-app/web/src/Routes.tsx", + "/Users/mojombo/rw-app/web/src/index.css", + "/Users/mojombo/rw-app/web/src/scaffold.css", + ], + "/Users/mojombo/rw-app/web/src/Routes.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/vite/dist/client.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/web/src/layouts/NavigationLayout/NavigationLayout.tsx", + "/Users/mojombo/rw-app/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx", + "/Users/mojombo/rw-app/web/src/pages/NotFoundPage/NotFoundPage.tsx", + ], + "/Users/mojombo/rw-app/web/src/layouts/NavigationLayout/NavigationLayout.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/web/src/layouts/NavigationLayout/NavigationLayout.css", + ], + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsers/EmptyUsers.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js", + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx", + "/Users/mojombo/rw-app/web/src/lib/formatters.tsx", + ], + } + `) + expect(clientComponentImports).toMatchInlineSnapshot(` + Map { + "/Users/mojombo/rw-app/web/src/components/Counter/AboutCounter.tsx" => [ + "react/jsx-runtime", + "react", + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.tsx", + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.module.css", + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.css", + ], + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/UpdateRandomButton.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/actions.ts", + ], + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.tsx" => [ + "react/jsx-runtime", + "react", + "/Users/mojombo/rw-app/node_modules/client-only/index.js", + "/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.tsx", + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.module.css", + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.css", + ], + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.tsx" => [ + "react/jsx-runtime", + "react", + "/Users/mojombo/rw-app/node_modules/client-only/index.js", + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.module.css", + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.css", + ], + "/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.tsx" => [ + "react/jsx-runtime", + "react", + "/Users/mojombo/rw-app/node_modules/client-only/index.js", + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.tsx", + "/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.module.css", + "/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.css", + ], + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsers/EmptyUsers.tsx", + ], + "/Users/mojombo/rw-app/web/src/components/EmptyUser/NewEmptyUser/NewEmptyUser.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js", + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUserForm/EmptyUserForm.tsx", + ], + "/Users/mojombo/rw-app/web/src/components/UserExample/NewUserExample/NewUserExample.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js", + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExampleForm/UserExampleForm.tsx", + ], + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExamplesCell/UserExamplesCell.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExamples/UserExamples.tsx", + ], + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExamples/UserExamples.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/components/GraphQLHooksProvider.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js", + "/Users/mojombo/rw-app/web/src/lib/formatters.tsx", + ], + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExample/UserExample.tsx" => [ + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js", + "/Users/mojombo/rw-app/web/src/lib/formatters.tsx", + ], + } + `) + }) + + it('correctly generates server to client component mapping', () => { + const serverComponentImports = new Map() + const clientComponentImports = new Map() + const clientComponentIds = Object.values(clientEntryFiles) + for (const [key, value] of componentImportMap.entries()) { + if (clientComponentIds.includes(key)) { + clientComponentImports.set(key, value) + } else { + serverComponentImports.set(key, value) + } + } + + const serverComponentClientImportIds = + generateServerComponentClientComponentMapping( + serverComponentImports, + clientComponentImports, + ) + + expect(serverComponentClientImportIds).toMatchInlineSnapshot(` + Map { + "/Users/mojombo/rw-app/web/src/entry.server.tsx" => [], + "/Users/mojombo/rw-app/web/src/entries.ts" => [], + "/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUsersPage/EmptyUsersPage.tsx" => [ + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx", + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsers/EmptyUsers.tsx", + ], + "/Users/mojombo/rw-app/web/src/pages/UserExample/UserExamplesPage/UserExamplesPage.tsx" => [ + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExamplesCell/UserExamplesCell.tsx", + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExamples/UserExamples.tsx", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/components/GraphQLHooksProvider.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js", + "/Users/mojombo/rw-app/web/src/lib/formatters.tsx", + ], + "/Users/mojombo/rw-app/web/src/pages/UserExample/NewUserExamplePage/NewUserExamplePage.tsx" => [ + "/Users/mojombo/rw-app/web/src/components/UserExample/NewUserExample/NewUserExample.tsx", + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js", + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExampleForm/UserExampleForm.tsx", + ], + "/Users/mojombo/rw-app/web/src/pages/UserExample/UserExamplePage/UserExamplePage.tsx" => [], + "/Users/mojombo/rw-app/web/src/pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage.tsx" => [ + "/Users/mojombo/rw-app/web/src/components/EmptyUser/NewEmptyUser/NewEmptyUser.tsx", + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js", + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUserForm/EmptyUserForm.tsx", + ], + "/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage.css" => [], + "/Users/mojombo/rw-app/web/src/index.css" => [], + "/Users/mojombo/rw-app/web/src/scaffold.css" => [], + "/Users/mojombo/rw-app/web/src/pages/MultiCellPage/MultiCellPage.css" => [], + "/Users/mojombo/rw-app/web/src/pages/FatalErrorPage/FatalErrorPage.tsx" => [], + "/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.css" => [], + "/Users/mojombo/rw-app/web/src/pages/HomePage/actions.ts" => [], + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/actions.ts" => [], + "/Users/mojombo/rw-app/web/src/pages/NotFoundPage/NotFoundPage.tsx" => [], + "/Users/mojombo/rw-app/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx" => [], + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.css" => [], + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/RandomNumberServerCell.css" => [], + "/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.module.css" => [], + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.module.css" => [], + "/Users/mojombo/rw-app/web/src/layouts/NavigationLayout/NavigationLayout.css" => [], + "/Users/mojombo/rw-app/web/src/pages/HomePage/words.ts" => [], + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.module.css" => [], + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.css" => [], + "/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.module.css" => [], + "/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.css" => [], + "/Users/mojombo/rw-app/web/src/lib/formatters.tsx" => [], + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/RandomNumberServerCell.tsx" => [], + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExampleServerCell/UserExampleServerCell.tsx" => [ + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExample/UserExample.tsx", + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/toast/index.js", + "/Users/mojombo/rw-app/web/src/lib/formatters.tsx", + ], + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUserForm/EmptyUserForm.tsx" => [], + "/Users/mojombo/rw-app/web/src/components/UserExample/UserExampleForm/UserExampleForm.tsx" => [], + "/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage.tsx" => [ + "/Users/mojombo/rw-app/web/src/components/Counter/AboutCounter.tsx", + "react/jsx-runtime", + "react", + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.tsx", + "/Users/mojombo/rw-app/node_modules/client-only/index.js", + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.module.css", + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.css", + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.module.css", + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.css", + ], + "/Users/mojombo/rw-app/web/src/pages/MultiCellPage/MultiCellPage.tsx" => [ + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/UpdateRandomButton.tsx", + "react/jsx-runtime", + "/Users/mojombo/rw-app/web/src/components/RandomNumberServerCell/actions.ts", + ], + "/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.tsx" => [ + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.tsx", + "react/jsx-runtime", + "react", + "/Users/mojombo/rw-app/node_modules/client-only/index.js", + "/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.tsx", + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.tsx", + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.module.css", + "/Users/mojombo/rw-app/web/src/components/DeepSubCounter/DeepSubCounter.css", + "/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.module.css", + "/Users/mojombo/rw-app/web/src/components/SubCounter/SubCounter.css", + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.module.css", + "/Users/mojombo/rw-app/web/src/components/Counter/Counter.css", + ], + "/Users/mojombo/rw-app/web/src/Document.tsx" => [], + "/Users/mojombo/rw-app/web/src/App.tsx" => [], + "/Users/mojombo/rw-app/web/src/Routes.tsx" => [], + "/Users/mojombo/rw-app/web/src/layouts/NavigationLayout/NavigationLayout.tsx" => [], + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsers/EmptyUsers.tsx" => [ + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsersCell/EmptyUsersCell.tsx", + "react/jsx-runtime", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/web/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/node_modules/graphql-tag/lib/index.js", + "/Users/mojombo/rw-app/node_modules/@redwoodjs/router/dist/index.js?commonjs-es-import", + "/Users/mojombo/rw-app/web/src/components/EmptyUser/EmptyUsers/EmptyUsers.tsx", + ], + } + `) + }) +}) + diff --git a/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts b/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts index 5727e5d02f5b..582a4b069ea0 100644 --- a/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts +++ b/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts @@ -3,10 +3,16 @@ import path from 'node:path' import * as swc from '@swc/core' import type { Plugin } from 'vite' +import { getPaths } from '@redwoodjs/project-config' + export function rscAnalyzePlugin( clientEntryCallback: (id: string) => void, serverEntryCallback: (id: string) => void, + componentImportsCallback: (id: string, importId: readonly string[]) => void, ): Plugin { + const clientEntryIdSet = new Set() + const webSrcPath = getPaths().web.src + return { name: 'rsc-analyze-plugin', transform(code, id) { @@ -25,6 +31,7 @@ export function rscAnalyzePlugin( ) { if (item.expression.value === 'use client') { clientEntryCallback(id) + clientEntryIdSet.add(id) } else if (item.expression.value === 'use server') { serverEntryCallback(id) } @@ -34,5 +41,11 @@ export function rscAnalyzePlugin( return code }, + moduleParsed(moduleInfo) { + // TODO: Maybe this is not needed? + if (moduleInfo.id.startsWith(webSrcPath)) { + componentImportsCallback(moduleInfo.id, moduleInfo.importedIds) + } + }, } } diff --git a/packages/vite/src/plugins/vite-plugin-rsc-css-preinit.ts b/packages/vite/src/plugins/vite-plugin-rsc-css-preinit.ts new file mode 100644 index 000000000000..d3d25e2ea9bf --- /dev/null +++ b/packages/vite/src/plugins/vite-plugin-rsc-css-preinit.ts @@ -0,0 +1,240 @@ +import fs from 'fs' +import path from 'path' + +import generate from '@babel/generator' +import { parse as babelParse } from '@babel/parser' +import type { NodePath } from '@babel/traverse' +import traverse from '@babel/traverse' +import * as t from '@babel/types' +import type { Plugin } from 'vite' + +import { getPaths } from '@redwoodjs/project-config' + +export function generateCssMapping(clientBuildManifest: any) { + const clientBuildManifestCss = new Map() + const lookupCssAssets = (id: string): string[] => { + const assets: string[] = [] + const asset = clientBuildManifest[id] + if (!asset) { + return assets + } + if (asset.css) { + assets.push(...asset.css) + } + if (asset.imports) { + for (const importId of asset.imports) { + assets.push(...lookupCssAssets(importId)) + } + } + return assets + } + for (const key of Object.keys(clientBuildManifest)) { + clientBuildManifestCss.set(key, lookupCssAssets(key)) + } + return clientBuildManifestCss +} + +export function splitClientAndServerComponents( + clientEntryFiles: Record, + componentImportMap: Map, +) { + const serverComponentImports = new Map() + const clientComponentImports = new Map() + const clientComponentIds = Object.values(clientEntryFiles) + for (const [key, value] of componentImportMap.entries()) { + if (clientComponentIds.includes(key)) { + clientComponentImports.set(key, value) + } else { + serverComponentImports.set(key, value) + } + } + return { serverComponentImports, clientComponentImports } +} + +export function generateServerComponentClientComponentMapping( + serverComponentImports: Map, + clientComponentImports: Map, +) { + const serverComponentClientImportIds = new Map() + const gatherClientImports = ( + id: string, + clientImports: Set, + ): void => { + const imports = clientComponentImports.get(id) ?? [] + for (const importId of imports) { + if (!clientImports.has(importId)) { + clientImports.add(importId) + gatherClientImports(importId, clientImports) + } + } + } + for (const serverComponentId of serverComponentImports.keys()) { + const clientImports = new Set() + const topLevelClientImports = + serverComponentImports.get(serverComponentId) ?? [] + for (const importId of topLevelClientImports) { + if (clientComponentImports.has(importId)) { + clientImports.add(importId) + } + gatherClientImports(importId, clientImports) + } + serverComponentClientImportIds.set( + serverComponentId, + Array.from(clientImports), + ) + } + return serverComponentClientImportIds +} + +export function rscCssPreinitPlugin( + clientEntryFiles: Record, + componentImportMap: Map, +): Plugin { + const webSrc = getPaths().web.src + + // This plugin is build only and we expect the client build manifest to be + // available at this point. We use it to find the correct css assets names + const clientBuildManifest = JSON.parse( + fs.readFileSync( + path.join(getPaths().web.distClient, 'client-build-manifest.json'), + 'utf-8', + ), + ) + + // We generate a mapping of all the css assets that a client build manifest + // entry contains (looking deep into the tree of entries) + const clientBuildManifestCss = generateCssMapping(clientBuildManifest) + + // We filter to have individual maps for server components and client + // components + const { serverComponentImports, clientComponentImports } = + splitClientAndServerComponents(clientEntryFiles, componentImportMap) + + // We generate a mapping of server components to all the client components + // that they import (directly or indirectly) + const serverComponentClientImportIds = + generateServerComponentClientComponentMapping( + serverComponentImports, + clientComponentImports, + ) + + return { + name: 'rsc-css-preinit', + apply: 'build', + transform: async function (code, id) { + // We only care about code in the project itself + if (!id.startsWith(webSrc)) { + return null + } + + // We only care about server components + if (!serverComponentImports.has(id)) { + return null + } + + // Get the client components this server component imports (directly or + // indirectly) + const clientImportIds = serverComponentClientImportIds.get(id) ?? [] + if (clientImportIds.length === 0) { + return null + } + + // Extract all the CSS asset names from all the client components that + // this server component imports + const assetNames = new Set() + for (const clientImportId of clientImportIds) { + const shortName = path.basename(clientImportId) + const longName = clientImportId.substring(webSrc.length + 1) + const entries = + clientBuildManifestCss.get(shortName) ?? + clientBuildManifestCss.get(longName) ?? + [] + for (const entry of entries) { + assetNames.add(entry) + } + } + + if (assetNames.size === 0) { + return null + } + + // Analyse the AST to get all the components that we have to insert preinit + // calls into + const ext = path.extname(id) + + const plugins = [] + if (ext === '.jsx') { + plugins.push('jsx') + } + const ast = babelParse(code, { + sourceType: 'unambiguous', + // @ts-expect-error TODO fix me + plugins, + }) + + // Gather a list of the names of exported components + const namedExportNames: string[] = [] + traverse(ast, { + ExportDefaultDeclaration(path: NodePath) { + const declaration = path.node.declaration + if (t.isIdentifier(declaration)) { + namedExportNames.push(declaration.name) + } + }, + }) + + // Insert: import { preinit } from 'react-dom' + ast.program.body.unshift( + t.importDeclaration( + [t.importSpecifier(t.identifier('preinit'), t.identifier('preinit'))], + t.stringLiteral('react-dom'), + ), + ) + + // TODO: Confirm this is a react component by looking for `jsxs` in the AST + // For each named export, insert a preinit call for each asset that it will + // eventually need for all it's child client components + traverse(ast, { + VariableDeclaration(path: NodePath) { + const declaration = path.node.declarations[0] + if ( + t.isVariableDeclarator(declaration) && + t.isIdentifier(declaration.id) && + namedExportNames.includes(declaration.id.name) + ) { + if (t.isArrowFunctionExpression(declaration.init)) { + const body = declaration.init.body + if (t.isBlockStatement(body)) { + for (const assetName of assetNames) { + body.body.unshift( + t.expressionStatement( + t.callExpression(t.identifier('preinit'), [ + t.stringLiteral(assetName), + t.objectExpression([ + t.objectProperty( + t.identifier('as'), + t.stringLiteral('style'), + ), + ]), + ]), + ), + ) + } + } + } + } + }, + }) + + // Just for debugging/verbose logging + console.log( + 'css-preinit:', + id.substring(webSrc.length + 1), + 'x' + assetNames.size, + '(' + Array.from(assetNames).join(', ') + ')', + ) + + return generate(ast).code + }, + } +} diff --git a/packages/vite/src/rsc/rscBuildAnalyze.ts b/packages/vite/src/rsc/rscBuildAnalyze.ts index 88889c56c378..fbd9a0e383a3 100644 --- a/packages/vite/src/rsc/rscBuildAnalyze.ts +++ b/packages/vite/src/rsc/rscBuildAnalyze.ts @@ -20,6 +20,7 @@ export async function rscBuildAnalyze() { const rwPaths = getPaths() const clientEntryFileSet = new Set() const serverEntryFileSet = new Set() + const componentImportMap = new Map() if (!rwPaths.web.entries) { throw new Error('RSC entries file not found') @@ -45,6 +46,10 @@ export async function rscBuildAnalyze() { rscAnalyzePlugin( (id) => clientEntryFileSet.add(id), (id) => serverEntryFileSet.add(id), + (id, imports) => { + const existingImports = componentImportMap.get(id) ?? [] + componentImportMap.set(id, [...existingImports, ...imports]) + }, ), ], ssr: { @@ -96,5 +101,9 @@ export async function rscBuildAnalyze() { console.log('clientEntryFiles', clientEntryFiles) console.log('serverEntryFiles', serverEntryFiles) - return { clientEntryFiles, serverEntryFiles } + return { + clientEntryFiles, + serverEntryFiles, + componentImportMap, + } } diff --git a/packages/vite/src/rsc/rscBuildForServer.ts b/packages/vite/src/rsc/rscBuildForServer.ts index e47e0db9f935..7ce7a838a3d1 100644 --- a/packages/vite/src/rsc/rscBuildForServer.ts +++ b/packages/vite/src/rsc/rscBuildForServer.ts @@ -5,6 +5,7 @@ import { build as viteBuild } from 'vite' import { getPaths } from '@redwoodjs/project-config' import { onWarn } from '../lib/onWarn.js' +import { rscCssPreinitPlugin } from '../plugins/vite-plugin-rsc-css-preinit.js' import { rscTransformUseClientPlugin } from '../plugins/vite-plugin-rsc-transform-client.js' import { rscTransformUseServerPlugin } from '../plugins/vite-plugin-rsc-transform-server.js' @@ -17,6 +18,7 @@ export async function rscBuildForServer( clientEntryFiles: Record, serverEntryFiles: Record, customModules: Record, + componentImportMap: Map, ) { console.log('\n') console.log('3. rscBuildForServer') @@ -65,6 +67,7 @@ export async function rscBuildForServer( // (It does other things as well, but that's why it needs clientEntryFiles) rscTransformUseClientPlugin(clientEntryFiles), rscTransformUseServerPlugin(), + rscCssPreinitPlugin(clientEntryFiles, componentImportMap), ], build: { ssr: true, diff --git a/yarn.lock b/yarn.lock index 07172cfdc2f8..41c476cc7a0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8859,6 +8859,9 @@ __metadata: version: 0.0.0-use.local resolution: "@redwoodjs/vite@workspace:packages/vite" dependencies: + "@babel/generator": "npm:7.23.6" + "@babel/parser": "npm:^7.22.16" + "@babel/traverse": "npm:^7.22.20" "@redwoodjs/babel-config": "workspace:*" "@redwoodjs/internal": "workspace:*" "@redwoodjs/project-config": "workspace:*" From 14a2d0b76e9c37bc53db8376ca97f40a06e2c175 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Fri, 15 Mar 2024 10:22:48 +0100 Subject: [PATCH 13/34] RSC: Fix a prisma client warning during build (#10222) --- packages/vite/src/rsc/rscBuildAnalyze.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/rsc/rscBuildAnalyze.ts b/packages/vite/src/rsc/rscBuildAnalyze.ts index fbd9a0e383a3..de217412aa59 100644 --- a/packages/vite/src/rsc/rscBuildAnalyze.ts +++ b/packages/vite/src/rsc/rscBuildAnalyze.ts @@ -57,8 +57,8 @@ export async function rscBuildAnalyze() { // going to be RSCs noExternal: /^(?!node:)/, // TODO (RSC): Figure out what the `external` list should be. Right - // now it's just copied from waku - external: ['react', 'minimatch'], + // now it's just copied from waku, plus we added prisma + external: ['react', 'minimatch', '@prisma/client'], resolve: { externalConditions: ['react-server'], }, From 8842a671bcc8e1a88d87be7d8c5843a567493733 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Fri, 15 Mar 2024 05:14:08 -0700 Subject: [PATCH 14/34] chore(rsc): prefix `rsc-analyze-plugin` plugin (#10221) All our other plugins are prefixed with `redwood-` and it makes it easier to find in the logs and while debugging. All the Vite plugins are `vite:...`; not sure if that's an internal only convention or not but maybe we want to consider that. But not in this PR. --- packages/vite/src/plugins/vite-plugin-rsc-analyze.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts b/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts index 582a4b069ea0..5b7b2d206a32 100644 --- a/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts +++ b/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts @@ -14,7 +14,7 @@ export function rscAnalyzePlugin( const webSrcPath = getPaths().web.src return { - name: 'rsc-analyze-plugin', + name: 'redwood-rsc-analyze-plugin', transform(code, id) { const ext = path.extname(id) From 841f003eaf939cf1f8ba2c0eb3052ff423692869 Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:46:53 +0100 Subject: [PATCH 15/34] chore(tests): avoid needlessly matching on `src` (#10228) When I updated projects to use vitest I kept some instances where we used `run src` and then that got copied to nearly everywhere else. This isn't all the useful as when you are debugging and passing a file path as it runs all tests since the `src` matches all tests. --- packages/adapters/fastify/web/package.json | 4 ++-- packages/api/package.json | 4 ++-- packages/auth-providers/auth0/api/package.json | 4 ++-- packages/auth-providers/auth0/setup/package.json | 4 ++-- packages/auth-providers/auth0/web/package.json | 4 ++-- packages/auth-providers/azureActiveDirectory/api/package.json | 4 ++-- .../auth-providers/azureActiveDirectory/setup/package.json | 4 ++-- packages/auth-providers/azureActiveDirectory/web/package.json | 4 ++-- packages/auth-providers/clerk/api/package.json | 4 ++-- packages/auth-providers/clerk/web/package.json | 4 ++-- packages/auth-providers/custom/setup/package.json | 4 ++-- packages/auth-providers/dbAuth/api/package.json | 4 ++-- packages/auth-providers/firebase/api/package.json | 4 ++-- packages/auth-providers/firebase/setup/package.json | 4 ++-- packages/auth-providers/netlify/api/package.json | 4 ++-- packages/auth-providers/netlify/setup/package.json | 4 ++-- packages/auth-providers/netlify/web/package.json | 4 ++-- packages/auth-providers/supabase/api/package.json | 4 ++-- packages/auth-providers/supabase/web/package.json | 4 ++-- packages/auth-providers/supertokens/api/package.json | 4 ++-- packages/auth-providers/supertokens/setup/package.json | 4 ++-- packages/auth-providers/supertokens/web/package.json | 4 ++-- packages/mailer/core/package.json | 4 ++-- packages/prerender/package.json | 4 ++-- packages/project-config/package.json | 4 ++-- packages/record/package.json | 4 ++-- packages/telemetry/package.json | 4 ++-- packages/vite/package.json | 4 ++-- 28 files changed, 56 insertions(+), 56 deletions(-) diff --git a/packages/adapters/fastify/web/package.json b/packages/adapters/fastify/web/package.json index 8c36b62f2310..2c700f4dd1af 100644 --- a/packages/adapters/fastify/web/package.json +++ b/packages/adapters/fastify/web/package.json @@ -17,8 +17,8 @@ "build:pack": "yarn pack -o redwoodjs-fastify-web.tgz", "build:types": "tsc --build --verbose", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@fastify/http-proxy": "9.4.0", diff --git a/packages/api/package.json b/packages/api/package.json index c4af732482a1..b07f9773917c 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -28,8 +28,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/auth0/api/package.json b/packages/auth-providers/auth0/api/package.json index 7e45960d3363..0c5d7c3af80e 100644 --- a/packages/auth-providers/auth0/api/package.json +++ b/packages/auth-providers/auth0/api/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/auth0/setup/package.json b/packages/auth-providers/auth0/setup/package.json index 5926c341624f..3ad93f9f7cba 100644 --- a/packages/auth-providers/auth0/setup/package.json +++ b/packages/auth-providers/auth0/setup/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/auth0/web/package.json b/packages/auth-providers/auth0/web/package.json index 477350fbc9da..119eec71526c 100644 --- a/packages/auth-providers/auth0/web/package.json +++ b/packages/auth-providers/auth0/web/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/azureActiveDirectory/api/package.json b/packages/auth-providers/azureActiveDirectory/api/package.json index 90470db49a9c..98ec055bf5a5 100644 --- a/packages/auth-providers/azureActiveDirectory/api/package.json +++ b/packages/auth-providers/azureActiveDirectory/api/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/azureActiveDirectory/setup/package.json b/packages/auth-providers/azureActiveDirectory/setup/package.json index 040bfb9b24d1..3df712bff57e 100644 --- a/packages/auth-providers/azureActiveDirectory/setup/package.json +++ b/packages/auth-providers/azureActiveDirectory/setup/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/azureActiveDirectory/web/package.json b/packages/auth-providers/azureActiveDirectory/web/package.json index d04d5f3421a1..a9ef5061bb07 100644 --- a/packages/auth-providers/azureActiveDirectory/web/package.json +++ b/packages/auth-providers/azureActiveDirectory/web/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/clerk/api/package.json b/packages/auth-providers/clerk/api/package.json index 83f9bb8bc517..823fe729314a 100644 --- a/packages/auth-providers/clerk/api/package.json +++ b/packages/auth-providers/clerk/api/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/clerk/web/package.json b/packages/auth-providers/clerk/web/package.json index dd626f9debf3..60a4f8e5f11e 100644 --- a/packages/auth-providers/clerk/web/package.json +++ b/packages/auth-providers/clerk/web/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/custom/setup/package.json b/packages/auth-providers/custom/setup/package.json index ea5d968fb034..398a93c03372 100644 --- a/packages/auth-providers/custom/setup/package.json +++ b/packages/auth-providers/custom/setup/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/dbAuth/api/package.json b/packages/auth-providers/dbAuth/api/package.json index 1ecd56ba9a93..8f3536e1a3eb 100644 --- a/packages/auth-providers/dbAuth/api/package.json +++ b/packages/auth-providers/dbAuth/api/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/firebase/api/package.json b/packages/auth-providers/firebase/api/package.json index 33ec30e67e48..da2ec6ef3222 100644 --- a/packages/auth-providers/firebase/api/package.json +++ b/packages/auth-providers/firebase/api/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/firebase/setup/package.json b/packages/auth-providers/firebase/setup/package.json index 17a676c96c9b..8d41684ea392 100644 --- a/packages/auth-providers/firebase/setup/package.json +++ b/packages/auth-providers/firebase/setup/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/netlify/api/package.json b/packages/auth-providers/netlify/api/package.json index 6caa37fe5bba..eb3ef01797bf 100644 --- a/packages/auth-providers/netlify/api/package.json +++ b/packages/auth-providers/netlify/api/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/netlify/setup/package.json b/packages/auth-providers/netlify/setup/package.json index 6715b0b5d3f1..7d69102cfa50 100644 --- a/packages/auth-providers/netlify/setup/package.json +++ b/packages/auth-providers/netlify/setup/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/netlify/web/package.json b/packages/auth-providers/netlify/web/package.json index 9b31eafc0403..65cb16ab88a1 100644 --- a/packages/auth-providers/netlify/web/package.json +++ b/packages/auth-providers/netlify/web/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/supabase/api/package.json b/packages/auth-providers/supabase/api/package.json index 0fe801f7569d..5b4c1783f90b 100644 --- a/packages/auth-providers/supabase/api/package.json +++ b/packages/auth-providers/supabase/api/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/supabase/web/package.json b/packages/auth-providers/supabase/web/package.json index 311de2369810..cea439c4ccd0 100644 --- a/packages/auth-providers/supabase/web/package.json +++ b/packages/auth-providers/supabase/web/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/supertokens/api/package.json b/packages/auth-providers/supertokens/api/package.json index 53a6711be090..038b848cd875 100644 --- a/packages/auth-providers/supertokens/api/package.json +++ b/packages/auth-providers/supertokens/api/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/supertokens/setup/package.json b/packages/auth-providers/supertokens/setup/package.json index 471fff617e45..9f10f1d1fd72 100644 --- a/packages/auth-providers/supertokens/setup/package.json +++ b/packages/auth-providers/supertokens/setup/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/auth-providers/supertokens/web/package.json b/packages/auth-providers/supertokens/web/package.json index abde8ab042f0..c2bf6b9c6c5e 100644 --- a/packages/auth-providers/supertokens/web/package.json +++ b/packages/auth-providers/supertokens/web/package.json @@ -19,8 +19,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/mailer/core/package.json b/packages/mailer/core/package.json index 6e2a22d1eaf9..a83deed88d78 100644 --- a/packages/mailer/core/package.json +++ b/packages/mailer/core/package.json @@ -18,8 +18,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "devDependencies": { "@redwoodjs/api": "workspace:*", diff --git a/packages/prerender/package.json b/packages/prerender/package.json index f389e8af381a..29e82f11112f 100644 --- a/packages/prerender/package.json +++ b/packages/prerender/package.json @@ -21,8 +21,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/project-config/package.json b/packages/project-config/package.json index b5cabcab028a..589f665833f9 100644 --- a/packages/project-config/package.json +++ b/packages/project-config/package.json @@ -23,8 +23,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,ts,tsx\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@iarna/toml": "2.2.5", diff --git a/packages/record/package.json b/packages/record/package.json index 2fb782f538c2..3b84a1932f94 100644 --- a/packages/record/package.json +++ b/packages/record/package.json @@ -19,8 +19,8 @@ "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build\"", "datamodel:parse": "node src/scripts/parse.js", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 4141ab9232cb..dfa1446747c2 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -18,8 +18,8 @@ "build:pack": "yarn pack -o redwoodjs-telemetry.tgz", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/runtime-corejs3": "7.24.0", diff --git a/packages/vite/package.json b/packages/vite/package.json index 0416f7271790..b05668007e38 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -60,8 +60,8 @@ "build": "tsx build.mts && yarn build:types", "build:pack": "yarn pack -o redwoodjs-vite.tgz", "build:types": "tsc --build --verbose", - "test": "vitest run src", - "test:watch": "vitest watch src" + "test": "vitest run", + "test:watch": "vitest watch" }, "dependencies": { "@babel/generator": "7.23.6", From c61fee07383182f35c1c021c6b244929742c8f08 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Fri, 15 Mar 2024 06:01:13 -0700 Subject: [PATCH 16/34] chore(ci): update `yarn rwfw project:tarsync` to handle react resolutions (#10229) Upgrading to the canary versions of the react packages in https://github.com/redwoodjs/redwood/pull/10194 is confounded by the fact that a Redwood project's web-side package.json requests versions 18.2.0. Our sync commands should be setting the resolutions of those packages as well as the Redwood ones. This PR updates `yarn rwfw project:tarsync` to set resolutions for `react` and `react-dom`. --- tasks/framework-tools/tarsync.mjs | 214 ++++++++++++++++++------------ 1 file changed, 126 insertions(+), 88 deletions(-) diff --git a/tasks/framework-tools/tarsync.mjs b/tasks/framework-tools/tarsync.mjs index da7751b0f6c5..6eb3c9d49334 100644 --- a/tasks/framework-tools/tarsync.mjs +++ b/tasks/framework-tools/tarsync.mjs @@ -1,5 +1,4 @@ #!/usr/bin/env node -/* eslint-env node */ import { performance } from 'node:perf_hooks' import { fileURLToPath } from 'node:url' @@ -8,12 +7,90 @@ import { parseArgs as nodeUtilParseArgs } from 'node:util' import ora from 'ora' import { cd, chalk, fs, glob, path, within, $ } from 'zx' -const mockSpinner = { - text: '', - succeed: () => {}, -} +const FRAMEWORK_PATH = fileURLToPath(new URL('../../', import.meta.url)) +const TARBALL_DEST_DIRNAME = 'tarballs' async function main() { + const { projectPath, verbose } = await getOptions() + $.verbose = verbose + + cd(FRAMEWORK_PATH) + performance.mark('startFramework') + + const spinner = getFrameworkSpinner({ text: 'building and packing packages' }) + await buildTarballs() + + spinner.text = 'moving tarballs' + await moveTarballs(projectPath) + + spinner.text = 'updating resolutions' + await updateResolutions(projectPath) + + performance.mark('endFramework') + performance.measure('framework', 'startFramework', 'endFramework') + const [entry] = performance.getEntriesByName('framework') + spinner.succeed(`finished in ${(entry.duration / 1000).toFixed(2)} seconds`) + + await yarnInstall(projectPath) + + const entries = performance.getEntriesByType('measure').map((entry) => { + return `β€’ ${entry.name} => ${(entry.duration / 1000).toFixed(2)} seconds` + }) + + for (const entry of entries) { + verbose && console.log(entry) + } +} + +main() + +// Helpers +// ------- + +async function parseArgs() { + const { positionals, values } = nodeUtilParseArgs({ + allowPositionals: true, + + options: { + verbose: { + type: 'boolean', + default: false, + short: 'v', + }, + }, + }) + + const [projectPath] = positionals + + const options = { + verbose: values.verbose, + } + + options.projectPath = projectPath ? projectPath : process.env.RWJS_CWD + + if (!options.projectPath) { + throw new Error( + [ + 'Error: You have to provide the path to a Redwood project as', + '', + ' 1. the first positional argument', + '', + chalk.gray(' yarn project:tarsync /path/to/redwood/project'), + '', + ' 2. the `RWJS_CWD` env var', + '', + chalk.gray(' RWJS_CWD=/path/to/redwood/project yarn project:tarsync'), + ].join('\n') + ) + } + + // This makes `projectPath` an absolute path and throws if it doesn't exist. + options.projectPath = await fs.realpath(options.projectPath) + + return options +} + +async function getOptions() { let options try { @@ -26,33 +103,35 @@ async function main() { const { projectPath, verbose } = options - $.verbose = verbose - - // Closing over `verbose` here. - function getProjectSpinner({ text }) { - return verbose - ? mockSpinner - : ora({ prefixText: `${chalk.green('[ project ]')}`, text }).start() + return { + projectPath, + verbose, } +} - function getFrameworkSpinner({ text }) { - return verbose - ? mockSpinner - : ora({ prefixText: `${chalk.cyan('[framework]')}`, text }).start() - } +const mockSpinner = { + text: '', + succeed: () => { }, +} - const frameworkPath = fileURLToPath(new URL('../../', import.meta.url)) - cd(frameworkPath) - performance.mark('startFramework') +function getProjectSpinner({ text }) { + return $.verbose + ? mockSpinner + : ora({ prefixText: `${chalk.green('[ project ]')}`, text }).start() +} - const spinner = getFrameworkSpinner({ text: 'building and packing packages' }) +function getFrameworkSpinner({ text }) { + return $.verbose + ? mockSpinner + : ora({ prefixText: `${chalk.cyan('[framework]')}`, text }).start() +} +async function buildTarballs() { await $`yarn nx run-many -t build:pack --exclude create-redwood-app` +} - spinner.text = 'moving tarballs' - - const tarballDestDirname = 'tarballs' - const tarballDest = path.join(projectPath, tarballDestDirname) +async function moveTarballs(projectPath) { + const tarballDest = path.join(projectPath, TARBALL_DEST_DIRNAME) await fs.ensureDir(tarballDest) const tarballs = await glob(['./packages/**/*.tgz']) @@ -64,9 +143,25 @@ async function main() { }) ) ) +} - spinner.text = 'updating resolutions' +async function getReactResolutions() { + const packageConfig = await fs.readJson(path.join(FRAMEWORK_PATH, 'packages/web/package.json')) + + const react = packageConfig.peerDependencies.react + const reactDom = packageConfig.peerDependencies['react-dom'] + + if (!react || !reactDom) { + throw new Error("Couldn't find react or react-dom in @redwoodjs/web's peerDependencies") + } + return { + react, + 'react-dom': reactDom, + } +} + +async function updateResolutions(projectPath) { const resolutions = (await $`yarn workspaces list --json`).stdout .trim() .split('\n') @@ -77,9 +172,8 @@ async function main() { return { ...resolutions, // Turn a Redwood package name like `@redwoodjs/project-config` into `redwoodjs-project-config.tgz`. - [name]: `./${tarballDestDirname}/${ - name.replace('@', '').replaceAll('/', '-') + '.tgz' - }`, + [name]: `./${TARBALL_DEST_DIRNAME}/${name.replace('@', '').replaceAll('/', '-') + '.tgz' + }`, } }, {}) @@ -93,20 +187,16 @@ async function main() { resolutions: { ...projectPackageJson.resolutions, ...resolutions, + ...(await getReactResolutions()) }, }, { spaces: 2, } ) +} - performance.mark('endFramework') - performance.measure('framework', 'startFramework', 'endFramework') - - const [entry] = performance.getEntriesByName('framework') - - spinner.succeed(`finished in ${(entry.duration / 1000).toFixed(2)} seconds`) - +async function yarnInstall(projectPath) { await within(async () => { cd(projectPath) performance.mark('startProject') @@ -123,56 +213,4 @@ async function main() { spinner.succeed(`finished in ${(entry.duration / 1000).toFixed(2)} seconds`) }) - const entries = performance.getEntriesByType('measure').map((entry) => { - return `β€’ ${entry.name} => ${(entry.duration / 1000).toFixed(2)} seconds` - }) - - for (const entry of entries) { - verbose && console.log(entry) - } -} - -main() - -async function parseArgs() { - const { positionals, values } = nodeUtilParseArgs({ - allowPositionals: true, - - options: { - verbose: { - type: 'boolean', - default: false, - short: 'v', - }, - }, - }) - - const [projectPath] = positionals - - const options = { - verbose: values.verbose, - } - - options.projectPath = projectPath ? projectPath : process.env.RWJS_CWD - - if (!options.projectPath) { - throw new Error( - [ - 'Error: You have to provide the path to a Redwood project as', - '', - ' 1. the first positional argument', - '', - chalk.gray(' yarn project:tarsync /path/to/redwood/project'), - '', - ' 2. the `RWJS_CWD` env var', - '', - chalk.gray(' RWJS_CWD=/path/to/redwood/project yarn project:tarsync'), - ].join('\n') - ) - } - - // This makes `projectPath` an absolute path and throws if it doesn't exist. - options.projectPath = await fs.realpath(options.projectPath) - - return options } From d9eec97ce5fc89818a0e1371fd180a5692c71426 Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Sat, 16 Mar 2024 13:08:33 +0100 Subject: [PATCH 17/34] RSC: fix css and add smoke tests for it (#10226) **Changes** 1. Add smoke tests that expect certain CSS to have loaded as we would expect. This should hopefully identify CSS regressions. 2. Had to externalize the `react-dom` package to get the CSS preinit plugin to work again. We can circle back to this. --------- Co-authored-by: Tobbe Lundberg --- .../vite-plugin-rsc-css-preinit.test.mts | 20 +++++++++++-- .../src/plugins/vite-plugin-rsc-analyze.ts | 3 +- .../plugins/vite-plugin-rsc-css-preinit.ts | 12 ++++---- packages/vite/src/rsc/rscBuildForServer.ts | 5 ++-- tasks/smoke-tests/rsc-dev/tests/rsc.spec.ts | 30 +++++++++++++++++++ .../rsc-external-packages-and-cells.spec.ts | 21 +++++++++++++ tasks/smoke-tests/rsc/tests/rsc.spec.ts | 30 +++++++++++++++++++ 7 files changed, 109 insertions(+), 12 deletions(-) diff --git a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit.test.mts b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit.test.mts index 8a8ce90a679d..149c1fdbbbca 100644 --- a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit.test.mts +++ b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit.test.mts @@ -1,5 +1,6 @@ import path from 'node:path' import { vol } from 'memfs' +import { normalizePath } from 'vite' import { generateCssMapping, rscCssPreinitPlugin, generateServerComponentClientComponentMapping, splitClientAndServerComponents } from '../vite-plugin-rsc-css-preinit' import { afterAll, beforeAll, describe, it, expect, vi } from 'vitest' @@ -9,6 +10,7 @@ import { clientEntryFiles, componentImportMap, } from './vite-plugin-rsc-css-preinit-fixture-values' +import { getPaths } from '@redwoodjs/project-config' vi.mock('fs', async () => ({ default: (await import('memfs')).fs })) @@ -16,11 +18,22 @@ const RWJS_CWD = process.env.RWJS_CWD let consoleLogSpy beforeAll(() => { + // Add the toml so that getPaths will work process.env.RWJS_CWD = '/Users/mojombo/rw-app/' vol.fromJSON({ 'redwood.toml': '', - [path.join('web', 'dist', 'client', 'client-build-manifest.json')]: JSON.stringify(clientBuildManifest), }, process.env.RWJS_CWD) + + // Add the client build manifest + const manifestPath = path.join( + getPaths().web.distClient, + 'client-build-manifest.json', + ).substring(process.env.RWJS_CWD.length) + vol.fromJSON({ + 'redwood.toml': '', + [manifestPath]: JSON.stringify(clientBuildManifest), + }, process.env.RWJS_CWD) + consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) }) @@ -29,7 +42,7 @@ afterAll(() => { consoleLogSpy.mockRestore() }) -describe.skip('rscCssPreinitPlugin', () => { +describe('rscCssPreinitPlugin', () => { it('should insert preinits for all nested client components', async () => { const plugin = rscCssPreinitPlugin(clientEntryFiles, componentImportMap) @@ -39,6 +52,7 @@ describe.skip('rscCssPreinitPlugin', () => { // Calling `bind` to please TS // See https://stackoverflow.com/a/70463512/88106 + const id = path.join(process.env.RWJS_CWD!, 'web', 'src', 'pages', 'HomePage', 'HomePage.tsx') const output = await plugin.transform.bind({})( `import { jsx, jsxs } from "react/jsx-runtime"; import { RscForm } from "@tobbe.dev/rsc-test"; @@ -70,7 +84,7 @@ describe.skip('rscCssPreinitPlugin', () => { ] }); }; export default HomePage;`, - path.join(process.env.RWJS_CWD!, 'web', 'src', 'pages', 'HomePage', 'HomePage.tsx') + normalizePath(id) ) // You will see that this snapshot contains: diff --git a/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts b/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts index 5b7b2d206a32..84e919a7717f 100644 --- a/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts +++ b/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts @@ -2,6 +2,7 @@ import path from 'node:path' import * as swc from '@swc/core' import type { Plugin } from 'vite' +import { normalizePath } from 'vite' import { getPaths } from '@redwoodjs/project-config' @@ -43,7 +44,7 @@ export function rscAnalyzePlugin( }, moduleParsed(moduleInfo) { // TODO: Maybe this is not needed? - if (moduleInfo.id.startsWith(webSrcPath)) { + if (moduleInfo.id.startsWith(normalizePath(webSrcPath))) { componentImportsCallback(moduleInfo.id, moduleInfo.importedIds) } }, diff --git a/packages/vite/src/plugins/vite-plugin-rsc-css-preinit.ts b/packages/vite/src/plugins/vite-plugin-rsc-css-preinit.ts index d3d25e2ea9bf..605c7d9a138d 100644 --- a/packages/vite/src/plugins/vite-plugin-rsc-css-preinit.ts +++ b/packages/vite/src/plugins/vite-plugin-rsc-css-preinit.ts @@ -7,6 +7,7 @@ import type { NodePath } from '@babel/traverse' import traverse from '@babel/traverse' import * as t from '@babel/types' import type { Plugin } from 'vite' +import { normalizePath } from 'vite' import { getPaths } from '@redwoodjs/project-config' @@ -94,12 +95,11 @@ export function rscCssPreinitPlugin( // This plugin is build only and we expect the client build manifest to be // available at this point. We use it to find the correct css assets names - const clientBuildManifest = JSON.parse( - fs.readFileSync( - path.join(getPaths().web.distClient, 'client-build-manifest.json'), - 'utf-8', - ), + const manifestPath = path.join( + getPaths().web.distClient, + 'client-build-manifest.json', ) + const clientBuildManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) // We generate a mapping of all the css assets that a client build manifest // entry contains (looking deep into the tree of entries) @@ -123,7 +123,7 @@ export function rscCssPreinitPlugin( apply: 'build', transform: async function (code, id) { // We only care about code in the project itself - if (!id.startsWith(webSrc)) { + if (!id.startsWith(normalizePath(webSrc))) { return null } diff --git a/packages/vite/src/rsc/rscBuildForServer.ts b/packages/vite/src/rsc/rscBuildForServer.ts index 7ce7a838a3d1..2505a15310cb 100644 --- a/packages/vite/src/rsc/rscBuildForServer.ts +++ b/packages/vite/src/rsc/rscBuildForServer.ts @@ -49,8 +49,9 @@ export async function rscBuildForServer( // it's likely way less efficient because we have to do so many files. // Files included in `noExternal` are files we want Vite to analyze noExternal: /^(?!node:)/, - // Can't inline prisma client - external: ['@prisma/client'], + // Can't inline prisma client (db calls fail at runtime) or react-dom + // (css preinit failure) + external: ['@prisma/client', 'react-dom'], resolve: { // These conditions are used in the plugin pipeline, and only affect non-externalized // dependencies during the SSR build. Which because of `noExternal: /^(?!node:)/` means diff --git a/tasks/smoke-tests/rsc-dev/tests/rsc.spec.ts b/tasks/smoke-tests/rsc-dev/tests/rsc.spec.ts index 4363508453be..a8c4de8cb83b 100644 --- a/tasks/smoke-tests/rsc-dev/tests/rsc.spec.ts +++ b/tasks/smoke-tests/rsc-dev/tests/rsc.spec.ts @@ -17,6 +17,36 @@ test('Setting up RSC should give you a test project with a client side counter c page.close() }) +test('CSS has been loaded', async ({ page }) => { + await page.goto('/') + + // Check color of server component h3 + const serverH3 = page.getByText('This is a server component.') + await expect(serverH3).toBeVisible() + const serverH3Color = await serverH3.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('color') + }) + // rgb(255, 165, 0) is orange + expect(serverH3Color).toBe('rgb(255, 165, 0)') + + // Check color of client component h3 + const clientH3 = page.getByText('This is a client component.') + await expect(clientH3).toBeVisible() + const clientH3Color = await clientH3.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('color') + }) + // rgb(255, 165, 0) is orange + expect(clientH3Color).toBe('rgb(255, 165, 0)') + + // Check font style of client component h3 + const clientH3Font = await clientH3.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('font-style') + }) + expect(clientH3Font).toBe('italic') + + page.close() +}) + test('RWJS_* env vars', async ({ page }) => { await page.goto('/about') diff --git a/tasks/smoke-tests/rsc-external-packages-and-cells/tests/rsc-external-packages-and-cells.spec.ts b/tasks/smoke-tests/rsc-external-packages-and-cells/tests/rsc-external-packages-and-cells.spec.ts index 868d88b32531..4bbecb8c8dcd 100644 --- a/tasks/smoke-tests/rsc-external-packages-and-cells/tests/rsc-external-packages-and-cells.spec.ts +++ b/tasks/smoke-tests/rsc-external-packages-and-cells/tests/rsc-external-packages-and-cells.spec.ts @@ -15,6 +15,27 @@ test('Client components should work', async ({ page }) => { page.close() }) +test('CSS has been loaded', async ({ page }) => { + await page.goto('/') + + // Check color of client component h3 + const clientH3 = page.getByText('This is a client component.') + await expect(clientH3).toBeVisible() + const clientH3Color = await clientH3.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('color') + }) + // rgb(255, 165, 0) is orange + expect(clientH3Color).toBe('rgb(255, 165, 0)') + + // Check font style of client component h3 + const clientH3Font = await clientH3.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('font-style') + }) + expect(clientH3Font).toBe('italic') + + page.close() +}) + test('Submitting the form should return a response', async ({ page }) => { await page.goto('/') diff --git a/tasks/smoke-tests/rsc/tests/rsc.spec.ts b/tasks/smoke-tests/rsc/tests/rsc.spec.ts index a72ca9251734..1ecf3317c194 100644 --- a/tasks/smoke-tests/rsc/tests/rsc.spec.ts +++ b/tasks/smoke-tests/rsc/tests/rsc.spec.ts @@ -17,6 +17,36 @@ test('Setting up RSC should give you a test project with a client side counter c page.close() }) +test('CSS has been loaded', async ({ page }) => { + await page.goto('/') + + // Check color of server component h3 + const serverH3 = page.getByText('This is a server component.') + await expect(serverH3).toBeVisible() + const serverH3Color = await serverH3.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('color') + }) + // rgb(255, 165, 0) is orange + expect(serverH3Color).toBe('rgb(255, 165, 0)') + + // Check color of client component h3 + const clientH3 = page.getByText('This is a client component.') + await expect(clientH3).toBeVisible() + const clientH3Color = await clientH3.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('color') + }) + // rgb(255, 165, 0) is orange + expect(clientH3Color).toBe('rgb(255, 165, 0)') + + // Check font style of client component h3 + const clientH3Font = await clientH3.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('font-style') + }) + expect(clientH3Font).toBe('italic') + + page.close() +}) + test('RWJS_* env vars', async ({ page }) => { await page.goto('/about') From e6e07fe468852b95c3e2d6d045422e57af8f9dde Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sat, 16 Mar 2024 07:42:53 -0700 Subject: [PATCH 18/34] chore(rsc): unminify dist (#10227) For RSCs, while we're debugging and figuring things out, it'd be more helpful for me at least to unminify dist for now. --- packages/vite/src/lib/getMergedConfig.ts | 2 ++ packages/vite/src/rsc/rscBuildAnalyze.ts | 2 ++ packages/vite/src/rsc/rscBuildClient.ts | 2 ++ packages/vite/src/rsc/rscBuildForServer.ts | 2 ++ packages/vite/src/streaming/buildForStreamingServer.ts | 2 ++ 5 files changed, 10 insertions(+) diff --git a/packages/vite/src/lib/getMergedConfig.ts b/packages/vite/src/lib/getMergedConfig.ts index db64382c064e..379a2cb6b012 100644 --- a/packages/vite/src/lib/getMergedConfig.ts +++ b/packages/vite/src/lib/getMergedConfig.ts @@ -111,6 +111,8 @@ export function getMergedConfig(rwConfig: Config, rwPaths: Paths) { }, }, build: { + // TODO (RSC): Remove `minify: false` when we don't need to debug as often + minify: false, // NOTE this gets overridden when build gets called anyway! outDir: // @MARK: For RSC and Streaming, we build to dist/client directory diff --git a/packages/vite/src/rsc/rscBuildAnalyze.ts b/packages/vite/src/rsc/rscBuildAnalyze.ts index de217412aa59..4eadc467fe98 100644 --- a/packages/vite/src/rsc/rscBuildAnalyze.ts +++ b/packages/vite/src/rsc/rscBuildAnalyze.ts @@ -64,6 +64,8 @@ export async function rscBuildAnalyze() { }, }, build: { + // TODO (RSC): Remove `minify: false` when we don't need to debug as often + minify: false, manifest: 'rsc-build-manifest.json', write: false, // TODO (RSC): In the future we want to generate the entries file diff --git a/packages/vite/src/rsc/rscBuildClient.ts b/packages/vite/src/rsc/rscBuildClient.ts index 15405455d553..1194dbe80db4 100644 --- a/packages/vite/src/rsc/rscBuildClient.ts +++ b/packages/vite/src/rsc/rscBuildClient.ts @@ -31,6 +31,8 @@ export async function rscBuildClient(clientEntryFiles: Record) { const clientBuildOutput = await viteBuild({ envFile: false, build: { + // TODO (RSC): Remove `minify: false` when we don't need to debug as often + minify: false, outDir: rwPaths.web.distClient, emptyOutDir: true, // Needed because `outDir` is not inside `root` rollupOptions: { diff --git a/packages/vite/src/rsc/rscBuildForServer.ts b/packages/vite/src/rsc/rscBuildForServer.ts index 2505a15310cb..423431917382 100644 --- a/packages/vite/src/rsc/rscBuildForServer.ts +++ b/packages/vite/src/rsc/rscBuildForServer.ts @@ -71,6 +71,8 @@ export async function rscBuildForServer( rscCssPreinitPlugin(clientEntryFiles, componentImportMap), ], build: { + // TODO (RSC): Remove `minify: false` when we don't need to debug as often + minify: false, ssr: true, ssrEmitAssets: true, outDir: rwPaths.web.distRsc, diff --git a/packages/vite/src/streaming/buildForStreamingServer.ts b/packages/vite/src/streaming/buildForStreamingServer.ts index 132b56250335..afcec3462b65 100644 --- a/packages/vite/src/streaming/buildForStreamingServer.ts +++ b/packages/vite/src/streaming/buildForStreamingServer.ts @@ -23,6 +23,8 @@ export async function buildForStreamingServer({ }), ], build: { + // TODO (RSC): Remove `minify: false` when we don't need to debug as often + minify: false, outDir: rwPaths.web.distServer, ssr: true, emptyOutDir: true, From 18a55d47a4329196d5b9c16f40d9de3afd647aec Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Sat, 16 Mar 2024 16:26:43 +0100 Subject: [PATCH 19/34] feat: Send RSC Flight Payload to Studio (#10213) This PR sends the rendered RSC payload (aka "flight") to Studio to be ingested, persisted, and visualized. See: https://github.com/redwoodjs/studio/pull/52 Am encoding the payload as base64 text as a convenience so it gets persisted as encoded text an not some unwieldily JSON with escape characters. Studio will decoded and handle the flight payload accordingly. This is a proof of concept and still many todos: 1. Ensure only sends in dev and studio setup 2. Could do some refactoring to separate enrichment ### Studio image and single preview for the moment ... still UI todo image --------- Co-authored-by: Tobbe Lundberg --- .changesets/four-adults-jog.md | 5 + packages/vite/src/rsc/rscRequestHandler.ts | 13 ++ packages/vite/src/rsc/rscStudioHandlers.ts | 169 +++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 .changesets/four-adults-jog.md create mode 100644 packages/vite/src/rsc/rscStudioHandlers.ts diff --git a/.changesets/four-adults-jog.md b/.changesets/four-adults-jog.md new file mode 100644 index 000000000000..20c48cc64aea --- /dev/null +++ b/.changesets/four-adults-jog.md @@ -0,0 +1,5 @@ +- PR feat: Send RSC Flight Payload to Studio (10213) by @dthyresson + +This PR sends the rendered RSC payload (aka "flight") to Studio to be ingested, persisted, and fetched. + +Performance and metadata enrichments are performed in order to visualize in Studio diff --git a/packages/vite/src/rsc/rscRequestHandler.ts b/packages/vite/src/rsc/rscRequestHandler.ts index 377d453a43f4..6388c9f9d8fe 100644 --- a/packages/vite/src/rsc/rscRequestHandler.ts +++ b/packages/vite/src/rsc/rscRequestHandler.ts @@ -4,6 +4,7 @@ import RSDWServer from 'react-server-dom-webpack/server.node.unbundled' import { hasStatusCode } from '../lib/StatusError.js' +import { sendRscFlightToStudio } from './rscStudioHandlers.js' import { renderRsc } from './rscWorkerCommunication.js' const { decodeReply, decodeReplyFromBusboy } = RSDWServer @@ -124,8 +125,20 @@ export function createRscRequestHandler() { try { const pipeable = await renderRsc({ rscId, props, rsfId, args }) + + await sendRscFlightToStudio({ + rscId, + props, + rsfId, + args, + basePath, + req, + handleError, + }) + // TODO (RSC): See if we can/need to do more error handling here // pipeable.on(handleError) + pipeable.pipe(res) } catch (e) { handleError(e) diff --git a/packages/vite/src/rsc/rscStudioHandlers.ts b/packages/vite/src/rsc/rscStudioHandlers.ts new file mode 100644 index 000000000000..29334cc8b107 --- /dev/null +++ b/packages/vite/src/rsc/rscStudioHandlers.ts @@ -0,0 +1,169 @@ +import http from 'node:http' +import type { PassThrough } from 'node:stream' + +import type { Request } from 'express' + +import { getRawConfig, getConfig } from '@redwoodjs/project-config' + +import { renderRsc } from './rscWorkerCommunication.js' +import type { RenderInput } from './rscWorkerCommunication.js' + +const isTest = () => { + return process.env.NODE_ENV === 'test' +} + +const isDevelopment = () => { + return process.env.NODE_ENV !== 'production' && !isTest() +} + +const isStudioEnabled = () => { + return getRawConfig()['studio'] !== undefined +} + +const shouldSendToStudio = () => { + // TODO (RSC): This should be just isDevelopment() + // but since RSC apps currently run in production mode + // we need to check for 'production' (aka not 'development') + // for now when sending to Studio + return isStudioEnabled() && !isDevelopment() +} + +const getStudioPort = () => { + return getConfig().studio.basePort +} + +const processRenderRscStream = async ( + pipeable: PassThrough, +): Promise => { + return new Promise((resolve, reject) => { + const chunks = [] as any + + pipeable.on('data', (chunk: any) => { + chunks.push(chunk) + }) + + pipeable.on('end', () => { + const resultBuffer = Buffer.concat(chunks) + const resultString = resultBuffer.toString('utf-8') as string + resolve(resultString) + }) + + pipeable.on('error', (error) => { + reject(error) + }) + }) +} + +const postFlightToStudio = (payload: string, metadata: Record) => { + if (shouldSendToStudio()) { + const base64Payload = Buffer.from(payload).toString('base64') + const encodedMetadata = Buffer.from(JSON.stringify(metadata)).toString( + 'base64', + ) + const jsonBody = JSON.stringify({ + flight: { + encodedPayload: base64Payload, + encoding: 'base64', + encodedMetadata, + }, + }) + + // Options to configure the HTTP POST request + // TODO (RSC): Get these from the toml and Studio config + const options = { + hostname: 'localhost', + port: getStudioPort(), + path: '/.redwood/functions/rsc-flight', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(jsonBody), + }, + } + + const req = http.request(options, (res) => { + res.setEncoding('utf8') + }) + + req.on('error', (e: Error) => { + console.error( + `An error occurred sending the Flight Payload to Studio: ${e.message}`, + ) + }) + + req.write(jsonBody) + req.end() + } +} + +const createStudioFlightHandler = ( + pipeable: PassThrough, + metadata: Record, +) => { + if (shouldSendToStudio()) { + processRenderRscStream(pipeable) + .then((payload) => { + console.debug('Sending RSC Rendered stream to Studio') + postFlightToStudio(payload, metadata) + console.debug('Sent RSC Rendered stream to Studio', payload, metadata) + }) + .catch((error) => { + console.error('An error occurred getting RSC Rendered steam:', error) + }) + } else { + console.debug('Studio is not enabled') + } +} + +interface StudioRenderInput extends RenderInput { + basePath: string + req: Request + handleError: (e: Error) => void +} + +export const sendRscFlightToStudio = async (input: StudioRenderInput) => { + if (!shouldSendToStudio()) { + console.debug('Studio is not enabled') + return + } + const { rscId, props, rsfId, args, basePath, req, handleError } = input + + try { + // surround renderRsc with performance metrics + const startedAt = Date.now() + const start = performance.now() + const pipeable = await renderRsc({ rscId, props, rsfId, args }) + const endedAt = Date.now() + const end = performance.now() + const duration = end - start + + // collect render request metadata + const metadata = { + rsc: { + rscId, + rsfId, + props, + args, + }, + request: { + basePath, + originalUrl: req.originalUrl, + url: req.url, + headers: req.headers, + }, + performance: { + startedAt, + endedAt, + duration, + }, + } + + // send rendered request to Studio + createStudioFlightHandler(pipeable as PassThrough, metadata) + } catch (e) { + if (e instanceof Error) { + console.error('An error occurred rendering RSC and sending to Studio:', e) + handleError(e) + } + } +} From cc79787288a7c591d033f3eb0ee58846b6f2ef6f Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 16 Mar 2024 17:18:26 +0100 Subject: [PATCH 20/34] RSC: Random vite package cleanup and tweaks (#10239) --- packages/vite/src/client.ts | 5 +++-- packages/vite/src/rsc/rscBuildForServer.ts | 2 -- packages/vite/src/rsc/rscWebpackShims.ts | 2 ++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index 0774d9eb8170..6b119fde6b4a 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -20,7 +20,7 @@ const checkStatus = async ( const BASE_PATH = '/rw-rsc/' -export function renderFromRscServer(rscId: string) { +export function renderFromRscServer(rscId: string) { console.log('serve rscId', rscId) // Temporarily skip rendering this component during SSR @@ -58,6 +58,7 @@ export function renderFromRscServer(rscId: string) { // and that element will be FormData callServer: async function (rsfId: string, args: unknown[]) { console.log('client.ts :: callServer rsfId', rsfId, 'args', args) + const isMutating = !!mutationMode const searchParams = new URLSearchParams() searchParams.set('action_id', rsfId) @@ -113,7 +114,7 @@ export function renderFromRscServer(rscId: string) { // Create temporary client component that wraps the ServerComponent returned // by the `createFromFetch` call. - const ServerComponent = (props: Props) => { + const ServerComponent = (props: TProps) => { console.log('ServerComponent', rscId, 'props', props) // FIXME we blindly expect JSON.stringify usage is deterministic diff --git a/packages/vite/src/rsc/rscBuildForServer.ts b/packages/vite/src/rsc/rscBuildForServer.ts index 423431917382..08b6800347e6 100644 --- a/packages/vite/src/rsc/rscBuildForServer.ts +++ b/packages/vite/src/rsc/rscBuildForServer.ts @@ -1,5 +1,3 @@ -// import path from 'node:path' - import { build as viteBuild } from 'vite' import { getPaths } from '@redwoodjs/project-config' diff --git a/packages/vite/src/rsc/rscWebpackShims.ts b/packages/vite/src/rsc/rscWebpackShims.ts index a62cd0958f72..60ed3c6510c0 100644 --- a/packages/vite/src/rsc/rscWebpackShims.ts +++ b/packages/vite/src/rsc/rscWebpackShims.ts @@ -1,9 +1,11 @@ export const rscWebpackShims = `globalThis.__rw_module_cache__ = new Map(); globalThis.__webpack_chunk_load__ = (id) => { + console.log('rscWebpackShims chunk load id', id) return import(id).then((m) => globalThis.__rw_module_cache__.set(id, m)) }; globalThis.__webpack_require__ = (id) => { + console.log('rscWebpackShims require id', id) return globalThis.__rw_module_cache__.get(id) };\n` From 9ede22cfca01b1857d4cf9a82bde745c0fc2a187 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 16 Mar 2024 17:24:37 +0100 Subject: [PATCH 21/34] RSC: Tweak comment in vite/client (#10240) --- packages/vite/src/client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index 6b119fde6b4a..2ee3e7e45591 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -23,9 +23,10 @@ const BASE_PATH = '/rw-rsc/' export function renderFromRscServer(rscId: string) { console.log('serve rscId', rscId) - // Temporarily skip rendering this component during SSR - // I don't know what we actually should do during SSR yet + // TODO (RSC): Remove this when we have a babel plugin to call another + // function during SSR if (typeof window === 'undefined') { + // Temporarily skip rendering this component during SSR return null } From bf9965e54f80506a4518aecf09e07db760a27ff9 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sat, 16 Mar 2024 14:06:20 -0700 Subject: [PATCH 22/34] fix(rsc): use query string in rsc url (#10243) Seemed like this was supposed to be a question mark to make a query string url and @Tobbe confirmed --- packages/vite/src/client.ts | 6 +++--- packages/vite/src/rsc/rscRequestHandler.ts | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index 2ee3e7e45591..69a73dd2c300 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -72,7 +72,7 @@ export function renderFromRscServer(rscId: string) { id = '_' } - const response = fetch(BASE_PATH + id + '/' + searchParams, { + const response = fetch(BASE_PATH + id + '?' + searchParams, { method: 'POST', body: await encodeReply(args), headers: { @@ -96,12 +96,12 @@ export function renderFromRscServer(rscId: string) { console.log( 'fetchRSC before createFromFetch', - BASE_PATH + rscId + '/' + searchParams, + BASE_PATH + rscId + '?' + searchParams, ) const response = prefetched || - fetch(BASE_PATH + rscId + '/' + searchParams, { + fetch(BASE_PATH + rscId + '?' + searchParams, { headers: { 'rw-rsc': '1', }, diff --git a/packages/vite/src/rsc/rscRequestHandler.ts b/packages/vite/src/rsc/rscRequestHandler.ts index 6388c9f9d8fe..935f3cb5a34e 100644 --- a/packages/vite/src/rsc/rscRequestHandler.ts +++ b/packages/vite/src/rsc/rscRequestHandler.ts @@ -33,17 +33,15 @@ export function createRscRequestHandler() { console.log('url.pathname', url.pathname) if (url.pathname.startsWith(basePath)) { - const index = url.pathname.lastIndexOf('/') - const params = new URLSearchParams(url.pathname.slice(index + 1)) - rscId = url.pathname.slice(basePath.length, index) - rsfId = params.get('action_id') || undefined + rscId = url.pathname.split('/').pop() + rsfId = url.searchParams.get('action_id') || undefined console.log('rscId', rscId) console.log('rsfId', rsfId) if (rscId && rscId !== '_') { res.setHeader('Content-Type', 'text/x-component') - props = JSON.parse(params.get('props') || '{}') + props = JSON.parse(url.searchParams.get('props') || '{}') } else { rscId = undefined } From dfef3b318dcff441b26f1a5be21be14fe44eb13f Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sat, 16 Mar 2024 14:06:37 -0700 Subject: [PATCH 23/34] fix(rsc): avoid overriding `NODE_ENV` (#10237) Similar to https://github.com/redwoodjs/redwood/pull/10227, I can't pull in the development build of react or any of react's packages for debugging because we're hardcoding `NODE_ENV` in rw-vite-build. --- packages/vite/bins/rw-vite-build.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/vite/bins/rw-vite-build.mjs b/packages/vite/bins/rw-vite-build.mjs index eed357df4b34..33eeeaf3192e 100755 --- a/packages/vite/bins/rw-vite-build.mjs +++ b/packages/vite/bins/rw-vite-build.mjs @@ -42,7 +42,9 @@ const buildWebSide = async (webDir) => { throw new Error('Could not locate your web/vite.config.{js,ts} file') } - process.env.NODE_ENV = 'production' + if (!process.env.NODE_ENV) { + process.env.NODE_ENV = 'production' + } if (getConfig().experimental?.streamingSsr?.enabled) { // Webdir checks handled in the rwjs/vite package in new build system From 6d7ab6910f79f0ee5145f803d3a2da788169ecf0 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 17 Mar 2024 10:33:30 +0100 Subject: [PATCH 24/34] RSC: Use vite plugin to inject webpack shims (#10245) --- packages/vite/src/index.ts | 7 +++- .../vite-plugin-rsc-transform-entry.ts | 38 +++++++++++++++++++ packages/vite/src/streaming/streamHelpers.ts | 7 ++-- 3 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index 47225c3bd633..a26e6beab500 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -11,6 +11,7 @@ import { getConfig, getPaths } from '@redwoodjs/project-config' import { getMergedConfig } from './lib/getMergedConfig.js' import handleJsAsJsx from './plugins/vite-plugin-jsx-loader.js' import removeFromBundle from './plugins/vite-plugin-remove-from-bundle.js' +import { rscTransformEntryPlugin } from './plugins/vite-plugin-rsc-transform-entry.js' import swapApolloProvider from './plugins/vite-plugin-swap-apollo-provider.js' /** @@ -36,7 +37,11 @@ export default function redwoodPluginVite(): PluginOption[] { .readFileSync(path.join(rwPaths.api.base, 'package.json'), 'utf-8') .includes('@redwoodjs/realtime') + const streamingEnabled = rwConfig.experimental.streamingSsr.enabled + const rscEnabled = rwConfig.experimental.rsc.enabled + return [ + rscEnabled && rscTransformEntryPlugin(), { name: 'redwood-plugin-vite-html-env', @@ -130,7 +135,7 @@ export default function redwoodPluginVite(): PluginOption[] { config: getMergedConfig(rwConfig, rwPaths), }, // We can remove when streaming is stable - rwConfig.experimental.streamingSsr.enabled && swapApolloProvider(), + streamingEnabled && swapApolloProvider(), handleJsAsJsx(), // Remove the splash-page from the bundle. removeFromBundle([ diff --git a/packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts b/packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts new file mode 100644 index 000000000000..7138132aaafa --- /dev/null +++ b/packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts @@ -0,0 +1,38 @@ +import { normalizePath, type Plugin } from 'vite' + +import { getPaths } from '@redwoodjs/project-config' + +export function rscTransformEntryPlugin(): Plugin { + const entryServerPath = normalizePath(getPaths().web.entryServer || '') + const entryClientPath = normalizePath(getPaths().web.entryClient || '') + + const rscWebpackShims = ` +globalThis.__rw_module_cache__ ||= new Map(); + +globalThis.__webpack_chunk_load__ ||= (id) => { + console.log('rscWebpackShims chunk load id', id) + return import(id).then((m) => globalThis.__rw_module_cache__.set(id, m)) +}; + +globalThis.__webpack_require__ ||= (id) => { + console.log('rscWebpackShims require id', id) + return globalThis.__rw_module_cache__.get(id) +};\n` + + return { + name: 'rsc-transform-entry', + transform: async function (code, id) { + if (id.includes('entry')) { + console.log('rsc-transform-entry id', id) + } + + if (id === entryServerPath || id === entryClientPath) { + console.log('rsc-transform-entry id', id) + + return code + rscWebpackShims + } + + return code + }, + } +} diff --git a/packages/vite/src/streaming/streamHelpers.ts b/packages/vite/src/streaming/streamHelpers.ts index 0b4a1f543300..e43c2183ce8f 100644 --- a/packages/vite/src/streaming/streamHelpers.ts +++ b/packages/vite/src/streaming/streamHelpers.ts @@ -18,7 +18,6 @@ import { } from '@redwoodjs/web/dist/components/ServerInject' import type { MiddlewareResponse } from '../middleware/MiddlewareResponse.js' -import { rscWebpackShims } from '../rsc/rscWebpackShims.js' import { createBufferedTransformStream } from './transforms/bufferedTransform.js' import { createTimeoutTransform } from './transforms/cancelTimeoutTransform.js' @@ -89,7 +88,7 @@ export async function reactRenderToStreamResponse( const timeoutTransform = createTimeoutTransform(timeoutHandle) // Possible that we need to upgrade the @types/* packages - // @ts-expect-error Something in React's packages mean types dont come through + // @ts-expect-error Something in React's packages mean types don't come through const { renderToReadableStream } = await import('react-dom/server.edge') const renderRoot = (path: string) => { @@ -125,9 +124,9 @@ export async function reactRenderToStreamResponse( */ const bootstrapOptions = { bootstrapScriptContent: - // Only insert assetMap if clientside JS will be loaded + // Only insert assetMap if client side JS will be loaded jsBundles.length > 0 - ? `window.__REDWOOD__ASSET_MAP = ${assetMap}; ${rscWebpackShims}` + ? `window.__REDWOOD__ASSET_MAP = ${assetMap};` : undefined, bootstrapModules: jsBundles, } From 7b984dcdbc0d77c2dfdaad7aa537f0abc54c7864 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 17 Mar 2024 11:03:58 +0100 Subject: [PATCH 25/34] RSC: Clean up webpack shims (#10246) --- .../src/plugins/vite-plugin-rsc-transform-entry.ts | 6 ------ packages/vite/src/rsc/rscWebpackShims.ts | 11 ----------- 2 files changed, 17 deletions(-) delete mode 100644 packages/vite/src/rsc/rscWebpackShims.ts diff --git a/packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts b/packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts index 7138132aaafa..5538c6cd4af7 100644 --- a/packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts +++ b/packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts @@ -22,13 +22,7 @@ globalThis.__webpack_require__ ||= (id) => { return { name: 'rsc-transform-entry', transform: async function (code, id) { - if (id.includes('entry')) { - console.log('rsc-transform-entry id', id) - } - if (id === entryServerPath || id === entryClientPath) { - console.log('rsc-transform-entry id', id) - return code + rscWebpackShims } diff --git a/packages/vite/src/rsc/rscWebpackShims.ts b/packages/vite/src/rsc/rscWebpackShims.ts deleted file mode 100644 index 60ed3c6510c0..000000000000 --- a/packages/vite/src/rsc/rscWebpackShims.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const rscWebpackShims = `globalThis.__rw_module_cache__ = new Map(); - -globalThis.__webpack_chunk_load__ = (id) => { - console.log('rscWebpackShims chunk load id', id) - return import(id).then((m) => globalThis.__rw_module_cache__.set(id, m)) -}; - -globalThis.__webpack_require__ = (id) => { - console.log('rscWebpackShims require id', id) - return globalThis.__rw_module_cache__.get(id) -};\n` From c0e92efcbbb3ab2439ace37efe3e6c02a6947058 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 17 Mar 2024 12:18:33 +0100 Subject: [PATCH 26/34] RSC: Remove `` component (#10247) --- .../web/src/pages/AboutPage/AboutPage.tsx | 10 -- .../web/src/pages/HomePage/HomePage.tsx | 10 -- .../web/src/pages/AboutPage/AboutPage.tsx | 10 -- .../web/src/pages/HomePage/HomePage.tsx | 11 +- .../src/pages/MultiCellPage/MultiCellPage.tsx | 10 -- .../templates/rsc/AboutPage.tsx.template | 10 -- .../templates/rsc/HomePage.tsx.template | 10 -- packages/vite/package.json | 8 -- .../src/fully-react/DevRwRscServerGlobal.ts | 63 ----------- .../src/fully-react/ProdRwRscServerGlobal.ts | 53 --------- .../vite/src/fully-react/RwRscServerGlobal.ts | 20 ---- packages/vite/src/fully-react/assets.tsx | 82 -------------- packages/vite/src/fully-react/find-styles.ts | 106 ------------------ .../src/fully-react/findAssetsInManifest.ts | 47 -------- packages/vite/src/fully-react/rwRscGlobal.ts | 10 -- 15 files changed, 1 insertion(+), 459 deletions(-) delete mode 100644 packages/vite/src/fully-react/DevRwRscServerGlobal.ts delete mode 100644 packages/vite/src/fully-react/ProdRwRscServerGlobal.ts delete mode 100644 packages/vite/src/fully-react/RwRscServerGlobal.ts delete mode 100644 packages/vite/src/fully-react/assets.tsx delete mode 100644 packages/vite/src/fully-react/find-styles.ts delete mode 100644 packages/vite/src/fully-react/findAssetsInManifest.ts delete mode 100644 packages/vite/src/fully-react/rwRscGlobal.ts diff --git a/__fixtures__/test-project-rsa/web/src/pages/AboutPage/AboutPage.tsx b/__fixtures__/test-project-rsa/web/src/pages/AboutPage/AboutPage.tsx index 2706e12e63db..88044cf3965f 100644 --- a/__fixtures__/test-project-rsa/web/src/pages/AboutPage/AboutPage.tsx +++ b/__fixtures__/test-project-rsa/web/src/pages/AboutPage/AboutPage.tsx @@ -1,20 +1,10 @@ -import { Assets } from '@redwoodjs/vite/assets' -import { ProdRwRscServerGlobal } from '@redwoodjs/vite/rwRscGlobal' - import { AboutCounter } from '../../components/Counter/AboutCounter' import './AboutPage.css' -// TODO (RSC) Something like this will probably be needed -// const RwRscGlobal = import.meta.env.PROD ? ProdRwRscServerGlobal : DevRwRscServerGlobal; - -globalThis.rwRscGlobal = new ProdRwRscServerGlobal() - const AboutPage = () => { return (
- {/* TODO (RSC) should be part of the router later */} -

About Redwood

diff --git a/__fixtures__/test-project-rsa/web/src/pages/HomePage/HomePage.tsx b/__fixtures__/test-project-rsa/web/src/pages/HomePage/HomePage.tsx index aae0c8ed7e73..8d5882eefa07 100644 --- a/__fixtures__/test-project-rsa/web/src/pages/HomePage/HomePage.tsx +++ b/__fixtures__/test-project-rsa/web/src/pages/HomePage/HomePage.tsx @@ -1,6 +1,3 @@ -import { Assets } from '@redwoodjs/vite/assets' -import { ProdRwRscServerGlobal } from '@redwoodjs/vite/rwRscGlobal' - import { onSend } from './chat' import { Form } from './Form' // @ts-expect-error no types @@ -8,16 +5,9 @@ import styles from './HomePage.module.css' import './HomePage.css' -// TODO (RSC) Something like this will probably be needed -// const RwRscGlobal = import.meta.env.PROD ? ProdRwRscServerGlobal : DevRwRscServerGlobal; - -globalThis.rwRscGlobal = new ProdRwRscServerGlobal() - const HomePage = ({ name = 'Anonymous' }) => { return (
- {/* TODO (RSC) should be part of the router later */} -

Hello {name}!!

diff --git a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/AboutPage/AboutPage.tsx b/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/AboutPage/AboutPage.tsx index 2706e12e63db..88044cf3965f 100644 --- a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/AboutPage/AboutPage.tsx +++ b/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/AboutPage/AboutPage.tsx @@ -1,20 +1,10 @@ -import { Assets } from '@redwoodjs/vite/assets' -import { ProdRwRscServerGlobal } from '@redwoodjs/vite/rwRscGlobal' - import { AboutCounter } from '../../components/Counter/AboutCounter' import './AboutPage.css' -// TODO (RSC) Something like this will probably be needed -// const RwRscGlobal = import.meta.env.PROD ? ProdRwRscServerGlobal : DevRwRscServerGlobal; - -globalThis.rwRscGlobal = new ProdRwRscServerGlobal() - const AboutPage = () => { return (
- {/* TODO (RSC) should be part of the router later */} -

About Redwood

diff --git a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/HomePage/HomePage.tsx b/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/HomePage/HomePage.tsx index 383eca179ef4..fb71fcabd787 100644 --- a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/HomePage/HomePage.tsx +++ b/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/HomePage/HomePage.tsx @@ -1,25 +1,16 @@ import { RscForm } from '@tobbe.dev/rsc-test' -import { Assets } from '@redwoodjs/vite/assets' -import { ProdRwRscServerGlobal } from '@redwoodjs/vite/rwRscGlobal' - import { Counter } from '../../components/Counter/Counter' + import { onSend } from './actions' // @ts-expect-error no types import styles from './HomePage.module.css' import './HomePage.css' -// TODO (RSC) Something like this will probably be needed -// const RwRscGlobal = import.meta.env.PROD ? ProdRwRscServerGlobal : DevRwRscServerGlobal; - -globalThis.rwRscGlobal = new ProdRwRscServerGlobal() - const HomePage = ({ name = 'Anonymous' }) => { return (
- {/* TODO (RSC) should be part of the router later */} -

Hello {name}!!

diff --git a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/MultiCellPage/MultiCellPage.tsx b/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/MultiCellPage/MultiCellPage.tsx index e0d517c08701..5a13f7d68964 100644 --- a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/MultiCellPage/MultiCellPage.tsx +++ b/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/pages/MultiCellPage/MultiCellPage.tsx @@ -1,22 +1,12 @@ -import { Assets } from '@redwoodjs/vite/assets' -import { ProdRwRscServerGlobal } from '@redwoodjs/vite/rwRscGlobal' - import { updateRandom } from 'src/components/RandomNumberServerCell/actions' import RandomNumberServerCell from 'src/components/RandomNumberServerCell/RandomNumberServerCell' import { UpdateRandomButton } from 'src/components/RandomNumberServerCell/UpdateRandomButton' import './MultiCellPage.css' -// TODO (RSC) Something like this will probably be needed -// const RwRscGlobal = import.meta.env.PROD ? ProdRwRscServerGlobal : DevRwRscServerGlobal; - -globalThis.rwRscGlobal = new ProdRwRscServerGlobal() - const MultiCellPage = () => { return (
- {/* TODO (RSC) should be part of the router later */} -
diff --git a/packages/cli/src/commands/experimental/templates/rsc/AboutPage.tsx.template b/packages/cli/src/commands/experimental/templates/rsc/AboutPage.tsx.template index 3eedd6b25d06..5ad03a9ecb20 100644 --- a/packages/cli/src/commands/experimental/templates/rsc/AboutPage.tsx.template +++ b/packages/cli/src/commands/experimental/templates/rsc/AboutPage.tsx.template @@ -1,20 +1,10 @@ -import { Assets } from '@redwoodjs/vite/assets' -import { ProdRwRscServerGlobal } from '@redwoodjs/vite/rwRscGlobal' - import { AboutCounter } from 'src/components/Counter/AboutCounter' import './AboutPage.css' -// TODO (RSC) Something like this will probably be needed -// const RwRscGlobal = import.meta.env.PROD ? ProdRwRscServerGlobal : DevRwRscServerGlobal; - -globalThis.rwRscGlobal = new ProdRwRscServerGlobal() - const AboutPage = () => { return (
- {/* TODO (RSC) should be part of the router later */} -

About Redwood

diff --git a/packages/cli/src/commands/experimental/templates/rsc/HomePage.tsx.template b/packages/cli/src/commands/experimental/templates/rsc/HomePage.tsx.template index 3163079aacd2..8acaa4eab684 100644 --- a/packages/cli/src/commands/experimental/templates/rsc/HomePage.tsx.template +++ b/packages/cli/src/commands/experimental/templates/rsc/HomePage.tsx.template @@ -1,22 +1,12 @@ -import { Assets } from '@redwoodjs/vite/assets' -import { ProdRwRscServerGlobal } from '@redwoodjs/vite/rwRscGlobal' - import { Counter } from 'src/components/Counter' // @ts-expect-error no types import styles from './HomePage.module.css' import './HomePage.css' -// TODO (RSC) Something like this will probably be needed -// const RwRscGlobal = import.meta.env.PROD ? ProdRwRscServerGlobal : DevRwRscServerGlobal; - -globalThis.rwRscGlobal = new ProdRwRscServerGlobal() - const HomePage = ({ name = 'Anonymous' }) => { return (
- {/* TODO (RSC) should be part of the router later */} -

Hello {name}!!

This is a server component.

diff --git a/packages/vite/package.json b/packages/vite/package.json index b05668007e38..b246bfcbca55 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -22,14 +22,6 @@ "types": "./dist/client.d.ts", "default": "./dist/client.js" }, - "./assets": { - "types": "./dist/fully-react/assets.d.ts", - "default": "./dist/fully-react/assets.js" - }, - "./rwRscGlobal": { - "types": "./dist/fully-react/rwRscGlobal.d.ts", - "default": "./dist/fully-react/rwRscGlobal.js" - }, "./buildFeServer": { "types": "./dist/buildFeServer.d.ts", "default": "./dist/buildFeServer.js" diff --git a/packages/vite/src/fully-react/DevRwRscServerGlobal.ts b/packages/vite/src/fully-react/DevRwRscServerGlobal.ts deleted file mode 100644 index c704ab740620..000000000000 --- a/packages/vite/src/fully-react/DevRwRscServerGlobal.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { relative } from 'node:path' - -import { lazy } from 'react' - -import { getPaths } from '@redwoodjs/project-config' - -import { collectStyles } from './find-styles.js' -import { RwRscServerGlobal } from './RwRscServerGlobal.js' - -// import viteDevServer from '../dev-server' -const viteDevServer: any = {} - -export class DevRwRscServerGlobal extends RwRscServerGlobal { - /** @type {import('vite').ViteDevServer} */ - viteServer - - constructor() { - super() - this.viteServer = viteDevServer - // this.routeManifest = viteDevServer.routesManifest - } - - bootstrapModules() { - // return [`/@fs${import.meta.env.CLIENT_ENTRY}`] - // TODO (RSC) No idea if this is correct or even what format CLIENT_ENTRY has. - return [`/@fs${getPaths().web.entryClient}`] - } - - bootstrapScriptContent() { - return undefined - } - - async loadModule(id: string) { - return await viteDevServer.ssrLoadModule(id) - } - - lazyComponent(id: string) { - const importPath = `/@fs${id}` - return lazy( - async () => - await this.viteServer.ssrLoadModule(/* @vite-ignore */ importPath), - ) - } - - chunkId(chunk: string) { - // return relative(this.srcAppRoot, chunk) - return relative(getPaths().web.src, chunk) - } - - async findAssetsForModules(modules: string[]) { - const styles = await collectStyles( - this.viteServer, - modules.filter((i) => !!i), - ) - - return [...Object.entries(styles ?? {}).map(([key, _value]) => key)] - } - - async findAssets() { - const deps = this.getDependenciesForURL('/') - return await this.findAssetsForModules(deps) - } -} diff --git a/packages/vite/src/fully-react/ProdRwRscServerGlobal.ts b/packages/vite/src/fully-react/ProdRwRscServerGlobal.ts deleted file mode 100644 index 1d13a78a681f..000000000000 --- a/packages/vite/src/fully-react/ProdRwRscServerGlobal.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { readFileSync } from 'node:fs' -import { join, relative } from 'node:path' - -import type { Manifest as BuildManifest } from 'vite' - -import { getPaths } from '@redwoodjs/project-config' - -import { findAssetsInManifest } from './findAssetsInManifest.js' -import { RwRscServerGlobal } from './RwRscServerGlobal.js' - -function readJSON(path: string) { - return JSON.parse(readFileSync(path, 'utf-8')) -} - -export class ProdRwRscServerGlobal extends RwRscServerGlobal { - serverManifest: BuildManifest - - constructor() { - super() - - const rwPaths = getPaths() - - this.serverManifest = readJSON( - join(rwPaths.web.distRsc, 'server-build-manifest.json'), - ) - } - - chunkId(chunk: string) { - return relative(getPaths().web.src, chunk) - } - - async findAssetsForModules(modules: string[]) { - return modules?.map((i) => this.findAssetsForModule(i)).flat() ?? [] - } - - findAssetsForModule(module: string) { - return [ - ...findAssetsInManifest(this.serverManifest, module).filter( - (asset) => !asset.endsWith('.js') && !asset.endsWith('.mjs'), - ), - ] - } - - async findAssets(): Promise { - // TODO (RSC) This is a hack. We need to figure out how to get the - // dependencies for the current page. - const deps = Object.keys(this.serverManifest).filter((name) => - /\.(tsx|jsx|js)$/.test(name), - ) - - return await this.findAssetsForModules(deps) - } -} diff --git a/packages/vite/src/fully-react/RwRscServerGlobal.ts b/packages/vite/src/fully-react/RwRscServerGlobal.ts deleted file mode 100644 index 4240d232c9c9..000000000000 --- a/packages/vite/src/fully-react/RwRscServerGlobal.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { lazy } from 'react' - -export class RwRscServerGlobal { - async loadModule(id: string) { - return await import(/* @vite-ignore */ id) - } - - lazyComponent(id: string) { - return lazy(() => this.loadModule(id)) - } - - // Will be implemented by subclasses - async findAssets(_id: string): Promise { - return [] - } - - getDependenciesForURL(_route: string): string[] { - return [] - } -} diff --git a/packages/vite/src/fully-react/assets.tsx b/packages/vite/src/fully-react/assets.tsx deleted file mode 100644 index 6aa11011a68c..000000000000 --- a/packages/vite/src/fully-react/assets.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// Copied from -// https://github.com/nksaraf/fully-react/blob/4f738132a17d94486c8da19d8729044c3998fc54/packages/fully-react/src/shared/assets.tsx -// And then modified to work with our codebase - -import React, { use } from 'react' - -const linkProps = [ - ['js', { rel: 'modulepreload', crossOrigin: '' }], - ['jsx', { rel: 'modulepreload', crossOrigin: '' }], - ['ts', { rel: 'modulepreload', crossOrigin: '' }], - ['tsx', { rel: 'modulepreload', crossOrigin: '' }], - ['css', { rel: 'stylesheet', precedence: 'high' }], - ['woff', { rel: 'preload', as: 'font', type: 'font/woff', crossOrigin: '' }], - [ - 'woff2', - { rel: 'preload', as: 'font', type: 'font/woff2', crossOrigin: '' }, - ], - ['gif', { rel: 'preload', as: 'image', type: 'image/gif' }], - ['jpg', { rel: 'preload', as: 'image', type: 'image/jpeg' }], - ['jpeg', { rel: 'preload', as: 'image', type: 'image/jpeg' }], - ['png', { rel: 'preload', as: 'image', type: 'image/png' }], - ['webp', { rel: 'preload', as: 'image', type: 'image/webp' }], - ['svg', { rel: 'preload', as: 'image', type: 'image/svg+xml' }], - ['ico', { rel: 'preload', as: 'image', type: 'image/x-icon' }], - ['avif', { rel: 'preload', as: 'image', type: 'image/avif' }], - ['mp4', { rel: 'preload', as: 'video', type: 'video/mp4' }], - ['webm', { rel: 'preload', as: 'video', type: 'video/webm' }], -] as const - -type Linkprop = (typeof linkProps)[number][1] - -const linkPropsMap = new Map(linkProps) - -/** - * Generates a link tag for a given file. This will load stylesheets and preload - * everything else. It uses the file extension to determine the type. - */ -export const Asset = ({ file }: { file: string }) => { - const ext = file.split('.').pop() - const props = ext ? linkPropsMap.get(ext) : null - - if (!props) { - return null - } - - return -} - -export function Assets() { - // TODO (RSC) Currently we only handle server assets. - // Will probably need to handle client assets as well. - // Do we also need special code for SSR? - // if (isClient) return - - return -} - -const findAssets = async () => { - return [...new Set([...(await rwRscGlobal.findAssets(''))]).values()] -} - -const AssetList = ({ assets }: { assets: string[] }) => { - return ( - <> - {assets.map((asset) => { - return - })} - - ) -} - -async function ServerAssets() { - const allAssets = await findAssets() - - return -} - -export function ClientAssets() { - const allAssets = use(findAssets()) - - return -} diff --git a/packages/vite/src/fully-react/find-styles.ts b/packages/vite/src/fully-react/find-styles.ts deleted file mode 100644 index 6c94bf4e5eed..000000000000 --- a/packages/vite/src/fully-react/find-styles.ts +++ /dev/null @@ -1,106 +0,0 @@ -import path from 'node:path' - -import type { ModuleNode, ViteDevServer } from 'vite' - -async function find_deps( - vite: ViteDevServer, - node: ModuleNode, - deps: Set, -) { - // since `ssrTransformResult.deps` contains URLs instead of `ModuleNode`s, this process is asynchronous. - // instead of using `await`, we resolve all branches in parallel. - const branches: Promise[] = [] - - async function add(node: ModuleNode) { - if (!deps.has(node)) { - deps.add(node) - await find_deps(vite, node, deps) - } - } - - async function add_by_url(url: string) { - const node = await vite.moduleGraph.getModuleByUrl(url) - - if (node) { - await add(node) - } - } - - if (node.ssrTransformResult) { - if (node.ssrTransformResult.deps) { - node.ssrTransformResult.deps.forEach((url) => - branches.push(add_by_url(url)), - ) - } - - // if (node.ssrTransformResult.dynamicDeps) { - // node.ssrTransformResult.dynamicDeps.forEach(url => branches.push(add_by_url(url))); - // } - } else { - node.importedModules.forEach((node) => branches.push(add(node))) - } - - await Promise.all(branches) -} - -// Vite doesn't expose this so we just copy the list for now -// https://github.com/vitejs/vite/blob/3edd1af56e980aef56641a5a51cf2932bb580d41/packages/vite/src/node/plugins/css.ts#L96 -const style_pattern = /\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/ -// TODO (RSC) fully-react didn't use this anywhere. But do we need it for module support? -// const module_style_pattern = -// /\.module\.(css|less|sass|scss|styl|stylus|pcss|postcss)$/ - -export async function collectStyles(devServer: ViteDevServer, match: string[]) { - const styles: { [key: string]: string } = {} - const deps = new Set() - try { - for (const file of match) { - const resolvedId = await devServer.pluginContainer.resolveId(file) - - if (!resolvedId) { - console.log('not found') - continue - } - - const id = resolvedId.id - - const normalizedPath = path.resolve(id).replace(/\\/g, '/') - let node = devServer.moduleGraph.getModuleById(normalizedPath) - if (!node) { - const absolutePath = path.resolve(file) - await devServer.ssrLoadModule(absolutePath) - node = await devServer.moduleGraph.getModuleByUrl(absolutePath) - - if (!node) { - console.log('not found') - return - } - } - - await find_deps(devServer, node, deps) - } - } catch (e) { - console.error(e) - } - - for (const dep of deps) { - // const parsed = new URL(dep.url, 'http://localhost/') - // const query = parsed.searchParams - - if (style_pattern.test(dep.file ?? '')) { - try { - const mod = await devServer.ssrLoadModule(dep.url) - // if (module_style_pattern.test(dep.file)) { - // styles[dep.url] = env.cssModules?.[dep.file]; - // } else { - styles[dep.url] = mod.default - // } - } catch { - // this can happen with dynamically imported modules, I think - // because the Vite module graph doesn't distinguish between - // static and dynamic imports? TODO investigate, submit fix - } - } - } - return styles -} diff --git a/packages/vite/src/fully-react/findAssetsInManifest.ts b/packages/vite/src/fully-react/findAssetsInManifest.ts deleted file mode 100644 index 0f0b47cffe1d..000000000000 --- a/packages/vite/src/fully-react/findAssetsInManifest.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Manifest as BuildManifest } from 'vite' - -/** - * Traverses the module graph and collects assets for a given chunk - * - * @param manifest Client manifest - * @param id Chunk id - * @returns Array of asset URLs - */ -export const findAssetsInManifest = ( - manifest: BuildManifest, - id: string, -): Array => { - // TODO (RSC) Can we take assetMap as a parameter to reuse it across calls? - // It's what the original implementation of this function does. But no - // callers pass it in where we currently use this function. - const assetMap: Map> = new Map() - - function traverse(id: string): Array { - const cached = assetMap.get(id) - if (cached) { - return cached - } - - const chunk = manifest[id] - if (!chunk) { - return [] - } - - const assets = [ - ...(chunk.assets || []), - ...(chunk.css || []), - ...(chunk.imports?.flatMap(traverse) || []), - ] - const imports = chunk.imports?.flatMap(traverse) || [] - const all = [...assets, ...imports].filter( - Boolean as unknown as (a: string | undefined) => a is string, - ) - - all.push(chunk.file) - assetMap.set(id, all) - - return Array.from(new Set(all)) - } - - return traverse(id) -} diff --git a/packages/vite/src/fully-react/rwRscGlobal.ts b/packages/vite/src/fully-react/rwRscGlobal.ts deleted file mode 100644 index 688a48f305de..000000000000 --- a/packages/vite/src/fully-react/rwRscGlobal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RwRscServerGlobal } from './RwRscServerGlobal.js' -export { RwRscServerGlobal } from './RwRscServerGlobal.js' -export { DevRwRscServerGlobal } from './DevRwRscServerGlobal.js' -export { ProdRwRscServerGlobal } from './ProdRwRscServerGlobal.js' -export type AssetDesc = string | { type: 'style'; style: string; src?: string } - -declare global { - /* eslint-disable no-var */ - var rwRscGlobal: RwRscServerGlobal -} From c52cdb418c5e6c07540ab4fb6b3f4df22d282013 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 17 Mar 2024 12:43:44 +0100 Subject: [PATCH 27/34] RSC: Don't edit index.html during setup (#10248) --- .../test-project-rsa/web/src/index.html | 16 ------------ .../web/src/index.html | 16 ------------ .../commands/experimental/setupRscHandler.js | 25 ------------------- 3 files changed, 57 deletions(-) delete mode 100644 __fixtures__/test-project-rsa/web/src/index.html delete mode 100644 __fixtures__/test-project-rsc-external-packages-and-cells/web/src/index.html diff --git a/__fixtures__/test-project-rsa/web/src/index.html b/__fixtures__/test-project-rsa/web/src/index.html deleted file mode 100644 index 6b3b066be037..000000000000 --- a/__fixtures__/test-project-rsa/web/src/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - -
- - - diff --git a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/index.html b/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/index.html deleted file mode 100644 index 6b3b066be037..000000000000 --- a/__fixtures__/test-project-rsc-external-packages-and-cells/web/src/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - -
- - - diff --git a/packages/cli/src/commands/experimental/setupRscHandler.js b/packages/cli/src/commands/experimental/setupRscHandler.js index d1935ebe264d..569fef993ff5 100644 --- a/packages/cli/src/commands/experimental/setupRscHandler.js +++ b/packages/cli/src/commands/experimental/setupRscHandler.js @@ -250,31 +250,6 @@ export const handler = async ({ force, verbose }) => { writeFile(cssPath, cssTemplate, { overwriteExisting: force }) }, }, - { - title: 'Updating index.html...', - task: async () => { - let indexHtml = fs.readFileSync(rwPaths.web.html, 'utf-8') - - if ( - /\n\s*', - ) - - writeFile(rwPaths.web.html, indexHtml, { - overwriteExisting: true, - }) - }, - }, { title: 'Overwriting index.css...', task: async () => { From 8d78142540d9735eacf09f619e1e27474a2822e9 Mon Sep 17 00:00:00 2001 From: Dani <158021342+LAdanimo@users.noreply.github.com> Date: Sun, 17 Mar 2024 05:18:05 -0700 Subject: [PATCH 28/34] Update first-test.md (#10244) Fixed minor typos --- docs/docs/tutorial/chapter5/first-test.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/tutorial/chapter5/first-test.md b/docs/docs/tutorial/chapter5/first-test.md index f5d13ed06fe4..55bbd4d24ec0 100644 --- a/docs/docs/tutorial/chapter5/first-test.md +++ b/docs/docs/tutorial/chapter5/first-test.md @@ -208,7 +208,7 @@ When trying to find the *full* text of the body, it should *not* be present. ```javascript expect(matchedBody).toBeInTheDocument() ``` -Assert that the truncated text is . +Assert that the truncated text is present. ```javascript expect(ellipsis).toBeInTheDocument() @@ -227,7 +227,7 @@ To double check that we're testing what we think we're testing, open up `Article ### What's the Deal with Mocks? -Did you wonder where the articles were coming from in our test? Was it the development database? Nope: that data came from a **Mock**. That's the `ArticlesCell.mock.js` file that lives next to your component, test and stories files. Mocks are used when you want to define the data that would normally be returned by GraphQL in your Storybook stories or tests. In cells, a GraphQL call goes out (the query defined by the variable `QUERY` at the top of the file) and returned to the `Success` component. We don't want to have to run the api-side server and have real data in the database just for Storybook or our tests, so Redwood intercepts those GraphQL calls and returns the data from the mock instead. +Did you wonder where the articles were coming from in our test? Was it the development database? Nope: that data came from a **Mock**. That's the `ArticlesCell.mock.js` file that lives next to your component, test and stories files. Mocks are used when you want to define the data that would normally be returned by GraphQL in your Storybook stories or tests. In cells, a GraphQL call goes out (the query defined by the variable `QUERY` at the top of the file) and is returned to the `Success` component. We don't want to have to run the api-side server and have real data in the database just for Storybook or our tests, so Redwood intercepts those GraphQL calls and returns the data from the mock instead. :::info If the server is being mocked, how do we test the api-side code? From acbc56d47e45c8d098b386751bb9f83222ba29f9 Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Sun, 17 Mar 2024 17:14:51 +0100 Subject: [PATCH 29/34] RSC: Differentiate routes autoloading behaviour (#10241) --- packages/babel-config/src/index.ts | 3 + ...wood-routes-auto-loader-rsc-client.test.ts | 100 ++++++++++++ ...wood-routes-auto-loader-rsc-server.test.ts | 98 +++++++++++ ...-plugin-redwood-routes-auto-loader.test.ts | 49 ------ ...n-redwood-routes-auto-loader-rsc-client.ts | 120 ++++++++++++++ ...n-redwood-routes-auto-loader-rsc-server.ts | 119 ++++++++++++++ ...babel-plugin-redwood-routes-auto-loader.ts | 153 +++++++----------- packages/babel-config/src/web.ts | 11 +- packages/vite/package.json | 4 + packages/vite/src/buildFeServer.ts | 2 +- packages/vite/src/client.ts | 1 + packages/vite/src/clientSsr.ts | 3 + packages/vite/src/devFeServer.ts | 24 +++ packages/vite/src/index.ts | 17 +- packages/vite/src/rsc/rscBuildAnalyze.ts | 8 + packages/vite/src/rsc/rscBuildClient.ts | 21 +++ packages/vite/src/rsc/rscBuildForServer.ts | 19 +++ packages/vite/src/rsc/rscWorker.ts | 20 ++- .../src/streaming/buildForStreamingServer.ts | 24 +++ 19 files changed, 637 insertions(+), 159 deletions(-) create mode 100644 packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-client.test.ts create mode 100644 packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-server.test.ts create mode 100644 packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-client.ts create mode 100644 packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-server.ts create mode 100644 packages/vite/src/clientSsr.ts diff --git a/packages/babel-config/src/index.ts b/packages/babel-config/src/index.ts index 7658f3e7a67c..2b51277bd605 100644 --- a/packages/babel-config/src/index.ts +++ b/packages/babel-config/src/index.ts @@ -34,3 +34,6 @@ export { parseTypeScriptConfigFiles, registerBabel, } from './common' + +export { redwoodRoutesAutoLoaderRscClientPlugin } from './plugins/babel-plugin-redwood-routes-auto-loader-rsc-client' +export { redwoodRoutesAutoLoaderRscServerPlugin } from './plugins/babel-plugin-redwood-routes-auto-loader-rsc-server' diff --git a/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-client.test.ts b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-client.test.ts new file mode 100644 index 000000000000..2632b6ca5ef3 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-client.test.ts @@ -0,0 +1,100 @@ +import fs from 'fs' +import path from 'path' + +import * as babel from '@babel/core' + +import { getPaths } from '@redwoodjs/project-config' + +import { redwoodRoutesAutoLoaderRscClientPlugin } from '../babel-plugin-redwood-routes-auto-loader-rsc-client' + +const transform = (filename: string) => { + const code = fs.readFileSync(filename, 'utf-8') + return babel.transform(code, { + filename, + presets: ['@babel/preset-react'], + plugins: [[redwoodRoutesAutoLoaderRscClientPlugin, {}]], + }) +} + +describe('injects the correct loading logic', () => { + const RSC_FIXTURE_PATH = path.resolve( + __dirname, + '../../../../../__fixtures__/test-project-rsc-external-packages-and-cells/', + ) + let result: babel.BabelFileResult | null + + beforeAll(() => { + process.env.RWJS_CWD = RSC_FIXTURE_PATH + result = transform(getPaths().web.routes) + }) + + afterAll(() => { + delete process.env.RWJS_CWD + }) + + test('pages are loaded with renderFromRscServer', () => { + const codeOutput = result?.code + + // We shouldn't see classic lazy loading like in non-RSC redwood + expect(codeOutput).not.toContain(`const HomePage = { + name: "HomePage", + prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage")), + LazyComponent: lazy(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage")) +`) + + // We should import the function + expect(codeOutput).toContain( + 'import { renderFromRscServer } from "@redwoodjs/vite/client"', + ) + + // Un-imported pages get added with renderFromRscServer + expect(codeOutput).toContain( + 'const HomePage = renderFromRscServer("HomePage")', + ) + expect(codeOutput).toContain( + 'const AboutPage = renderFromRscServer("AboutPage")', + ) + expect(codeOutput).toContain( + 'const UserExampleNewUserExamplePage = renderFromRscServer("UserExampleNewUserExamplePage")', + ) + }) + + test('already imported pages are left alone.', () => { + expect(result?.code).toContain( + `import NotFoundPage from './pages/NotFoundPage/NotFoundPage'`, + ) + + expect(result?.code).not.toContain( + `const NotFoundPage = renderFromRscServer("NotFoundPage")`, + ) + }) +}) + +describe('mulitiple files ending in Page.{js,jsx,ts,tsx}', () => { + const FAILURE_FIXTURE_PATH = path.resolve( + __dirname, + './__fixtures__/route-auto-loader/failure', + ) + + beforeAll(() => { + process.env.RWJS_CWD = FAILURE_FIXTURE_PATH + }) + + afterAll(() => { + delete process.env.RWJS_CWD + }) + + test('fails with appropriate message', () => { + expect(() => { + transform(getPaths().web.routes) + }).toThrow( + "Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage", + ) + + expect(() => { + transform(getPaths().web.routes) + }).toThrow( + "Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage", + ) + }) +}) diff --git a/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-server.test.ts b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-server.test.ts new file mode 100644 index 000000000000..2283607c92ce --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-server.test.ts @@ -0,0 +1,98 @@ +import fs from 'fs' +import path from 'path' + +import * as babel from '@babel/core' + +import { getPaths } from '@redwoodjs/project-config' + +import { redwoodRoutesAutoLoaderRscServerPlugin } from '../babel-plugin-redwood-routes-auto-loader-rsc-server' + +const transform = (filename: string) => { + const code = fs.readFileSync(filename, 'utf-8') + return babel.transform(code, { + filename, + presets: ['@babel/preset-react'], + plugins: [[redwoodRoutesAutoLoaderRscServerPlugin, {}]], + }) +} + +const RSC_FIXTURE_PATH = path.resolve( + __dirname, + '../../../../../__fixtures__/test-project-rsc-external-packages-and-cells/', +) + +describe('injects the correct loading logic', () => { + let result: babel.BabelFileResult | null + beforeAll(() => { + process.env.RWJS_CWD = RSC_FIXTURE_PATH + result = transform(getPaths().web.routes) + }) + + afterAll(() => { + delete process.env.RWJS_CWD + }) + + test('Pages are loaded with renderFromDist', () => { + const codeOutput = result?.code + + // We shouldn't see classic lazy loading like in non-RSC redwood + expect(codeOutput).not.toContain(`const HomePage = { + name: "HomePage", + prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage")), + LazyComponent: lazy(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage")) +`) + + // We should import the function + expect(codeOutput).toContain( + 'import { renderFromDist } from "@redwoodjs/vite/clientSsr"', + ) + + // Un-imported pages get added with renderFromDist + expect(codeOutput).toContain('const HomePage = renderFromDist("HomePage")') + expect(codeOutput).toContain( + 'const AboutPage = renderFromDist("AboutPage")', + ) + expect(codeOutput).toContain( + 'const UserExampleNewUserExamplePage = renderFromDist("UserExampleNewUserExamplePage")', + ) + }) + + test('already imported pages are left alone.', () => { + expect(result?.code).toContain( + `import NotFoundPage from './pages/NotFoundPage/NotFoundPage'`, + ) + + expect(result?.code).not.toContain( + `const NotFoundPage = renderFromDist("NotFoundPage")`, + ) + }) +}) + +describe('mulitiple files ending in Page.{js,jsx,ts,tsx}', () => { + const FAILURE_FIXTURE_PATH = path.resolve( + __dirname, + './__fixtures__/route-auto-loader/failure', + ) + + beforeAll(() => { + process.env.RWJS_CWD = FAILURE_FIXTURE_PATH + }) + + afterAll(() => { + delete process.env.RWJS_CWD + }) + + test('fails with appropriate message', () => { + expect(() => { + transform(getPaths().web.routes) + }).toThrow( + "Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage", + ) + + expect(() => { + transform(getPaths().web.routes) + }).toThrow( + "Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage", + ) + }) +}) diff --git a/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader.test.ts b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader.test.ts index d4403ba3b7cb..d4c1226a34a0 100644 --- a/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader.test.ts +++ b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader.test.ts @@ -78,52 +78,3 @@ describe('page auto loader correctly imports pages', () => { ) }) }) - -describe('page auto loader handles imports for RSC', () => { - const FIXTURE_PATH = path.resolve( - __dirname, - '../../../../../__fixtures__/example-todo-main/', - ) - - let result: babel.BabelFileResult | null - - beforeAll(() => { - process.env.RWJS_CWD = FIXTURE_PATH - result = transform(getPaths().web.routes, { forRscClient: true }) - }) - - afterAll(() => { - delete process.env.RWJS_CWD - }) - - test('Pages are loaded with renderFromRscServer', () => { - const codeOutput = result?.code - expect(codeOutput).not.toContain(`const HomePage = { - name: "HomePage", - prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage")), - LazyComponent: lazy(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage")) -`) - - expect(codeOutput).toContain( - 'import { renderFromRscServer } from "@redwoodjs/vite/client"', - ) - - expect(codeOutput).toContain( - 'const HomePage = renderFromRscServer("HomePage")', - ) - - // Un-imported pages get added with renderFromRscServer - // so it calls the RSC worker to get a flight response - expect(codeOutput).toContain( - 'const HomePage = renderFromRscServer("HomePage")', - ) - expect(codeOutput).toContain( - 'const BarPage = renderFromRscServer("BarPage")', - ) - }) - - // TODO(RSC): Figure out what the behavior should be? - test('Already imported pages are left alone.', () => { - expect(result?.code).toContain(`import FooPage from 'src/pages/FooPage'`) - }) -}) diff --git a/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-client.ts b/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-client.ts new file mode 100644 index 000000000000..7786d9d04b74 --- /dev/null +++ b/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-client.ts @@ -0,0 +1,120 @@ +import type { PluginObj, types } from '@babel/core' + +import { + ensurePosixPath, + importStatementPath, + processPagesDir, +} from '@redwoodjs/project-config' + +import { + getPathRelativeToSrc, + withRelativeImports, +} from './babel-plugin-redwood-routes-auto-loader' + +export function redwoodRoutesAutoLoaderRscClientPlugin({ + types: t, +}: { + types: typeof types +}): PluginObj { + // @NOTE: This var gets mutated inside the visitors + let pages = processPagesDir().map(withRelativeImports) + + // Currently processPagesDir() can return duplicate entries when there are multiple files + // ending in Page in the individual page directories. This will cause an error upstream. + // Here we check for duplicates and throw a more helpful error message. + const duplicatePageImportNames = new Set() + const sortedPageImportNames = pages.map((page) => page.importName).sort() + for (let i = 0; i < sortedPageImportNames.length - 1; i++) { + if (sortedPageImportNames[i + 1] === sortedPageImportNames[i]) { + duplicatePageImportNames.add(sortedPageImportNames[i]) + } + } + + if (duplicatePageImportNames.size > 0) { + throw new Error( + `Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: ${Array.from( + duplicatePageImportNames, + ) + .map((name) => `'${name}'`) + .join(', ')}`, + ) + } + + return { + name: 'babel-plugin-redwood-routes-auto-loader', + visitor: { + // Remove any pages that have been explicitly imported in the Routes file, + // because when one is present, the user is requesting that the module be + // included in the main bundle. + ImportDeclaration(p) { + if (pages.length === 0) { + return + } + + const userImportRelativePath = getPathRelativeToSrc( + importStatementPath(p.node.source?.value), + ) + + const defaultSpecifier = p.node.specifiers.filter((specifiers) => + t.isImportDefaultSpecifier(specifiers), + )[0] + + if (userImportRelativePath && defaultSpecifier) { + // Remove the page from pages list, if it is already explicitly imported, so that we don't add loaders for these pages. + // We use the path & defaultSpecifier because the const name could be anything + pages = pages.filter( + (page) => + !( + page.relativeImport === ensurePosixPath(userImportRelativePath) + ), + ) + } + }, + Program: { + enter() { + pages = processPagesDir().map(withRelativeImports) + }, + exit(p) { + if (pages.length === 0) { + return + } + const nodes = [] + + // For RSC Client builds add + // import { renderFromRscServer } from '@redwoodjs/vite/client' + // This will perform a fetch request to the remote RSC server + nodes.unshift( + t.importDeclaration( + [ + t.importSpecifier( + t.identifier('renderFromRscServer'), + t.identifier('renderFromRscServer'), + ), + ], + t.stringLiteral('@redwoodjs/vite/client'), + ), + ) + + // Prepend all imports to the top of the file + for (const { importName } of pages) { + // RSC client wants this format + // const AboutPage = renderFromRscServer('AboutPage') + nodes.push( + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier(importName), + t.callExpression(t.identifier('renderFromRscServer'), [ + t.stringLiteral(importName), + ]), + ), + ]), + ) + } + + // Insert at the top of the file + p.node.body.unshift(...nodes) + }, + }, + }, + } +} diff --git a/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-server.ts b/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-server.ts new file mode 100644 index 000000000000..5ed732018c21 --- /dev/null +++ b/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-server.ts @@ -0,0 +1,119 @@ +import type { PluginObj, types } from '@babel/core' + +import { + ensurePosixPath, + importStatementPath, + processPagesDir, +} from '@redwoodjs/project-config' + +import { + getPathRelativeToSrc, + withRelativeImports, +} from './babel-plugin-redwood-routes-auto-loader' + +export function redwoodRoutesAutoLoaderRscServerPlugin({ + types: t, +}: { + types: typeof types +}): PluginObj { + // @NOTE: This var gets mutated inside the visitors + let pages = processPagesDir().map(withRelativeImports) + + // Currently processPagesDir() can return duplicate entries when there are multiple files + // ending in Page in the individual page directories. This will cause an error upstream. + // Here we check for duplicates and throw a more helpful error message. + const duplicatePageImportNames = new Set() + const sortedPageImportNames = pages.map((page) => page.importName).sort() + for (let i = 0; i < sortedPageImportNames.length - 1; i++) { + if (sortedPageImportNames[i + 1] === sortedPageImportNames[i]) { + duplicatePageImportNames.add(sortedPageImportNames[i]) + } + } + if (duplicatePageImportNames.size > 0) { + throw new Error( + `Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: ${Array.from( + duplicatePageImportNames, + ) + .map((name) => `'${name}'`) + .join(', ')}`, + ) + } + + return { + name: 'babel-plugin-redwood-routes-auto-loader', + visitor: { + // Remove any pages that have been explicitly imported in the Routes file, + // because when one is present, the user is requesting that the module be + // included in the main bundle. + ImportDeclaration(p) { + if (pages.length === 0) { + return + } + + const userImportRelativePath = getPathRelativeToSrc( + importStatementPath(p.node.source?.value), + ) + + const defaultSpecifier = p.node.specifiers.filter((specifiers) => + t.isImportDefaultSpecifier(specifiers), + )[0] + + if (userImportRelativePath && defaultSpecifier) { + // Remove the page from pages list, if it is already explicitly imported, so that we don't add loaders for these pages. + // We use the path & defaultSpecifier because the const name could be anything + pages = pages.filter( + (page) => + !( + page.relativeImport === ensurePosixPath(userImportRelativePath) + ), + ) + } + }, + Program: { + enter() { + pages = processPagesDir().map(withRelativeImports) + }, + exit(p) { + if (pages.length === 0) { + return + } + const nodes = [] + + // For RSC Server builds add + // import { renderFromDist } from '@redwoodjs/vite/clientSsr' + // This will directly read the component from the dist folder + nodes.unshift( + t.importDeclaration( + [ + t.importSpecifier( + t.identifier('renderFromDist'), + t.identifier('renderFromDist'), + ), + ], + t.stringLiteral('@redwoodjs/vite/clientSsr'), + ), + ) + + // Prepend all imports to the top of the file + for (const { importName } of pages) { + // RSC server wants this format + // const AboutPage = renderFromDist('AboutPage') + nodes.push( + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier(importName), + t.callExpression(t.identifier('renderFromDist'), [ + t.stringLiteral(importName), + ]), + ), + ]), + ) + } + + // Insert at the top of the file + p.node.body.unshift(...nodes) + }, + }, + }, + } +} diff --git a/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader.ts b/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader.ts index fc342f2fd317..ac5e26e7f6ab 100644 --- a/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader.ts +++ b/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader.ts @@ -13,7 +13,6 @@ import { export interface PluginOptions { forPrerender?: boolean forVite?: boolean - forRscClient?: boolean } /** @@ -21,7 +20,7 @@ export interface PluginOptions { * For dev/build/prerender (forJest == false): 'src/pages/ExamplePage' -> './pages/ExamplePage' * For test (forJest == true): 'src/pages/ExamplePage' -> '/Users/blah/pathToProject/web/src/pages/ExamplePage' */ -const getPathRelativeToSrc = (maybeAbsolutePath: string) => { +export const getPathRelativeToSrc = (maybeAbsolutePath: string) => { // If the path is already relative if (!path.isAbsolute(maybeAbsolutePath)) { return maybeAbsolutePath @@ -30,7 +29,7 @@ const getPathRelativeToSrc = (maybeAbsolutePath: string) => { return `./${path.relative(getPaths().web.src, maybeAbsolutePath)}` } -const withRelativeImports = (page: PagesDependency) => { +export const withRelativeImports = (page: PagesDependency) => { return { ...page, relativeImport: ensurePosixPath(getPathRelativeToSrc(page.importPath)), @@ -39,11 +38,7 @@ const withRelativeImports = (page: PagesDependency) => { export default function ( { types: t }: { types: typeof types }, - { - forPrerender = false, - forVite = false, - forRscClient = false, - }: PluginOptions, + { forPrerender = false, forVite = false }: PluginOptions, ): PluginObj { // @NOTE: This var gets mutated inside the visitors let pages = processPagesDir().map(withRelativeImports) @@ -150,102 +145,70 @@ export default function ( ), ) - // For RSC Client builds add - // import { renderFromRscServer } from '@redwoodjs/vite/client' - if (forRscClient) { - nodes.unshift( - t.importDeclaration( - [ - t.importSpecifier( - t.identifier('renderFromRscServer'), - t.identifier('renderFromRscServer'), - ), - ], - t.stringLiteral('@redwoodjs/vite/client'), - ), - ) - } - // Prepend all imports to the top of the file for (const { importName, relativeImport } of pages) { const importArgument = t.stringLiteral(relativeImport) - if (forRscClient) { - // rsc CLIENT wants this format - // const AboutPage = renderFromRscServer('AboutPage') - // this basically allows the page to be rendered via flight response - nodes.push( - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(importName), - t.callExpression(t.identifier('renderFromRscServer'), [ - t.stringLiteral(importName), - ]), - ), - ]), - ) - } else { - // const = { - // name: , - // prerenderLoader: (name) => prerenderLoaderImpl - // LazyComponent: lazy(() => import(/* webpackChunkName: "..." */ ) - // } + // const = { + // name: , + // prerenderLoader: (name) => prerenderLoaderImpl + // LazyComponent: lazy(() => import(/* webpackChunkName: "..." */ ) + // } - // - // Real example - // const LoginPage = { - // name: "LoginPage", - // prerenderLoader: () => __webpack_require__(require.resolveWeak("./pages/LoginPage/LoginPage")), - // LazyComponent: lazy(() => import("/* webpackChunkName: "LoginPage" *//pages/LoginPage/LoginPage.tsx")) - // } - // - importArgument.leadingComments = [ - { - type: 'CommentBlock', - value: ` webpackChunkName: "${importName}" `, - }, - ] + // + // Real example + // const LoginPage = { + // name: "LoginPage", + // prerenderLoader: () => __webpack_require__(require.resolveWeak("./pages/LoginPage/LoginPage")), + // LazyComponent: lazy(() => import("/* webpackChunkName: "LoginPage" *//pages/LoginPage/LoginPage.tsx")) + // } + // + importArgument.leadingComments = [ + { + type: 'CommentBlock', + value: ` webpackChunkName: "${importName}" `, + }, + ] - nodes.push( - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(importName), - t.objectExpression([ - t.objectProperty( - t.identifier('name'), - t.stringLiteral(importName), + nodes.push( + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier(importName), + t.objectExpression([ + t.objectProperty( + t.identifier('name'), + t.stringLiteral(importName), + ), + // prerenderLoader for ssr/prerender and first load of + // prerendered pages in browser (csr) + // prerenderLoader: (name) => { prerenderLoaderImpl } + t.objectProperty( + t.identifier('prerenderLoader'), + t.arrowFunctionExpression( + [t.identifier('name')], + prerenderLoaderImpl( + forPrerender, + forVite, + relativeImport, + t, + ), ), - // prerenderLoader for ssr/prerender and first load of - // prerendered pages in browser (csr) - // prerenderLoader: (name) => { prerenderLoaderImpl } - t.objectProperty( - t.identifier('prerenderLoader'), + ), + t.objectProperty( + t.identifier('LazyComponent'), + t.callExpression(t.identifier('lazy'), [ t.arrowFunctionExpression( - [t.identifier('name')], - prerenderLoaderImpl( - forPrerender, - forVite, - relativeImport, - t, - ), + [], + t.callExpression(t.identifier('import'), [ + importArgument, + ]), ), - ), - t.objectProperty( - t.identifier('LazyComponent'), - t.callExpression(t.identifier('lazy'), [ - t.arrowFunctionExpression( - [], - t.callExpression(t.identifier('import'), [ - importArgument, - ]), - ), - ]), - ), - ]), - ), - ]), - ) - } + ]), + ), + ]), + ), + ]), + ) } // Insert at the top of the file diff --git a/packages/babel-config/src/web.ts b/packages/babel-config/src/web.ts index a739fc577286..8a16ca068155 100644 --- a/packages/babel-config/src/web.ts +++ b/packages/babel-config/src/web.ts @@ -22,7 +22,7 @@ export interface Flags { forJest?: boolean // will change the alias for module-resolver plugin forPrerender?: boolean // changes what babel-plugin-redwood-routes-auto-loader does forVite?: boolean - forRscClient?: boolean + forRSC?: boolean } export const getWebSideBabelPlugins = ( @@ -109,10 +109,10 @@ export const getWebSideBabelPlugins = ( } export const getWebSideOverrides = ( - { forPrerender, forVite, forRscClient }: Flags = { + { forPrerender, forVite, forRSC }: Flags = { forPrerender: false, forVite: false, - forRscClient: false, + forRSC: false, }, ): Array => { // Have to use a readonly array here because of a limitation in TS @@ -124,7 +124,9 @@ export const getWebSideOverrides = ( }, // Automatically import files in `./web/src/pages/*` in to // the `./web/src/Routes.[ts|jsx]` file. - { + // We do not do this for RSC because there are differences between server and client + // so each specific build stage handles the auto-importing of routes + !forRSC && { test: /Routes.(js|tsx|jsx)$/, plugins: [ [ @@ -134,7 +136,6 @@ export const getWebSideOverrides = ( { forPrerender, forVite, - forRscClient, } satisfies RoutesAutoLoaderOptions, ], ], diff --git a/packages/vite/package.json b/packages/vite/package.json index b246bfcbca55..6b0f1b70fd76 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -22,6 +22,10 @@ "types": "./dist/client.d.ts", "default": "./dist/client.js" }, + "./clientSsr": { + "types": "./dist/clientSsr.d.ts", + "default": "./dist/clientSsr.js" + }, "./buildFeServer": { "types": "./dist/buildFeServer.d.ts", "default": "./dist/buildFeServer.js" diff --git a/packages/vite/src/buildFeServer.ts b/packages/vite/src/buildFeServer.ts index b24fd53382d6..0f08230a6fbf 100644 --- a/packages/vite/src/buildFeServer.ts +++ b/packages/vite/src/buildFeServer.ts @@ -52,7 +52,7 @@ export const buildFeServer = async ({ verbose, webDir }: BuildOptions = {}) => { await buildWeb({ verbose }) } - await buildForStreamingServer({ verbose }) + await buildForStreamingServer({ verbose, rscEnabled }) await buildRouteHooks(verbose, rwPaths) diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index 69a73dd2c300..be884e066aae 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -27,6 +27,7 @@ export function renderFromRscServer(rscId: string) { // function during SSR if (typeof window === 'undefined') { // Temporarily skip rendering this component during SSR + // return null return null } diff --git a/packages/vite/src/clientSsr.ts b/packages/vite/src/clientSsr.ts new file mode 100644 index 000000000000..ea7fcfedfd2b --- /dev/null +++ b/packages/vite/src/clientSsr.ts @@ -0,0 +1,3 @@ +export function renderFromDist(...args: any[]) { + console.log('renderFromDist', args) +} diff --git a/packages/vite/src/devFeServer.ts b/packages/vite/src/devFeServer.ts index cb5373ca30e7..1793f005b0f9 100644 --- a/packages/vite/src/devFeServer.ts +++ b/packages/vite/src/devFeServer.ts @@ -1,9 +1,14 @@ +import react from '@vitejs/plugin-react' import { createServerAdapter } from '@whatwg-node/server' import express from 'express' import type { ViteDevServer } from 'vite' import { createServer as createViteServer } from 'vite' import { cjsInterop } from 'vite-plugin-cjs-interop' +import { + redwoodRoutesAutoLoaderRscClientPlugin, + getWebSideDefaultBabelConfig, +} from '@redwoodjs/babel-config' import type { RouteSpec } from '@redwoodjs/internal/dist/routes' import { getProjectRoutes } from '@redwoodjs/internal/dist/routes' import type { Paths } from '@redwoodjs/project-config' @@ -27,6 +32,8 @@ async function createServer() { const app = express() const rwPaths = getPaths() + const rscEnabled = getConfig().experimental.rsc?.enabled ?? false + // ~~~ Dev time validations ~~~~ // TODO (STREAMING) When Streaming is released Vite will be the only bundler, // and this file should always exist. So the error message needs to change @@ -46,12 +53,29 @@ async function createServer() { } // ~~~~ Dev time validations ~~~~ + const reactBabelConfig = getWebSideDefaultBabelConfig({ + forVite: true, + forRSC: rscEnabled, + }) + if (rscEnabled) { + reactBabelConfig.overrides.push({ + test: /Routes.(js|tsx|jsx)$/, + plugins: [[redwoodRoutesAutoLoaderRscClientPlugin, {}]], + babelrc: false, + ignore: ['node_modules'], + }) + } + // Create Vite server in middleware mode and configure the app type as // 'custom', disabling Vite's own HTML serving logic so parent server // can take control const vite = await createViteServer({ configFile: rwPaths.web.viteConfig, plugins: [ + rscEnabled && + react({ + babel: reactBabelConfig, + }), cjsInterop({ dependencies: ['@redwoodjs/**'], }), diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index a26e6beab500..12b30a0eb87c 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -149,13 +149,14 @@ export default function redwoodPluginVite(): PluginOption[] { id: /@redwoodjs\/web\/dist\/apollo\/sseLink/, }, ]), - react({ - babel: { - ...getWebSideDefaultBabelConfig({ - forVite: true, - forRscClient: rwConfig.experimental.rsc?.enabled, - }), - }, - }), + !rscEnabled && + react({ + babel: { + ...getWebSideDefaultBabelConfig({ + forVite: true, + forRSC: rscEnabled, + }), + }, + }), ] } diff --git a/packages/vite/src/rsc/rscBuildAnalyze.ts b/packages/vite/src/rsc/rscBuildAnalyze.ts index 4eadc467fe98..8a0805917e3c 100644 --- a/packages/vite/src/rsc/rscBuildAnalyze.ts +++ b/packages/vite/src/rsc/rscBuildAnalyze.ts @@ -1,5 +1,7 @@ +import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' +import { getWebSideDefaultBabelConfig } from '@redwoodjs/babel-config' import { getPaths } from '@redwoodjs/project-config' import { onWarn } from '../lib/onWarn.js' @@ -43,6 +45,12 @@ export async function rscBuildAnalyze() { // debugging, but we're keeping it silent by default. logLevel: 'silent', plugins: [ + react({ + babel: getWebSideDefaultBabelConfig({ + forVite: true, + forRSC: true, + }), + }), rscAnalyzePlugin( (id) => clientEntryFileSet.add(id), (id) => serverEntryFileSet.add(id), diff --git a/packages/vite/src/rsc/rscBuildClient.ts b/packages/vite/src/rsc/rscBuildClient.ts index 1194dbe80db4..f8e9afbc80ae 100644 --- a/packages/vite/src/rsc/rscBuildClient.ts +++ b/packages/vite/src/rsc/rscBuildClient.ts @@ -1,5 +1,10 @@ +import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' +import { + redwoodRoutesAutoLoaderRscClientPlugin, + getWebSideDefaultBabelConfig, +} from '@redwoodjs/babel-config' import { getPaths } from '@redwoodjs/project-config' import { onWarn } from '../lib/onWarn.js' @@ -28,6 +33,17 @@ export async function rscBuildClient(clientEntryFiles: Record) { throw new Error('Missing web/src/entry.client') } + const reactBabelConfig = getWebSideDefaultBabelConfig({ + forVite: true, + forRSC: true, + }) + reactBabelConfig.overrides.push({ + test: /Routes.(js|tsx|jsx)$/, + plugins: [[redwoodRoutesAutoLoaderRscClientPlugin, {}]], + babelrc: false, + ignore: ['node_modules'], + }) + const clientBuildOutput = await viteBuild({ envFile: false, build: { @@ -64,6 +80,11 @@ export async function rscBuildClient(clientEntryFiles: Record) { esbuild: { logLevel: 'debug', }, + plugins: [ + react({ + babel: reactBabelConfig, + }), + ], }) if (!('output' in clientBuildOutput)) { diff --git a/packages/vite/src/rsc/rscBuildForServer.ts b/packages/vite/src/rsc/rscBuildForServer.ts index 08b6800347e6..0fa4b82c0176 100644 --- a/packages/vite/src/rsc/rscBuildForServer.ts +++ b/packages/vite/src/rsc/rscBuildForServer.ts @@ -1,5 +1,10 @@ +import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' +import { + redwoodRoutesAutoLoaderRscServerPlugin, + getWebSideDefaultBabelConfig, +} from '@redwoodjs/babel-config' import { getPaths } from '@redwoodjs/project-config' import { onWarn } from '../lib/onWarn.js' @@ -35,6 +40,17 @@ export async function rscBuildForServer( ...customModules, } + const reactBabelConfig = getWebSideDefaultBabelConfig({ + forVite: true, + forRSC: true, + }) + reactBabelConfig.overrides.push({ + test: /Routes.(js|tsx|jsx)$/, + plugins: [[redwoodRoutesAutoLoaderRscServerPlugin, {}]], + babelrc: false, + ignore: ['node_modules'], + }) + // TODO (RSC): No redwood-vite plugin, add it in here const rscServerBuildOutput = await viteBuild({ envFile: false, @@ -58,6 +74,9 @@ export async function rscBuildForServer( }, }, plugins: [ + react({ + babel: reactBabelConfig, + }), // The rscTransformPlugin maps paths like // /Users/tobbe/.../rw-app/node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js // to diff --git a/packages/vite/src/rsc/rscWorker.ts b/packages/vite/src/rsc/rscWorker.ts index e7d94490f039..bec53dd94923 100644 --- a/packages/vite/src/rsc/rscWorker.ts +++ b/packages/vite/src/rsc/rscWorker.ts @@ -3,7 +3,6 @@ // `--condition react-server`. If we did try to do that the main process // couldn't do SSR because it would be missing client-side React functions // like `useState` and `createContext`. - import { Buffer } from 'node:buffer' import { Server } from 'node:http' import path from 'node:path' @@ -12,10 +11,15 @@ import { parentPort } from 'node:worker_threads' import { createElement } from 'react' +import react from '@vitejs/plugin-react' import RSDWServer from 'react-server-dom-webpack/server' import type { ResolvedConfig } from 'vite' import { createServer, resolveConfig } from 'vite' +import { + getWebSideDefaultBabelConfig, + redwoodRoutesAutoLoaderRscServerPlugin, +} from '@redwoodjs/babel-config' import { getPaths } from '@redwoodjs/project-config' import type { defineEntries, GetEntry } from '../entries.js' @@ -121,6 +125,17 @@ registerFwGlobals() // is already in use`. const dummyServer = new Server() +const reactBabelConfig = getWebSideDefaultBabelConfig({ + forVite: true, + forRSC: true, +}) +reactBabelConfig.overrides.push({ + test: /Routes.(js|tsx|jsx)$/, + plugins: [[redwoodRoutesAutoLoaderRscServerPlugin, {}]], + babelrc: false, + ignore: ['node_modules'], +}) + // TODO (RSC): `createServer` is mostly used to create a dev server. Is it OK // to use it like a production server like this? // TODO (RSC): Do we need to pass `define` here with RWJS_ENV etc? What about @@ -129,6 +144,9 @@ const dummyServer = new Server() // https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#middleware-mode const vitePromise = createServer({ plugins: [ + react({ + babel: reactBabelConfig, + }), rscReloadPlugin((type) => { if (!parentPort) { throw new Error('parentPort is undefined') diff --git a/packages/vite/src/streaming/buildForStreamingServer.ts b/packages/vite/src/streaming/buildForStreamingServer.ts index afcec3462b65..396e0a15f6c7 100644 --- a/packages/vite/src/streaming/buildForStreamingServer.ts +++ b/packages/vite/src/streaming/buildForStreamingServer.ts @@ -1,12 +1,19 @@ +import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' import { cjsInterop } from 'vite-plugin-cjs-interop' +import { + redwoodRoutesAutoLoaderRscServerPlugin, + getWebSideDefaultBabelConfig, +} from '@redwoodjs/babel-config' import { getPaths } from '@redwoodjs/project-config' export async function buildForStreamingServer({ verbose = false, + rscEnabled = false, }: { verbose?: boolean + rscEnabled?: boolean }) { console.log('Starting streaming server build...\n') const rwPaths = getPaths() @@ -15,12 +22,29 @@ export async function buildForStreamingServer({ throw new Error('Vite config not found') } + const reactBabelConfig = getWebSideDefaultBabelConfig({ + forVite: true, + forRSC: true, + }) + if (rscEnabled) { + reactBabelConfig.overrides.push({ + test: /Routes.(js|tsx|jsx)$/, + plugins: [[redwoodRoutesAutoLoaderRscServerPlugin, {}]], + babelrc: false, + ignore: ['node_modules'], + }) + } + await viteBuild({ configFile: rwPaths.web.viteConfig, plugins: [ cjsInterop({ dependencies: ['@redwoodjs/**'], }), + rscEnabled && + react({ + babel: reactBabelConfig, + }), ], build: { // TODO (RSC): Remove `minify: false` when we don't need to debug as often From 93459f9dc9f5f78053b7d00da2b7c0d71c3013fa Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sun, 17 Mar 2024 17:33:08 +0100 Subject: [PATCH 30/34] chore(rsc): upgrade to react canary (#10194) Want to see how CI reacts to upgrading to React canary. ~I may have over-upgraded (i.e. some fixtures and maybe the docs don't need to be upgraded), but let's see what happens.~ This upgrade was complicated by https://github.com/facebook/react/pull/27436 which added a poisoned import to `react-server-dom-webpack/server`, meaning that if you tried to import from that entry point without the "react-server" condition, it would throw. In a much-earlier PR, Sebastian mentions there generally being two ways to do things: > Either you can have the server code prebundled using Webpack (what Next.js does in practice) or you can use an unbundled Node.js server (what the reference implementation does). See https://github.com/facebook/react/pull/26172. This PR goes with the former, prebundling `react-server-dom-webpack/server` with the "react-server" condition so that we can avoid having to specify it at runtime. It's hard to tell if this the better option long-term, but since we're keen on getting rid of the worker right now, this moves us closer in that direction. The alternative solution was to move decoding the reply into the worker where the condition is specified. --------- Co-authored-by: Tobbe Lundberg --- .github/renovate.json | 5 +- .../fragment-test-project/web/package.json | 4 +- .../test-project-rsa/web/package.json | 4 +- .../web/package.json | 4 +- __fixtures__/test-project/web/package.json | 4 +- .../auth-providers/auth0/web/package.json | 2 +- .../azureActiveDirectory/web/package.json | 2 +- .../auth-providers/clerk/web/package.json | 2 +- .../auth-providers/dbAuth/web/package.json | 2 +- .../auth-providers/firebase/web/package.json | 2 +- .../auth-providers/netlify/web/package.json | 2 +- .../auth-providers/supabase/web/package.json | 2 +- .../supertokens/web/package.json | 2 +- packages/auth/package.json | 2 +- .../templates/js/web/package.json | 4 +- .../templates/ts/web/package.json | 4 +- packages/forms/package.json | 6 +- packages/prerender/package.json | 4 +- packages/router/package.json | 8 +- packages/vite/ambient.d.ts | 2 + packages/vite/build.mts | 25 +++- packages/vite/modules.d.ts | 63 +++++++++- packages/vite/package.json | 4 +- .../react-server-dom-webpack.server.ts | 7 ++ packages/vite/src/devFeServer.ts | 4 +- packages/vite/src/index.ts | 2 - ...lobals.ts => registerFwGlobalsAndShims.ts} | 31 ++++- .../vite-plugin-rsc-transform-entry.ts | 32 ----- packages/vite/src/rsc/rscRequestHandler.ts | 10 +- packages/vite/src/rsc/rscWorker.ts | 4 +- packages/vite/src/runFeServer.ts | 4 +- packages/vite/src/streaming/streamHelpers.ts | 16 ++- packages/web/package.json | 8 +- yarn.lock | 116 +++++++++--------- 34 files changed, 241 insertions(+), 152 deletions(-) create mode 100644 packages/vite/src/bundled/react-server-dom-webpack.server.ts rename packages/vite/src/lib/{registerGlobals.ts => registerFwGlobalsAndShims.ts} (75%) delete mode 100644 packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts diff --git a/.github/renovate.json b/.github/renovate.json index 57d9a23edf24..56c54a889e15 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -69,10 +69,7 @@ "enabled": false, "matchPackageNames": [ - "@apollo/experimental-nextjs-app-support", - "react", - "react-dom", - "react-server-dom-webpack" + "@apollo/experimental-nextjs-app-support" ] } ] diff --git a/__fixtures__/fragment-test-project/web/package.json b/__fixtures__/fragment-test-project/web/package.json index c3d2056398cd..f527cdb9c3d2 100644 --- a/__fixtures__/fragment-test-project/web/package.json +++ b/__fixtures__/fragment-test-project/web/package.json @@ -16,8 +16,8 @@ "@redwoodjs/router": "7.0.0", "@redwoodjs/web": "7.0.0", "humanize-string": "2.1.0", - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913" + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314" }, "devDependencies": { "@redwoodjs/vite": "7.0.0", diff --git a/__fixtures__/test-project-rsa/web/package.json b/__fixtures__/test-project-rsa/web/package.json index d0ee63ccaa15..2137e301b9d7 100644 --- a/__fixtures__/test-project-rsa/web/package.json +++ b/__fixtures__/test-project-rsa/web/package.json @@ -15,8 +15,8 @@ "@redwoodjs/forms": "8.0.0-canary.144", "@redwoodjs/router": "8.0.0-canary.144", "@redwoodjs/web": "8.0.0-canary.144", - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913" + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314" }, "devDependencies": { "@redwoodjs/vite": "8.0.0-canary.144", diff --git a/__fixtures__/test-project-rsc-external-packages-and-cells/web/package.json b/__fixtures__/test-project-rsc-external-packages-and-cells/web/package.json index 30d029e8d1a4..67ca702530a7 100644 --- a/__fixtures__/test-project-rsc-external-packages-and-cells/web/package.json +++ b/__fixtures__/test-project-rsc-external-packages-and-cells/web/package.json @@ -18,8 +18,8 @@ "@redwoodjs/web": "7.0.0-canary.1011", "@tobbe.dev/rsc-test": "0.0.5", "client-only": "0.0.1", - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913" + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314" }, "devDependencies": { "@redwoodjs/vite": "7.0.0-canary.1011", diff --git a/__fixtures__/test-project/web/package.json b/__fixtures__/test-project/web/package.json index 9dda5afc2f12..d39585dc947e 100644 --- a/__fixtures__/test-project/web/package.json +++ b/__fixtures__/test-project/web/package.json @@ -16,8 +16,8 @@ "@redwoodjs/router": "7.0.0", "@redwoodjs/web": "7.0.0", "humanize-string": "2.1.0", - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913" + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314" }, "devDependencies": { "@redwoodjs/vite": "7.0.0", diff --git a/packages/auth-providers/auth0/web/package.json b/packages/auth-providers/auth0/web/package.json index 119eec71526c..c6706cceaf98 100644 --- a/packages/auth-providers/auth0/web/package.json +++ b/packages/auth-providers/auth0/web/package.json @@ -32,7 +32,7 @@ "@babel/cli": "7.23.9", "@babel/core": "^7.22.20", "@types/react": "^18.2.55", - "react": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", "typescript": "5.3.3", "vitest": "1.3.1" }, diff --git a/packages/auth-providers/azureActiveDirectory/web/package.json b/packages/auth-providers/azureActiveDirectory/web/package.json index a9ef5061bb07..ff46b153787b 100644 --- a/packages/auth-providers/azureActiveDirectory/web/package.json +++ b/packages/auth-providers/azureActiveDirectory/web/package.json @@ -33,7 +33,7 @@ "@babel/core": "^7.22.20", "@types/netlify-identity-widget": "1.9.6", "@types/react": "^18.2.55", - "react": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", "typescript": "5.3.3", "vitest": "1.3.1" }, diff --git a/packages/auth-providers/clerk/web/package.json b/packages/auth-providers/clerk/web/package.json index 60a4f8e5f11e..7d7edc54a415 100644 --- a/packages/auth-providers/clerk/web/package.json +++ b/packages/auth-providers/clerk/web/package.json @@ -33,7 +33,7 @@ "@clerk/clerk-react": "4.30.7", "@clerk/types": "3.60.0", "@types/react": "^18.2.55", - "react": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", "typescript": "5.3.3", "vitest": "1.3.1" }, diff --git a/packages/auth-providers/dbAuth/web/package.json b/packages/auth-providers/dbAuth/web/package.json index 09cecb7de684..82538742dba3 100644 --- a/packages/auth-providers/dbAuth/web/package.json +++ b/packages/auth-providers/dbAuth/web/package.json @@ -36,7 +36,7 @@ "@types/react": "^18.2.55", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "react": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", "typescript": "5.3.3" }, "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1" diff --git a/packages/auth-providers/firebase/web/package.json b/packages/auth-providers/firebase/web/package.json index d573341e66f1..2b742d66488d 100644 --- a/packages/auth-providers/firebase/web/package.json +++ b/packages/auth-providers/firebase/web/package.json @@ -34,7 +34,7 @@ "firebase": "10.7.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "react": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", "typescript": "5.3.3" }, "peerDependencies": { diff --git a/packages/auth-providers/netlify/web/package.json b/packages/auth-providers/netlify/web/package.json index 65cb16ab88a1..cbdf4b3b663f 100644 --- a/packages/auth-providers/netlify/web/package.json +++ b/packages/auth-providers/netlify/web/package.json @@ -32,7 +32,7 @@ "@babel/core": "^7.22.20", "@types/netlify-identity-widget": "1.9.6", "@types/react": "^18.2.55", - "react": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", "typescript": "5.3.3", "vitest": "1.3.1" }, diff --git a/packages/auth-providers/supabase/web/package.json b/packages/auth-providers/supabase/web/package.json index cea439c4ccd0..d965328fc872 100644 --- a/packages/auth-providers/supabase/web/package.json +++ b/packages/auth-providers/supabase/web/package.json @@ -31,7 +31,7 @@ "@babel/core": "^7.22.20", "@supabase/supabase-js": "2.39.7", "@types/react": "^18.2.55", - "react": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", "typescript": "5.3.3", "vitest": "1.3.1" }, diff --git a/packages/auth-providers/supertokens/web/package.json b/packages/auth-providers/supertokens/web/package.json index c2bf6b9c6c5e..ea4c3147c3e5 100644 --- a/packages/auth-providers/supertokens/web/package.json +++ b/packages/auth-providers/supertokens/web/package.json @@ -31,7 +31,7 @@ "@babel/cli": "7.23.9", "@babel/core": "^7.22.20", "@types/react": "^18.2.55", - "react": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", "supertokens-auth-react": "0.34.0", "typescript": "5.3.3", "vitest": "1.3.1" diff --git a/packages/auth/package.json b/packages/auth/package.json index 73b93346bff7..21b5a51d69a0 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -25,7 +25,7 @@ "dependencies": { "@babel/runtime-corejs3": "7.24.0", "core-js": "3.35.1", - "react": "0.0.0-experimental-e5205658f-20230913" + "react": "18.3.0-canary-a870b2d54-20240314" }, "devDependencies": { "@babel/cli": "7.23.9", diff --git a/packages/create-redwood-app/templates/js/web/package.json b/packages/create-redwood-app/templates/js/web/package.json index b227f2923871..171b5331ccc6 100644 --- a/packages/create-redwood-app/templates/js/web/package.json +++ b/packages/create-redwood-app/templates/js/web/package.json @@ -14,8 +14,8 @@ "@redwoodjs/forms": "7.0.0", "@redwoodjs/router": "7.0.0", "@redwoodjs/web": "7.0.0", - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913" + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314" }, "devDependencies": { "@redwoodjs/vite": "7.0.0", diff --git a/packages/create-redwood-app/templates/ts/web/package.json b/packages/create-redwood-app/templates/ts/web/package.json index b227f2923871..171b5331ccc6 100644 --- a/packages/create-redwood-app/templates/ts/web/package.json +++ b/packages/create-redwood-app/templates/ts/web/package.json @@ -14,8 +14,8 @@ "@redwoodjs/forms": "7.0.0", "@redwoodjs/router": "7.0.0", "@redwoodjs/web": "7.0.0", - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913" + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314" }, "devDependencies": { "@redwoodjs/vite": "7.0.0", diff --git a/packages/forms/package.json b/packages/forms/package.json index 43abc0e0850b..674d39c796e6 100644 --- a/packages/forms/package.json +++ b/packages/forms/package.json @@ -40,13 +40,13 @@ "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "nodemon": "3.0.2", - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314", "typescript": "5.3.3", "vitest": "1.3.1" }, "peerDependencies": { - "react": "0.0.0-experimental-e5205658f-20230913" + "react": "18.3.0-canary-a870b2d54-20240314" }, "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1" } diff --git a/packages/prerender/package.json b/packages/prerender/package.json index 29e82f11112f..ed93122b3607 100644 --- a/packages/prerender/package.json +++ b/packages/prerender/package.json @@ -48,8 +48,8 @@ "vitest": "1.3.1" }, "peerDependencies": { - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913" + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314" }, "externals": { "react": "react", diff --git a/packages/router/package.json b/packages/router/package.json index bdaa35535eb5..bedb5df07919 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -36,14 +36,14 @@ "@types/react-dom": "^18.2.19", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314", "tstyche": "1.0.0", "typescript": "5.3.3" }, "peerDependencies": { - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913" + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314" }, "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1" } diff --git a/packages/vite/ambient.d.ts b/packages/vite/ambient.d.ts index d2b450c857c6..a08e82f4c947 100644 --- a/packages/vite/ambient.d.ts +++ b/packages/vite/ambient.d.ts @@ -25,6 +25,8 @@ declare global { var __REDWOOD__PRERENDER_PAGES: any var __REDWOOD__HELMET_CONTEXT: { helmet?: HelmetServerState } + + var __rw_module_cache__: Map } export {} diff --git a/packages/vite/build.mts b/packages/vite/build.mts index 16175a6725c0..5da1709c29f6 100644 --- a/packages/vite/build.mts +++ b/packages/vite/build.mts @@ -1,3 +1,24 @@ -import { build } from '@redwoodjs/framework-tools' +import { build, defaultIgnorePatterns } from '@redwoodjs/framework-tools' -await build() +import * as esbuild from 'esbuild' + +await build({ + entryPointOptions: { + ignore: [...defaultIgnorePatterns, '**/bundled'], + } +}) + +// We bundle some react packages with the "react-server" condition +// so that we don't need to specify it at runtime. + +await esbuild.build({ + entryPoints: ['src/bundled/*'], + outdir: 'dist/bundled', + + bundle: true, + conditions: ['react-server'], + platform: 'node', + target: ['node20'], + + logLevel: 'info', +}) diff --git a/packages/vite/modules.d.ts b/packages/vite/modules.d.ts index a5679f7b8cd9..e13664683bc6 100644 --- a/packages/vite/modules.d.ts +++ b/packages/vite/modules.d.ts @@ -1,6 +1,4 @@ declare module 'react-server-dom-webpack/node-loader' -declare module 'react-server-dom-webpack/server' -declare module 'react-server-dom-webpack/server.node.unbundled' declare module 'react-server-dom-webpack/client' { // https://github.com/facebook/react/blob/dfaed5582550f11b27aae967a8e7084202dd2d90/packages/react-server-dom-webpack/src/ReactFlightDOMClientBrowser.js#L31 @@ -21,5 +19,66 @@ declare module 'react-server-dom-webpack/client' { ): Promise } +declare module 'react-server-dom-webpack/server' { + import type { Writable } from 'stream' + + import type { Busboy } from 'busboy' + + // It's difficult to know the true type of `ServerManifest`. + // A lot of react's source files are stubs that are replaced at build time. + // Going off this reference for now: https://github.com/facebook/react/blob/b09e102ff1e2aaaf5eb6585b04609ac7ff54a5c8/packages/react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack.js#L40 + type ImportManifestEntry = { + id: string + chunks: Array + name: string + } + + type ServerManifest = { + [id: string]: ImportManifestEntry + } + + // The types for `decodeReply` and `decodeReplyFromBusboy` were taken from + // https://github.com/facebook/react/blob/b09e102ff1e2aaaf5eb6585b04609ac7ff54a5c8/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js + // which is what 'react-server-dom-webpack/server' resolves to with the 'react-server' condition. + + /** + * WARNING: The types for this were handwritten by looking at React's source and could be wrong. + */ + export function decodeReply( + body: string | FormData, + webpackMap?: ServerManifest, + ): Promise + + /** + * WARNING: The types for this were handwritten by looking at React's source and could be wrong. + */ + export function decodeReplyFromBusboy( + busboyStream: Busboy, + webpackMap?: ServerManifest, + ): Promise + + type ClientReferenceManifestEntry = ImportManifestEntry + + type ClientManifest = { + [id: string]: ClientReferenceManifestEntry + } + + type PipeableStream = { + abort(reason: any): void + pipe(destination: T): T + } + + // The types for `renderToPipeableStream` are incomplete and were taken from + // https://github.com/facebook/react/blob/b09e102ff1e2aaaf5eb6585b04609ac7ff54a5c8/packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js#L75. + + /** + * WARNING: The types for this were handwritten by looking at React's source and could be wrong. + */ + export function renderToPipeableStream( + model: ReactClientValue, + webpackMap: ClientManifest, + ): PipeableStream +} + declare module 'acorn-loose' declare module 'vite-plugin-cjs-interop' diff --git a/packages/vite/package.json b/packages/vite/package.json index 6b0f1b70fd76..d03b26d4a8f1 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -80,8 +80,8 @@ "express": "4.18.2", "http-proxy-middleware": "2.0.6", "isbot": "3.7.1", - "react": "0.0.0-experimental-e5205658f-20230913", - "react-server-dom-webpack": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", + "react-server-dom-webpack": "18.3.0-canary-a870b2d54-20240314", "vite": "5.1.6", "vite-plugin-cjs-interop": "2.1.0", "yargs-parser": "21.1.1" diff --git a/packages/vite/src/bundled/react-server-dom-webpack.server.ts b/packages/vite/src/bundled/react-server-dom-webpack.server.ts new file mode 100644 index 000000000000..cec9df8b5569 --- /dev/null +++ b/packages/vite/src/bundled/react-server-dom-webpack.server.ts @@ -0,0 +1,7 @@ +// We bundle out these functions with the "react-server" condition +// so that we don't need to specify it at runtime. + +export { + decodeReply, + decodeReplyFromBusboy, +} from 'react-server-dom-webpack/server' diff --git a/packages/vite/src/devFeServer.ts b/packages/vite/src/devFeServer.ts index 1793f005b0f9..3aeeeea7a126 100644 --- a/packages/vite/src/devFeServer.ts +++ b/packages/vite/src/devFeServer.ts @@ -14,7 +14,7 @@ import { getProjectRoutes } from '@redwoodjs/internal/dist/routes' import type { Paths } from '@redwoodjs/project-config' import { getConfig, getPaths } from '@redwoodjs/project-config' -import { registerFwGlobals } from './lib/registerGlobals.js' +import { registerFwGlobalsAndShims } from './lib/registerFwGlobalsAndShims.js' import { invoke } from './middleware/invokeMiddleware.js' import { createRscRequestHandler } from './rsc/rscRequestHandler.js' import { collectCssPaths, componentsModules } from './streaming/collectCss.js' @@ -27,7 +27,7 @@ globalThis.__REDWOOD__PRERENDER_PAGES = {} async function createServer() { ensureProcessDirWeb() - registerFwGlobals() + registerFwGlobalsAndShims() const app = express() const rwPaths = getPaths() diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index 12b30a0eb87c..cc4dfa68cfb4 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -11,7 +11,6 @@ import { getConfig, getPaths } from '@redwoodjs/project-config' import { getMergedConfig } from './lib/getMergedConfig.js' import handleJsAsJsx from './plugins/vite-plugin-jsx-loader.js' import removeFromBundle from './plugins/vite-plugin-remove-from-bundle.js' -import { rscTransformEntryPlugin } from './plugins/vite-plugin-rsc-transform-entry.js' import swapApolloProvider from './plugins/vite-plugin-swap-apollo-provider.js' /** @@ -41,7 +40,6 @@ export default function redwoodPluginVite(): PluginOption[] { const rscEnabled = rwConfig.experimental.rsc.enabled return [ - rscEnabled && rscTransformEntryPlugin(), { name: 'redwood-plugin-vite-html-env', diff --git a/packages/vite/src/lib/registerGlobals.ts b/packages/vite/src/lib/registerFwGlobalsAndShims.ts similarity index 75% rename from packages/vite/src/lib/registerGlobals.ts rename to packages/vite/src/lib/registerFwGlobalsAndShims.ts index 49d02568685a..51ae8ca18e5d 100644 --- a/packages/vite/src/lib/registerGlobals.ts +++ b/packages/vite/src/lib/registerFwGlobalsAndShims.ts @@ -5,13 +5,18 @@ import { getConfig, getPaths } from '@redwoodjs/project-config' /** * Use this function on the web server * - * Because although this is defined in Vite/index.ts + * Because although this is defined in vite/index.ts * They are only available in the user's code (and not in FW code) * because define STATICALLY replaces it in user's code, not in node_modules * * It's still available on the client side though, probably because its processed by Vite */ -export const registerFwGlobals = () => { +export const registerFwGlobalsAndShims = () => { + registerFwGlobals() + registerFwShims() +} + +function registerFwGlobals() { const rwConfig = getConfig() const rwPaths = getPaths() @@ -87,6 +92,28 @@ export const registerFwGlobals = () => { } } +/** + * This function is used to register shims for react-server-dom-webpack in a Vite + * (or at least non-Webpack) environment. + * + * We have to call it early in the app's lifecycle, before code that depends on it runs + * and do so at the server start in (src/devFeServer.ts and src/runFeServer.ts). + */ +function registerFwShims() { + globalThis.__rw_module_cache__ ||= new Map() + + globalThis.__webpack_chunk_load__ ||= (id) => { + console.log('rscWebpackShims chunk load id', id) + return import(id).then((m) => globalThis.__rw_module_cache__.set(id, m)) + } + + // @ts-expect-error This is a webpack shim typed as any by @types/webpack + globalThis.__webpack_require__ ||= (id) => { + console.log('rscWebpackShims require id', id) + return globalThis.__rw_module_cache__.get(id) + } +} + function swapLocalhostFor127(hostString: string) { return hostString.replace('localhost', '127.0.0.1') } diff --git a/packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts b/packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts deleted file mode 100644 index 5538c6cd4af7..000000000000 --- a/packages/vite/src/plugins/vite-plugin-rsc-transform-entry.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { normalizePath, type Plugin } from 'vite' - -import { getPaths } from '@redwoodjs/project-config' - -export function rscTransformEntryPlugin(): Plugin { - const entryServerPath = normalizePath(getPaths().web.entryServer || '') - const entryClientPath = normalizePath(getPaths().web.entryClient || '') - - const rscWebpackShims = ` -globalThis.__rw_module_cache__ ||= new Map(); - -globalThis.__webpack_chunk_load__ ||= (id) => { - console.log('rscWebpackShims chunk load id', id) - return import(id).then((m) => globalThis.__rw_module_cache__.set(id, m)) -}; - -globalThis.__webpack_require__ ||= (id) => { - console.log('rscWebpackShims require id', id) - return globalThis.__rw_module_cache__.get(id) -};\n` - - return { - name: 'rsc-transform-entry', - transform: async function (code, id) { - if (id === entryServerPath || id === entryClientPath) { - return code + rscWebpackShims - } - - return code - }, - } -} diff --git a/packages/vite/src/rsc/rscRequestHandler.ts b/packages/vite/src/rsc/rscRequestHandler.ts index 935f3cb5a34e..ae6eeaf60723 100644 --- a/packages/vite/src/rsc/rscRequestHandler.ts +++ b/packages/vite/src/rsc/rscRequestHandler.ts @@ -1,14 +1,15 @@ import busboy from 'busboy' import type { Request, Response } from 'express' -import RSDWServer from 'react-server-dom-webpack/server.node.unbundled' +import { + decodeReply, + decodeReplyFromBusboy, +} from '../bundled/react-server-dom-webpack.server' import { hasStatusCode } from '../lib/StatusError.js' import { sendRscFlightToStudio } from './rscStudioHandlers.js' import { renderRsc } from './rscWorkerCommunication.js' -const { decodeReply, decodeReplyFromBusboy } = RSDWServer - export function createRscRequestHandler() { // This is mounted at /rw-rsc, so will have /rw-rsc stripped from req.url return async (req: Request, res: Response, next: () => void) => { @@ -53,7 +54,8 @@ export function createRscRequestHandler() { if (req.headers['content-type']?.startsWith('multipart/form-data')) { console.log('RSA: multipart/form-data') const bb = busboy({ headers: req.headers }) - const reply = decodeReplyFromBusboy(bb) + // TODO (RSC): The generic here could be typed better + const reply = decodeReplyFromBusboy(bb) req.pipe(bb) args = await reply diff --git a/packages/vite/src/rsc/rscWorker.ts b/packages/vite/src/rsc/rscWorker.ts index bec53dd94923..82f082a28c20 100644 --- a/packages/vite/src/rsc/rscWorker.ts +++ b/packages/vite/src/rsc/rscWorker.ts @@ -23,7 +23,7 @@ import { import { getPaths } from '@redwoodjs/project-config' import type { defineEntries, GetEntry } from '../entries.js' -import { registerFwGlobals } from '../lib/registerGlobals.js' +import { registerFwGlobalsAndShims } from '../lib/registerFwGlobalsAndShims.js' import { StatusError } from '../lib/StatusError.js' import { rscReloadPlugin } from '../plugins/vite-plugin-rsc-reload.js' import { rscTransformUseClientPlugin } from '../plugins/vite-plugin-rsc-transform-client.js' @@ -118,7 +118,7 @@ const handleRender = async ({ id, input }: MessageReq & { type: 'render' }) => { // This is a worker, so it doesn't share the same global variables as the main // server. So we have to register them here again. -registerFwGlobals() +registerFwGlobalsAndShims() // TODO (RSC): this was copied from waku; they have a todo to remove it. // We need this to fix a WebSocket error in dev, `WebSocket server error: Port diff --git a/packages/vite/src/runFeServer.ts b/packages/vite/src/runFeServer.ts index e407fc081cfd..b6870750f741 100644 --- a/packages/vite/src/runFeServer.ts +++ b/packages/vite/src/runFeServer.ts @@ -17,7 +17,7 @@ import type { Manifest as ViteBuildManifest } from 'vite' import { getConfig, getPaths } from '@redwoodjs/project-config' -import { registerFwGlobals } from './lib/registerGlobals.js' +import { registerFwGlobalsAndShims } from './lib/registerFwGlobalsAndShims.js' import { invoke } from './middleware/invokeMiddleware.js' import { createRscRequestHandler } from './rsc/rscRequestHandler.js' import { setClientEntries } from './rsc/rscWorkerCommunication.js' @@ -48,7 +48,7 @@ export async function runFeServer() { const rwConfig = getConfig() const rscEnabled = rwConfig.experimental?.rsc?.enabled - registerFwGlobals() + registerFwGlobalsAndShims() if (rscEnabled) { try { diff --git a/packages/vite/src/streaming/streamHelpers.ts b/packages/vite/src/streaming/streamHelpers.ts index e43c2183ce8f..60666f201e49 100644 --- a/packages/vite/src/streaming/streamHelpers.ts +++ b/packages/vite/src/streaming/streamHelpers.ts @@ -39,6 +39,20 @@ interface StreamOptions { onError?: (err: Error) => void } +const rscWebpackShims = `\ +globalThis.__rw_module_cache__ ||= new Map(); + +globalThis.__webpack_chunk_load__ ||= (id) => { + console.log('rscWebpackShims chunk load id', id) + return import(id).then((m) => globalThis.__rw_module_cache__.set(id, m)) +}; + +globalThis.__webpack_require__ ||= (id) => { + console.log('rscWebpackShims require id', id) + return globalThis.__rw_module_cache__.get(id) +}; +` + export async function reactRenderToStreamResponse( mwRes: MiddlewareResponse, renderOptions: RenderToStreamArgs, @@ -126,7 +140,7 @@ export async function reactRenderToStreamResponse( bootstrapScriptContent: // Only insert assetMap if client side JS will be loaded jsBundles.length > 0 - ? `window.__REDWOOD__ASSET_MAP = ${assetMap};` + ? `window.__REDWOOD__ASSET_MAP = ${assetMap}; ${rscWebpackShims}` : undefined, bootstrapModules: jsBundles, } diff --git a/packages/web/package.json b/packages/web/package.json index 4274a729fe69..1a295a89bde7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -61,15 +61,15 @@ "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "nodemon": "3.0.2", - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913", + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314", "tstyche": "1.0.0", "typescript": "5.3.3", "vitest": "1.3.1" }, "peerDependencies": { - "react": "0.0.0-experimental-e5205658f-20230913", - "react-dom": "0.0.0-experimental-e5205658f-20230913" + "react": "18.3.0-canary-a870b2d54-20240314", + "react-dom": "18.3.0-canary-a870b2d54-20240314" }, "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1" } diff --git a/yarn.lock b/yarn.lock index 41c476cc7a0c..20ba6c5bf79b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7639,7 +7639,7 @@ __metadata: "@redwoodjs/auth": "workspace:*" "@types/react": "npm:^18.2.55" core-js: "npm:3.35.1" - react: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" typescript: "npm:5.3.3" vitest: "npm:1.3.1" peerDependencies: @@ -7692,7 +7692,7 @@ __metadata: "@types/netlify-identity-widget": "npm:1.9.6" "@types/react": "npm:^18.2.55" core-js: "npm:3.35.1" - react: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" typescript: "npm:5.3.3" vitest: "npm:1.3.1" peerDependencies: @@ -7742,7 +7742,7 @@ __metadata: "@redwoodjs/auth": "workspace:*" "@types/react": "npm:^18.2.55" core-js: "npm:3.35.1" - react: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" typescript: "npm:5.3.3" vitest: "npm:1.3.1" peerDependencies: @@ -7818,7 +7818,7 @@ __metadata: core-js: "npm:3.35.1" jest: "npm:29.7.0" jest-environment-jsdom: "npm:29.7.0" - react: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" typescript: "npm:5.3.3" languageName: unknown linkType: soft @@ -7867,7 +7867,7 @@ __metadata: firebase: "npm:10.7.0" jest: "npm:29.7.0" jest-environment-jsdom: "npm:29.7.0" - react: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" typescript: "npm:5.3.3" peerDependencies: firebase: 10.7.0 @@ -7917,7 +7917,7 @@ __metadata: "@types/netlify-identity-widget": "npm:1.9.6" "@types/react": "npm:^18.2.55" core-js: "npm:3.35.1" - react: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" typescript: "npm:5.3.3" vitest: "npm:1.3.1" peerDependencies: @@ -7966,7 +7966,7 @@ __metadata: "@supabase/supabase-js": "npm:2.39.7" "@types/react": "npm:^18.2.55" core-js: "npm:3.35.1" - react: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" typescript: "npm:5.3.3" vitest: "npm:1.3.1" peerDependencies: @@ -8019,7 +8019,7 @@ __metadata: "@redwoodjs/auth": "workspace:*" "@types/react": "npm:^18.2.55" core-js: "npm:3.35.1" - react: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" supertokens-auth-react: "npm:0.34.0" typescript: "npm:5.3.3" vitest: "npm:1.3.1" @@ -8041,7 +8041,7 @@ __metadata: jest: "npm:29.7.0" jest-environment-jsdom: "npm:29.7.0" msw: "npm:1.3.2" - react: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" typescript: "npm:5.3.3" languageName: unknown linkType: soft @@ -8418,13 +8418,13 @@ __metadata: graphql: "npm:16.8.1" nodemon: "npm:3.0.2" pascalcase: "npm:1.0.0" - react: "npm:0.0.0-experimental-e5205658f-20230913" - react-dom: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" + react-dom: "npm:18.3.0-canary-a870b2d54-20240314" react-hook-form: "npm:7.49.3" typescript: "npm:5.3.3" vitest: "npm:1.3.1" peerDependencies: - react: 0.0.0-experimental-e5205658f-20230913 + react: 18.3.0-canary-a870b2d54-20240314 languageName: unknown linkType: soft @@ -8649,8 +8649,8 @@ __metadata: typescript: "npm:5.3.3" vitest: "npm:1.3.1" peerDependencies: - react: 0.0.0-experimental-e5205658f-20230913 - react-dom: 0.0.0-experimental-e5205658f-20230913 + react: 18.3.0-canary-a870b2d54-20240314 + react-dom: 18.3.0-canary-a870b2d54-20240314 languageName: unknown linkType: soft @@ -8730,13 +8730,13 @@ __metadata: core-js: "npm:3.35.1" jest: "npm:29.7.0" jest-environment-jsdom: "npm:29.7.0" - react: "npm:0.0.0-experimental-e5205658f-20230913" - react-dom: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" + react-dom: "npm:18.3.0-canary-a870b2d54-20240314" tstyche: "npm:1.0.0" typescript: "npm:5.3.3" peerDependencies: - react: 0.0.0-experimental-e5205658f-20230913 - react-dom: 0.0.0-experimental-e5205658f-20230913 + react: 18.3.0-canary-a870b2d54-20240314 + react-dom: 18.3.0-canary-a870b2d54-20240314 languageName: unknown linkType: soft @@ -8885,8 +8885,8 @@ __metadata: glob: "npm:10.3.10" http-proxy-middleware: "npm:2.0.6" isbot: "npm:3.7.1" - react: "npm:0.0.0-experimental-e5205658f-20230913" - react-server-dom-webpack: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" + react-server-dom-webpack: "npm:18.3.0-canary-a870b2d54-20240314" rollup: "npm:4.12.1" tsx: "npm:4.6.2" typescript: "npm:5.3.3" @@ -8944,8 +8944,8 @@ __metadata: graphql-sse: "npm:2.5.2" graphql-tag: "npm:2.12.6" nodemon: "npm:3.0.2" - react: "npm:0.0.0-experimental-e5205658f-20230913" - react-dom: "npm:0.0.0-experimental-e5205658f-20230913" + react: "npm:18.3.0-canary-a870b2d54-20240314" + react-dom: "npm:18.3.0-canary-a870b2d54-20240314" react-helmet-async: "npm:2.0.3" react-hot-toast: "npm:2.4.1" stacktracey: "npm:2.1.8" @@ -8954,8 +8954,8 @@ __metadata: typescript: "npm:5.3.3" vitest: "npm:1.3.1" peerDependencies: - react: 0.0.0-experimental-e5205658f-20230913 - react-dom: 0.0.0-experimental-e5205658f-20230913 + react: 18.3.0-canary-a870b2d54-20240314 + react-dom: 18.3.0-canary-a870b2d54-20240314 bin: cross-env: ./dist/bins/cross-env.js msw: ./dist/bins/msw.js @@ -11554,11 +11554,11 @@ __metadata: linkType: hard "@types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.2.19": - version: 18.2.19 - resolution: "@types/react-dom@npm:18.2.19" + version: 18.2.21 + resolution: "@types/react-dom@npm:18.2.21" dependencies: "@types/react": "npm:*" - checksum: 10c0/88d7c6daa4659f661d0c97985d9fca492f24b421a34bb614dcd94c343aed7bea121463149e97fb01ecaa693be17b7d1542cf71ddb1705f3889a81eb2639a88aa + checksum: 10c0/a887b4b647071df48173f054854713b68fdacfceeba7fa14f64ba26688d7d43574d7dc88a2a346e28f2e667eeab1b9bdbcad8a54353869835e52638607f61ff5 languageName: node linkType: hard @@ -28863,18 +28863,6 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:0.0.0-experimental-e5205658f-20230913": - version: 0.0.0-experimental-e5205658f-20230913 - resolution: "react-dom@npm:0.0.0-experimental-e5205658f-20230913" - dependencies: - loose-envify: "npm:^1.1.0" - scheduler: "npm:0.0.0-experimental-e5205658f-20230913" - peerDependencies: - react: 0.0.0-experimental-e5205658f-20230913 - checksum: 10c0/b8e0e0edf05161a39cd8495ac11dbebccda0e69245d1f33d07697122e65649a1f0539ff8ad7277d833aabc9cbee8da9d80de14d0766262412da5bf824d5eb823 - languageName: node - linkType: hard - "react-dom@npm:18.2.0": version: 18.2.0 resolution: "react-dom@npm:18.2.0" @@ -28887,6 +28875,17 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:18.3.0-canary-a870b2d54-20240314": + version: 18.3.0-canary-a870b2d54-20240314 + resolution: "react-dom@npm:18.3.0-canary-a870b2d54-20240314" + dependencies: + scheduler: "npm:0.24.0-canary-a870b2d54-20240314" + peerDependencies: + react: 18.3.0-canary-a870b2d54-20240314 + checksum: 10c0/6896473a3a7ed802f6b85c9601c64b0f1fe58ffbf3829ee5ac819a503fa16f5ec4d39b4b3f188cb2bf9ba9fb74cbdc28844c03cc2a4208303595220e5877d1b5 + languageName: node + linkType: hard + "react-element-to-jsx-string@npm:^15.0.0": version: 15.0.0 resolution: "react-element-to-jsx-string@npm:15.0.0" @@ -29013,18 +29012,17 @@ __metadata: languageName: node linkType: hard -"react-server-dom-webpack@npm:0.0.0-experimental-e5205658f-20230913": - version: 0.0.0-experimental-e5205658f-20230913 - resolution: "react-server-dom-webpack@npm:0.0.0-experimental-e5205658f-20230913" +"react-server-dom-webpack@npm:18.3.0-canary-a870b2d54-20240314": + version: 18.3.0-canary-a870b2d54-20240314 + resolution: "react-server-dom-webpack@npm:18.3.0-canary-a870b2d54-20240314" dependencies: acorn-loose: "npm:^8.3.0" - loose-envify: "npm:^1.1.0" neo-async: "npm:^2.6.1" peerDependencies: - react: 0.0.0-experimental-e5205658f-20230913 - react-dom: 0.0.0-experimental-e5205658f-20230913 + react: 18.3.0-canary-a870b2d54-20240314 + react-dom: 18.3.0-canary-a870b2d54-20240314 webpack: ^5.59.0 - checksum: 10c0/94c29f986209c3d174a3b200526a8f1e8e10c9c831d29e9938e5f6e08146020a37a5ec19410af73d21e32906c35c7c1f68b044c720ef5d785d4de6cbb0438a88 + checksum: 10c0/9040df3d8549898dbf4afd7bc86a01948e47b6eda998bda0e663a69fefa6922fa5977aaec0d0795938aac62c97d1da9307636051d0d12ec429c3c416cad23ffb languageName: node linkType: hard @@ -29045,15 +29043,6 @@ __metadata: languageName: node linkType: hard -"react@npm:0.0.0-experimental-e5205658f-20230913": - version: 0.0.0-experimental-e5205658f-20230913 - resolution: "react@npm:0.0.0-experimental-e5205658f-20230913" - dependencies: - loose-envify: "npm:^1.1.0" - checksum: 10c0/69f384cd192d6fc83bd77457b539c171cd89b44fd105c67c77a2c57b237c1068c598470ddf524084bdb7d9b0bc16362918493dbc3fbcb909af8edd92c6be9759 - languageName: node - linkType: hard - "react@npm:18.2.0": version: 18.2.0 resolution: "react@npm:18.2.0" @@ -29063,6 +29052,13 @@ __metadata: languageName: node linkType: hard +"react@npm:18.3.0-canary-a870b2d54-20240314": + version: 18.3.0-canary-a870b2d54-20240314 + resolution: "react@npm:18.3.0-canary-a870b2d54-20240314" + checksum: 10c0/f89b119c6fefc0956c815ad99e39ba83bc44485c4187143003a01906ef1d750a02f0018210eb192d1d0bdcd28a280139bf399bc59c972fc9e8f9938ee8c63387 + languageName: node + linkType: hard + "read-cmd-shim@npm:4.0.0, read-cmd-shim@npm:^4.0.0": version: 4.0.0 resolution: "read-cmd-shim@npm:4.0.0" @@ -30188,12 +30184,10 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:0.0.0-experimental-e5205658f-20230913": - version: 0.0.0-experimental-e5205658f-20230913 - resolution: "scheduler@npm:0.0.0-experimental-e5205658f-20230913" - dependencies: - loose-envify: "npm:^1.1.0" - checksum: 10c0/20475be7524bb89002818cfc0f54af122c0e2c07c07ddb92275e10c0e8d1a51a9c7ca3b4b95e9b27017da4e22f1510c478afe08becc1073decfd03ae5823f452 +"scheduler@npm:0.24.0-canary-a870b2d54-20240314": + version: 0.24.0-canary-a870b2d54-20240314 + resolution: "scheduler@npm:0.24.0-canary-a870b2d54-20240314" + checksum: 10c0/ac70f95c1d0cbf6de8bf0d1b2f1c8bb063d0ea0ce9410de720b9eeb17d85dc18bc9bc3c2ab89332cb0d7e746b68f7599ccc4915bcf3ea3a4541797bb1f2ec587 languageName: node linkType: hard From 9283e2b6dc529b7d158a75382cbaf8bbdf706c2d Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sun, 17 Mar 2024 18:07:18 +0100 Subject: [PATCH 31/34] chore(ci): refactor out set up steps (#10249) Want to keep working on CI every now and then. Starting by cleaning up some things so it's easier to reason about. Most jobs run the following steps: - enable corepack (this should go away one day when the setup node action supports it) - set up node - enable corepack again (for windows) - set up yarn cache - yarn install - build Refactoring these out into a composite action so that we can save some lines in ci.yml which is starting to get quite long. I can also document some things better this way. --- .github/actions/set-up-job/action.yml | 61 ++++ .github/workflows/check-changelog.yml | 14 +- .../workflows/check-create-redwood-app.yml | 14 +- .../workflows/check-test-project-fixture.yml | 22 +- .github/workflows/ci.yml | 264 ++---------------- .github/workflows/publish-canary.yml | 20 +- .../workflows/publish-release-candidate.yml | 23 +- 7 files changed, 107 insertions(+), 311 deletions(-) create mode 100644 .github/actions/set-up-job/action.yml diff --git a/.github/actions/set-up-job/action.yml b/.github/actions/set-up-job/action.yml new file mode 100644 index 000000000000..354c1ce23673 --- /dev/null +++ b/.github/actions/set-up-job/action.yml @@ -0,0 +1,61 @@ +name: Set up job +description: > + Everything you need to run a job in CI. + Checkout this repo (redwoodjs/redwood), set up Node.js, yarn install, and build. + +inputs: + set-up-yarn-cache: + description: > + For some actions, setting up the yarn cache takes longer than it would to just yarn install. + required: false + default: true + + yarn-install-directory: + description: > + The directory to run `yarn install` in. + required: false + + build: + description: > + Whether or not to run `yarn build` to build all the framework packages. + required: false + default: true + +runs: + using: composite + + steps: + - name: β¬’ Enable Corepack + shell: bash + run: corepack enable + + - name: β¬’ Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + # We have to enable Corepack again for Windows. 🀷 + # In general, we're waiting on [this issue](https://github.com/actions/setup-node/issues/531) + # to be resolved so that `actions/setup-node@v4` has first-class Corepack support. + - name: β¬’ Enable Corepack + if: runner.os == 'Windows' + shell: bash + run: corepack enable + + - name: 🐈 Set up yarn cache + if: inputs.set-up-yarn-cache == 'true' + uses: ./.github/actions/set-up-yarn-cache + + # One of our dependencies is on GitHub instead of NPM and without authentication + # we'll get rate limited and this step becomes flaky. + - name: 🐈 Yarn install + shell: bash + working-directory: ${{ inputs.yarn-install-directory }} + env: + GITHUB_TOKEN: ${{ github.token }} + run: yarn install --inline-builds + + - name: πŸ—οΈ Build + if: inputs.build == 'true' + shell: bash + run: yarn build diff --git a/.github/workflows/check-changelog.yml b/.github/workflows/check-changelog.yml index de48c3b134db..b50623f54571 100644 --- a/.github/workflows/check-changelog.yml +++ b/.github/workflows/check-changelog.yml @@ -16,16 +16,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - uses: actions/setup-node@v4 + - name: Set up job + uses: ./.github/actions/set-up-job with: - node-version: 20 - - - run: yarn install - working-directory: ./.github/actions/check_changesets + set-up-yarn-cache: false + yarn-install-directory: ./.github/actions/check_changesets + build: false - name: Check changesets uses: ./.github/actions/check_changesets diff --git a/.github/workflows/check-create-redwood-app.yml b/.github/workflows/check-create-redwood-app.yml index 1f22ece18aa8..cdcfa0d9c7cc 100644 --- a/.github/workflows/check-create-redwood-app.yml +++ b/.github/workflows/check-create-redwood-app.yml @@ -16,16 +16,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - uses: actions/setup-node@v4 + - name: Set up job + uses: ./.github/actions/set-up-job with: - node-version: 20 - - - run: yarn install - working-directory: ./.github/actions/check_create_redwood_app + set-up-yarn-cache: false + yarn-install-directory: ./.github/actions/check_create_redwood_app + build: false - name: Check create redwood app uses: ./.github/actions/check_create_redwood_app diff --git a/.github/workflows/check-test-project-fixture.yml b/.github/workflows/check-test-project-fixture.yml index 5808634283a3..b3d4016d420a 100644 --- a/.github/workflows/check-test-project-fixture.yml +++ b/.github/workflows/check-test-project-fixture.yml @@ -17,27 +17,9 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: 🐈 Set up yarn cache - if: "!contains(github.event.pull_request.labels.*.name, 'fixture-ok')" - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - if: "!contains(github.event.pull_request.labels.*.name, 'fixture-ok')" - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: πŸ”¨ Build + - name: Set up job if: "!contains(github.event.pull_request.labels.*.name, 'fixture-ok')" - run: yarn build + uses: ./.github/actions/set-up-job - name: Rebuild test-project fixture if: "!contains(github.event.pull_request.labels.*.name, 'fixture-ok')" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d514b24d9b3c..a2f8094e1b4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,20 +28,12 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 + - name: Set up job + uses: ./.github/actions/set-up-job with: - node-version: 20 - - - name: 🐈 Yarn install - working-directory: ./.github/actions/detect-changes - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} + set-up-yarn-cache: false + yarn-install-directory: ./.github/actions/detect-changes + build: false - name: πŸ” Detect changes id: detect-changes @@ -56,20 +48,12 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 + - name: Set up job + uses: ./.github/actions/set-up-job with: - node-version: 20 - - - name: 🐈 Yarn install - working-directory: ./tasks/check - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} + set-up-yarn-cache: false + yarn-install-directory: ./tasks/check + build: false - name: βœ… Check constraints, dependencies, and package.json's uses: ./tasks/check @@ -100,28 +84,8 @@ jobs: run: echo "echo "::remove-matcher owner=tsc::"" - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Enable Corepack - run: corepack enable - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: πŸ”¨ Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - name: πŸ”Ž Lint run: yarn lint @@ -163,25 +127,8 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: πŸ”¨ Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - name: 🌲 Install Cypress run: yarn cypress install @@ -259,28 +206,8 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Enable Corepack - run: corepack enable - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: πŸ”¨ Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - name: 🌲 Set up test project id: set-up-test-project @@ -370,28 +297,8 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Enable Corepack - run: corepack enable - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: πŸ”¨ Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - name: 🌲 Set up test project id: set-up-test-project @@ -498,28 +405,8 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Enable Corepack - run: corepack enable - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: πŸ”¨ Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - name: πŸ“’ Listen for telemetry (CRWA) run: node ./.github/actions/telemetry_check/check.mjs --mode crwa @@ -560,28 +447,8 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Enable Corepack - run: corepack enable - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: πŸ”¨ Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - name: 🌲 Set up RSC project id: set-up-rsc-project @@ -670,25 +537,8 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: πŸ”¨ Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - name: 🌲 Set up test project id: set-up-test-project @@ -759,28 +609,8 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Enable Corepack - run: corepack enable - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: πŸ”¨ Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - name: 🌲 Set up test project id: set-up-test-project @@ -857,25 +687,8 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: β¬’ Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: πŸ—οΈ Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - name: Set up test project run: | @@ -953,25 +766,8 @@ jobs: steps: - uses: actions/checkout@v4 - - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: πŸ”¨ Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - run: yarn vitest run working-directory: ./tasks/server-tests diff --git a/.github/workflows/publish-canary.yml b/.github/workflows/publish-canary.yml index 862707ef9d61..e4d5d968e260 100644 --- a/.github/workflows/publish-canary.yml +++ b/.github/workflows/publish-canary.yml @@ -31,24 +31,8 @@ jobs: - name: Enable Corepack run: corepack enable - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: βœ… Check constraints, dependencies, and package.json's - uses: ./tasks/check - - - name: πŸ— Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - name: πŸ”Ž Lint run: yarn lint diff --git a/.github/workflows/publish-release-candidate.yml b/.github/workflows/publish-release-candidate.yml index a6fb174f204f..1b34beff9346 100644 --- a/.github/workflows/publish-release-candidate.yml +++ b/.github/workflows/publish-release-candidate.yml @@ -65,27 +65,8 @@ jobs: # This is required because lerna uses tags to determine the version. fetch-depth: 0 - - name: Enable Corepack - run: corepack enable - - - name: β¬’ Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: 🐈 Set up yarn cache - uses: ./.github/actions/set-up-yarn-cache - - - name: 🐈 Yarn install - run: yarn install --inline-builds - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: βœ… Check constraints, dependencies, and package.json's - uses: ./tasks/check - - - name: πŸ— Build - run: yarn build + - name: Set up job + uses: ./.github/actions/set-up-job - name: πŸ”Ž Lint run: yarn lint From 432fcbd482e11351bf9f3c2347cbc6cef49d2853 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sun, 17 Mar 2024 18:12:19 +0100 Subject: [PATCH 32/34] chore(ci): remove unused workflows (#10250) There's a few CI workflows in `.github/workflows` that we don't use anymore; removing them here. --- .../update_all_contributors/action.yml | 5 -- .../update_all_contributors.mjs | 72 ------------------- .../workflows/monthly_issue_metrics_json.yml | 39 ---------- .github/workflows/update-all-contributors.yml | 35 --------- .../workflows/weekly_issue_metrics_json.yml | 41 ----------- 5 files changed, 192 deletions(-) delete mode 100644 .github/actions/update_all_contributors/action.yml delete mode 100644 .github/actions/update_all_contributors/update_all_contributors.mjs delete mode 100644 .github/workflows/monthly_issue_metrics_json.yml delete mode 100644 .github/workflows/update-all-contributors.yml delete mode 100644 .github/workflows/weekly_issue_metrics_json.yml diff --git a/.github/actions/update_all_contributors/action.yml b/.github/actions/update_all_contributors/action.yml deleted file mode 100644 index 46dd2eb16cc1..000000000000 --- a/.github/actions/update_all_contributors/action.yml +++ /dev/null @@ -1,5 +0,0 @@ -name: Update all contributors -description: Updates all contributors -runs: - using: node20 - main: update_all_contributors.mjs diff --git a/.github/actions/update_all_contributors/update_all_contributors.mjs b/.github/actions/update_all_contributors/update_all_contributors.mjs deleted file mode 100644 index 3a15bdd91432..000000000000 --- a/.github/actions/update_all_contributors/update_all_contributors.mjs +++ /dev/null @@ -1,72 +0,0 @@ -import { getExecOutput, exec } from '@actions/exec' - -const runAllContributors = (args) => { - args = Array.isArray(args) ? args : [args] - - return getExecOutput('yarn', [ - 'run', - 'all-contributors', - '--config=.all-contributorsrc', - ...args - ], { - cwd: './tasks/all-contributors' - }) -} - -const ALL_CONTRIBUTORS_IGNORE_LIST = [ - // core team - 'agiannelli', - 'ajcwebdev', - 'alicelovescake', - 'aldonline', - 'callingmedic911', - 'cannikin', - 'dac09', - 'dthyresson', - 'forresthayes', - 'jtoar', - 'kimadeline', - 'KrisCoulson', - 'mojombo', - 'noire-munich', - 'peterp', - 'realStandal', - 'RobertBroersma', - 'simoncrypta', - 'Tobbe', - 'thedavidprice', - 'virtuoushub', - - // bots - 'codesee-maps[bot]', - 'dependabot[bot]', - 'dependabot-preview[bot]', - 'redwoodjsbot', - 'renovate[bot]', -] - -const { stdout } = await runAllContributors('check') - -const contributors = stdout - .trim() - .split('\n')[1] - .split(',') - .map((contributor) => contributor.trim()) - .filter( - (contributor) => !ALL_CONTRIBUTORS_IGNORE_LIST.includes(contributor) - ) - -if (contributors.length === 0) { - console.log('No contributors to add') -} else { - for (const contributor of contributors) { - await runAllContributors(['add', contributor, 'code']) - } - - await runAllContributors(['generate', '--contributorsPerLine=5']) - - await exec('git', ['config', 'user.name', 'github-actions']) - await exec('git', ['config', 'user.email', 'github-actions@github.com']) - await exec('git', ['commit', '-am chore: update all contributors']) - await exec('git', ['push']) -} diff --git a/.github/workflows/monthly_issue_metrics_json.yml b/.github/workflows/monthly_issue_metrics_json.yml deleted file mode 100644 index cfd3be71eacd..000000000000 --- a/.github/workflows/monthly_issue_metrics_json.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Monthly issue metrics with JSON output -on: - workflow_dispatch: - schedule: - # 3:04 AM on the 1st day of every month - - cron: '4 3 1 * *' - -permissions: - issues: write - pull-requests: read - -jobs: - build: - name: monthly issue metrics (json) - runs-on: ubuntu-latest - - steps: - - name: Get dates for last month - shell: bash - run: | - # Calculate the first day of the previous month - first_day=$(date -d "last month" +%Y-%m-01) - - # Calculate the last day of the previous month - last_day=$(date -d "$first_day +1 month -1 day" +%Y-%m-%d) - - #Set an environment variable with the date range - echo "$first_day..$last_day" - echo "last_month=$first_day..$last_day" >> "$GITHUB_ENV" - - - name: Run issue-metrics tool - id: issue-metrics - uses: github/issue-metrics@v2 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SEARCH_QUERY: 'repo:redwoodjs/redwood is:issue created:${{ env.last_month }} -reason:"not planned"' - - - name: Print output of issue metrics tool - run: echo "${{ steps.issue-metrics.outputs.metrics }}" diff --git a/.github/workflows/update-all-contributors.yml b/.github/workflows/update-all-contributors.yml deleted file mode 100644 index 63317934a946..000000000000 --- a/.github/workflows/update-all-contributors.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Update all contributors - -on: - schedule: - # This runs once a week: https://crontab.guru/once-a-week. - # * is a special character in YAML so you have to quote this string. - - cron: '0 0 * * 0' - workflow_dispatch: - -# Cancel in-progress runs of this workflow. -# See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-only-cancel-in-progress-jobs-or-runs-for-the-current-workflow. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - update-all-contributors: - name: Update all contributors - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - token: ${{ secrets.JTOAR_TOKEN }} - - - name: Enable Corepack - run: corepack enable - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - run: yarn install - - - name: Update all contributors - uses: ./.github/actions/update_all_contributors diff --git a/.github/workflows/weekly_issue_metrics_json.yml b/.github/workflows/weekly_issue_metrics_json.yml deleted file mode 100644 index da0a8bd6fc27..000000000000 --- a/.github/workflows/weekly_issue_metrics_json.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Weekly issue metrics with JSON output -on: - workflow_dispatch: - schedule: - # 2:33 AM every Monday - - cron: '33 2 * * 1' - -permissions: - issues: write - pull-requests: read - -jobs: - build: - name: weekly issue metrics json - runs-on: ubuntu-latest - steps: - - name: Get dates for last week - shell: bash - run: | - # Calculate the first day of the previous week (and as we all know - # weeks start on Mondays ;)) - first_day=$(date -d "last Sunday - 6 days" +%Y-%m-%d) - - # Calculate the last day of the previous week - last_day=$(date -d "last Sunday" +%Y-%m-%d) - - #Set an environment variable with the date range - echo "$first_day..$last_day" - echo "last_week=$first_day..$last_day" >> "$GITHUB_ENV" - - - name: Run issue-metrics tool - uses: github/issue-metrics@v2 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SEARCH_QUERY: 'repo:redwoodjs/redwood is:issue created:${{ env.last_week }} -reason:"not planned"' - - - name: Print output of issue metrics tool - run: | - cat ./issue_metrics.json - cat ./issue_metrics.json | jq .total_item_count - cat ./issue_metrics.json | jq .average_time_to_first_response From b7d3cfca7eaed72d28c5de05b7801e5e24d87514 Mon Sep 17 00:00:00 2001 From: Nichita Sindie <66722479+thenewnickyzz@users.noreply.github.com> Date: Sun, 17 Mar 2024 19:45:54 +0200 Subject: [PATCH 33/34] docs: fix typo on router page (#10251) Router documentation page - remove extra `>` character from the Route path. ## Before: Screenshot 2024-03-14 at 08 32 31 ## After: Screenshot 2024-03-17 at 19 15 04 --- docs/docs/router.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/router.md b/docs/docs/router.md index aa606ac91909..94ef8fa3d06b 100644 --- a/docs/docs/router.md +++ b/docs/docs/router.md @@ -319,7 +319,7 @@ See below for more info on route parameters. To match variable data in a path, you can use route parameters, which are specified by a parameter name surrounded by curly braces: ```jsx title="Routes.js" - + ``` This route will match URLs like `/user/7` or `/user/mojombo`. You can have as many route parameters as you like: From 43ca27a6f3fc74b104481994fc27ec2ea1050e92 Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Sun, 17 Mar 2024 23:20:40 +0100 Subject: [PATCH 34/34] RSC: Vite plugin for route loading (#10252) **Problem** We have to be smarter about how we automatically load route pages during dev. As a result, we have refactored out the logic into a vite plugin which has access to knowledge about whether it is being built for (or served for) SSR. **Changes** 1. Removed babel plugins for the RSC server and client route loading 2. Added a vite plugin which uses babel to automatically import the route pages 3. Refactored the `react` vite plugin back into the root config in `index.ts` as it no longer has to be specifically applied in the various vite build or create calls. --- __fixtures__/test-project/web/package.json | 2 +- packages/babel-config/src/index.ts | 3 - ...wood-routes-auto-loader-rsc-client.test.ts | 100 ---- ...wood-routes-auto-loader-rsc-server.test.ts | 98 ---- ...n-redwood-routes-auto-loader-rsc-client.ts | 120 ----- ...n-redwood-routes-auto-loader-rsc-server.ts | 119 ----- packages/vite/src/client.ts | 3 +- packages/vite/src/clientSsr.ts | 11 +- packages/vite/src/devFeServer.ts | 24 +- packages/vite/src/index.ts | 18 +- ...vite-plugin-rsc-route-auto-loader.test.mts | 447 ++++++++++++++++++ .../vite-plugin-rsc-routes-auto-loader.ts | 154 ++++++ packages/vite/src/rsc/rscBuildAnalyze.ts | 8 - packages/vite/src/rsc/rscBuildClient.ts | 23 +- packages/vite/src/rsc/rscBuildForServer.ts | 21 +- packages/vite/src/rsc/rscWorker.ts | 21 +- .../src/streaming/buildForStreamingServer.ts | 25 +- 17 files changed, 631 insertions(+), 566 deletions(-) delete mode 100644 packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-client.test.ts delete mode 100644 packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-server.test.ts delete mode 100644 packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-client.ts delete mode 100644 packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-server.ts create mode 100644 packages/vite/src/plugins/__tests__/vite-plugin-rsc-route-auto-loader.test.mts create mode 100644 packages/vite/src/plugins/vite-plugin-rsc-routes-auto-loader.ts diff --git a/__fixtures__/test-project/web/package.json b/__fixtures__/test-project/web/package.json index d39585dc947e..62e918264c24 100644 --- a/__fixtures__/test-project/web/package.json +++ b/__fixtures__/test-project/web/package.json @@ -24,7 +24,7 @@ "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", "autoprefixer": "^10.4.18", - "postcss": "^8.4.35", + "postcss": "^8.4.36", "postcss-loader": "^8.1.1", "prettier-plugin-tailwindcss": "^0.5.12", "tailwindcss": "^3.4.1" diff --git a/packages/babel-config/src/index.ts b/packages/babel-config/src/index.ts index 2b51277bd605..7658f3e7a67c 100644 --- a/packages/babel-config/src/index.ts +++ b/packages/babel-config/src/index.ts @@ -34,6 +34,3 @@ export { parseTypeScriptConfigFiles, registerBabel, } from './common' - -export { redwoodRoutesAutoLoaderRscClientPlugin } from './plugins/babel-plugin-redwood-routes-auto-loader-rsc-client' -export { redwoodRoutesAutoLoaderRscServerPlugin } from './plugins/babel-plugin-redwood-routes-auto-loader-rsc-server' diff --git a/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-client.test.ts b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-client.test.ts deleted file mode 100644 index 2632b6ca5ef3..000000000000 --- a/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-client.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import fs from 'fs' -import path from 'path' - -import * as babel from '@babel/core' - -import { getPaths } from '@redwoodjs/project-config' - -import { redwoodRoutesAutoLoaderRscClientPlugin } from '../babel-plugin-redwood-routes-auto-loader-rsc-client' - -const transform = (filename: string) => { - const code = fs.readFileSync(filename, 'utf-8') - return babel.transform(code, { - filename, - presets: ['@babel/preset-react'], - plugins: [[redwoodRoutesAutoLoaderRscClientPlugin, {}]], - }) -} - -describe('injects the correct loading logic', () => { - const RSC_FIXTURE_PATH = path.resolve( - __dirname, - '../../../../../__fixtures__/test-project-rsc-external-packages-and-cells/', - ) - let result: babel.BabelFileResult | null - - beforeAll(() => { - process.env.RWJS_CWD = RSC_FIXTURE_PATH - result = transform(getPaths().web.routes) - }) - - afterAll(() => { - delete process.env.RWJS_CWD - }) - - test('pages are loaded with renderFromRscServer', () => { - const codeOutput = result?.code - - // We shouldn't see classic lazy loading like in non-RSC redwood - expect(codeOutput).not.toContain(`const HomePage = { - name: "HomePage", - prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage")), - LazyComponent: lazy(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage")) -`) - - // We should import the function - expect(codeOutput).toContain( - 'import { renderFromRscServer } from "@redwoodjs/vite/client"', - ) - - // Un-imported pages get added with renderFromRscServer - expect(codeOutput).toContain( - 'const HomePage = renderFromRscServer("HomePage")', - ) - expect(codeOutput).toContain( - 'const AboutPage = renderFromRscServer("AboutPage")', - ) - expect(codeOutput).toContain( - 'const UserExampleNewUserExamplePage = renderFromRscServer("UserExampleNewUserExamplePage")', - ) - }) - - test('already imported pages are left alone.', () => { - expect(result?.code).toContain( - `import NotFoundPage from './pages/NotFoundPage/NotFoundPage'`, - ) - - expect(result?.code).not.toContain( - `const NotFoundPage = renderFromRscServer("NotFoundPage")`, - ) - }) -}) - -describe('mulitiple files ending in Page.{js,jsx,ts,tsx}', () => { - const FAILURE_FIXTURE_PATH = path.resolve( - __dirname, - './__fixtures__/route-auto-loader/failure', - ) - - beforeAll(() => { - process.env.RWJS_CWD = FAILURE_FIXTURE_PATH - }) - - afterAll(() => { - delete process.env.RWJS_CWD - }) - - test('fails with appropriate message', () => { - expect(() => { - transform(getPaths().web.routes) - }).toThrow( - "Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage", - ) - - expect(() => { - transform(getPaths().web.routes) - }).toThrow( - "Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage", - ) - }) -}) diff --git a/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-server.test.ts b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-server.test.ts deleted file mode 100644 index 2283607c92ce..000000000000 --- a/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-routes-auto-loader-rsc-server.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import fs from 'fs' -import path from 'path' - -import * as babel from '@babel/core' - -import { getPaths } from '@redwoodjs/project-config' - -import { redwoodRoutesAutoLoaderRscServerPlugin } from '../babel-plugin-redwood-routes-auto-loader-rsc-server' - -const transform = (filename: string) => { - const code = fs.readFileSync(filename, 'utf-8') - return babel.transform(code, { - filename, - presets: ['@babel/preset-react'], - plugins: [[redwoodRoutesAutoLoaderRscServerPlugin, {}]], - }) -} - -const RSC_FIXTURE_PATH = path.resolve( - __dirname, - '../../../../../__fixtures__/test-project-rsc-external-packages-and-cells/', -) - -describe('injects the correct loading logic', () => { - let result: babel.BabelFileResult | null - beforeAll(() => { - process.env.RWJS_CWD = RSC_FIXTURE_PATH - result = transform(getPaths().web.routes) - }) - - afterAll(() => { - delete process.env.RWJS_CWD - }) - - test('Pages are loaded with renderFromDist', () => { - const codeOutput = result?.code - - // We shouldn't see classic lazy loading like in non-RSC redwood - expect(codeOutput).not.toContain(`const HomePage = { - name: "HomePage", - prerenderLoader: name => __webpack_require__(require.resolveWeak("./pages/HomePage/HomePage")), - LazyComponent: lazy(() => import( /* webpackChunkName: "HomePage" */"./pages/HomePage/HomePage")) -`) - - // We should import the function - expect(codeOutput).toContain( - 'import { renderFromDist } from "@redwoodjs/vite/clientSsr"', - ) - - // Un-imported pages get added with renderFromDist - expect(codeOutput).toContain('const HomePage = renderFromDist("HomePage")') - expect(codeOutput).toContain( - 'const AboutPage = renderFromDist("AboutPage")', - ) - expect(codeOutput).toContain( - 'const UserExampleNewUserExamplePage = renderFromDist("UserExampleNewUserExamplePage")', - ) - }) - - test('already imported pages are left alone.', () => { - expect(result?.code).toContain( - `import NotFoundPage from './pages/NotFoundPage/NotFoundPage'`, - ) - - expect(result?.code).not.toContain( - `const NotFoundPage = renderFromDist("NotFoundPage")`, - ) - }) -}) - -describe('mulitiple files ending in Page.{js,jsx,ts,tsx}', () => { - const FAILURE_FIXTURE_PATH = path.resolve( - __dirname, - './__fixtures__/route-auto-loader/failure', - ) - - beforeAll(() => { - process.env.RWJS_CWD = FAILURE_FIXTURE_PATH - }) - - afterAll(() => { - delete process.env.RWJS_CWD - }) - - test('fails with appropriate message', () => { - expect(() => { - transform(getPaths().web.routes) - }).toThrow( - "Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage", - ) - - expect(() => { - transform(getPaths().web.routes) - }).toThrow( - "Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'HomePage", - ) - }) -}) diff --git a/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-client.ts b/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-client.ts deleted file mode 100644 index 7786d9d04b74..000000000000 --- a/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-client.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { PluginObj, types } from '@babel/core' - -import { - ensurePosixPath, - importStatementPath, - processPagesDir, -} from '@redwoodjs/project-config' - -import { - getPathRelativeToSrc, - withRelativeImports, -} from './babel-plugin-redwood-routes-auto-loader' - -export function redwoodRoutesAutoLoaderRscClientPlugin({ - types: t, -}: { - types: typeof types -}): PluginObj { - // @NOTE: This var gets mutated inside the visitors - let pages = processPagesDir().map(withRelativeImports) - - // Currently processPagesDir() can return duplicate entries when there are multiple files - // ending in Page in the individual page directories. This will cause an error upstream. - // Here we check for duplicates and throw a more helpful error message. - const duplicatePageImportNames = new Set() - const sortedPageImportNames = pages.map((page) => page.importName).sort() - for (let i = 0; i < sortedPageImportNames.length - 1; i++) { - if (sortedPageImportNames[i + 1] === sortedPageImportNames[i]) { - duplicatePageImportNames.add(sortedPageImportNames[i]) - } - } - - if (duplicatePageImportNames.size > 0) { - throw new Error( - `Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: ${Array.from( - duplicatePageImportNames, - ) - .map((name) => `'${name}'`) - .join(', ')}`, - ) - } - - return { - name: 'babel-plugin-redwood-routes-auto-loader', - visitor: { - // Remove any pages that have been explicitly imported in the Routes file, - // because when one is present, the user is requesting that the module be - // included in the main bundle. - ImportDeclaration(p) { - if (pages.length === 0) { - return - } - - const userImportRelativePath = getPathRelativeToSrc( - importStatementPath(p.node.source?.value), - ) - - const defaultSpecifier = p.node.specifiers.filter((specifiers) => - t.isImportDefaultSpecifier(specifiers), - )[0] - - if (userImportRelativePath && defaultSpecifier) { - // Remove the page from pages list, if it is already explicitly imported, so that we don't add loaders for these pages. - // We use the path & defaultSpecifier because the const name could be anything - pages = pages.filter( - (page) => - !( - page.relativeImport === ensurePosixPath(userImportRelativePath) - ), - ) - } - }, - Program: { - enter() { - pages = processPagesDir().map(withRelativeImports) - }, - exit(p) { - if (pages.length === 0) { - return - } - const nodes = [] - - // For RSC Client builds add - // import { renderFromRscServer } from '@redwoodjs/vite/client' - // This will perform a fetch request to the remote RSC server - nodes.unshift( - t.importDeclaration( - [ - t.importSpecifier( - t.identifier('renderFromRscServer'), - t.identifier('renderFromRscServer'), - ), - ], - t.stringLiteral('@redwoodjs/vite/client'), - ), - ) - - // Prepend all imports to the top of the file - for (const { importName } of pages) { - // RSC client wants this format - // const AboutPage = renderFromRscServer('AboutPage') - nodes.push( - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(importName), - t.callExpression(t.identifier('renderFromRscServer'), [ - t.stringLiteral(importName), - ]), - ), - ]), - ) - } - - // Insert at the top of the file - p.node.body.unshift(...nodes) - }, - }, - }, - } -} diff --git a/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-server.ts b/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-server.ts deleted file mode 100644 index 5ed732018c21..000000000000 --- a/packages/babel-config/src/plugins/babel-plugin-redwood-routes-auto-loader-rsc-server.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { PluginObj, types } from '@babel/core' - -import { - ensurePosixPath, - importStatementPath, - processPagesDir, -} from '@redwoodjs/project-config' - -import { - getPathRelativeToSrc, - withRelativeImports, -} from './babel-plugin-redwood-routes-auto-loader' - -export function redwoodRoutesAutoLoaderRscServerPlugin({ - types: t, -}: { - types: typeof types -}): PluginObj { - // @NOTE: This var gets mutated inside the visitors - let pages = processPagesDir().map(withRelativeImports) - - // Currently processPagesDir() can return duplicate entries when there are multiple files - // ending in Page in the individual page directories. This will cause an error upstream. - // Here we check for duplicates and throw a more helpful error message. - const duplicatePageImportNames = new Set() - const sortedPageImportNames = pages.map((page) => page.importName).sort() - for (let i = 0; i < sortedPageImportNames.length - 1; i++) { - if (sortedPageImportNames[i + 1] === sortedPageImportNames[i]) { - duplicatePageImportNames.add(sortedPageImportNames[i]) - } - } - if (duplicatePageImportNames.size > 0) { - throw new Error( - `Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: ${Array.from( - duplicatePageImportNames, - ) - .map((name) => `'${name}'`) - .join(', ')}`, - ) - } - - return { - name: 'babel-plugin-redwood-routes-auto-loader', - visitor: { - // Remove any pages that have been explicitly imported in the Routes file, - // because when one is present, the user is requesting that the module be - // included in the main bundle. - ImportDeclaration(p) { - if (pages.length === 0) { - return - } - - const userImportRelativePath = getPathRelativeToSrc( - importStatementPath(p.node.source?.value), - ) - - const defaultSpecifier = p.node.specifiers.filter((specifiers) => - t.isImportDefaultSpecifier(specifiers), - )[0] - - if (userImportRelativePath && defaultSpecifier) { - // Remove the page from pages list, if it is already explicitly imported, so that we don't add loaders for these pages. - // We use the path & defaultSpecifier because the const name could be anything - pages = pages.filter( - (page) => - !( - page.relativeImport === ensurePosixPath(userImportRelativePath) - ), - ) - } - }, - Program: { - enter() { - pages = processPagesDir().map(withRelativeImports) - }, - exit(p) { - if (pages.length === 0) { - return - } - const nodes = [] - - // For RSC Server builds add - // import { renderFromDist } from '@redwoodjs/vite/clientSsr' - // This will directly read the component from the dist folder - nodes.unshift( - t.importDeclaration( - [ - t.importSpecifier( - t.identifier('renderFromDist'), - t.identifier('renderFromDist'), - ), - ], - t.stringLiteral('@redwoodjs/vite/clientSsr'), - ), - ) - - // Prepend all imports to the top of the file - for (const { importName } of pages) { - // RSC server wants this format - // const AboutPage = renderFromDist('AboutPage') - nodes.push( - t.variableDeclaration('const', [ - t.variableDeclarator( - t.identifier(importName), - t.callExpression(t.identifier('renderFromDist'), [ - t.stringLiteral(importName), - ]), - ), - ]), - ) - } - - // Insert at the top of the file - p.node.body.unshift(...nodes) - }, - }, - }, - } -} diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index be884e066aae..1c143a43a0bc 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -21,13 +21,12 @@ const checkStatus = async ( const BASE_PATH = '/rw-rsc/' export function renderFromRscServer(rscId: string) { - console.log('serve rscId', rscId) + console.log('serve rscId (renderFromRscServer)', rscId) // TODO (RSC): Remove this when we have a babel plugin to call another // function during SSR if (typeof window === 'undefined') { // Temporarily skip rendering this component during SSR - // return null return null } diff --git a/packages/vite/src/clientSsr.ts b/packages/vite/src/clientSsr.ts index ea7fcfedfd2b..338169e942f0 100644 --- a/packages/vite/src/clientSsr.ts +++ b/packages/vite/src/clientSsr.ts @@ -1,3 +1,10 @@ -export function renderFromDist(...args: any[]) { - console.log('renderFromDist', args) +export function renderFromDist(rscId: string) { + console.log('renderFromDist', rscId) + + // TODO: Actually render the component that was requested + const SsrComponent = () => { + return 'Loading...' + } + + return SsrComponent } diff --git a/packages/vite/src/devFeServer.ts b/packages/vite/src/devFeServer.ts index 3aeeeea7a126..ed0d24b67640 100644 --- a/packages/vite/src/devFeServer.ts +++ b/packages/vite/src/devFeServer.ts @@ -1,14 +1,9 @@ -import react from '@vitejs/plugin-react' import { createServerAdapter } from '@whatwg-node/server' import express from 'express' import type { ViteDevServer } from 'vite' import { createServer as createViteServer } from 'vite' import { cjsInterop } from 'vite-plugin-cjs-interop' -import { - redwoodRoutesAutoLoaderRscClientPlugin, - getWebSideDefaultBabelConfig, -} from '@redwoodjs/babel-config' import type { RouteSpec } from '@redwoodjs/internal/dist/routes' import { getProjectRoutes } from '@redwoodjs/internal/dist/routes' import type { Paths } from '@redwoodjs/project-config' @@ -16,6 +11,7 @@ import { getConfig, getPaths } from '@redwoodjs/project-config' import { registerFwGlobalsAndShims } from './lib/registerFwGlobalsAndShims.js' import { invoke } from './middleware/invokeMiddleware.js' +import { rscRoutesAutoLoader } from './plugins/vite-plugin-rsc-routes-auto-loader.js' import { createRscRequestHandler } from './rsc/rscRequestHandler.js' import { collectCssPaths, componentsModules } from './streaming/collectCss.js' import { createReactStreamingHandler } from './streaming/createReactStreamingHandler.js' @@ -53,32 +49,16 @@ async function createServer() { } // ~~~~ Dev time validations ~~~~ - const reactBabelConfig = getWebSideDefaultBabelConfig({ - forVite: true, - forRSC: rscEnabled, - }) - if (rscEnabled) { - reactBabelConfig.overrides.push({ - test: /Routes.(js|tsx|jsx)$/, - plugins: [[redwoodRoutesAutoLoaderRscClientPlugin, {}]], - babelrc: false, - ignore: ['node_modules'], - }) - } - // Create Vite server in middleware mode and configure the app type as // 'custom', disabling Vite's own HTML serving logic so parent server // can take control const vite = await createViteServer({ configFile: rwPaths.web.viteConfig, plugins: [ - rscEnabled && - react({ - babel: reactBabelConfig, - }), cjsInterop({ dependencies: ['@redwoodjs/**'], }), + rscEnabled && rscRoutesAutoLoader(), ], server: { middlewareMode: true }, logLevel: 'info', diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index cc4dfa68cfb4..aeb447e96a7d 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -37,7 +37,6 @@ export default function redwoodPluginVite(): PluginOption[] { .includes('@redwoodjs/realtime') const streamingEnabled = rwConfig.experimental.streamingSsr.enabled - const rscEnabled = rwConfig.experimental.rsc.enabled return [ { @@ -147,14 +146,13 @@ export default function redwoodPluginVite(): PluginOption[] { id: /@redwoodjs\/web\/dist\/apollo\/sseLink/, }, ]), - !rscEnabled && - react({ - babel: { - ...getWebSideDefaultBabelConfig({ - forVite: true, - forRSC: rscEnabled, - }), - }, - }), + react({ + babel: { + ...getWebSideDefaultBabelConfig({ + forVite: true, + forRSC: rwConfig.experimental?.rsc?.enabled, + }), + }, + }), ] } diff --git a/packages/vite/src/plugins/__tests__/vite-plugin-rsc-route-auto-loader.test.mts b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-route-auto-loader.test.mts new file mode 100644 index 000000000000..daa5c46afb9b --- /dev/null +++ b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-route-auto-loader.test.mts @@ -0,0 +1,447 @@ +import path from 'node:path' +import { vol } from 'memfs' +import { normalizePath } from 'vite' + +import { afterAll, beforeAll, describe, it, expect, vi, Mock, beforeEach, afterEach } from 'vitest' + +import { processPagesDir } from '@redwoodjs/project-config' +import type ProjectConfig from '@redwoodjs/project-config' + +import { rscRoutesAutoLoader } from '../vite-plugin-rsc-routes-auto-loader' + +vi.mock('fs', async () => ({ default: (await import('memfs')).fs })) + +const RWJS_CWD = process.env.RWJS_CWD + +vi.mock('@redwoodjs/project-config', async (importOriginal) => { + const originalGetPaths = await importOriginal() + return { + ...originalGetPaths, + getPaths: () => { + return { + ...originalGetPaths.getPaths(), + web: { + ...originalGetPaths.getPaths().web, + routes: '/Users/mojombo/rw-app/web/src/Routes.tsx', + }, + } + }, + processPagesDir: vi.fn(), + } +}) + +beforeAll(() => { + // Add a toml entry for getPaths et al. + process.env.RWJS_CWD = '/Users/mojombo/rw-app/' + vol.fromJSON({ + 'redwood.toml': '', + }, process.env.RWJS_CWD) +}) + +afterAll(() => { + process.env.RWJS_CWD = RWJS_CWD +}) + +describe('rscRoutesAutoLoader', () => { + beforeEach(() => { + (processPagesDir as Mock).mockReturnValue(pages) + }) + + afterEach(() => { + vi.resetAllMocks() + }) + + it('should insert the correct imports for non-ssr', async () => { + const plugin = rscRoutesAutoLoader() + if (typeof plugin.transform !== 'function') { + return + } + + // Calling `bind` to please TS + // See https://stackoverflow.com/a/70463512/88106 + const id = path.join(process.env.RWJS_CWD!, 'web', 'src', 'Routes.tsx') + const output = await plugin.transform.bind({})( + `import { jsx, jsxs } from "react/jsx-runtime"; + import { Router, Route, Set } from "@redwoodjs/router"; + import NavigationLayout from "./layouts/NavigationLayout/NavigationLayout"; + import ScaffoldLayout from "./layouts/ScaffoldLayout/ScaffoldLayout"; + import NotFoundPage from "./pages/NotFoundPage/NotFoundPage"; + const Routes = () => { + return /* @__PURE__ */ jsxs(Router, { children: [ + /* @__PURE__ */ jsxs(Set, { wrap: NavigationLayout, children: [ + /* @__PURE__ */ jsx(Route, { path: "/", page: HomePage, name: "home" }), + /* @__PURE__ */ jsx(Route, { path: "/about", page: AboutPage, name: "about" }), + /* @__PURE__ */ jsx(Route, { path: "/multi-cell", page: MultiCellPage, name: "multiCell" }), + /* @__PURE__ */ jsxs(Set, { wrap: ScaffoldLayout, title: "EmptyUsers", titleTo: "emptyUsers", buttonLabel: "New EmptyUser", buttonTo: "newEmptyUser", children: [ + /* @__PURE__ */ jsx(Route, { path: "/empty-users/new", page: EmptyUserNewEmptyUserPage, name: "newEmptyUser" }), + /* @__PURE__ */ jsx(Route, { path: "/empty-users", page: EmptyUserEmptyUsersPage, name: "emptyUsers" }) + ] }), + /* @__PURE__ */ jsxs(Set, { wrap: ScaffoldLayout, title: "UserExamples", titleTo: "userExamples", buttonLabel: "New UserExample", buttonTo: "newUserExample", children: [ + /* @__PURE__ */ jsx(Route, { path: "/user-examples/new", page: UserExampleNewUserExamplePage, name: "newUserExample" }), + /* @__PURE__ */ jsx(Route, { path: "/user-examples/{id:Int}", page: UserExampleUserExamplePage, name: "userExample" }), + /* @__PURE__ */ jsx(Route, { path: "/user-examples", page: UserExampleUserExamplesPage, name: "userExamples" }) + ] }) + ] }), + /* @__PURE__ */ jsx(Route, { notfound: true, page: NotFoundPage }) + ] }); + }; + export default Routes;`, + normalizePath(id), + // Passing undefined here to explicitly demonstrate that we're not passing { ssr: true } + undefined + ) + + // What we are interested in seeing here is: + // - The import of `renderFromRscServer` from `@redwoodjs/vite/client` + // - The call to `renderFromRscServer` for each page that wasn't already imported + expect(output).toMatchInlineSnapshot(` + "import { renderFromRscServer } from "@redwoodjs/vite/client"; + const EmptyUserNewEmptyUserPage = renderFromRscServer("EmptyUserNewEmptyUserPage"); + const EmptyUserEmptyUsersPage = renderFromRscServer("EmptyUserEmptyUsersPage"); + const EmptyUserEmptyUserPage = renderFromRscServer("EmptyUserEmptyUserPage"); + const EmptyUserEditEmptyUserPage = renderFromRscServer("EmptyUserEditEmptyUserPage"); + const HomePage = renderFromRscServer("HomePage"); + const FatalErrorPage = renderFromRscServer("FatalErrorPage"); + const AboutPage = renderFromRscServer("AboutPage"); + import { jsx, jsxs } from "react/jsx-runtime"; + import { Router, Route, Set } from "@redwoodjs/router"; + import NavigationLayout from "./layouts/NavigationLayout/NavigationLayout"; + import ScaffoldLayout from "./layouts/ScaffoldLayout/ScaffoldLayout"; + import NotFoundPage from "./pages/NotFoundPage/NotFoundPage"; + const Routes = () => { + return /* @__PURE__ */jsxs(Router, { + children: [/* @__PURE__ */jsxs(Set, { + wrap: NavigationLayout, + children: [/* @__PURE__ */jsx(Route, { + path: "/", + page: HomePage, + name: "home" + }), /* @__PURE__ */jsx(Route, { + path: "/about", + page: AboutPage, + name: "about" + }), /* @__PURE__ */jsx(Route, { + path: "/multi-cell", + page: MultiCellPage, + name: "multiCell" + }), /* @__PURE__ */jsxs(Set, { + wrap: ScaffoldLayout, + title: "EmptyUsers", + titleTo: "emptyUsers", + buttonLabel: "New EmptyUser", + buttonTo: "newEmptyUser", + children: [/* @__PURE__ */jsx(Route, { + path: "/empty-users/new", + page: EmptyUserNewEmptyUserPage, + name: "newEmptyUser" + }), /* @__PURE__ */jsx(Route, { + path: "/empty-users", + page: EmptyUserEmptyUsersPage, + name: "emptyUsers" + })] + }), /* @__PURE__ */jsxs(Set, { + wrap: ScaffoldLayout, + title: "UserExamples", + titleTo: "userExamples", + buttonLabel: "New UserExample", + buttonTo: "newUserExample", + children: [/* @__PURE__ */jsx(Route, { + path: "/user-examples/new", + page: UserExampleNewUserExamplePage, + name: "newUserExample" + }), /* @__PURE__ */jsx(Route, { + path: "/user-examples/{id:Int}", + page: UserExampleUserExamplePage, + name: "userExample" + }), /* @__PURE__ */jsx(Route, { + path: "/user-examples", + page: UserExampleUserExamplesPage, + name: "userExamples" + })] + })] + }), /* @__PURE__ */jsx(Route, { + notfound: true, + page: NotFoundPage + })] + }); + }; + export default Routes;" + `) + }) + + it('should insert the correct imports for ssr', async () => { + const plugin = rscRoutesAutoLoader() + if (typeof plugin.transform !== 'function') { + return + } + + // Calling `bind` to please TS + // See https://stackoverflow.com/a/70463512/88106 + const id = path.join(process.env.RWJS_CWD!, 'web', 'src', 'Routes.tsx') + const output = await plugin.transform.bind({})( + `import { jsx, jsxs } from "react/jsx-runtime"; + import { Router, Route, Set } from "@redwoodjs/router"; + import NavigationLayout from "./layouts/NavigationLayout/NavigationLayout"; + import ScaffoldLayout from "./layouts/ScaffoldLayout/ScaffoldLayout"; + import NotFoundPage from "./pages/NotFoundPage/NotFoundPage"; + const Routes = () => { + return /* @__PURE__ */ jsxs(Router, { children: [ + /* @__PURE__ */ jsxs(Set, { wrap: NavigationLayout, children: [ + /* @__PURE__ */ jsx(Route, { path: "/", page: HomePage, name: "home" }), + /* @__PURE__ */ jsx(Route, { path: "/about", page: AboutPage, name: "about" }), + /* @__PURE__ */ jsx(Route, { path: "/multi-cell", page: MultiCellPage, name: "multiCell" }), + /* @__PURE__ */ jsxs(Set, { wrap: ScaffoldLayout, title: "EmptyUsers", titleTo: "emptyUsers", buttonLabel: "New EmptyUser", buttonTo: "newEmptyUser", children: [ + /* @__PURE__ */ jsx(Route, { path: "/empty-users/new", page: EmptyUserNewEmptyUserPage, name: "newEmptyUser" }), + /* @__PURE__ */ jsx(Route, { path: "/empty-users", page: EmptyUserEmptyUsersPage, name: "emptyUsers" }) + ] }), + /* @__PURE__ */ jsxs(Set, { wrap: ScaffoldLayout, title: "UserExamples", titleTo: "userExamples", buttonLabel: "New UserExample", buttonTo: "newUserExample", children: [ + /* @__PURE__ */ jsx(Route, { path: "/user-examples/new", page: UserExampleNewUserExamplePage, name: "newUserExample" }), + /* @__PURE__ */ jsx(Route, { path: "/user-examples/{id:Int}", page: UserExampleUserExamplePage, name: "userExample" }), + /* @__PURE__ */ jsx(Route, { path: "/user-examples", page: UserExampleUserExamplesPage, name: "userExamples" }) + ] }) + ] }), + /* @__PURE__ */ jsx(Route, { notfound: true, page: NotFoundPage }) + ] }); + }; + export default Routes;`, + normalizePath(id), + { ssr: true } + ) + + // What we are interested in seeing here is: + // - The import of `renderFromDist` from `@redwoodjs/vite/clientSsr` + // - The call to `renderFromDist` for each page that wasn't already imported + expect(output).toMatchInlineSnapshot(` + "import { renderFromDist } from "@redwoodjs/vite/clientSsr"; + const EmptyUserNewEmptyUserPage = renderFromDist("EmptyUserNewEmptyUserPage"); + const EmptyUserEmptyUsersPage = renderFromDist("EmptyUserEmptyUsersPage"); + const EmptyUserEmptyUserPage = renderFromDist("EmptyUserEmptyUserPage"); + const EmptyUserEditEmptyUserPage = renderFromDist("EmptyUserEditEmptyUserPage"); + const HomePage = renderFromDist("HomePage"); + const FatalErrorPage = renderFromDist("FatalErrorPage"); + const AboutPage = renderFromDist("AboutPage"); + import { jsx, jsxs } from "react/jsx-runtime"; + import { Router, Route, Set } from "@redwoodjs/router"; + import NavigationLayout from "./layouts/NavigationLayout/NavigationLayout"; + import ScaffoldLayout from "./layouts/ScaffoldLayout/ScaffoldLayout"; + import NotFoundPage from "./pages/NotFoundPage/NotFoundPage"; + const Routes = () => { + return /* @__PURE__ */jsxs(Router, { + children: [/* @__PURE__ */jsxs(Set, { + wrap: NavigationLayout, + children: [/* @__PURE__ */jsx(Route, { + path: "/", + page: HomePage, + name: "home" + }), /* @__PURE__ */jsx(Route, { + path: "/about", + page: AboutPage, + name: "about" + }), /* @__PURE__ */jsx(Route, { + path: "/multi-cell", + page: MultiCellPage, + name: "multiCell" + }), /* @__PURE__ */jsxs(Set, { + wrap: ScaffoldLayout, + title: "EmptyUsers", + titleTo: "emptyUsers", + buttonLabel: "New EmptyUser", + buttonTo: "newEmptyUser", + children: [/* @__PURE__ */jsx(Route, { + path: "/empty-users/new", + page: EmptyUserNewEmptyUserPage, + name: "newEmptyUser" + }), /* @__PURE__ */jsx(Route, { + path: "/empty-users", + page: EmptyUserEmptyUsersPage, + name: "emptyUsers" + })] + }), /* @__PURE__ */jsxs(Set, { + wrap: ScaffoldLayout, + title: "UserExamples", + titleTo: "userExamples", + buttonLabel: "New UserExample", + buttonTo: "newUserExample", + children: [/* @__PURE__ */jsx(Route, { + path: "/user-examples/new", + page: UserExampleNewUserExamplePage, + name: "newUserExample" + }), /* @__PURE__ */jsx(Route, { + path: "/user-examples/{id:Int}", + page: UserExampleUserExamplePage, + name: "userExample" + }), /* @__PURE__ */jsx(Route, { + path: "/user-examples", + page: UserExampleUserExamplesPage, + name: "userExamples" + })] + })] + }), /* @__PURE__ */jsx(Route, { + notfound: true, + page: NotFoundPage + })] + }); + }; + export default Routes;" + `) + }) + + it('should throw for duplicate page import names', async () => { + (processPagesDir as Mock).mockReturnValue(pagesWithDuplicate) + + const getOutput = async () => { + const plugin = rscRoutesAutoLoader() + if (typeof plugin.transform !== 'function') { + return + } + + // Calling `bind` to please TS + // See https://stackoverflow.com/a/70463512/88106 + const id = path.join(process.env.RWJS_CWD!, 'web', 'src', 'Routes.tsx') + const output = await plugin.transform.bind({})( + `import { jsx, jsxs } from "react/jsx-runtime"; + import { Router, Route, Set } from "@redwoodjs/router"; + import NavigationLayout from "./layouts/NavigationLayout/NavigationLayout"; + import ScaffoldLayout from "./layouts/ScaffoldLayout/ScaffoldLayout"; + import NotFoundPage from "./pages/NotFoundPage/NotFoundPage"; + const Routes = () => { + return /* @__PURE__ */ jsxs(Router, { children: [ + /* @__PURE__ */ jsxs(Set, { wrap: NavigationLayout, children: [ + /* @__PURE__ */ jsx(Route, { path: "/", page: HomePage, name: "home" }), + /* @__PURE__ */ jsx(Route, { path: "/about", page: AboutPage, name: "about" }), + /* @__PURE__ */ jsx(Route, { path: "/multi-cell", page: MultiCellPage, name: "multiCell" }), + /* @__PURE__ */ jsxs(Set, { wrap: ScaffoldLayout, title: "EmptyUsers", titleTo: "emptyUsers", buttonLabel: "New EmptyUser", buttonTo: "newEmptyUser", children: [ + /* @__PURE__ */ jsx(Route, { path: "/empty-users/new", page: EmptyUserNewEmptyUserPage, name: "newEmptyUser" }), + /* @__PURE__ */ jsx(Route, { path: "/empty-users", page: EmptyUserEmptyUsersPage, name: "emptyUsers" }) + ] }), + /* @__PURE__ */ jsxs(Set, { wrap: ScaffoldLayout, title: "UserExamples", titleTo: "userExamples", buttonLabel: "New UserExample", buttonTo: "newUserExample", children: [ + /* @__PURE__ */ jsx(Route, { path: "/user-examples/new", page: UserExampleNewUserExamplePage, name: "newUserExample" }), + /* @__PURE__ */ jsx(Route, { path: "/user-examples/{id:Int}", page: UserExampleUserExamplePage, name: "userExample" }), + /* @__PURE__ */ jsx(Route, { path: "/user-examples", page: UserExampleUserExamplesPage, name: "userExamples" }) + ] }) + ] }), + /* @__PURE__ */ jsx(Route, { notfound: true, page: NotFoundPage }) + ] }); + }; + export default Routes;`, + normalizePath(id), + ) + + return output + } + + expect(getOutput).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: 'AboutPage']` + ) + }) + + it('should handle existing imports in the routes file', async () => { + const plugin = rscRoutesAutoLoader() + if (typeof plugin.transform !== 'function') { + return + } + + // Calling `bind` to please TS + // See https://stackoverflow.com/a/70463512/88106 + const id = path.join(process.env.RWJS_CWD!, 'web', 'src', 'Routes.tsx') + const output = await plugin.transform.bind({})( + `import { jsx, jsxs } from "react/jsx-runtime"; + import { Router, Route, Set } from "@redwoodjs/router"; + import NavigationLayout from "./layouts/NavigationLayout/NavigationLayout"; + import ScaffoldLayout from "./layouts/ScaffoldLayout/ScaffoldLayout"; + import NotFoundPage from "./pages/NotFoundPage/NotFoundPage"; + import AboutPage from "./pages/AboutPage/AboutPage"; + const Routes = () => { + return /* @__PURE__ */ jsxs(Router, { children: [ + /* @__PURE__ */ jsxs(Set, { wrap: NavigationLayout, children: [ + /* @__PURE__ */ jsx(Route, { path: "/", page: HomePage, name: "home" }), + /* @__PURE__ */ jsx(Route, { path: "/about", page: AboutPage, name: "about" }), + /* @__PURE__ */ jsx(Route, { path: "/multi-cell", page: MultiCellPage, name: "multiCell" }), + /* @__PURE__ */ jsxs(Set, { wrap: ScaffoldLayout, title: "EmptyUsers", titleTo: "emptyUsers", buttonLabel: "New EmptyUser", buttonTo: "newEmptyUser", children: [ + /* @__PURE__ */ jsx(Route, { path: "/empty-users/new", page: EmptyUserNewEmptyUserPage, name: "newEmptyUser" }), + /* @__PURE__ */ jsx(Route, { path: "/empty-users", page: EmptyUserEmptyUsersPage, name: "emptyUsers" }) + ] }), + /* @__PURE__ */ jsxs(Set, { wrap: ScaffoldLayout, title: "UserExamples", titleTo: "userExamples", buttonLabel: "New UserExample", buttonTo: "newUserExample", children: [ + /* @__PURE__ */ jsx(Route, { path: "/user-examples/new", page: UserExampleNewUserExamplePage, name: "newUserExample" }), + /* @__PURE__ */ jsx(Route, { path: "/user-examples/{id:Int}", page: UserExampleUserExamplePage, name: "userExample" }), + /* @__PURE__ */ jsx(Route, { path: "/user-examples", page: UserExampleUserExamplesPage, name: "userExamples" }) + ] }) + ] }), + /* @__PURE__ */ jsx(Route, { notfound: true, page: NotFoundPage }) + ] }); + }; + export default Routes;`, + normalizePath(id), + undefined + ) + + // We don't have to add calls for the AboutPage as it was already imported + expect(output).not.toContain('renderFromDist("AboutPage")') + expect(output).not.toContain('renderFromRscServer("AboutPage")') + }) + +}) + +const pages = [ + { + importName: 'AboutPage', + const: 'AboutPage', + importPath: '/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage', + path: '/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage.tsx', + importStatement: "const AboutPage = { name: 'AboutPage', loader: import('/Users/mojombo/rw-app/web/src/pages/AboutPage/AboutPage') }" + }, + { + importName: 'FatalErrorPage', + const: 'FatalErrorPage', + importPath: '/Users/mojombo/rw-app/web/src/pages/FatalErrorPage/FatalErrorPage', + path: '/Users/mojombo/rw-app/web/src/pages/FatalErrorPage/FatalErrorPage.tsx', + importStatement: "const FatalErrorPage = { name: 'FatalErrorPage', loader: import('/Users/mojombo/rw-app/web/src/pages/FatalErrorPage/FatalErrorPage') }" + }, + { + importName: 'HomePage', + const: 'HomePage', + importPath: '/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage', + path: '/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage.tsx', + importStatement: "const HomePage = { name: 'HomePage', loader: import('/Users/mojombo/rw-app/web/src/pages/HomePage/HomePage') }" + }, + { + importName: 'NotFoundPage', + const: 'NotFoundPage', + importPath: '/Users/mojombo/rw-app/web/src/pages/NotFoundPage/NotFoundPage', + path: '/Users/mojombo/rw-app/web/src/pages/NotFoundPage/NotFoundPage.tsx', + importStatement: "const NotFoundPage = { name: 'NotFoundPage', loader: import('/Users/mojombo/rw-app/web/src/pages/NotFoundPage/NotFoundPage') }" + }, + { + importName: 'EmptyUserEditEmptyUserPage', + const: 'EmptyUserEditEmptyUserPage', + importPath: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EditEmptyUserPage/EditEmptyUserPage', + path: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EditEmptyUserPage/EditEmptyUserPage.tsx', + importStatement: "const EmptyUserEditEmptyUserPage = { name: 'EmptyUserEditEmptyUserPage', loader: import('/Users/mojombo/rw-app/web/src/pages/EmptyUser/EditEmptyUserPage/EditEmptyUserPage') }" + }, + { + importName: 'EmptyUserEmptyUserPage', + const: 'EmptyUserEmptyUserPage', + importPath: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUserPage/EmptyUserPage', + path: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUserPage/EmptyUserPage.tsx', + importStatement: "const EmptyUserEmptyUserPage = { name: 'EmptyUserEmptyUserPage', loader: import('/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUserPage/EmptyUserPage') }" + }, + { + importName: 'EmptyUserEmptyUsersPage', + const: 'EmptyUserEmptyUsersPage', + importPath: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUsersPage/EmptyUsersPage', + path: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUsersPage/EmptyUsersPage.tsx', + importStatement: "const EmptyUserEmptyUsersPage = { name: 'EmptyUserEmptyUsersPage', loader: import('/Users/mojombo/rw-app/web/src/pages/EmptyUser/EmptyUsersPage/EmptyUsersPage') }" + }, + { + importName: 'EmptyUserNewEmptyUserPage', + const: 'EmptyUserNewEmptyUserPage', + importPath: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage', + path: '/Users/mojombo/rw-app/web/src/pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage.tsx', + importStatement: "const EmptyUserNewEmptyUserPage = { name: 'EmptyUserNewEmptyUserPage', loader: import('/Users/mojombo/rw-app/web/src/pages/EmptyUser/NewEmptyUserPage/NewEmptyUserPage') }" + }, +] + +const pagesWithDuplicate = [ + ...pages, + pages[0] +] diff --git a/packages/vite/src/plugins/vite-plugin-rsc-routes-auto-loader.ts b/packages/vite/src/plugins/vite-plugin-rsc-routes-auto-loader.ts new file mode 100644 index 000000000000..ae081741b729 --- /dev/null +++ b/packages/vite/src/plugins/vite-plugin-rsc-routes-auto-loader.ts @@ -0,0 +1,154 @@ +import path from 'path' + +import generate from '@babel/generator' +import { parse as babelParse } from '@babel/parser' +import traverse from '@babel/traverse' +import * as t from '@babel/types' +import type { Plugin } from 'vite' +import { normalizePath } from 'vite' + +import type { PagesDependency } from '@redwoodjs/project-config' +import { + ensurePosixPath, + getPaths, + importStatementPath, + processPagesDir, +} from '@redwoodjs/project-config' + +const getPathRelativeToSrc = (maybeAbsolutePath: string) => { + // If the path is already relative + if (!path.isAbsolute(maybeAbsolutePath)) { + return maybeAbsolutePath + } + + return `./${path.relative(getPaths().web.src, maybeAbsolutePath)}` +} + +const withRelativeImports = (page: PagesDependency) => { + return { + ...page, + relativeImport: ensurePosixPath(getPathRelativeToSrc(page.importPath)), + } +} + +export function rscRoutesAutoLoader(): Plugin { + // Vite IDs are always normalized and so we avoid windows path issues + // by normalizing the path here. + const routesFileId = normalizePath(getPaths().web.routes) + + // Get the current pages + // @NOTE: This var gets mutated inside the visitors + const pages = processPagesDir().map(withRelativeImports) + + // Currently processPagesDir() can return duplicate entries when there are multiple files + // ending in Page in the individual page directories. This will cause an error upstream. + // Here we check for duplicates and throw a more helpful error message. + const duplicatePageImportNames = new Set() + const sortedPageImportNames = pages.map((page) => page.importName).sort() + for (let i = 0; i < sortedPageImportNames.length - 1; i++) { + if (sortedPageImportNames[i + 1] === sortedPageImportNames[i]) { + duplicatePageImportNames.add(sortedPageImportNames[i]) + } + } + if (duplicatePageImportNames.size > 0) { + throw new Error( + `Unable to find only a single file ending in 'Page.{js,jsx,ts,tsx}' in the follow page directories: ${Array.from( + duplicatePageImportNames, + ) + .map((name) => `'${name}'`) + .join(', ')}`, + ) + } + + return { + name: 'rsc-routes-auto-loader-dev', + transform: async function (code, id, options) { + // We only care about the routes file + if (id !== routesFileId) { + return null + } + + // If we have no pages then we have no reason to do anything here + if (pages.length === 0) { + return null + } + + // We have to handle the loading of routes in two different ways depending on if + // we are doing SSR or not. During SSR we want to load files directly whereas on + // the client we have to fetch things over the network. + const isSsr = options?.ssr ?? false + + const loadFunctionModule = isSsr + ? '@redwoodjs/vite/clientSsr' + : '@redwoodjs/vite/client' + const loadFunctionName = isSsr ? 'renderFromDist' : 'renderFromRscServer' + + // Parse the code as AST + const ext = path.extname(id) + const plugins: any[] = [] + if (ext === '.jsx') { + plugins.push('jsx') + } + const ast = babelParse(code, { + sourceType: 'unambiguous', + plugins, + }) + + // We have to filter out any pages which the user has already explicitly imported + // in the routes file otherwise there would be conflicts. + const importedNames = new Set() + traverse(ast, { + ImportDeclaration(p) { + const importPath = p.node.source.value + if (importPath === null) { + return + } + + const userImportRelativePath = getPathRelativeToSrc( + importStatementPath(p.node.source?.value), + ) + + const defaultSpecifier = p.node.specifiers.filter((specifiers) => + t.isImportDefaultSpecifier(specifiers), + )[0] + + if (userImportRelativePath && defaultSpecifier) { + importedNames.add(defaultSpecifier.local.name) + } + }, + }) + const nonImportedPages = pages.filter( + (page) => !importedNames.has(page.importName), + ) + + // Insert the page loading into the code + for (const page of nonImportedPages) { + ast.program.body.unshift( + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier(page.const), + t.callExpression(t.identifier(loadFunctionName), [ + t.stringLiteral(page.const), + ]), + ), + ]), + ) + } + + // Insert an import for the load function we need + ast.program.body.unshift( + t.importDeclaration( + [ + t.importSpecifier( + t.identifier(loadFunctionName), + t.identifier(loadFunctionName), + ), + ], + t.stringLiteral(loadFunctionModule), + ), + ) + + return generate(ast).code + }, + } +} diff --git a/packages/vite/src/rsc/rscBuildAnalyze.ts b/packages/vite/src/rsc/rscBuildAnalyze.ts index 8a0805917e3c..4eadc467fe98 100644 --- a/packages/vite/src/rsc/rscBuildAnalyze.ts +++ b/packages/vite/src/rsc/rscBuildAnalyze.ts @@ -1,7 +1,5 @@ -import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' -import { getWebSideDefaultBabelConfig } from '@redwoodjs/babel-config' import { getPaths } from '@redwoodjs/project-config' import { onWarn } from '../lib/onWarn.js' @@ -45,12 +43,6 @@ export async function rscBuildAnalyze() { // debugging, but we're keeping it silent by default. logLevel: 'silent', plugins: [ - react({ - babel: getWebSideDefaultBabelConfig({ - forVite: true, - forRSC: true, - }), - }), rscAnalyzePlugin( (id) => clientEntryFileSet.add(id), (id) => serverEntryFileSet.add(id), diff --git a/packages/vite/src/rsc/rscBuildClient.ts b/packages/vite/src/rsc/rscBuildClient.ts index f8e9afbc80ae..be88f23b1b80 100644 --- a/packages/vite/src/rsc/rscBuildClient.ts +++ b/packages/vite/src/rsc/rscBuildClient.ts @@ -1,13 +1,9 @@ -import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' -import { - redwoodRoutesAutoLoaderRscClientPlugin, - getWebSideDefaultBabelConfig, -} from '@redwoodjs/babel-config' import { getPaths } from '@redwoodjs/project-config' import { onWarn } from '../lib/onWarn.js' +import { rscRoutesAutoLoader } from '../plugins/vite-plugin-rsc-routes-auto-loader.js' import { ensureProcessDirWeb } from '../utils.js' /** @@ -33,17 +29,6 @@ export async function rscBuildClient(clientEntryFiles: Record) { throw new Error('Missing web/src/entry.client') } - const reactBabelConfig = getWebSideDefaultBabelConfig({ - forVite: true, - forRSC: true, - }) - reactBabelConfig.overrides.push({ - test: /Routes.(js|tsx|jsx)$/, - plugins: [[redwoodRoutesAutoLoaderRscClientPlugin, {}]], - babelrc: false, - ignore: ['node_modules'], - }) - const clientBuildOutput = await viteBuild({ envFile: false, build: { @@ -80,11 +65,7 @@ export async function rscBuildClient(clientEntryFiles: Record) { esbuild: { logLevel: 'debug', }, - plugins: [ - react({ - babel: reactBabelConfig, - }), - ], + plugins: [rscRoutesAutoLoader()], }) if (!('output' in clientBuildOutput)) { diff --git a/packages/vite/src/rsc/rscBuildForServer.ts b/packages/vite/src/rsc/rscBuildForServer.ts index 0fa4b82c0176..1c20e8e15e14 100644 --- a/packages/vite/src/rsc/rscBuildForServer.ts +++ b/packages/vite/src/rsc/rscBuildForServer.ts @@ -1,14 +1,10 @@ -import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' -import { - redwoodRoutesAutoLoaderRscServerPlugin, - getWebSideDefaultBabelConfig, -} from '@redwoodjs/babel-config' import { getPaths } from '@redwoodjs/project-config' import { onWarn } from '../lib/onWarn.js' import { rscCssPreinitPlugin } from '../plugins/vite-plugin-rsc-css-preinit.js' +import { rscRoutesAutoLoader } from '../plugins/vite-plugin-rsc-routes-auto-loader.js' import { rscTransformUseClientPlugin } from '../plugins/vite-plugin-rsc-transform-client.js' import { rscTransformUseServerPlugin } from '../plugins/vite-plugin-rsc-transform-server.js' @@ -40,17 +36,6 @@ export async function rscBuildForServer( ...customModules, } - const reactBabelConfig = getWebSideDefaultBabelConfig({ - forVite: true, - forRSC: true, - }) - reactBabelConfig.overrides.push({ - test: /Routes.(js|tsx|jsx)$/, - plugins: [[redwoodRoutesAutoLoaderRscServerPlugin, {}]], - babelrc: false, - ignore: ['node_modules'], - }) - // TODO (RSC): No redwood-vite plugin, add it in here const rscServerBuildOutput = await viteBuild({ envFile: false, @@ -74,9 +59,6 @@ export async function rscBuildForServer( }, }, plugins: [ - react({ - babel: reactBabelConfig, - }), // The rscTransformPlugin maps paths like // /Users/tobbe/.../rw-app/node_modules/@tobbe.dev/rsc-test/dist/rsc-test.es.js // to @@ -86,6 +68,7 @@ export async function rscBuildForServer( rscTransformUseClientPlugin(clientEntryFiles), rscTransformUseServerPlugin(), rscCssPreinitPlugin(clientEntryFiles, componentImportMap), + rscRoutesAutoLoader(), ], build: { // TODO (RSC): Remove `minify: false` when we don't need to debug as often diff --git a/packages/vite/src/rsc/rscWorker.ts b/packages/vite/src/rsc/rscWorker.ts index 82f082a28c20..095f52595715 100644 --- a/packages/vite/src/rsc/rscWorker.ts +++ b/packages/vite/src/rsc/rscWorker.ts @@ -11,21 +11,17 @@ import { parentPort } from 'node:worker_threads' import { createElement } from 'react' -import react from '@vitejs/plugin-react' import RSDWServer from 'react-server-dom-webpack/server' import type { ResolvedConfig } from 'vite' import { createServer, resolveConfig } from 'vite' -import { - getWebSideDefaultBabelConfig, - redwoodRoutesAutoLoaderRscServerPlugin, -} from '@redwoodjs/babel-config' import { getPaths } from '@redwoodjs/project-config' import type { defineEntries, GetEntry } from '../entries.js' import { registerFwGlobalsAndShims } from '../lib/registerFwGlobalsAndShims.js' import { StatusError } from '../lib/StatusError.js' import { rscReloadPlugin } from '../plugins/vite-plugin-rsc-reload.js' +import { rscRoutesAutoLoader } from '../plugins/vite-plugin-rsc-routes-auto-loader.js' import { rscTransformUseClientPlugin } from '../plugins/vite-plugin-rsc-transform-client.js' import { rscTransformUseServerPlugin } from '../plugins/vite-plugin-rsc-transform-server.js' @@ -125,17 +121,6 @@ registerFwGlobalsAndShims() // is already in use`. const dummyServer = new Server() -const reactBabelConfig = getWebSideDefaultBabelConfig({ - forVite: true, - forRSC: true, -}) -reactBabelConfig.overrides.push({ - test: /Routes.(js|tsx|jsx)$/, - plugins: [[redwoodRoutesAutoLoaderRscServerPlugin, {}]], - babelrc: false, - ignore: ['node_modules'], -}) - // TODO (RSC): `createServer` is mostly used to create a dev server. Is it OK // to use it like a production server like this? // TODO (RSC): Do we need to pass `define` here with RWJS_ENV etc? What about @@ -144,9 +129,6 @@ reactBabelConfig.overrides.push({ // https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#middleware-mode const vitePromise = createServer({ plugins: [ - react({ - babel: reactBabelConfig, - }), rscReloadPlugin((type) => { if (!parentPort) { throw new Error('parentPort is undefined') @@ -157,6 +139,7 @@ const vitePromise = createServer({ }), rscTransformUseClientPlugin({}), rscTransformUseServerPlugin(), + rscRoutesAutoLoader(), ], ssr: { resolve: { diff --git a/packages/vite/src/streaming/buildForStreamingServer.ts b/packages/vite/src/streaming/buildForStreamingServer.ts index 396e0a15f6c7..d0f936abc065 100644 --- a/packages/vite/src/streaming/buildForStreamingServer.ts +++ b/packages/vite/src/streaming/buildForStreamingServer.ts @@ -1,13 +1,10 @@ -import react from '@vitejs/plugin-react' import { build as viteBuild } from 'vite' import { cjsInterop } from 'vite-plugin-cjs-interop' -import { - redwoodRoutesAutoLoaderRscServerPlugin, - getWebSideDefaultBabelConfig, -} from '@redwoodjs/babel-config' import { getPaths } from '@redwoodjs/project-config' +import { rscRoutesAutoLoader } from '../plugins/vite-plugin-rsc-routes-auto-loader' + export async function buildForStreamingServer({ verbose = false, rscEnabled = false, @@ -22,29 +19,13 @@ export async function buildForStreamingServer({ throw new Error('Vite config not found') } - const reactBabelConfig = getWebSideDefaultBabelConfig({ - forVite: true, - forRSC: true, - }) - if (rscEnabled) { - reactBabelConfig.overrides.push({ - test: /Routes.(js|tsx|jsx)$/, - plugins: [[redwoodRoutesAutoLoaderRscServerPlugin, {}]], - babelrc: false, - ignore: ['node_modules'], - }) - } - await viteBuild({ configFile: rwPaths.web.viteConfig, plugins: [ cjsInterop({ dependencies: ['@redwoodjs/**'], }), - rscEnabled && - react({ - babel: reactBabelConfig, - }), + rscEnabled && rscRoutesAutoLoader(), ], build: { // TODO (RSC): Remove `minify: false` when we don't need to debug as often