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/.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/.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-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/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 e8eb2da6a80b..160d6b8a457b 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,13 +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() - - console.log(`Installing node_modules in ${TEST_PROJECT_PATH}`) - await execInProject('yarn install') - console.log() + await execInFramework('yarn project:tarsync --verbose', { env: { RWJS_CWD: TEST_PROJECT_PATH } }) if (canary) { console.log(`Upgrading project to canary`) @@ -96,9 +64,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 +73,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/actions/set-up-yarn-cache/action.yml b/.github/actions/set-up-yarn-cache/action.yml index 8bafd1508d4a..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 - uses: actions/cache@v3 + - 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 - uses: actions/cache@v3 + - 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@v3 + 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 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/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/.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 28f92aeea855..a2f8094e1b4b 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: @@ -27,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 @@ -55,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 @@ -99,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 @@ -162,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 @@ -258,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 @@ -338,59 +266,89 @@ 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: Set up job + uses: ./.github/actions/set-up-job + + - 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` @@ -402,51 +360,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: @@ -467,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 @@ -529,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 @@ -639,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 @@ -728,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 @@ -826,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: | @@ -922,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/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/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 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 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-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-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/package.json b/__fixtures__/test-project-rsc-external-packages-and-cells/web/package.json index 9a1bb651ddbd..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 @@ -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": "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-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/__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/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', 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/__fixtures__/test-project/web/package.json b/__fixtures__/test-project/web/package.json index 9dda5afc2f12..62e918264c24 100644 --- a/__fixtures__/test-project/web/package.json +++ b/__fixtures__/test-project/web/package.json @@ -16,15 +16,15 @@ "@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", "@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/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: 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? 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..c6706cceaf98 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", @@ -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/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..ff46b153787b 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", @@ -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/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..7d7edc54a415 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", @@ -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/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/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) && 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/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/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/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..cbdf4b3b663f 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", @@ -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/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..d965328fc872 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", @@ -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/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..ea4c3147c3e5 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", @@ -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/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.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/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 () => { 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/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/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/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..ed93122b3607 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", @@ -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/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/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/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/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/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 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 c60996b38c39..d03b26d4a8f1 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -22,13 +22,9 @@ "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" + "./clientSsr": { + "types": "./dist/clientSsr.d.ts", + "default": "./dist/clientSsr.js" }, "./buildFeServer": { "types": "./dist/buildFeServer.d.ts", @@ -60,10 +56,13 @@ "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", + "@babel/parser": "^7.22.16", + "@babel/traverse": "^7.22.20", "@redwoodjs/babel-config": "workspace:*", "@redwoodjs/internal": "workspace:*", "@redwoodjs/project-config": "workspace:*", @@ -81,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/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/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/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/client.ts b/packages/vite/src/client.ts index 0774d9eb8170..1c143a43a0bc 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -20,12 +20,13 @@ const checkStatus = async ( const BASE_PATH = '/rw-rsc/' -export function renderFromRscServer(rscId: string) { - console.log('serve rscId', rscId) +export function renderFromRscServer(rscId: string) { + console.log('serve rscId (renderFromRscServer)', 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 } @@ -58,6 +59,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) @@ -70,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: { @@ -94,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', }, @@ -113,7 +115,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/clientSsr.ts b/packages/vite/src/clientSsr.ts new file mode 100644 index 000000000000..338169e942f0 --- /dev/null +++ b/packages/vite/src/clientSsr.ts @@ -0,0 +1,10 @@ +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 cb5373ca30e7..ed0d24b67640 100644 --- a/packages/vite/src/devFeServer.ts +++ b/packages/vite/src/devFeServer.ts @@ -9,8 +9,9 @@ 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 { 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' @@ -22,11 +23,13 @@ globalThis.__REDWOOD__PRERENDER_PAGES = {} async function createServer() { ensureProcessDirWeb() - registerFwGlobals() + registerFwGlobalsAndShims() 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 @@ -55,6 +58,7 @@ async function createServer() { cjsInterop({ dependencies: ['@redwoodjs/**'], }), + rscEnabled && rscRoutesAutoLoader(), ], server: { middlewareMode: true }, logLevel: 'info', 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 -} diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index 47225c3bd633..aeb447e96a7d 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -36,6 +36,8 @@ export default function redwoodPluginVite(): PluginOption[] { .readFileSync(path.join(rwPaths.api.base, 'package.json'), 'utf-8') .includes('@redwoodjs/realtime') + const streamingEnabled = rwConfig.experimental.streamingSsr.enabled + return [ { name: 'redwood-plugin-vite-html-env', @@ -130,7 +132,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([ @@ -148,7 +150,7 @@ export default function redwoodPluginVite(): PluginOption[] { babel: { ...getWebSideDefaultBabelConfig({ forVite: true, - forRscClient: rwConfig.experimental.rsc?.enabled, + forRSC: rwConfig.experimental?.rsc?.enabled, }), }, }), 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/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/__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..149c1fdbbbca --- /dev/null +++ b/packages/vite/src/plugins/__tests__/vite-plugin-rsc-css-preinit.test.mts @@ -0,0 +1,590 @@ +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' + +import { + clientBuildManifest, + 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 })) + +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': '', + }, 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(() => {}) +}) + +afterAll(() => { + process.env.RWJS_CWD = RWJS_CWD + consoleLogSpy.mockRestore() +}) + +describe('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 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"; + 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;`, + normalizePath(id) + ) + + // 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/__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/__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-analyze.ts b/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts index 5727e5d02f5b..84e919a7717f 100644 --- a/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts +++ b/packages/vite/src/plugins/vite-plugin-rsc-analyze.ts @@ -2,13 +2,20 @@ 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' 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', + name: 'redwood-rsc-analyze-plugin', transform(code, id) { const ext = path.extname(id) @@ -25,6 +32,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 +42,11 @@ export function rscAnalyzePlugin( return code }, + moduleParsed(moduleInfo) { + // TODO: Maybe this is not needed? + 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 new file mode 100644 index 000000000000..605c7d9a138d --- /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 { normalizePath } 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 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) + 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(normalizePath(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/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/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/rscBuildAnalyze.ts b/packages/vite/src/rsc/rscBuildAnalyze.ts index b84d1c40e95f..4eadc467fe98 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: { @@ -52,28 +57,27 @@ 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'], }, }, build: { + // TODO (RSC): Remove `minify: false` when we don't need to debug as often + minify: false, 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, - }, }, }, }) @@ -99,5 +103,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/rscBuildClient.ts b/packages/vite/src/rsc/rscBuildClient.ts index 15405455d553..be88f23b1b80 100644 --- a/packages/vite/src/rsc/rscBuildClient.ts +++ b/packages/vite/src/rsc/rscBuildClient.ts @@ -3,6 +3,7 @@ import { build as viteBuild } from 'vite' 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' /** @@ -31,6 +32,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: { @@ -62,6 +65,7 @@ export async function rscBuildClient(clientEntryFiles: Record) { esbuild: { logLevel: 'debug', }, + plugins: [rscRoutesAutoLoader()], }) if (!('output' in clientBuildOutput)) { diff --git a/packages/vite/src/rsc/rscBuildForServer.ts b/packages/vite/src/rsc/rscBuildForServer.ts index 3e6c0a633036..1c20e8e15e14 100644 --- a/packages/vite/src/rsc/rscBuildForServer.ts +++ b/packages/vite/src/rsc/rscBuildForServer.ts @@ -1,11 +1,12 @@ -import path from 'node:path' - 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 { 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' /** * RSC build. Step 3. @@ -16,6 +17,7 @@ export async function rscBuildForServer( clientEntryFiles: Record, serverEntryFiles: Record, customModules: Record, + componentImportMap: Map, ) { console.log('\n') console.log('3. rscBuildForServer') @@ -38,42 +40,22 @@ 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 (db calls fail at runtime) or react-dom + // (css preinit failure) + external: ['@prisma/client', 'react-dom'], 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: [ @@ -83,9 +65,14 @@ 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(), + rscCssPreinitPlugin(clientEntryFiles, componentImportMap), + rscRoutesAutoLoader(), ], 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/rsc/rscRequestHandler.ts b/packages/vite/src/rsc/rscRequestHandler.ts index 377d453a43f4..ae6eeaf60723 100644 --- a/packages/vite/src/rsc/rscRequestHandler.ts +++ b/packages/vite/src/rsc/rscRequestHandler.ts @@ -1,13 +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) => { @@ -32,17 +34,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 } @@ -54,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 @@ -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) + } + } +} diff --git a/packages/vite/src/rsc/rscWebpackShims.ts b/packages/vite/src/rsc/rscWebpackShims.ts deleted file mode 100644 index a62cd0958f72..000000000000 --- a/packages/vite/src/rsc/rscWebpackShims.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const rscWebpackShims = `globalThis.__rw_module_cache__ = new Map(); - -globalThis.__webpack_chunk_load__ = (id) => { - return import(id).then((m) => globalThis.__rw_module_cache__.set(id, m)) -}; - -globalThis.__webpack_require__ = (id) => { - return globalThis.__rw_module_cache__.get(id) -};\n` diff --git a/packages/vite/src/rsc/rscWorker.ts b/packages/vite/src/rsc/rscWorker.ts index 95900c2eab74..095f52595715 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' @@ -19,10 +18,12 @@ import { createServer, resolveConfig } from 'vite' 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 { rscTransformPlugin } from '../plugins/vite-plugin-rsc-transform.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' import type { RenderInput, @@ -113,10 +114,11 @@ 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: 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,9 @@ const vitePromise = createServer({ const message: MessageRes = { type } parentPort.postMessage(message) }), - rscTransformPlugin({}), + rscTransformUseClientPlugin({}), + rscTransformUseServerPlugin(), + rscRoutesAutoLoader(), ], ssr: { resolve: { 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/buildForStreamingServer.ts b/packages/vite/src/streaming/buildForStreamingServer.ts index 132b56250335..d0f936abc065 100644 --- a/packages/vite/src/streaming/buildForStreamingServer.ts +++ b/packages/vite/src/streaming/buildForStreamingServer.ts @@ -3,10 +3,14 @@ import { cjsInterop } from 'vite-plugin-cjs-interop' import { getPaths } from '@redwoodjs/project-config' +import { rscRoutesAutoLoader } from '../plugins/vite-plugin-rsc-routes-auto-loader' + export async function buildForStreamingServer({ verbose = false, + rscEnabled = false, }: { verbose?: boolean + rscEnabled?: boolean }) { console.log('Starting streaming server build...\n') const rwPaths = getPaths() @@ -21,8 +25,11 @@ export async function buildForStreamingServer({ cjsInterop({ dependencies: ['@redwoodjs/**'], }), + rscEnabled && rscRoutesAutoLoader(), ], 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, diff --git a/packages/vite/src/streaming/streamHelpers.ts b/packages/vite/src/streaming/streamHelpers.ts index 0b4a1f543300..60666f201e49 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' @@ -40,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, @@ -89,7 +102,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,7 +138,7 @@ 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}` : undefined, 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/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 } 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') diff --git a/yarn.lock b/yarn.lock index 85b1b3b71f82..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 @@ -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" @@ -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 @@ -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:*" @@ -8882,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" @@ -8941,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" @@ -8951,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 @@ -11551,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 @@ -28860,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" @@ -28884,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" @@ -29010,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 @@ -29042,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" @@ -29060,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" @@ -30185,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 @@ -33981,9 +33978,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 +34011,7 @@ __metadata: optional: true bin: webpack: bin/webpack.js - checksum: 10c0/4acec1a719a9c5b890a30a9fb18519e671e55382f2c51120b76a2c1c1c521285b6510327faf79f85a4b11c7a2c5c01e1d2e7bf73e5cddbada1503f4d51a63441 + checksum: 10c0/f737aa871cadbbae89833eb85387f1bf9ee0768f039100a3c8134f2fdcc78c3230ca775c373b1aa467b272f74c6831e119f7a8a1c14dcac97327212be9c93eeb languageName: node linkType: hard