diff --git a/.changeset/cuddly-bags-push.md b/.changeset/cuddly-bags-push.md new file mode 100644 index 00000000000..4436e3a7236 --- /dev/null +++ b/.changeset/cuddly-bags-push.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/router': minor +--- + +FIX: Route loaders now redirect in their JSON response instead of using HTTP redirect diff --git a/.changeset/flat-hornets-fetch.md b/.changeset/flat-hornets-fetch.md new file mode 100644 index 00000000000..48b6df0ff66 --- /dev/null +++ b/.changeset/flat-hornets-fetch.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/router': minor +--- + +FEAT: route loaders now accept `eTag` to shortcut data retrieval when the data has not changed. See the documentation for more details on this option and how to use it. diff --git a/.changeset/fresh-loaders-update.md b/.changeset/fresh-loaders-update.md new file mode 100644 index 00000000000..45f3d36dfd5 --- /dev/null +++ b/.changeset/fresh-loaders-update.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/router': patch +--- + +fix(router): Update route loader metadata in the dev route trie when route source files change. diff --git a/.changeset/green-experts-turn.md b/.changeset/green-experts-turn.md new file mode 100644 index 00000000000..5653f1b0124 --- /dev/null +++ b/.changeset/green-experts-turn.md @@ -0,0 +1,7 @@ +--- +'@qwik.dev/router': major +--- + +BREAKING: Route loaders are now AsyncSignals. This means that `value.failed` is no longer used to indicate a loader failure. Instead, if a loader fails, the error will be stored in `error`, and reading `value` will throw that error. + +This also means that you can now pass `expires`, `poll`, and `allowStale` options to route loaders. See the documentation for more details on these options and how to use them. diff --git a/.changeset/loaders-no-action-state.md b/.changeset/loaders-no-action-state.md new file mode 100644 index 00000000000..866f411edcb --- /dev/null +++ b/.changeset/loaders-no-action-state.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/router': major +--- + +BREAKING: `routeLoader$` cannot read action state any more, so that they are cacheable and predictable. All use cases can be achieved in other ways, like reading the action signal directly in the component or having the loader read from URL-derived state that the action updates. diff --git a/.changeset/nasty-pans-tickle.md b/.changeset/nasty-pans-tickle.md new file mode 100644 index 00000000000..2ee22619ae0 --- /dev/null +++ b/.changeset/nasty-pans-tickle.md @@ -0,0 +1,13 @@ +--- +'@qwik.dev/router': major +--- + +BREAKING: Qwik Router no longer retrieves `q-data.json` files on every SPA navigation. You can remove any caching rules for these. + +Instead, Qwik Router now retrieves `q-loader-${hash}.${manifestHash}.json` for RouteLoader data. You can specify expiry for each RouteLoader individually, and it is automatically used to set browser caching headers. The `manifestHash` ensures that when you build a new version of your app, the old cached data will be invalidated and the new data will be fetched. + +The default expiry for RouteLoader data is 2 minutes, so that prefetching and caching of RouteLoader data is useful. You control this with the `expiry` option on each RouteLoader, and you can set it to `0` to disable caching. + +Furthermore, any RouteLoader that has `expiry: 0` will be generated as a file during SSG, which allows SPA navigation to work even without a server. + +Note: Be careful with caching for RouteLoaders that return user-specific data, especially regarding logout and CDN caching. Use low expiry times for these and use `eTag` to still allow caching benefits. diff --git a/.changeset/warm-deer-take.md b/.changeset/warm-deer-take.md new file mode 100644 index 00000000000..dd1d078b6fe --- /dev/null +++ b/.changeset/warm-deer-take.md @@ -0,0 +1,7 @@ +--- +'@qwik.dev/router': minor +--- + +FEAT: route loaders now accept `search` to only allow certain query parameters to trigger the loader. This means that random search parameters won't cause the loader to re-run. If you do not pass `search`, then all search parameters will be passed to the loader and will trigger it when they change. + +However, `qwikRouter` now has the option `strictLoaders` which is `true` by default, which means that if you do not specify `search` for a loader, then it will not receive any search parameters and will not re-run when the search parameters change. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cb4a467506..be499fc63d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: run: echo event_name=${{ github.event_name }} - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set Dist Tag id: set_dist_tag @@ -139,7 +139,7 @@ jobs: - name: 'check cache: qwik' id: cache-qwik - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: lookup-only: true path: packages/qwik/dist @@ -149,7 +149,7 @@ jobs: - run: 'echo ${{ steps.cache-qwik.outputs.cache-primary-key }} > qwik-key.txt' - name: 'check cache: rust' id: cache-rust - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: lookup-only: true path: packages/optimizer/bindings @@ -157,7 +157,7 @@ jobs: - run: 'echo ${{ steps.cache-rust.outputs.cache-primary-key }} > rust-key.txt' - name: 'check cache: others' id: cache-others - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: lookup-only: true path: | @@ -170,14 +170,14 @@ jobs: - run: 'echo ${{ steps.cache-others.outputs.cache-primary-key }} > others-key.txt' - name: 'check cache: docs' id: cache-docs - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: lookup-only: true path: docs-build-completed.txt key: ${{ hashfiles('others-key.txt', 'packages/docs/**/*') }} - name: 'check cache: insights' id: cache-insights - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: lookup-only: true path: | @@ -186,35 +186,35 @@ jobs: key: ${{ hashfiles('others-key.txt', 'packages/insights/**/*') }} - name: 'check cache: unit tests' id: cache-unit - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: lookup-only: true path: unit-tests-completed.txt key: ${{ hashfiles('others-key.txt', 'packages/**/*.unit.*') }} - name: 'check cache: benchmarks' id: cache-bench - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: lookup-only: true path: bench-tests-completed.txt key: ${{ hashfiles('others-key.txt', 'packages/**/*.bench.*', 'scripts/validate-bench*', 'packages/docs/**/*') }} - name: 'check cache: e2e tests' id: cache-e2e - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: lookup-only: true path: e2e-tests-completed.txt key: ${{ hashfiles('others-key.txt', 'starters/**/*') }} - name: 'check cache: cli e2e tests' id: cache-cli-e2e - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: lookup-only: true path: cli-e2e-tests-completed.txt key: ${{ hashfiles('others-key.txt', 'e2e/**/*') }} - name: 'check cache: docs e2e tests' id: cache-docs-e2e - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: lookup-only: true path: docs-e2e-tests-completed.txt @@ -248,13 +248,13 @@ jobs: run: exit 1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 if: needs.changes.outputs.build-qwik == 'true' - name: Setup Node if: needs.changes.outputs.build-qwik == 'true' - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: 'pnpm' @@ -293,7 +293,7 @@ jobs: - name: Restore qwik cache if: needs.changes.outputs.build-qwik != 'true' - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: fail-on-cache-miss: true path: packages/qwik/dist @@ -305,13 +305,13 @@ jobs: - name: Save qwik cache if: needs.changes.outputs.build-qwik == 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: ${{ needs.changes.outputs.hash-qwik }} path: packages/qwik/dist - name: Upload Qwik artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: artifact-qwik include-hidden-files: true @@ -342,16 +342,16 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: components: clippy,rustfmt - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - name: Setup Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: 'pnpm' @@ -389,7 +389,7 @@ jobs: run: ls -lR packages/optimizer/bindings/ - name: Upload Platform Binding Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: artifact-bindings-${{ matrix.settings.target }} include-hidden-files: true @@ -413,7 +413,7 @@ jobs: run: exit 1 - name: Restore artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 - name: Move Rust Artifacts if: needs.changes.outputs.build-rust == 'true' @@ -431,13 +431,13 @@ jobs: - name: Save rust cache if: needs.changes.outputs.build-rust == 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: ${{ needs.changes.outputs.hash-rust }} path: packages/optimizer/ - name: Upload optimizer artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: artifact-optimizer include-hidden-files: true @@ -475,26 +475,26 @@ jobs: - name: Checkout if: needs.changes.outputs.build-others == 'true' - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 if: needs.changes.outputs.build-others == 'true' - name: Setup Node if: needs.changes.outputs.build-others == 'true' - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: 'pnpm' registry-url: https://registry.npmjs.org/ - name: Restore Qwik artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: artifact-qwik path: packages/qwik/dist/ - name: Restore Optimizer artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: artifact-optimizer path: packages/optimizer/ @@ -508,7 +508,7 @@ jobs: run: pnpm build --tsc --api --qwikrouter --cli --qwikreact --eslint --set-dist-tag="${{ needs.changes.outputs.disttag }}" - name: Save others cache if: needs.changes.outputs.build-others == 'true' - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: ${{ needs.changes.outputs.hash-others }} path: | @@ -519,7 +519,7 @@ jobs: - name: 'restore: qwik-router & others' if: needs.changes.outputs.build-others != 'true' - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: | packages/qwik-router/lib @@ -532,7 +532,7 @@ jobs: run: tree -a packages/qwik-router/lib/ - name: Upload QwikRouter Build Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: artifact-qwikrouter include-hidden-files: true @@ -543,7 +543,7 @@ jobs: run: tree -a packages/qwik-react/lib/ - name: Upload qwik-react Build Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: artifact-qwikreact include-hidden-files: true @@ -555,7 +555,7 @@ jobs: run: tree -a packages/create-qwik/dist/ - name: Upload Create Qwik CLI Build Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: artifact-create-qwik include-hidden-files: true @@ -566,7 +566,7 @@ jobs: run: tree -a packages/eslint-plugin-qwik/dist/ - name: Upload Eslint rules Build Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: artifact-eslint-plugin-qwik include-hidden-files: true @@ -592,18 +592,18 @@ jobs: run: exit 1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - name: Setup Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: 'pnpm' registry-url: https://registry.npmjs.org/ - name: Download Build Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 - name: Move Distribution Artifacts run: pnpm ci.restore-artifacts @@ -613,7 +613,7 @@ jobs: run: pnpm run build.packages.insights - name: Save Insights Cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: ${{ needs.changes.outputs.hash-insights }} path: | @@ -639,18 +639,18 @@ jobs: run: exit 1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - name: Setup Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: 'pnpm' registry-url: https://registry.npmjs.org/ - name: Download Build Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 - name: Move Distribution Artifacts run: pnpm ci.restore-artifacts @@ -660,7 +660,7 @@ jobs: run: pnpm build --tsc-docs && pnpm run build.packages.docs && echo ok > docs-build-completed.txt - name: Save Docs Artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: artifact-docs include-hidden-files: true @@ -669,7 +669,7 @@ jobs: packages/docs/server - name: Save Docs Cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: ${{ needs.changes.outputs.hash-docs }} path: docs-build-completed.txt @@ -696,18 +696,18 @@ jobs: run: exit 1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - name: Setup Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: 'pnpm' registry-url: https://registry.npmjs.org/ - name: Download Build Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 - name: Move Distribution Artifacts run: pnpm ci.restore-artifacts @@ -721,13 +721,13 @@ jobs: run: pnpm run test.e2e.docs && echo ok > docs-e2e-tests-completed.txt - name: Save Docs E2E Tests Cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: ${{ needs.changes.outputs.hash-docs-e2e }} path: docs-e2e-tests-completed.txt - name: Upload Playwright Trace - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() with: name: docs-playwright-report @@ -759,18 +759,18 @@ jobs: run: exit 1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - name: Setup Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: 'pnpm' registry-url: https://registry.npmjs.org/ - name: Download Build Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 - name: Move Distribution Artifacts run: pnpm ci.restore-artifacts @@ -801,7 +801,7 @@ jobs: run: echo ok > unit-tests-completed.txt - name: Save unit tests cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: ${{ needs.changes.outputs.hash-unit }} path: unit-tests-completed.txt @@ -830,18 +830,18 @@ jobs: run: exit 1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - name: Setup Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: 'pnpm' registry-url: https://registry.npmjs.org/ - name: Download Build Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 - name: Move Distribution Artifacts run: pnpm ci.restore-artifacts @@ -875,7 +875,7 @@ jobs: run: echo ok > bench-tests-completed.txt - name: Save benchmarks cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: ${{ needs.changes.outputs.hash-bench }} path: bench-tests-completed.txt @@ -921,18 +921,18 @@ jobs: run: exit 1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - name: Setup Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION_E2E }} cache: 'pnpm' registry-url: https://registry.npmjs.org/ - name: Download Build Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 - name: Move Distribution Artifacts run: pnpm ci.restore-artifacts @@ -946,7 +946,7 @@ jobs: run: pnpm run test.e2e.${{ matrix.settings.browser }} - name: Upload Playwright Trace - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: failure() # only when tests fail with: name: playwright-report @@ -1016,18 +1016,18 @@ jobs: run: exit 1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - name: Setup Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION_E2E }} cache: 'pnpm' registry-url: https://registry.npmjs.org/ - name: Download Build Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 - name: Move Distribution Artifacts run: pnpm ci.restore-artifacts @@ -1080,7 +1080,7 @@ jobs: run: echo ok > e2e-tests-completed.txt - name: Save e2e tests cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: ${{ needs.changes.outputs.hash-e2e }} path: e2e-tests-completed.txt @@ -1106,7 +1106,7 @@ jobs: run: echo ok > cli-e2e-tests-completed.txt - name: Save cli-e2e tests cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: ${{ needs.changes.outputs.hash-cli-e2e }} path: cli-e2e-tests-completed.txt @@ -1117,11 +1117,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - name: Setup Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: 'pnpm' @@ -1195,18 +1195,18 @@ jobs: run: exit 1 - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v5 - name: Setup Node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ env.CI_NODE_VERSION }} cache: 'pnpm' registry-url: https://registry.npmjs.org/ - name: Download Build Artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 - name: Move Distribution Artifacts run: pnpm ci.restore-artifacts diff --git a/.gitignore b/.gitignore index 9482e9335a0..676be2365ad 100644 --- a/.gitignore +++ b/.gitignore @@ -18,11 +18,11 @@ node_modules # Artifacts tsc-out dist -dist-dev +/dist-dev external lib tsdoc-metadata.json -target +/target # IDE and local environment .idea @@ -48,4 +48,4 @@ sandbox # Vitest coverage packages/coverage/* visualizer.html -**/lib-types/**/* \ No newline at end of file +**/lib-types/**/* diff --git a/e2e/qwik-e2e/apps/qwikrouter-ssg-snapshot/expected.ssg.html b/e2e/qwik-e2e/apps/qwikrouter-ssg-snapshot/expected.ssg.html index 2ffadd3e233..bebf2fd46ac 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-ssg-snapshot/expected.ssg.html +++ b/e2e/qwik-e2e/apps/qwikrouter-ssg-snapshot/expected.ssg.html @@ -1,3 +1,3 @@ Qwik Router SSG Snapshot

SSG Snapshot Fixture

routeLoader$ | useResource$ | useSignal

{"heading":"SSG Snapshot Fixture","stats":[2,3,5,8],"tags":["routeLoader$","useResource$","useSignal"],"profile":{"name":"router-state","status":"stable"}}
{"total":18,"selectedCount":2,"clicks":2}
{"headline":"router-state:stable","selected":["alpha","beta"],"weighted":[2,6,15,32],"summary":{"total":18,"selectedCount":2,"clicks":2}}
[state omitted] [vnode map omitted] - \ No newline at end of file + \ No newline at end of file diff --git a/e2e/qwik-e2e/apps/qwikrouter-ssg-snapshot/expected.state.txt b/e2e/qwik-e2e/apps/qwikrouter-ssg-snapshot/expected.state.txt index 42e0cc88b55..f4171ff80db 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-ssg-snapshot/expected.state.txt +++ b/e2e/qwik-e2e/apps/qwikrouter-ssg-snapshot/expected.state.txt @@ -13,10 +13,89 @@ Constant null ] ] -7 {number} 19 +7 {number} 26 8 Array [ {string} "ji2uyl-0" RootRef [omitted] + Store [ + Object [ + {string} "loaderPaths" + Object [ + {string} "qO14f4pG8tQ" + {string} "/" + ] + ] + {number} 1 + Constant undefined + Store [ + RootRef [omitted] + {number} 1 + ] + ] + Store [ + Object [ + RootRef [omitted] + AsyncSignal [ + QRL "[omitted]" + Set [ + EffectSubscription [ + VNode "14AAA" + {string} ":" + Constant null + ] + EffectSubscription [ + ComputedSignal [ + QRL "[omitted]" + Set [ + RootRef [omitted] + ] + Object [ + {string} "total" + {number} 18 + {string} "selectedCount" + {number} 2 + {string} "clicks" + {number} 2 + ] + ] + {string} "." + Constant null + ] + EffectSubscription [ + WrappedSignal [ + {number} 1 + Array [ + RootRef [omitted] + ] + {number} 7 + VNode "16A" + EffectSubscription [ + RootRef [omitted] + {string} "." + Constant null + ] + ] + {string} "." + Constant null + ] + ] + Constant undefined + Constant undefined + Constant undefined + {number} 256 + Constant NEEDS_COMPUTATION + {number} 120000 + ] + ] + {number} 0 + Map [ + RootRef [omitted] + Set [ + RootRef [omitted] + RootRef [omitted] + ] + ] + ] RootRef [omitted] Store [ Object [ @@ -40,7 +119,7 @@ {string} "frontmatter" Object 0 {string} "manifestHash" - {string} "MANIFEST_HASH" + RootRef [omitted] ] {number} 1 Map [ @@ -154,21 +233,45 @@ ] Signal [ Constant undefined + EffectSubscription [ + Task [ + QRL "[omitted]" + {number} 10 + {number} 23 + RootRef [omitted] + ] + {string} ":" + Constant null + ] EffectSubscription [ VNode "14A" {string} ":" Constant null ] ] + Signal [ + Constant undefined + EffectSubscription [ + Task [ + QRL "[omitted]" + {number} 2 + {number} 24 + RootRef [omitted] + ] + {string} ":" + Constant null + ] + ] Signal [ Object [ {string} "status" {number} 200 {string} "message" - Constant '' + {string} "OK" ] ] RootRef [omitted] + RootRef [omitted] {number} 1 {number} 1 {number} 1 @@ -178,13 +281,7 @@ {number} 1 {number} 1 {number} 1 - Task [ - QRL "[omitted]" - {number} 10 - {number} 17 - RootRef [omitted] - ] - RootRef [omitted] + ... ] 9 Array [ {string} "qr--a" @@ -199,6 +296,8 @@ RootRef [omitted] {string} "qr--l" RootRef [omitted] + {string} "qr--lc" + RootRef [omitted] {string} "qr--n" RootRef [omitted] {string} "qr--p" @@ -216,7 +315,7 @@ Constant null ] ] -13 QRL "[omitted]" +13 {string} "MANIFEST_HASH" 14 Object 0 15 Signal [ Object [ @@ -235,7 +334,7 @@ Set [ EffectSubscription [ WrappedSignal [ - {number} 1 + {number} 2 Array [ RootRef [omitted] ] @@ -263,112 +362,128 @@ 21 {string} "q-xxxxxxxx.js" 22 {string} "s_TG0a9Yb7gLI" 23 RootRef [omitted] -24 QRL "[omitted]" -25 Map [ +24 RootRef [omitted] +25 RootRef [omitted] +26 Constant undefined +27 RootRef [omitted] +28 Array [] +29 {string} "q-xxxxxxxx.js" +30 {string} "s_0vpftJHSMyo" +31 RootRef [omitted] +32 QRL "[omitted]" +33 Map [ {string} ":" RootRef [omitted] ] -26 {number} 1 -27 Array [ +34 {number} 2 +35 Array [ + RootRef [omitted] RootRef [omitted] ] -28 RootRef [omitted] -29 RootRef [omitted] -30 RootRef [omitted] -31 RootRef [omitted] -32 RootRef [omitted] -33 RootRef [omitted] -34 RootRef [omitted] -35 RootRef [omitted] -36 RootRef [omitted] -37 RootRef [omitted] -38 RootRef [omitted] -39 RootRef [omitted] -40 RootRef [omitted] -41 RootRef [omitted] -42 RootRef [omitted] -43 RootRef [omitted] -44 RootRef [omitted] -45 QRL "[omitted]" -46 Map [ +36 QRL "[omitted]" +37 Map [ {string} ":" RootRef [omitted] ] -47 {number} 2 -48 Array [ +38 {number} 4 +39 Array [ RootRef [omitted] RootRef [omitted] -] -49 QRL "[omitted]" -50 Map [ - {string} ":" - EffectSubscription [ - VNode "14AAA" - {string} ":" - Constant null - ] -] -51 {number} 4 -52 Array [ - Signal [ - {number} 2 - EffectSubscription [ - AsyncSignal [ - QRL "[omitted]" - Set [ - RootRef [omitted] - ] - Constant undefined - Constant undefined - Constant undefined - Constant undefined - Object [ - {string} "r" - Store [ - Object [ - {string} "headline" - {string} "router-state:stable" - {string} "selected" - Array [ - {string} "alpha" - {string} "beta" - ] - {string} "weighted" - Array [ - {number} 2 - {number} 6 - {number} 15 - {number} 32 - ] - {string} "summary" - Object [ - {string} "total" - {number} 18 - {string} "selectedCount" - {number} 2 - {string} "clicks" - {number} 2 - ] + RootRef [omitted] + Object [ + {string} "__brand" + {string} "resource" + {string} "signal" + AsyncSignal [ + QRL "[omitted]" + Set [ + RootRef [omitted] + ] + Constant undefined + Constant undefined + Constant undefined + Constant undefined + Object [ + {string} "r" + Store [ + Object [ + {string} "headline" + {string} "router-state:stable" + {string} "selected" + Array [ + {string} "alpha" + {string} "beta" + ] + {string} "weighted" + Array [ + {number} 2 + {number} 6 + {number} 15 + {number} 32 + ] + {string} "summary" + RootRef [omitted] + ] + {number} 1 + Map [ + {string} "then" + Set [ + RootRef [omitted] + ] + {string} "toJSON" + Set [ + RootRef [omitted] + ] + Constant STORE_ALL_PROPS + Set [ + RootRef [omitted] + ] + RootRef [omitted] + Set [ + RootRef [omitted] + ] + RootRef [omitted] + Set [ + RootRef [omitted] + ] + RootRef [omitted] + Set [ + RootRef [omitted] + ] + RootRef [omitted] + Set [ + RootRef [omitted] ] + ] + Store [ + RootRef [omitted] {number} 1 Map [ - {string} "then" - Set [ - RootRef [omitted] - ] - {string} "toJSON" + Constant STORE_ALL_PROPS Set [ RootRef [omitted] ] + ] + ] + Store [ + RootRef [omitted] + {number} 1 + Map [ Constant STORE_ALL_PROPS Set [ RootRef [omitted] ] + ] + ] + Store [ + RootRef [omitted] + {number} 1 + Map [ RootRef [omitted] Set [ RootRef [omitted] ] - RootRef [omitted] + Constant STORE_ALL_PROPS Set [ RootRef [omitted] ] @@ -380,147 +495,33 @@ Set [ RootRef [omitted] ] - ] - Store [ - RootRef [omitted] - {number} 1 - Map [ - Constant STORE_ALL_PROPS - Set [ - RootRef [omitted] - ] - ] - ] - Store [ RootRef [omitted] - {number} 1 - Map [ - Constant STORE_ALL_PROPS - Set [ - RootRef [omitted] - ] - ] - ] - Store [ - RootRef [omitted] - {number} 1 - Map [ - RootRef [omitted] - Set [ - RootRef [omitted] - ] - Constant STORE_ALL_PROPS - Set [ - RootRef [omitted] - ] - RootRef [omitted] - Set [ - RootRef [omitted] - ] - RootRef [omitted] - Set [ - RootRef [omitted] - ] + Set [ RootRef [omitted] - Set [ - RootRef [omitted] - ] ] ] ] ] - Constant undefined - {number} 0 - ] - {string} ":" - Constant null - ] - EffectSubscription [ - ComputedSignal [ - QRL "[omitted]" - Set [ - RootRef [omitted] - ] - RootRef [omitted] ] - {string} "." - Constant null + Constant undefined + {number} 0 ] - ] - RootRef [omitted] - RootRef [omitted] - Object [ - {string} "__brand" - {string} "resource" - {string} "signal" - RootRef [omitted] {string} "value" ForwardRef 0 {string} "loading" Constant false ] ] -53 RootRef [omitted] -54 RootRef [omitted] -55 RootRef [omitted] -56 Constant undefined -57 RootRef [omitted] -58 Object [ - {string} "id_ssg-snapshot-loader" - RootRef [omitted] -] -59 Object [ - RootRef [omitted] - Constant _UNINITIALIZED -] -60 Constant undefined -61 Object [ - {string} "url" - RootRef [omitted] - {string} "params" - Object 0 - {string} "isNavigating" - Constant false - {string} "prevUrl" -] -62 {string} "s_XpalYii770E" -63 {string} "s_69B0DK0eZJc" -64 RootRef [omitted] -65 {string} "s_0UhDFwlxeFQ" -66 RootRef [omitted] -67 RootRef [omitted] -68 Map [ - RootRef [omitted] - RootRef [omitted] -] -69 {string} "q-xxxxxxxx.js" -70 {string} "s_9CrWYOoCpgY" -71 {string} "s_QwONcWD5gIg" -72 RootRef [omitted] -73 {string} "q-xxxxxxxx.js" -74 {string} "s_LqnNyU1Iy8c" -75 RootRef [omitted] -76 QRL "[omitted]" -77 Object [ - {string} "r" +40 Signal [ + {number} 2 + EffectSubscription [ + RootRef [omitted] + {string} ":" + Constant null + ] RootRef [omitted] ] -78 {string} "q-xxxxxxxx.js" -79 {string} "_rsc" -80 RootRef [omitted] -81 RootRef [omitted] -82 RootRef [omitted] -83 RootRef [omitted] -84 RootRef [omitted] -85 RootRef [omitted] -86 RootRef [omitted] -87 RootRef [omitted] -88 RootRef [omitted] -89 RootRef [omitted] -90 RootRef [omitted] -91 RootRef [omitted] -92 RootRef [omitted] -93 Store [ +41 Store [ Object [ {string} "expanded" Constant true @@ -555,57 +556,128 @@ ] ] ] -94 AsyncSignal [ - QRL "[omitted]" - Set [ - RootRef [omitted] +42 RootRef [omitted] +43 {string} "q-xxxxxxxx.js" +44 {string} "s_cgSl2l0RC14" +45 RootRef [omitted] +46 RootRef [omitted] +47 Map [ + {string} "." + RootRef [omitted] +] +48 RootRef [omitted] +49 RootRef [omitted] +50 QRL "[omitted]" +51 Map [ + {string} ":" + RootRef [omitted] +] +52 {number} 1 +53 Array [ + RootRef [omitted] +] +54 RootRef [omitted] +55 RootRef [omitted] +56 RootRef [omitted] +57 RootRef [omitted] +58 RootRef [omitted] +59 RootRef [omitted] +60 RootRef [omitted] +61 RootRef [omitted] +62 RootRef [omitted] +63 RootRef [omitted] +64 RootRef [omitted] +65 RootRef [omitted] +66 RootRef [omitted] +67 RootRef [omitted] +68 RootRef [omitted] +69 RootRef [omitted] +70 Signal [ + Object [ RootRef [omitted] - EffectSubscription [ - WrappedSignal [ - {number} 2 - Array [ - RootRef [omitted] - ] - {number} 7 - VNode "16A" - EffectSubscription [ - RootRef [omitted] - {string} "." - Constant null - ] - ] - {string} "." - Constant null - ] + {number} 200 + {string} "statusMessage" + Constant undefined + {string} "action" + Constant undefined + {string} "actionResult" + Constant undefined + {string} "formData" ] + RootRef [omitted] ] -95 {string} "q-xxxxxxxx.js" -96 {string} "s_cgSl2l0RC14" -97 RootRef [omitted] -98 RootRef [omitted] +71 RootRef [omitted] +72 RootRef [omitted] +73 RootRef [omitted] +74 {string} "s_1sKEZ9Pg1ow" +75 RootRef [omitted] +76 RootRef [omitted] +77 {string} "s_yw17chooOac" +78 RootRef [omitted] +79 Constant undefined +80 RootRef [omitted] +81 Object [ + {string} "url" + RootRef [omitted] + {string} "params" + Object 0 + {string} "isNavigating" + Constant false + {string} "prevUrl" +] +82 {string} "s_XpalYii770E" +83 RootRef [omitted] +84 RootRef [omitted] +85 {string} "s_69B0DK0eZJc" +86 RootRef [omitted] +87 RootRef [omitted] +88 RootRef [omitted] +89 Map [ + RootRef [omitted] + RootRef [omitted] +] +90 {string} "s_QwONcWD5gIg" +91 RootRef [omitted] +92 {string} "q-xxxxxxxx.js" +93 {string} "s_LqnNyU1Iy8c" +94 RootRef [omitted] +95 QRL "[omitted]" +96 Object [ + {string} "r" + RootRef [omitted] +] +97 {string} "q-xxxxxxxx.js" +98 {string} "_rsc" 99 RootRef [omitted] 100 RootRef [omitted] 101 RootRef [omitted] 102 RootRef [omitted] -103 {string} "s_lKDAUb7ao7U" +103 RootRef [omitted] 104 RootRef [omitted] 105 RootRef [omitted] 106 RootRef [omitted] 107 RootRef [omitted] 108 RootRef [omitted] -109 URL "https://snapshot.qwik.dev/" -110 {string} "q-xxxxxxxx.js" -111 {string} "s_CFLMoh8rnzw" +109 RootRef [omitted] +110 RootRef [omitted] +111 RootRef [omitted] 112 RootRef [omitted] -113 Map [ - {string} "." - RootRef [omitted] -] +113 RootRef [omitted] 114 RootRef [omitted] -115 Promise [ +115 RootRef [omitted] +116 {string} "q-xxxxxxxx.js" +117 {string} "s_9CrWYOoCpgY" +118 RootRef [omitted] +119 RootRef [omitted] +120 RootRef [omitted] +121 RootRef [omitted] +122 RootRef [omitted] +123 {string} "s_lKDAUb7ao7U" +124 RootRef [omitted] +125 Promise [ Constant true RootRef [omitted] ] -116 ForwardRefs [ - 115 +126 ForwardRefs [ + 125 ] diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/data.ts b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/data.ts deleted file mode 100644 index cc5a2a726f7..00000000000 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/data.ts +++ /dev/null @@ -1 +0,0 @@ -export const data: string[] = []; diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/index.tsx index d89f6c775cb..d34b4d65611 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/index.tsx +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/index.tsx @@ -1,12 +1,17 @@ import { component$ } from '@qwik.dev/core'; import { Form, routeAction$ } from '@qwik.dev/router'; -import { data } from './data'; +import { SESSION_COOKIE, createSessionId, getSessionData } from './session-data'; export const useRootAction = routeAction$( - (form, { redirect }) => { - const name = form.name as string; + (form, { redirect, cookie }) => { + let sessionId = cookie.get(SESSION_COOKIE)?.value; + if (!sessionId) { + sessionId = createSessionId(); + cookie.set(SESSION_COOKIE, sessionId, { path: '/' }); + } + const data = getSessionData(sessionId); data.length = 0; - data.push(name); + data.push(form.name as string); throw redirect(303, '/qwikrouter-test/issue2644/other/'); }, { diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/other/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/other/index.tsx index 927d0bf2954..e05b40b097c 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/other/index.tsx +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/other/index.tsx @@ -1,12 +1,13 @@ import { Form, routeAction$, routeLoader$ } from '@qwik.dev/router'; import { component$ } from '@qwik.dev/core'; -import { data } from '../data'; +import { SESSION_COOKIE, getSessionData } from '../session-data'; -export const useGetData = routeLoader$(() => { - return data; +export const useGetData = routeLoader$(({ cookie }) => { + return getSessionData(cookie.get(SESSION_COOKIE)?.value); }); -export const useOtherAction = routeAction$((form) => { +export const useOtherAction = routeAction$((form, { cookie }) => { + const data = getSessionData(cookie.get(SESSION_COOKIE)?.value); const name = form.name as string; data.push(name); return { success: true }; diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/session-data.ts b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/session-data.ts new file mode 100644 index 00000000000..73389992134 --- /dev/null +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/session-data.ts @@ -0,0 +1,19 @@ +// Per-session storage with random session IDs stored in cookies. +const sessionData = new Map(); + +export const SESSION_COOKIE = 'qwik-e2e'; + +export const getSessionData = (sessionId: string | undefined): string[] => { + if (!sessionId) { + return []; + } + let data = sessionData.get(sessionId); + if (!data) { + data = []; + sessionData.set(sessionId, data); + } + return data; +}; + +export const createSessionId = () => + `s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/loaders/[id]/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/loaders/[id]/index.tsx index cb1a99ae9f4..788f1d9fa48 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/loaders/[id]/index.tsx +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/loaders/[id]/index.tsx @@ -12,23 +12,25 @@ import { delay } from '../../actions/login'; export const useDateLoader = routeLoader$(() => new Date('2021-01-01T00:00:00.000Z')); -export const useDependencyLoader = routeLoader$( - async ({ params, redirect, json, resolveValue }) => { - const formData = await resolveValue(useForm); - await delay(100); - if (params.id === 'redirect') { - throw redirect(302, '/qwikrouter-test/'); - } else if (params.id === 'redirect-welcome') { - throw redirect(302, '/qwikrouter-test/loaders/welcome/'); - } else if (params.id === 'json') { - throw json(200, { nu: 42 }); - } - return { - nu: 42, - name: formData?.name ?? params.id, - }; +// Note: a loader cannot read action state via `resolveValue(useForm)`. After an action +// submission, the client invalidates loaders and refetches them as standalone GET +// requests, which carry no action context — so `resolveValue(actionQrl)` returns +// undefined. Read action state directly from the action signal (e.g. in the head, or +// from `useForm.value` in components), not via a loader. +export const useDependencyLoader = routeLoader$(async ({ params, redirect, json }) => { + await delay(100); + if (params.id === 'redirect') { + throw redirect(302, '/qwikrouter-test/'); + } else if (params.id === 'redirect-welcome') { + throw redirect(302, '/qwikrouter-test/loaders/welcome/'); + } else if (params.id === 'json') { + throw json(200, { nu: 42 }); } -); + return { + nu: 42, + name: params.id, + }; +}); const useLoader = routeLoader$(() => { return [ diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/a/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/a/index.tsx new file mode 100644 index 00000000000..6715a551361 --- /dev/null +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/a/index.tsx @@ -0,0 +1,21 @@ +import { component$ } from '@qwik.dev/core'; +import { useNavigate } from '@qwik.dev/router'; + +export default component$(() => { + const nav = useNavigate(); + return ( +
+

Route A

+ +
+ ); +}); diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/b/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/b/index.tsx new file mode 100644 index 00000000000..aa98fb161cb --- /dev/null +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/b/index.tsx @@ -0,0 +1,16 @@ +import { component$ } from '@qwik.dev/core'; +import { routeLoader$ } from '@qwik.dev/router'; + +export const useData = routeLoader$(() => { + return 'data-b'; +}); + +export default component$(() => { + const data = useData(); + return ( +
+

Route B

+ {data.value} +
+ ); +}); diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/c/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/c/index.tsx new file mode 100644 index 00000000000..b4604136e59 --- /dev/null +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/c/index.tsx @@ -0,0 +1,16 @@ +import { component$ } from '@qwik.dev/core'; +import { routeLoader$ } from '@qwik.dev/router'; + +export const useData = routeLoader$(() => { + return 'data-c'; +}); + +export default component$(() => { + const data = useData(); + return ( +
+

Route C

+ {data.value} +
+ ); +}); diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/issue4502/broken/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/issue4502/broken/index.tsx index 76b43545026..8074e829264 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/issue4502/broken/index.tsx +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/issue4502/broken/index.tsx @@ -1,6 +1,19 @@ -import type { RequestHandler } from '@qwik.dev/router'; +import { component$ } from '@qwik.dev/core'; +import { routeLoader$, type RequestHandler } from '@qwik.dev/router'; + +// We need a routeLoader$ here to ensure the page is loaded via the loader URL (q-loader-*.json) which triggers the onRequest middleware. +export const useForceLoadingThisIndex = routeLoader$(() => null); export const onRequest: RequestHandler = async (onRequestArgs) => { const { redirect, url } = onRequestArgs; throw redirect(302, `${url.pathname}/route/`); }; + +export default component$(() => { + useForceLoadingThisIndex(); + return ( +
+ You should be redirecting to /broken/route +
+ ); +}); diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/issue7732/b/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/issue7732/b/index.tsx index b4817d2f55d..37c98af07c2 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/issue7732/b/index.tsx +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/issue7732/b/index.tsx @@ -1,10 +1,15 @@ import { component$ } from '@qwik.dev/core'; +import { routeLoader$ } from '@qwik.dev/router'; import type { RequestHandler } from '@qwik.dev/router'; export const onGet: RequestHandler = ({ redirect }) => { throw redirect(302, `/qwikrouter-test/issue7732/c/?redirected=true`); }; +// A routeLoader$ on this route ensures SPA navigation fetches from this path, +// triggering the onGet middleware above. +export const useRouteCheck = routeLoader$(() => null); + export default component$(() => { return
B route with redirect
; }); diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/index.tsx new file mode 100644 index 00000000000..cd7dda4139f --- /dev/null +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/index.tsx @@ -0,0 +1,19 @@ +import { component$ } from '@qwik.dev/core'; +import { useNavigate } from '@qwik.dev/router'; + +export default component$(() => { + const nav = useNavigate(); + return ( +
+

Loader Redirect Home

+ +
+ ); +}); diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/source/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/source/index.tsx new file mode 100644 index 00000000000..7fdfa8c17fe --- /dev/null +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/source/index.tsx @@ -0,0 +1,23 @@ +import { component$ } from '@qwik.dev/core'; +import { routeLoader$ } from '@qwik.dev/router'; +import type { RequestHandler } from '@qwik.dev/router'; + +export const onRequest: RequestHandler = ({ url, redirect }) => { + if (url.searchParams.has('redirect')) { + throw redirect(302, '/qwikrouter-test/loader-redirect/target/?done=true'); + } +}; + +export const useSourceData = routeLoader$(() => { + return 'source-data'; +}); + +export default component$(() => { + const data = useSourceData(); + return ( +
+

Source Route

+ {data.value} +
+ ); +}); diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/target/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/target/index.tsx new file mode 100644 index 00000000000..11d26534f36 --- /dev/null +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/target/index.tsx @@ -0,0 +1,16 @@ +import { component$ } from '@qwik.dev/core'; +import { routeLoader$ } from '@qwik.dev/router'; + +export const useTargetData = routeLoader$(() => { + return 'target-data'; +}); + +export default component$(() => { + const data = useTargetData(); + return ( +
+

Redirect Target

+ {data.value} +
+ ); +}); diff --git a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx index 1a57fb71eb9..4ddb0ecfb9b 100644 --- a/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx +++ b/e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx @@ -19,10 +19,13 @@ export const useNestedLoader = routeLoader$(async (requestEv) => { export default component$(() => { const testSignal = useTestLoader(); + const eagerSignal = useTestLoaderEager(); const toggle = useSignal(false); return ( <> {testSignal.value.test} + {/* Read eager signal in default render path so it's computed + serialized during SSR */} + {eagerSignal.value.foo} diff --git a/e2e/qwik-e2e/dev-server.ts b/e2e/qwik-e2e/dev-server.ts index df6b690e7e8..2aba3f95830 100644 --- a/e2e/qwik-e2e/dev-server.ts +++ b/e2e/qwik-e2e/dev-server.ts @@ -169,6 +169,8 @@ export { router } }, }, ], + // Legacy e2e tests rely on actions re-running all loaders + strictLoaders: false, }) as PluginOption // qwikRouterSsg.nodeServerAdapter({ // ssg: null, diff --git a/e2e/qwik-e2e/playwright.config.ts b/e2e/qwik-e2e/playwright.config.ts index ef78d9c7a43..1c2735545c3 100644 --- a/e2e/qwik-e2e/playwright.config.ts +++ b/e2e/qwik-e2e/playwright.config.ts @@ -33,9 +33,9 @@ const config: PlaywrightTestConfig = { /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, testIgnore: /.*example.spec.tsx?$/, - retries: 0, - // retries: 1, + retries: 1, expect: { timeout: inGithubCI ? 120000 : 3000 }, + outputDir: '../../test-results/', webServer: { command: 'pnpm node --require ./scripts/runBefore.ts e2e/qwik-e2e/dev-server.ts 3301', port: 3301, diff --git a/e2e/qwik-e2e/tests/qwikrouter/auth.e2e.ts b/e2e/qwik-e2e/tests/qwikrouter/auth.e2e.ts index 06bca15409c..e12fe06badc 100644 --- a/e2e/qwik-e2e/tests/qwikrouter/auth.e2e.ts +++ b/e2e/qwik-e2e/tests/qwikrouter/auth.e2e.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { assertPage, getPage, linkNavigate, load } from './util.js'; test.describe('Qwik Router Auth', () => { @@ -14,7 +14,11 @@ test.describe('Qwik Router Auth', () => { }); function tests() { - test('Qwik Router Auth', async ({ context, javaScriptEnabled }) => { + test('Qwik Router Auth', async ({ context, javaScriptEnabled, browserName }) => { + test.slow( + browserName === 'firefox', + 'Firefox auth navigations can be slow under parallel e2e load' + ); const ctx = await load(context, javaScriptEnabled, '/qwikrouter-test/sign-in/'); /** Sign In ********** */ @@ -38,6 +42,7 @@ function tests() { /** Unsuccessful Sign In ********** */ await linkNavigate(ctx, '[data-test-sign-in]', 403); + await expect(page.locator('input[name="username"]')).toHaveValue(''); page = getPage(ctx); await page.focus('input[name="username"]'); @@ -51,6 +56,7 @@ function tests() { /** Unsuccessful Sign In ********** */ await linkNavigate(ctx, '[data-test-sign-in]', 400); + await expect(page.locator('input[name="username"]')).toHaveValue(''); page = getPage(ctx); await page.focus('input[name="username"]'); @@ -64,6 +70,7 @@ function tests() { /** Successful Sign In, Dashboard ********** */ await linkNavigate(ctx, '[data-test-sign-in]', 302); + await page.waitForURL('/qwikrouter-test/dashboard/'); await assertPage(ctx, { pathname: '/qwikrouter-test/dashboard/', diff --git a/e2e/qwik-e2e/tests/qwikrouter/loaders.e2e.ts b/e2e/qwik-e2e/tests/qwikrouter/loaders.e2e.ts index 56d7e99dc39..f4b9d59f396 100644 --- a/e2e/qwik-e2e/tests/qwikrouter/loaders.e2e.ts +++ b/e2e/qwik-e2e/tests/qwikrouter/loaders.e2e.ts @@ -46,23 +46,31 @@ test.describe('loaders', () => { await expect(slow).toHaveText('slow: 123'); await expect(nestedDate).toHaveText('date: 2021-01-01T00:00:00.000Z'); await expect(nestedDep).toHaveText('dep: 84'); - await expect(nestedName).toHaveText('name: Manuel'); + // The loader-derived name is unchanged after the action: route loaders + // refetched after an action submission run as standalone GETs without + // action context, so they don't see action state via resolveValue(). + // Action state is observable directly via the action signal — see the title. + await expect(nestedName).toHaveText('name: hola'); await page.locator('#link-stuff').click(); + // Wait for URL to change first, then verify content + await page.waitForURL('**/loaders/stuff/**'); + await expect(nestedName).toHaveText('name: stuff'); await expect(title).toHaveText('Loaders - Qwik', { useInnerText: true }); await expect(date).toHaveText('date: 2021-01-01T00:00:00.000Z'); await expect(slow).toHaveText('slow: 123'); await expect(nestedDate).toHaveText('date: 2021-01-01T00:00:00.000Z'); await expect(nestedDep).toHaveText('dep: 84'); - await expect(nestedName).toHaveText('name: stuff'); await page.locator('#link-welcome').click(); + // Wait for URL to change first, then verify content + await page.waitForURL('**/loaders/welcome/**'); + await expect(nestedName).toHaveText('name: welcome'); await expect(title).toHaveText('Loaders - Qwik', { useInnerText: true }); await expect(date).toHaveText('date: 2021-01-01T00:00:00.000Z'); await expect(slow).toHaveText('slow: 123'); await expect(nestedDate).toHaveText('date: 2021-01-01T00:00:00.000Z'); await expect(nestedDep).toHaveText('dep: 84'); - await expect(nestedName).toHaveText('name: welcome'); }); test('should pass reactivity issue', async ({ page }) => { @@ -167,39 +175,5 @@ test.describe('loaders', () => { await expect(page.locator('#prop6')).toHaveText('should not serialize this nested'); } }); - - test('should retry with all loaders if one fails', async ({ page, javaScriptEnabled }) => { - let loadersRequestCount = 0; - let allLoadersRequestCount = 0; - page.on('request', (request) => { - if (request.url().includes('q-data.json?qloaders')) { - loadersRequestCount++; - } - if (request.url().endsWith('q-data.json')) { - allLoadersRequestCount++; - } - }); - - await page.route( - '*/**/qwikrouter-test/loaders-serialization/q-data.json?qloaders=*', - async (route) => { - await route.fulfill({ status: 404 }); - } - ); - await page.goto('/qwikrouter-test/loaders-serialization/'); - - if (javaScriptEnabled) { - await page.locator('#toggle-child').click(); - await page.waitForLoadState('networkidle'); - expect(loadersRequestCount).toBe(2); - expect(allLoadersRequestCount).toBe(1); - await expect(page.locator('#prop1')).toHaveText('some test value'); - await expect(page.locator('#prop2')).toHaveText('should not serialize this'); - await expect(page.locator('#prop3')).toHaveText('some eager test value'); - await expect(page.locator('#prop4')).toHaveText('should serialize this'); - await expect(page.locator('#prop5')).toHaveText('some test value nested'); - await expect(page.locator('#prop6')).toHaveText('should not serialize this nested'); - } - }); } }); diff --git a/e2e/qwik-e2e/tests/qwikrouter/nav.e2e.ts b/e2e/qwik-e2e/tests/qwikrouter/nav.e2e.ts index 45d3b86022b..1a8f2fee62b 100644 --- a/e2e/qwik-e2e/tests/qwikrouter/nav.e2e.ts +++ b/e2e/qwik-e2e/tests/qwikrouter/nav.e2e.ts @@ -1,4 +1,4 @@ -import { expect, test, type Locator } from '@playwright/test'; +import { expect, test, type Locator, type Page } from '@playwright/test'; import { assertPage, getScrollHeight, @@ -36,6 +36,26 @@ test.describe('nav', () => { }); } + async function reloadFromPage(page: Page) { + const loaded = page.waitForEvent('load'); + await page.evaluate(() => location.reload()); + await loaded; + } + + async function waitForPreventNavigateReady(page: Page) { + await expect + .poll(async () => + page.evaluate(async () => { + const count = document.querySelector('#pn-runcount'); + const before = count?.textContent; + window.dispatchEvent(new Event('beforeunload', { cancelable: true })); + await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined))); + return count?.textContent !== before; + }) + ) + .toBe(true); + } + test.describe('mpa', () => { test.use({ javaScriptEnabled: false }); tests(); @@ -61,8 +81,8 @@ test.describe('nav', () => { await expect(increment).toHaveText('Click me 1'); }); - test('should update history before async SPA route load completes', async ({ page }) => { - await page.route('**/products/jacket/q-data.json', async (route) => { + test('should update history and render immediately during SPA nav', async ({ page }) => { + await page.route('**/products/jacket/q-loader-*.json', async (route) => { await new Promise((resolve) => setTimeout(resolve, 600)); await route.continue(); }); @@ -74,15 +94,23 @@ test.describe('nav', () => { await expect(heading).toHaveText('Product: hat'); await link.click(); + // URL and params update immediately — the nav task does not wait for loaders. + // The heading uses params.id directly so it switches right away. await expect .poll(() => new URL(page.url()).pathname) .toBe('/qwikrouter-test/products/jacket/'); - await expect(heading).toHaveText('Product: hat'); await expect(heading).toHaveText('Product: jacket'); }); test.describe('scroll-restoration', () => { - test('should not refresh again on popstate after manual refresh', async ({ page }) => { + test('should not refresh again on popstate after manual refresh', async ({ + page, + browserName, + }) => { + test.slow( + browserName === 'firefox', + 'Firefox can be slow to resume SPA popstate handling after reload' + ); const documentLoadsKey = '__qwik_router_document_loads__'; await page.addInitScript((storageKey) => { const documentLoads = Number(sessionStorage.getItem(storageKey) || '0') + 1; @@ -102,20 +130,20 @@ test.describe('nav', () => { await expect(page.locator('h1')).toHaveText('Page Short'); await expect.poll(getDocumentLoads).toBe(1); - await page.reload(); + await reloadFromPage(page); await expect(page.locator('h1')).toHaveText('Page Short'); await expect.poll(getDocumentLoads).toBe(2); await page.goBack(); await expect(page).toHaveURL('/qwikrouter-test/scroll-restoration/page-long/'); - await expect(page.locator('h1')).toHaveText('Page Long'); + await expect(page.locator('h1')).toHaveText('Page Long', { timeout: 15000 }); await expect.poll(getDocumentLoads).toBe(2); await page.goForward(); await expect(page).toHaveURL('/qwikrouter-test/scroll-restoration/page-short/'); - await expect(page.locator('h1')).toHaveText('Page Short'); + await expect(page.locator('h1')).toHaveText('Page Short', { timeout: 15000 }); await expect.poll(getDocumentLoads).toBe(2); }); test('should scroll on hash change', async ({ page }) => { @@ -247,7 +275,11 @@ test.describe('nav', () => { } }); - test('preventNavigate', async ({ page }) => { + test('preventNavigate', async ({ page, browserName }) => { + test.slow( + browserName === 'firefox', + 'Firefox beforeunload and SPA prevent navigation can be slow under parallel e2e load' + ); await page.goto('/qwikrouter-test/prevent-navigate/'); const toggleDirty = page.locator('#pn-button'); const link = page.locator('#pn-link'); @@ -264,6 +296,8 @@ test.describe('nav', () => { await page.goBack(); await expect(count).toHaveText('0'); await expect(toggleDirty).toHaveText('is clean'); + await waitForPreventNavigateReady(page); + await expect(count).toHaveText('1'); await toggleDirty.click(); await expect(toggleDirty).toHaveText('is dirty'); // dirty browser nav @@ -273,7 +307,7 @@ test.describe('nav', () => { expect(dialog.type()).toBe('beforeunload'); await dialog.accept(); }); - await page.reload(); + await reloadFromPage(page); expect(didTrigger).toBe(true); await expect(count).toHaveText('0'); await toggleDirty.click(); @@ -303,6 +337,38 @@ test.describe('nav', () => { await mpaLink.click(); expect(didTrigger).toBe(true); }); + + test('second nav() call should win when two are fired back-to-back', async ({ page }) => { + await page.goto('/qwikrouter-test/double-nav/a/'); + await expect(page.locator('#double-nav-a')).toBeVisible(); + + const btn = page.locator('#double-nav-btn'); + await btn.click(); + + // Should end up at C (the second nav call), not B + await expect(page.locator('#double-nav-c')).toBeVisible(); + expect(new URL(page.url()).pathname).toBe('/qwikrouter-test/double-nav/c/'); + await expect(page.locator('#double-nav-c-data')).toHaveText('data-c'); + }); + + test('loader redirect via query string should SPA navigate to target', async ({ page }) => { + await page.goto('/qwikrouter-test/loader-redirect/'); + await expect(page.locator('#loader-redirect-home')).toBeVisible(); + + const btn = page.locator('#loader-redirect-btn'); + await btn.click(); + + // Should end up at the redirect target, not the source route. + // The redirect involves a network roundtrip (loader fetch sees a 302) plus + // two sequential SPA navigations (source → target). The DOM update may lag + // the history pushState, so wait directly for the target element. + await expect(page.locator('#loader-redirect-target')).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#loader-redirect-target-data')).toHaveText('target-data'); + expect(new URL(page.url()).searchParams.get('done')).toBe('true'); + // Old content must be gone — the redirect should replace, not append. + await expect(page.locator('#loader-redirect-home')).not.toBeVisible(); + await expect(page.locator('#loader-redirect-source')).not.toBeVisible(); + }); } function tests() { @@ -515,17 +581,6 @@ test.describe('nav', () => { await expect(page.locator('#redirected-result')).toHaveText('true'); }); - test('server plugin q-data redirect from /redirectme to /', async ({ baseURL }) => { - const res = await fetch(new URL('/qwikrouter-test/redirectme/q-data.json', baseURL), { - redirect: 'manual', - headers: { - Accept: 'application/json', - }, - }); - expect(res.status).toBe(301); - expect(res.headers.get('Location')).toBe('/qwikrouter-test/q-data.json'); - }); - test('should not execute task from removed layout, and should be executed only once for SPA', async ({ page, javaScriptEnabled, diff --git a/e2e/qwik-e2e/tests/starter-partytown.e2e.ts b/e2e/qwik-e2e/tests/starter-partytown.e2e.ts index a0ae77eac64..42646e5e0e3 100644 --- a/e2e/qwik-e2e/tests/starter-partytown.e2e.ts +++ b/e2e/qwik-e2e/tests/starter-partytown.e2e.ts @@ -11,7 +11,9 @@ test('rendered', async ({ page }) => { const state = page.locator('#state'); - await expect(state).toHaveText('finished'); + // Partytown spins up a web worker to run the simulated async work; under parallel + // test load the worker init can take well over the default 3s expect timeout. + await expect(state).toHaveText('finished', { timeout: 15000 }); }); test('update text', async ({ page }) => { diff --git a/packages/docs/src/routes/(blog)/blog/components/mdx/article-block.tsx b/packages/docs/src/routes/(blog)/blog/components/mdx/article-block.tsx index e0f4652548f..ef93dbf0386 100644 --- a/packages/docs/src/routes/(blog)/blog/components/mdx/article-block.tsx +++ b/packages/docs/src/routes/(blog)/blog/components/mdx/article-block.tsx @@ -9,7 +9,9 @@ export const ArticleBlock = component$(({ authorLink }) => { const location = useLocation(); const { frontmatter } = useDocumentHead(); const article = blogArticles.find(({ path }) => path === location.url.pathname); - const authorLinks = frontmatter.authors?.map((author: string) => authors[author].socialLink); + const authorLinks = (frontmatter.authors as string[] | undefined)?.map( + (author: string) => authors[author].socialLink + ); return (
diff --git a/packages/docs/src/routes/(blog)/blog/components/mdx/article-hero.tsx b/packages/docs/src/routes/(blog)/blog/components/mdx/article-hero.tsx index 76de6778e52..b2e08b5bfe0 100644 --- a/packages/docs/src/routes/(blog)/blog/components/mdx/article-hero.tsx +++ b/packages/docs/src/routes/(blog)/blog/components/mdx/article-hero.tsx @@ -2,7 +2,7 @@ import { component$ } from '@qwik.dev/core'; import { Link, useDocumentHead } from '@qwik.dev/router'; import { Image } from 'qwik-image'; -type Props = { image: string; authorLinks: string[] }; +type Props = { image: string; authorLinks: string[] | undefined }; export const ArticleHero = component$(({ image, authorLinks }) => { const { title, frontmatter } = useDocumentHead(); @@ -50,7 +50,7 @@ export const ArticleHero = component$(({ image, authorLinks }) => { {frontmatter.authors.length > 1 && 'Co-'}Written by{' '} {frontmatter.authors.map((author: string, index: number) => ( - + {author} {index < frontmatter.authors.length - 1 && diff --git a/packages/docs/src/routes/api/qwik-optimizer/api.json b/packages/docs/src/routes/api/qwik-optimizer/api.json index 2e42f35eae7..35b4421d860 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/api.json +++ b/packages/docs/src/routes/api/qwik-optimizer/api.json @@ -293,7 +293,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface QwikVitePluginApi \n```\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\ngetAssetsDir\n\n\n\n\n\n\n\n() => string \\| undefined\n\n\n\n\n\n
\n\ngetClientOutDir\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
\n\ngetClientPublicOutDir\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
\n\ngetManifest\n\n\n\n\n\n\n\n() => [QwikManifest](#qwikmanifest) \\| null\n\n\n\n\n\n
\n\ngetOptimizer\n\n\n\n\n\n\n\n() => Optimizer \\| null\n\n\n\n\n\n
\n\ngetOptions\n\n\n\n\n\n\n\n() => NormalizedQwikPluginOptions\n\n\n\n\n\n
\n\ngetRootDir\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
\n\nregisterBundleGraphAdder\n\n\n\n\n\n\n\n(adder: [BundleGraphAdder](#bundlegraphadder)) => void\n\n\n\n\n\n
", + "content": "```typescript\nexport interface QwikVitePluginApi \n```\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\ngetAssetsDir\n\n\n\n\n\n\n\n() => string \\| undefined\n\n\n\n\n\n
\n\ngetClientOutDir\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
\n\ngetClientPublicOutDir\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
\n\ngetManifest\n\n\n\n\n\n\n\n() => [QwikManifest](#qwikmanifest) \\| null\n\n\n\n\n\n
\n\ngetOptimizer\n\n\n\n\n\n\n\n() => Optimizer \\| null\n\n\n\n\n\n
\n\ngetOptions\n\n\n\n\n\n\n\n() => NormalizedQwikPluginOptions\n\n\n\n\n\n
\n\ngetRootDir\n\n\n\n\n\n\n\n() => string \\| null\n\n\n\n\n\n
\n\nonSegment\n\n\n\n\n\n\n\n(callback: SegmentCallback) => void\n\n\n\n\nRegister a callback that fires for each segment emitted during transform.\n\n\n
\n\nregisterBundleGraphAdder\n\n\n\n\n\n\n\n(adder: [BundleGraphAdder](#bundlegraphadder)) => void\n\n\n\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-vite/src/plugins/vite.ts", "mdFile": "qwik-vite.qwikvitepluginapi.md" }, diff --git a/packages/docs/src/routes/api/qwik-optimizer/index.mdx b/packages/docs/src/routes/api/qwik-optimizer/index.mdx index 99ea683aa79..2317a73053a 100644 --- a/packages/docs/src/routes/api/qwik-optimizer/index.mdx +++ b/packages/docs/src/routes/api/qwik-optimizer/index.mdx @@ -1319,6 +1319,21 @@ getRootDir +onSegment + + + + + +(callback: SegmentCallback) => void + + + +Register a callback that fires for each segment emitted during transform. + + + + registerBundleGraphAdder diff --git a/packages/docs/src/routes/api/qwik-router-ssg/api.json b/packages/docs/src/routes/api/qwik-router-ssg/api.json index 664eb48777c..93eb09e3de2 100644 --- a/packages/docs/src/routes/api/qwik-router-ssg/api.json +++ b/packages/docs/src/routes/api/qwik-router-ssg/api.json @@ -68,7 +68,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface SsgRenderOptions extends RenderOptions \n```\n**Extends:** RenderOptions\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nemit404Pages?\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Set to `false` if the static build should not write custom or default `404.html` pages. Defaults to `true`.\n\n\n
\n\nemitData?\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Set to `false` if the generated `q-data.json` data files should not be written to disk. Defaults to `true`.\n\n\n
\n\nemitHtml?\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Set to `false` if the generated static HTML files should not be written to disk. Setting to `false` is useful if the SSG should only write the `q-data.json` files to disk. Defaults to `true`.\n\n\n
\n\nexclude?\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should not be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. `exclude` always takes priority over `include`.\n\n\n
\n\ninclude?\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. If not provided, all routes will be static generated. `exclude` always takes priority over `include`.\n\n\n
\n\nlog?\n\n\n\n\n\n\n\n'debug' \\| 'quiet'\n\n\n\n\n_(Optional)_ Log level. `'quiet'` suppresses per-page output, `'debug'` enables verbose logging.\n\n\n
\n\nmaxTasksPerWorker?\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum number of tasks to be running at one time per worker. Defaults to `20`.\n\n\n
\n\nmaxWorkers?\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum number of workers to use while generating the static pages. Defaults to the number of CPUs available.\n\n\n
\n\norigin\n\n\n\n\n\n\n\nstring\n\n\n\n\nThe URL `origin`, which is a combination of the scheme (protocol) and hostname (domain). For example, `https://qwik.dev` has the protocol `https://` and domain `qwik.dev`. However, the `origin` does not include a `pathname`.\n\nThe `origin` is used to provide a full URL during Static Site Generation (SSG), and to simulate a complete URL rather than just the `pathname`. For example, in order to render a correct canonical tag URL or URLs within the `sitemap.xml`, the `origin` must be provided too.\n\nIf the site also starts with a pathname other than `/`, please use the `basePathname` option in the Qwik Router config options.\n\n\n
\n\noutDir\n\n\n\n\n\n\n\nstring\n\n\n\n\nFile system directory where the static files should be written.\n\n\n
\n\nsitemapOutFile?\n\n\n\n\n\n\n\nstring \\| null\n\n\n\n\n_(Optional)_ File system path to write the `sitemap.xml` to. Defaults to `sitemap.xml` and written to the root of the `outDir`. Setting to `null` will prevent the sitemap from being created.\n\n\n
", + "content": "```typescript\nexport interface SsgRenderOptions extends RenderOptions \n```\n**Extends:** RenderOptions\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nemit404Pages?\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Set to `false` if the static build should not write custom or default `404.html` pages. Defaults to `true`.\n\n\n
\n\nemitData?\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Set to `false` if the generated per-loader data files should not be written to disk. Defaults to `true`.\n\n\n
\n\nemitHtml?\n\n\n\n\n\n\n\nboolean\n\n\n\n\n_(Optional)_ Set to `false` if the generated static HTML files should not be written to disk. Setting to `false` is useful if the SSG should only write the per-loader data files to disk. Defaults to `true`.\n\n\n
\n\nexclude?\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should not be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. `exclude` always takes priority over `include`.\n\n\n
\n\ninclude?\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. If not provided, all routes will be static generated. `exclude` always takes priority over `include`.\n\n\n
\n\nlog?\n\n\n\n\n\n\n\n'debug' \\| 'quiet'\n\n\n\n\n_(Optional)_ Log level. `'quiet'` suppresses per-page output, `'debug'` enables verbose logging.\n\n\n
\n\nmaxTasksPerWorker?\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum number of tasks to be running at one time per worker. Defaults to `20`.\n\n\n
\n\nmaxWorkers?\n\n\n\n\n\n\n\nnumber\n\n\n\n\n_(Optional)_ Maximum number of workers to use while generating the static pages. Defaults to the number of CPUs available.\n\n\n
\n\norigin\n\n\n\n\n\n\n\nstring\n\n\n\n\nThe URL `origin`, which is a combination of the scheme (protocol) and hostname (domain). For example, `https://qwik.dev` has the protocol `https://` and domain `qwik.dev`. However, the `origin` does not include a `pathname`.\n\nThe `origin` is used to provide a full URL during Static Site Generation (SSG), and to simulate a complete URL rather than just the `pathname`. For example, in order to render a correct canonical tag URL or URLs within the `sitemap.xml`, the `origin` must be provided too.\n\nIf the site also starts with a pathname other than `/`, please use the `basePathname` option in the Qwik Router config options.\n\n\n
\n\noutDir\n\n\n\n\n\n\n\nstring\n\n\n\n\nFile system directory where the static files should be written.\n\n\n
\n\nsitemapOutFile?\n\n\n\n\n\n\n\nstring \\| null\n\n\n\n\n_(Optional)_ File system path to write the `sitemap.xml` to. Defaults to `sitemap.xml` and written to the root of the `outDir`. Setting to `null` will prevent the sitemap from being created.\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/ssg/types.ts", "mdFile": "router.ssgrenderoptions.md" }, diff --git a/packages/docs/src/routes/api/qwik-router-ssg/index.mdx b/packages/docs/src/routes/api/qwik-router-ssg/index.mdx index d336b6d20eb..230c2dede0f 100644 --- a/packages/docs/src/routes/api/qwik-router-ssg/index.mdx +++ b/packages/docs/src/routes/api/qwik-router-ssg/index.mdx @@ -273,7 +273,7 @@ boolean -_(Optional)_ Set to `false` if the generated `q-data.json` data files should not be written to disk. Defaults to `true`. +_(Optional)_ Set to `false` if the generated per-loader data files should not be written to disk. Defaults to `true`. @@ -288,7 +288,7 @@ boolean -_(Optional)_ Set to `false` if the generated static HTML files should not be written to disk. Setting to `false` is useful if the SSG should only write the `q-data.json` files to disk. Defaults to `true`. +_(Optional)_ Set to `false` if the generated static HTML files should not be written to disk. Setting to `false` is useful if the SSG should only write the per-loader data files to disk. Defaults to `true`. diff --git a/packages/docs/src/routes/api/qwik-router/api.json b/packages/docs/src/routes/api/qwik-router/api.json index ead2e925d37..9962daa060c 100644 --- a/packages/docs/src/routes/api/qwik-router/api.json +++ b/packages/docs/src/routes/api/qwik-router/api.json @@ -712,7 +712,7 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface QwikRouterEnvData \n```\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nev\n\n\n\n\n\n\n\nRequestEvent\n\n\n\n\n\n
\n\nloadedRoute\n\n\n\n\n\n\n\nLoadedRoute\n\n\n\n\n\n
\n\nparams\n\n\n\n\n\n\n\n[PathParams](#pathparams)\n\n\n\n\n\n
\n\nresponse\n\n\n\n\n\n\n\nEndpointResponse\n\n\n\n\n\n
\n\nrouteName\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
", + "content": "```typescript\nexport interface QwikRouterEnvData \n```\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\nev\n\n\n\n\n\n\n\nRequestEvent\n\n\n\n\n\n
\n\nloadedRoute\n\n\n\n\n\n\n\nLoadedRoute\n\n\n\n\n\n
\n\nloaderValues\n\n\n\n\n\n\n\nRecord<string, unknown>\n\n\n\n\n\n
\n\nparams\n\n\n\n\n\n\n\n[PathParams](#pathparams)\n\n\n\n\n\n
\n\nresponse\n\n\n\n\n\n\n\nEndpointResponse\n\n\n\n\n\n
\n\nrouteLoaderCtx\n\n\n\n\n\n\n\nRouteLoaderCtx\n\n\n\n\n\n
\n\nrouteName\n\n\n\n\n\n\n\nstring\n\n\n\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts", "mdFile": "router.qwikrouterenvdata.md" }, @@ -852,7 +852,7 @@ } ], "kind": "Variable", - "content": "```typescript\nrouteAction$: ActionConstructor\n```", + "content": "Define a route action that handles form submissions or programmatic invocations.\n\nActions run on the server when submitted from the client. The result is returned as an `ActionStore` with `.value` for success data and `.error` for errors (including validation errors from `zod$`/`valibot$` where `.fieldErrors` etc. are accessible directly on `.error`).\n\nBy default, after an action completes, ALL current route loaders are invalidated on the client and re-fetched as needed (so that the browser cache is correct). This can be controlled with:\n\n- `invalidate: [loader1, loader2]` — Only invalidate specific loaders. The client re-fetches them individually. Other loaders keep their current data. - `invalidate: []` — No loaders are invalidated. The action response only contains the action result. Use this when the action doesn't affect any loader data.\n\nThe `strictLoaders` Vite plugin option applies `invalidate: []` globally for all actions that don't specify an explicit `invalidate` option.\n\n\n```typescript\nrouteAction$: ActionConstructor\n```", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/server-functions.ts", "mdFile": "router.routeaction_.md" }, @@ -894,7 +894,7 @@ } ], "kind": "Interface", - "content": "A nested route trie structure. The root represents `/` and each level represents a URL segment.\n\nKeys starting with `_` are metadata; all other keys are child route segments.\n\n- Use `_W` as the key for a single dynamic segment (param); `_P` on that node names the param. - Use `_A` as the key for a rest/catch-all segment; `_P` on that node names the param. - For infix params like `pre[slug]post`, use `_W` with `_0` (prefix) and `_9` (suffix). - Use `_M` for an array of group (pathless layout) nodes, sorted by group name.\n\nWhen matching, exact segments are tried first (case-insensitive), then `_W` (with optional prefix/suffix), then `_A`. When no route matches, the closest `_E` (error.tsx) or `_4` (404.tsx) loader in the ancestor chain is used to render the error page.\n\n\n```typescript\nexport interface RouteData \n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n\\_0?\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ Prefix for infix params (e.g. \"pre\" for `pre[slug]post`) — only on `_W` nodes\n\n\n
\n\n\\_4?\n\n\n\n\n\n\n\nContentModuleLoader\n\n\n\n\n_(Optional)_ The not-found (404) module loader for this subtree\n\n\n
\n\n\\_9?\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ Suffix for infix params (e.g. \"post\" for `pre[slug]post`) — only on `_W` nodes\n\n\n
\n\n\\_B?\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_ The JS bundle names for this route (SSR only)\n\n\n
\n\n\\_E?\n\n\n\n\n\n\n\nContentModuleLoader\n\n\n\n\n_(Optional)_ The error page module loader for this subtree (error.tsx, takes precedence over \\_4)\n\n\n
\n\n\\_G?\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ Rewrite/goto target path. Matcher re-walks trie from root using this path's keys.\n\n\n
\n\n\\_I?\n\n\n\n\n\n\n\nContentModuleLoader \\| ModuleLoader\\[\\]\n\n\n\n\n_(Optional)_ This node's index/page loader. Single = normal (runtime prepends gathered \\_L). Array = override (layout stop / named layout — IS the complete chain).\n\n\n
\n\n\\_L?\n\n\n\n\n\n\n\nContentModuleLoader\n\n\n\n\n_(Optional)_ This node's layout loader (single). Runtime accumulates these during trie traversal.\n\n\n
\n\n\\_M?\n\n\n\n\n\n\n\n[RouteData](#routedata)\\[\\]\n\n\n\n\n_(Optional)_ Group (pathless layout) nodes merged into this level, sorted by group name\n\n\n
\n\n\\_N?\n\n\n\n\n\n\n\nMenuModuleLoader\n\n\n\n\n_(Optional)_ Menu loader for this subtree (from menu.md). Runtime uses nearest ancestor during traversal.\n\n\n
\n\n\\_P?\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ The parameter name when this node is reached via `_W` or `_A` from the parent\n\n\n
", + "content": "A nested route trie structure. The root represents `/` and each level represents a URL segment.\n\nKeys starting with `_` are metadata; all other keys are child route segments.\n\n- Use `_W` as the key for a single dynamic segment (param); `_P` on that node names the param. - Use `_A` as the key for a rest/catch-all segment; `_P` on that node names the param. - For infix params like `pre[slug]post`, use `_W` with `_0` (prefix) and `_9` (suffix). - Use `_M` for an array of group (pathless layout) nodes, sorted by group name.\n\nWhen matching, exact segments are tried first (case-insensitive), then `_W` (with optional prefix/suffix), then `_A`. When no route matches, the closest `_E` (error.tsx) or `_4` (404.tsx) loader in the ancestor chain is used to render the error page.\n\n\n```typescript\nexport interface RouteData \n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\n\\_0?\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ Prefix for infix params (e.g. \"pre\" for `pre[slug]post`) — only on `_W` nodes\n\n\n
\n\n\\_4?\n\n\n\n\n\n\n\nContentModuleLoader\n\n\n\n\n_(Optional)_ The not-found (404) module loader for this subtree\n\n\n
\n\n\\_9?\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ Suffix for infix params (e.g. \"post\" for `pre[slug]post`) — only on `_W` nodes\n\n\n
\n\n\\_B?\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_ The JS bundle names for this route (SSR only)\n\n\n
\n\n\\_E?\n\n\n\n\n\n\n\nContentModuleLoader\n\n\n\n\n_(Optional)_ The error page module loader for this subtree (error.tsx, takes precedence over \\_4)\n\n\n
\n\n\\_G?\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ Rewrite/goto target path. Matcher re-walks trie from root using this path's keys.\n\n\n
\n\n\\_I?\n\n\n\n\n\n\n\nContentModuleLoader \\| ModuleLoader\\[\\]\n\n\n\n\n_(Optional)_ This node's index/page loader. Single = normal (runtime prepends gathered \\_L). Array = override (layout stop / named layout — IS the complete chain).\n\n\n
\n\n\\_L?\n\n\n\n\n\n\n\nContentModuleLoader\n\n\n\n\n_(Optional)_ This node's layout loader (single). Runtime accumulates these during trie traversal.\n\n\n
\n\n\\_M?\n\n\n\n\n\n\n\n[RouteData](#routedata)\\[\\]\n\n\n\n\n_(Optional)_ Group (pathless layout) nodes merged into this level, sorted by group name\n\n\n
\n\n\\_N?\n\n\n\n\n\n\n\nMenuModuleLoader\n\n\n\n\n_(Optional)_ Menu loader for this subtree (from menu.md). Runtime uses nearest ancestor during traversal.\n\n\n
\n\n\\_P?\n\n\n\n\n\n\n\nstring\n\n\n\n\n_(Optional)_ The parameter name when this node is reached via `_W` or `_A` from the parent\n\n\n
\n\n\\_R?\n\n\n\n\n\n\n\nstring\\[\\]\n\n\n\n\n_(Optional)_ Array of routeLoader$ hashes for this node's loaders\n\n\n
", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts", "mdFile": "router.routedata.md" }, @@ -908,8 +908,8 @@ } ], "kind": "Variable", - "content": "```typescript\nrouteLoader$: LoaderConstructor\n```", - "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/server-functions.ts", + "content": "Define a route loader that fetches data before the route renders.\n\nRoute loaders run on the server during SSR and return data as an `AsyncSignal`. On the client, loaders automatically re-fetch when the route changes (SPA navigation). Each loader gets its own JSON endpoint (`q-loader-{id}.{hash}.json`), so only the loaders present on the target route are fetched.\n\n\\*\\*Important:\\*\\* Route loader data uses Qwik's custom serialization format, not standard JSON. This means the data supports features like circular references, Dates, and other non-JSON types, but it cannot be consumed by external clients expecting plain JSON.\n\n\\#\\# Options\n\n- `search: string[]` — Allowlist of URL search params the loader depends on. Only listed params are sent in the request and changes to other params are ignored. `search: []` means no search params are sent and only route path changes trigger a re-fetch. - `allowStale: false` — Clears the previous value when re-fetching, so components see a loading state instead of stale data during navigation. Useful when old data would be confusing. - `eTag` — Enable ETag-based caching. Can be `true` (auto-hash), a string, or a function. - `expires` / `poll` — Control client-side caching and polling behavior.\n\nThe `strictLoaders` Vite plugin option applies `search: []` globally for all loaders that don't specify an explicit `search` option.\n\n\n```typescript\nrouteLoader$: LoaderConstructor\n```", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/route-loaders.ts", "mdFile": "router.routeloader_.md" }, { diff --git a/packages/docs/src/routes/api/qwik-router/index.mdx b/packages/docs/src/routes/api/qwik-router/index.mdx index 69a09fd48a8..2d09090d734 100644 --- a/packages/docs/src/routes/api/qwik-router/index.mdx +++ b/packages/docs/src/routes/api/qwik-router/index.mdx @@ -1647,6 +1647,19 @@ LoadedRoute +loaderValues + + + + + +Record<string, unknown> + + + + + + params @@ -1673,6 +1686,19 @@ EndpointResponse +routeLoaderCtx + + + + + +RouteLoaderCtx + + + + + + routeName @@ -2030,6 +2056,16 @@ export type ResolvedDocumentHead<

routeAction$

+Define a route action that handles form submissions or programmatic invocations. + +Actions run on the server when submitted from the client. The result is returned as an `ActionStore` with `.value` for success data and `.error` for errors (including validation errors from `zod$`/`valibot$` where `.fieldErrors` etc. are accessible directly on `.error`). + +By default, after an action completes, ALL current route loaders are invalidated on the client and re-fetched as needed (so that the browser cache is correct). This can be controlled with: + +- `invalidate: [loader1, loader2]` — Only invalidate specific loaders. The client re-fetches them individually. Other loaders keep their current data. - `invalidate: []` — No loaders are invalidated. The action response only contains the action result. Use this when the action doesn't affect any loader data. + +The `strictLoaders` Vite plugin option applies `invalidate: []` globally for all actions that don't specify an explicit `invalidate` option. + ```typescript routeAction$: ActionConstructor; ``` @@ -2327,6 +2363,21 @@ string _(Optional)_ The parameter name when this node is reached via `_W` or `_A` from the parent + + + +\_R? + + + + + +string[] + + + +_(Optional)_ Array of routeLoader$ hashes for this node's loaders + @@ -2334,11 +2385,23 @@ _(Optional)_ The parameter name when this node is reached via `_W` or `_A` from

routeLoader$

+Define a route loader that fetches data before the route renders. + +Route loaders run on the server during SSR and return data as an `AsyncSignal`. On the client, loaders automatically re-fetch when the route changes (SPA navigation). Each loader gets its own JSON endpoint (`q-loader-{id}.{hash}.json`), so only the loaders present on the target route are fetched. + +\*\*Important:\*\* Route loader data uses Qwik's custom serialization format, not standard JSON. This means the data supports features like circular references, Dates, and other non-JSON types, but it cannot be consumed by external clients expecting plain JSON. + +\#\# Options + +- `search: string[]` — Allowlist of URL search params the loader depends on. Only listed params are sent in the request and changes to other params are ignored. `search: []` means no search params are sent and only route path changes trigger a re-fetch. - `allowStale: false` — Clears the previous value when re-fetching, so components see a loading state instead of stale data during navigation. Useful when old data would be confusing. - `eTag` — Enable ETag-based caching. Can be `true` (auto-hash), a string, or a function. - `expires` / `poll` — Control client-side caching and polling behavior. + +The `strictLoaders` Vite plugin option applies `search: []` globally for all loaders that don't specify an explicit `search` option. + ```typescript routeLoader$: LoaderConstructor; ``` -[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/server-functions.ts) +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/route-loaders.ts)

RouteLocation

diff --git a/packages/docs/src/routes/docs/(qwikrouter)/route-loader/index.mdx b/packages/docs/src/routes/docs/(qwikrouter)/route-loader/index.mdx index abab352c833..306f27ee0d3 100644 --- a/packages/docs/src/routes/docs/(qwikrouter)/route-loader/index.mdx +++ b/packages/docs/src/routes/docs/(qwikrouter)/route-loader/index.mdx @@ -15,7 +15,8 @@ contributors: - adamdbradley - gioboa - Varixo -updated_at: '2023-12-15T11:00:00Z' + - wmertens +updated_at: '2026-05-10T00:00:00Z' created_at: '2023-03-20T23:45:13Z' --- @@ -23,91 +24,94 @@ import { Note } from '~/components/note/note'; # `routeLoader$()` -Route Loaders load data in the server so it becomes available to use inside Qwik Components. They trigger when SPA/MPA navigation happens so they can be invoked by Qwik Components during rendering. +Route Loaders load data on the server so it becomes available inside Qwik Components during rendering. They load on every navigation that hits their route. -Please note that route loaders should be exported only from `layout.tsx` or `index.tsx` files. But they can be declared in any valid way ES modules allow. To reuse a route loader across multiple `layout.tsx` or `index.tsx` files, define it in a separate file, export it, then import it in `layout.tsx` or `index.tsx` files and [`re-export`](/docs/(qwikrouter)/re-exporting-loaders/index.mdx) it as a named export. - -If you want to manage common reusable `routeLoader$()`s it is essential that this function is re-exported from within 'layout.tsx' or 'index.tsx file of the existing route otherwise it will not run or throw exception. For more information [check this section](/docs/(qwikrouter)/re-exporting-loaders/index.mdx). - -```tsx /routeLoader$/ /useProductData/#a title="src/routes/product/[productId]/index.tsx" +```tsx /routeLoader$/ /useProductDetails/#a title="src/routes/product/[productId]/index.tsx" import { component$ } from '@qwik.dev/core'; import { routeLoader$ } from '@qwik.dev/router'; -export const useProductDetails = routeLoader$(async (requestEvent) => { - // This code runs only on the server, after every navigation - const res = await fetch(`https://.../products/${requestEvent.params.productId}`); - const product = await res.json(); - return product as Product; +export const useProductDetails = routeLoader$(async ({ params }) => { + // Runs only on the server, on this route, on every navigation + const res = await fetch(`https://.../products/${params.productId}`); + return (await res.json()) as Product; }); export default component$(() => { - // In order to access the `routeLoader$` data within a Qwik Component, you need to call the hook. - const signal = useProductDetails(); // Readonly> - return

Product name: {signal.value.product.name}

; + // AsyncSignal — read .value, watch .loading, check .error + const product = useProductDetails(); + return

Product name: {product.value.name}

; }); ``` -Route Loaders are perfect to fetch data from a database or an API. For example you can use them to fetch data from a CMS, a weather API, or a list of users from your database. +`routeLoader$()`s must be exported from a `layout.tsx` or `index.tsx` file. To share one across routes, define it in a separate file and [re-export](/docs/(qwikrouter)/re-exporting-loaders/index.mdx) it from each `index.tsx` / `layout.tsx` that needs it. -You should not use a `routeLoader$` to create a REST API, for that you’d be better off using an [Endpoint](/docs/endpoints/), which allows you to have tight control over the response headers and body. +Don't use `routeLoader$()` to build a REST API — use [Endpoints](/docs/endpoints/) for that. Loaders are for data your own Qwik Components consume. -## Multiple `routeLoader$`s +## How it works + +A few properties of route loaders: + +- **Server-only.** The loader function only runs on the server; the browser never sees the closure body. +- **Keyed by route, not by call site.** Two visits to the same URL share one logical "result" per "route + url params + search params" set, not one per `useProductDetails()` call. +- **Result is an `AsyncSignal`.** Components read `.value`, watch `.loading`, and check `.error`. Reading `.value` while the signal is in error state re-throws the error. +- **Each loader has its own JSON endpoint.** The client fetches `q-loader-{id}.{hash}.json` for navigation, polling, and post-action invalidation. Browser HTTP cache, ETags, and the `expires` option all apply. -Multiple `routeLoader$`s are allowed across the whole application, and they can be used in any Qwik Component. **You can even declare multiple `routeLoader$`s in the same file**. +This means a `routeLoader$()` should be a **pure function of the URL**: same `params` and `search` → same response. Mixing in any other input (action state, request bodies, one-shot tokens) makes the cached responses inconsistent — see [Loaders cannot read action state](#loaders-cannot-read-action-state). +## Reading the result in a component -```tsx title="src/routes/layout.tsx" +Hooks return an `AsyncSignal`. Branch on `.loading` and `.error` before reading `.value`: + +```tsx /product.loading/ /product.error/ /product.value/#a title="src/routes/product/[productId]/index.tsx" import { component$ } from '@qwik.dev/core'; import { routeLoader$ } from '@qwik.dev/router'; -import { Footer } from '../components/footer.tsx'; -export const useProductData = routeLoader$(async () => { - const res = await fetch('https://.../product'); - const product = (await res.json()) as Product; +export const useProductDetails = routeLoader$(async ({ params, fail }) => { + const product = await db.products.findById(params.productId); + if (!product) { + return fail(404, { message: 'Product not found' }); + } return product; }); export default component$(() => { - const signal = useProductData(); - return ( -
- -
-
- ); + const product = useProductDetails(); + + if (product.loading) { + return
Loading…
; + } + if (product.error) { + // ServerError — read .status and .data + return
{product.error.status}: {product.error.data.message}
; + } + return
Product name: {product.value.name}
; }); ``` -```tsx title="src/components/footer.tsx" -import { component$ } from '@qwik.dev/core'; +Don't read `signal.value` before checking `signal.error`. While the signal is in error state, accessing `.value` re-throws the error. -// Import the loader from the layout -import { useProductData } from '../routes/layout.tsx'; +### Signaling failure from a loader -export const Footer = component$(() => { - // Consume the loader data - const signal = useProductData(); - return
Product name: {signal.value.product.name}
; -}); -``` +Loaders signal failure two ways. Both put the signal into error state with `signal.error` set to a `ServerError` carrying `.status` (HTTP status) and `.data` (the payload): + +- **`return requestEvent.fail(status, data)`** — return a tagged failure. `signal.error.data` is `{ failed: true, ...data }`. +- **`throw requestEvent.error(status, data)`** — throw a `ServerError` directly. `signal.error.data` is `data`. + +## Multiple `routeLoader$`s -The above example shows using `useProductData()` in two different components across different files. This is intentional behavior. +Multiple loaders are allowed in any combination, including in the same file. They execute in parallel — declaring more does not slow rendering down by itself. ```tsx title="src/routes/admin/index.tsx" import { component$ } from '@qwik.dev/core'; import { routeLoader$ } from '@qwik.dev/router'; -export const useLoginStatus = routeLoader$(async ({ cookie }) => { - return { - isUserLoggedIn: checkCookie(cookie), - }; -}); +export const useLoginStatus = routeLoader$(async ({ cookie }) => ({ + isUserLoggedIn: checkCookie(cookie), +})); -export const useCurrentUser = routeLoader$(async ({ cookie }) => { - return { - user: currentUserFromCookie(cookie), - }; -}); +export const useCurrentUser = routeLoader$(async ({ cookie }) => ({ + user: currentUserFromCookie(cookie), +})); export default component$(() => { const loginStatus = useLoginStatus(); @@ -125,209 +129,214 @@ export default component$(() => { }); ``` -The above example shows two `routeLoader$`s being used in the same file. A generic `useLoginStatus` loader is used to check if the user is logged in, and a more specific `useCurrentUser` loader is used to retrieve the user data. +## When does the loader re-run? -## Loaders data serialization +A loader is re-fetched when: -By default, route loader data is not serialized and sent to the client. Instead, it is discarded after server-side rendering (SSR). If a component on the client requires the data, it will be refetched (lazy-loaded). +- the route path changes to one where the loader is declared (SPA navigation, MPA load) and it hasn't already run for this URL (and hasn't expired), +- a search param the loader subscribed to changes (see [Search params](#search-params-search)), +- it's invalidated by an action (see [`routeAction$`](/docs/(qwikrouter)/action/index.mdx) and the action's `invalidate` option), +- its data has expired and `poll` is enabled (see [Cache & freshness](#cache--freshness)). -You can customize this behavior using the `serializationStrategy` option in the `routeLoader$()`. This option accepts one of three values: -- `never` (Default): The data is never serialized. It is discarded after SSR and refetched on the client when needed. -- `always`: The data is always serialized and included in the initial HTML response. It is immediately available on the client without needing to be refetched. -- `auto`: Qwik automatically decides whether to serialize the data based on its size. If the data is small, it will be serialized. If it exceeds a certain threshold, it will be discarded and lazy-loaded on the client as needed. +A loader in `src/routes/product/[productId]/index.tsx` runs when the user navigates to `/product/1` or `/product/2`, but not when they navigate to `/about`. -```tsx title="src/routes/product/[user]/index.tsx" -import { routeLoader$ } from '@qwik.dev/router'; +Reading a loader hook from a component on a route where that loader is **not** declared returns `undefined` at runtime. The TypeScript signature does not reflect this — it's intentional, because route loaders that redirect can transiently be in this state during navigation. Treat it as an error on your part: if a component uses a loader, that loader's route must be the active route. -export const useProductDetails = routeLoader$(async (requestEvent) => { - const res = await fetch( - `https://.../products/${requestEvent.params.productId}` - ); - const product = await res.json(); - return product; -}, { - serializationStrategy: 'always', // 'never' | 'always' | 'auto' -}); +### Search params (`search`) + +The `search` option declares which URL search parameters (not the route params) the loader actually depends on: + +- **What gets sent.** The loader's JSON request URL only includes the listed params, sorted into a stable order. Other params are stripped, so unrelated query strings don't bust the cache. +- **When it re-fetches.** The signal only re-fetches when a listed search param (or the route path) changes. + +```tsx title="src/routes/products/index.tsx" +export const useFilteredProducts = routeLoader$( + async ({ url }) => { + const category = url.searchParams.get('category'); + const sort = url.searchParams.get('sort') ?? 'newest'; + return await db.products.list({ category, sort }); + }, + { + // Only `?category=` and `?sort=` matter; `?utm_source=` etc. are ignored. + search: ['category', 'sort'], + } +); ``` -### Handling loading state for lazy loaded route loader data +`search: []` means the loader doesn't depend on any search params — pure path-keyed cache, best reuse across visits. When `search` is omitted, all search params are forwarded and any change re-fetches (subject to `strictLoaders`, below). -When using lazy-loaded data, you can handle the loading state in your Qwik Components by checking if the data is available. If not, you can render a loading indicator or placeholder content. +The `strictLoaders` Vite plugin option (default `true` in v2) applies `search: []` to every loader that doesn't set it explicitly. This is the better default: each unique URL produces at most one cache entry per loader, so unrelated tracking params (`?utm_source=…`, `?fbclid=…`) don't generate fresh, never-reused cache entries for an identical response. To opt a loader into reacting to a query string, set `search` explicitly. Pass `strictLoaders: false` to `qwikRouter()` to restore "all params, all re-fetches" globally. -```tsx title="src/routes/product/[user]/index.tsx" -import { component$, useSignal } from '@qwik.dev/core'; -import { routeLoader$ } from '@qwik.dev/router'; +## Cache & freshness -export const useProductDetails = routeLoader$(async (requestEvent) => { - const res = await fetch( - `https://.../products/${requestEvent.params.productId}` - ); - const product = await res.json(); - return product; -}); +Loader responses come from per-loader JSON endpoints, so ordinary HTTP caching applies. The options below tune that behavior. -export default component$(() => { - const data = useProductDetails(); - const condition = useSignal(false); - return ( - <> - Product name: {data.value.product.name} - - {condition.value ? :
} - - ); -}); +### `expires` -// Will use lazy loaded data -export const Child = component$(() => { - const data = useProductDetails(); - return ( -
- {data.loading ? ( -

Loading product details...

- ) : ( -

Product description: {data.value.product.description}

- )} -
- ); -}); +Time in milliseconds before the data is considered stale. Sets `Cache-Control` on the JSON response and the AsyncSignal's expiry. When the data expires, the next read triggers a refetch and components update reactively when the new data arrives. + +```tsx title="src/routes/product/[productId]/index.tsx" +export const useProductDetails = routeLoader$( + async ({ params }) => fetchProduct(params.productId), + { expires: 60 * 1000 } // 1 minute +); ``` -### Global default serialization strategy -It is possible to set a global default serialization strategy for all `routeLoader$`s in your application. This can be done by configuring the `qwikRouter` Vite plugin. -Default value is `never`. +**`expires: 0` is special.** It means "never expires" — the data is only refetched when the application is rebuilt (the manifest hash busts the cache). For SSG, loaders with `expires: 0` are pre-generated as static `.json` files on disk, so route loaders work even on fully static sites. -```ts title="vite.config.ts" -import { qwikRouter } from '@qwik.dev/router/vite'; +### `poll` + +When `poll: true` and `expires` is set, the loader auto-refetches as soon as data expires — but only while components are reading the loader. Without `poll`, expired data is just *marked* stale; the refetch waits until something reads it. + +### `allowStale` + +By default expired data stays readable while a fresh value loads in the background, so the UI doesn't blank out. Set `allowStale: false` to clear the value on expiry instead, forcing readers to suspend until fresh data arrives. + +### `eTag` -export default () => { - return { - //... - plugins: [ - //... - qwikRouter({ - // Set the default serialization strategy for all route loaders - defaultLoadersSerializationStrategy: 'always', // 'never' | 'always' | 'auto' - }), - //... - ], +Adds an `ETag` header on the loader response and honors `If-None-Match`. Three modes: + +- **`eTag: true`** — auto-hash the serialized response. The loader still runs; only the response body is suppressed on a 304. +- **`eTag: 'some-string'`** — static eTag. The loader is skipped entirely on a 304. +- **`eTag: (requestEvent) => string | null`** — compute the eTag from request context (params, headers, cookies). The loader is skipped entirely on a 304. Return `null` to disable for this request. + +```tsx title="src/routes/subscription/index.tsx" +export const useSubscription = routeLoader$( + async (ev) => { + const user = ev.sharedMap.get('user')!; + return getSubscription(user.id); + }, + { + expires: 30 * 1000, + eTag: (ev) => { + const { id } = ev.sharedMap.get('user')!; + const day = new Date().getDate(); + return `${id}-${day}`; // valid for the rest of the day, per user + }, } -}; +); ``` -## RequestEvent +### `serializationStrategy` -Just like [middleware](/docs/middleware/) or [endpoint](/docs/endpoints/) `onRequest` and `onGet`, `routeLoader$`s have access to the [`RequestEvent`](/docs/middleware#requestevent) API which includes information about the current HTTP request. +By default the HTML response does **not** embed loader data. On hydration, the loader signal is empty until the browser fetches the JSON endpoint — meaning fast first paint, but a brief loading state for code that reads `.value` immediately. -This information comes in handy when the loader needs to conditionally return data based on the request, or it needs to override the response status, headers, or body manually. +| Value | Behavior | +| ---------- | ----------------------------------------------------------------------- | +| `'never'` | Default. Data is discarded after SSR; the client refetches lazily. | +| `'always'` | Embed data in the HTML. Available immediately, no refetch, bigger HTML. | +| `'auto'` | Embed when small, lazy-load when large. | -```tsx /requestEvent/ title="src/routes/product/[user]/index.tsx" -import { routeLoader$ } from '@qwik.dev/router'; +```tsx title="src/routes/product/[productId]/index.tsx" +export const useProductDetails = routeLoader$( + async ({ params }) => fetchProduct(params.productId), + { serializationStrategy: 'always' } +); +``` -export const useProductRecommendations = routeLoader$(async (requestEvent) => { - console.log('Request headers:', requestEvent.request.headers); - console.log('Request cookies:', requestEvent.cookie); - console.log('Request url:', requestEvent.url); - console.log('Request method:', requestEvent.method); - console.log('Request params:', requestEvent.params); +To set the default for the whole app, configure the Vite plugin: - // Use request details to fetch personalized data - const res = fetch(`https://.../recommendations?user=${requestEvent.params.user}`); - const recommendedProducts = (await res.json()) as Product[]; +```ts title="vite.config.ts" +import { qwikRouter } from '@qwik.dev/router/vite'; - return recommendedProducts; +export default () => ({ + plugins: [ + qwikRouter({ + defaultLoadersSerializationStrategy: 'always', + }), + ], }); ``` -## Access the `routeLoader$` data within another `routeLoader$` +When using lazy-loaded data, branch on `.loading` to show a placeholder until it arrives — see [Reading the result in a component](#reading-the-result-in-a-component). -You can access the data from one `routeLoader$` inside another `routeLoader$` using the `requestEvent.resolveValue` method. +## Accessing `RequestEvent` -```tsx /requestEvent/ title="src/routes/product/[productId]/index.tsx" -import { routeLoader$ } from '@qwik.dev/router'; +The loader function receives the same [`RequestEvent`](/docs/middleware#requestevent) as middleware. Use it to read params, headers, cookies, the URL, and so on. -export const useProductDetails = routeLoader$(async (requestEvent) => { - const res = await fetch(`https://.../products/${requestEvent.params.productId}`); - const product = await res.json(); - return product; +```tsx title="src/routes/recommendations/[user]/index.tsx" +export const useRecommendations = routeLoader$(async (requestEvent) => { + const userId = requestEvent.params.user; + const lang = requestEvent.request.headers.get('accept-language') ?? 'en'; + const res = await fetch( + `https://.../recommendations?user=${userId}&lang=${lang}` + ); + return (await res.json()) as Product[]; }); +``` + +## Composing loaders with `resolveValue` -export const useProductRecommendations = routeLoader$(async (requestEvent) => { - // Resolve the product details from the other loader - const product = await requestEvent.resolveValue(useProductDetails); +A loader can read another loader's result via `requestEvent.resolveValue(otherLoader)`: - // Use the product details to fetch personalized data - const res = fetch(`https://.../recommendations?product=${product.id}`); - const recommendedProducts = (await res.json()) as Product[]; +```tsx title="src/routes/user/[userId]/index.tsx" +export const useUser = routeLoader$(async ({ params }) => + fetchUser(params.userId) +); - return recommendedProducts; +export const useUserPosts = routeLoader$(async ({ resolveValue }) => { + const user = await resolveValue(useUser); + return fetchPostsFor(user.id); }); ``` -The same API can be used to access the data from a `routeAction$` or a `globalAction$`. - -## Failed values with `routeLoader$` +`resolveValue` runs the dependency loader once per request and caches the result, so reading the same loader from multiple places doesn't duplicate work. -`routeLoader$`s can use the `fail` method to return a failed value, which is a special value that indicates that the loader didn't succeed loading the expected data. +`resolveValue` only resolves other `routeLoader$`s — actions are **not** visible to it. See [Loaders cannot read action state](#loaders-cannot-read-action-state) below. -In addition, the `fail` function allows `routeLoader$` to override the HTTP status code, for example retuning 404. +## Loaders cannot read action state -This is useful when the loader needs to return an "error" value that is not `undefined`, but it also needs to indicate that the data failed to load. +`requestEvent.resolveValue(actionQrl)` always returns `undefined` inside a loader, regardless of how the loader was triggered (initial render, navigation, post-action invalidation, polling). -```tsx /requestEvent.fail/ /errorMessage/#a title="src/routes/product/[productId]/index.tsx" -import { component$ } from '@qwik.dev/core'; -import { routeLoader$ } from '@qwik.dev/router'; +The reason is the same as everywhere else on this page: **a loader's output must be a pure function of the URL.** Action results are transient — they only exist on the request that handled the submission, and the loader refetch that follows an action is a separate GET with no action context. Letting loaders see action state would silently produce different data on the inline-render path (no JS) than on the JSON refetch path (SPA), so the API does not expose action state to loaders at all. The same rule applies to anything else valid for one request — request bodies, one-shot tokens, `Set-Cookie` values written by middleware on this request. -export const useProductDetails = routeLoader$(async (requestEvent) => { - const product = await db.from('products').filter('id', 'eq', requestEvent.params.productId); - if (!product) { - // Return a failed value to indicate that product was not found - return requestEvent.fail(404, { - errorMessage: 'Product not found' - }); - } - return { - productName: product.name - }; -}); +Read action state directly from the action signal where you render. -export default component$(() => { - const product = useProductDetails(); +```tsx /resolveValue/#a title="src/routes/[id]/index.tsx" +export const useNameAction = routeAction$(async (data) => data); - if (product.value.errorMessage) { - // Render UI for failed value - return
{product.value.errorMessage}
; - } - return
Product name: {product.value.productName}
; +// ❌ `form` is always undefined, regardless of whether an action just ran. +export const useGreetingBroken = routeLoader$(async ({ resolveValue, params }) => { + const form = await resolveValue(useNameAction); + return { name: form?.name ?? params.id }; }); -``` - -## Handling Relative URLs in Loaders -In the server-side execution environment, it's crucial to convert relative URLs to absolute URLs for proper functionality. This can be achieved by prefixing the relative URL with the `origin` from the `useLocation()` function. - -```tsx /location.url.origin + relativeUrl/ title="Make Absolute URL" -import { component$ } from '@qwik.dev/core'; -import { useLocation } from '@qwik.dev/router'; +// ✅ Loader is a pure function of the URL; component blends in transient action state. +export const useGreeting = routeLoader$(({ params }) => ({ name: params.id })); export default component$(() => { - const location = useLocation(); - const relativeUrl = '/mock-data'; - const absoluteUrl = location.url.origin + relativeUrl; - + const greeting = useGreeting(); + const action = useNameAction(); + const name = action.value?.name ?? greeting.value.name; return ( -
-
Relative URL: {relativeUrl}
-
Absolute URL: {absoluteUrl}
-
+
+

Hello, {name}!

+ + +
); }); ``` -## Performance considerations +### What did people use action-state-in-loaders for? + +| Use case | Workaround | +| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Echo the just-submitted form value back to the page | Loader returns the URL-derived value; the component overlays `action.value` on top, e.g. `action.value?.name ?? loader.value.name`. | +| Recompute derived data from the submission | Put the inputs in the URL (search params, or `goto()` after the action) so the loader reads them like any other route input. | +| Refresh authoritative data after a mutation | The action writes to your store (DB, KV, etc.); the loader reads from that store. The action's `invalidate` list re-runs the loader and it picks up the new state — no action result needed. | +| Show success / error feedback | Read the action signal directly: `action.value`, `action.isRunning`, `action.value?.failed`. Ephemeral UI feedback doesn't belong in a loader. | + +## Options reference -Route Loaders are executed on the server, after every navigation. This means that they are executed every time a user navigates to a page in an SPA or MPA, and they are executed even if the user is navigating to the same page. +| Option | Default | Effect | +| ----------------------- | ------------ | ----------------------------------------------------------------------------------------------------- | +| `expires` | `0` (static) | Time-to-live in ms. Sets `Cache-Control` and the AsyncSignal expiry. `0` means never refetched. | +| `poll` | `false` | Auto-refetch when expired, but only while components are reading the loader. | +| `allowStale` | `true` | Show stale data while refetching. `false` blocks readers until fresh data arrives. | +| `serializationStrategy` | `'never'` | `'never'` / `'always'` / `'auto'` — embed loader data in the SSR HTML? | +| `eTag` | — | `true` (auto-hash), string, or `(ev) => string \| null`. Returns 304 on match. | +| `search` | `[]` * | Allowlist of search params the loader depends on. *(default `[]` when `strictLoaders` is enabled)* | +| `validation` | — | Array of [`DataValidator`](/docs/(qwikrouter)/validator/index.mdx)s applied before the loader runs. | -Loaders execute after the Qwik Middleware handlers (`onRequest`, `onGet`, `onPost`, etc), and before the Qwik Components are rendered. This allows the loaders to start fetching data as soon as possible, reducing latency. +The Vite plugin `qwikRouter()` exposes app-wide defaults via `defaultLoadersSerializationStrategy` and `strictLoaders`. diff --git a/packages/optimizer/core/src/fixtures/index.qwik.mjs b/packages/optimizer/core/src/fixtures/index.qwik.mjs index 8a3d2107246..f4dd2a98322 100644 --- a/packages/optimizer/core/src/fixtures/index.qwik.mjs +++ b/packages/optimizer/core/src/fixtures/index.qwik.mjs @@ -1,283 +1,325 @@ -import { jsx, Fragment, jsxs } from '@qwik.dev/core/jsx-runtime'; import { - component$, + componentQrl, + inlinedQrl, useErrorBoundary, useOnWindow, - $, + _captures, + _jsxSorted, Slot, - createContextId, - useContext, - implicit$FirstArg, - noSerialize, - useVisibleTask$, - useServerData, + isBrowser, useSignal, untrack, - sync$, + _qrlSync, + useVisibleTaskQrl, isDev, - withLocale, - event$, + _jsxSplit, + _getConstProps, + _getVarProps, + eventQrl, isServer, - useStyles$, + useStylesQrl, + useServerData, useStore, - isBrowser, useContextProvider, - useTask$, + useTaskQrl, getLocale, - jsx as jsx$1, + noSerialize, + useContext, SkipRender, + implicit$FirstArg, + withLocale, + _wrapProp, + _restProps, + _fnSignal, createElement, } from '@qwik.dev/core'; +import { Fragment } from '@qwik.dev/core/jsx-runtime'; +import * as qwikRouterConfig from '@qwik-router-config'; +import { p } from '@qwik.dev/core/preloader'; import { + l as loadRoute, g as getClientNavPath, s as shouldPreload, - p as preloadRouteBundles, - l as loadClientData, - i as isPromise, - a as isSamePath, - c as createLoaderSignal, - t as toUrl, - b as isSameOrigin, - d as loadRoute, - D as DEFAULT_LOADERS_SERIALIZATION_STRATEGY, - C as CLIENT_DATA_CACHE, - Q as Q_ROUTE, - e as clientNavigate, - f as QFN_KEY, - h as QACTION_KEY, - j as QDATA_KEY, -} from './chunks/routing.qwik.mjs'; -import * as qwikRouterConfig from '@qwik-router-config'; + i as isSamePath, + t as toPath, + c as createDocumentHead, + r as resolveHead, + a as isSameOrigin, + b as toUrl, +} from './chunks/head.qwik.mjs'; +import { + u as useNavigate, + a as useLocation, + b as useDocumentHead, + c as useQwikRouterEnv, + d as useAction, +} from './chunks/use-functions.qwik.mjs'; +export { + e as useContent, + f as useHttpStatus, + g as usePreventNavigate$, + h as usePreventNavigateQrl, +} from './chunks/use-functions.qwik.mjs'; import { + _deserialize, _getContextContainer, - SerializerSymbol, - _UNINITIALIZED, _hasStoreEffects, + _retryOnPromise, forceStoreEffects, + createAsyncQrl, _waitUntilRendered, _getContextHostElement, - _getContextEvent, _serialize, - _deserialize, - _resolveContextWithoutSequentialScope, } from '@qwik.dev/core/internal'; -import { _asyncRequestStore } from '@qwik.dev/router/middleware/request-handler'; +import { + Q as QACTION_KEY, + e as ensureRouteLoaderSignals, + s as setLoaderSignalValue, + C as ContentContext, + a as ContentInternalContext, + D as DocumentHeadContext, + H as HttpStatusContext, + R as RouteLocationContext, + b as RouteNavigateContext, + c as RouteStateContext, + d as RouteLoaderCtxContext, + f as RouteActionContext, + g as RoutePreventNavigateContext, + u as updateRouteLoaderCtx, + h as Q_ROUTE, + i as getRequestEvent, + j as QFN_KEY, + k as QDATA_KEY, +} from './chunks/route-loaders.qwik.mjs'; +export { r as routeLoader$, l as routeLoaderQrl } from './chunks/route-loaders.qwik.mjs'; import * as v from 'valibot'; import * as z from 'zod'; export { z } from 'zod'; import swRegister from '@qwik-router-sw-register'; import { renderToStream } from '@qwik.dev/core/server'; -import '@qwik.dev/core/preloader'; -import './chunks/types.qwik.mjs'; - -const ErrorBoundary = component$((props) => { - const store = useErrorBoundary(); - useOnWindow( - 'qerror', - $((e) => { - store.error = e.detail.error; - }) - ); - if (store.error && props.fallback$) { - return /* @__PURE__ */ jsx(Fragment, { children: props.fallback$(store.error) }); - } - return /* @__PURE__ */ jsx(Slot, {}); -}); - -const RouteStateContext = /* @__PURE__ */ createContextId('qc-s'); -const ContentContext = /* @__PURE__ */ createContextId('qc-c'); -const ContentInternalContext = /* @__PURE__ */ createContextId('qc-ic'); -const DocumentHeadContext = /* @__PURE__ */ createContextId('qc-h'); -const RouteLocationContext = /* @__PURE__ */ createContextId('qc-l'); -const RouteNavigateContext = /* @__PURE__ */ createContextId('qc-n'); -const RouteActionContext = /* @__PURE__ */ createContextId('qc-a'); -const RoutePreventNavigateContext = /* @__PURE__ */ createContextId('qc-p'); -const useContent = () => useContext(ContentContext); -const useDocumentHead = () => useContext(DocumentHeadContext); -const useLocation = () => useContext(RouteLocationContext); -const useNavigate = () => useContext(RouteNavigateContext); -const usePreventNavigateQrl = (fn) => { - if (!__EXPERIMENTAL__.preventNavigate) { - throw new Error( - 'usePreventNavigate$ is experimental and must be enabled with `experimental: ["preventNavigate"]` in the `qwikVite` plugin.' +const ErrorBoundary = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((props) => { + const store = useErrorBoundary(); + useOnWindow( + 'qerror', + /* @__PURE__ */ inlinedQrl( + (e) => { + const store2 = _captures[0]; + store2.error = e.detail.error; + }, + 'ErrorBoundary_component_useOnWindow_G0jFRpoNY0M', + [store] + ) ); - } - const registerPreventNav = useContext(RoutePreventNavigateContext); - useVisibleTask$(() => registerPreventNav(fn)); -}; -const usePreventNavigate$ = implicit$FirstArg(usePreventNavigateQrl); -const useAction = () => useContext(RouteActionContext); -const useQwikRouterEnv = () => noSerialize(useServerData('qwikrouter')); + if (store.error && props.fallback$) { + return /* @__PURE__ */ _jsxSorted( + Fragment, + null, + null, + props.fallback$(store.error), + 1, + 'bA_0' + ); + } + return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, 'bA_1'); + }, 'ErrorBoundary_component_pOa6vjtC7ik') +); -const Link = component$((props) => { - const nav = useNavigate(); - const loc = useLocation(); - const originalHref = props.href; - const anchorRef = useSignal(); - const { - onClick$, - prefetch: prefetchProp, - reload, - replaceState, - scroll, - ...linkProps - } = /* @__PURE__ */ (() => props)(); - const clientNavPath = untrack(getClientNavPath, { ...linkProps, reload }, loc); - linkProps.href = clientNavPath || originalHref; - const prefetchData = - (!!clientNavPath && prefetchProp !== false && prefetchProp !== 'js') || void 0; - const prefetch = - prefetchData || - (!!clientNavPath && prefetchProp !== false && untrack(shouldPreload, clientNavPath, loc)); - const handlePrefetch = prefetch - ? $((_, elm) => { - if (navigator.connection?.saveData) { - return; - } - if (elm && elm.href) { - const url = new URL(elm.href); - preloadRouteBundles(url.pathname); - if (elm.hasAttribute('data-prefetch')) { - loadClientData(url, { - preloadRouteBundles: false, - isPrefetch: true, - }); - } - } - }) - : void 0; - const preventDefault = clientNavPath - ? sync$((event) => { - if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) { - event.preventDefault(); - } - }) - : void 0; - const handleClientSideNavigation = clientNavPath - ? $((event, elm) => { - if (event.defaultPrevented) { - if (elm.href) { - elm.setAttribute('aria-pressed', 'true'); - nav(elm.href, { forceReload: reload, replaceState, scroll }).then(() => { - elm.removeAttribute('aria-pressed'); - }); - } - } - }) - : void 0; - const handlePreload = $((_, elm) => { - const url = new URL(elm.href); - preloadRouteBundles(url.pathname, 1); - }); - useVisibleTask$(({ track }) => { - track(() => loc.url.pathname); - const handler = linkProps.onQVisible$; - if (handler) { - const event = new CustomEvent('qvisible'); - if (Array.isArray(handler)) { - handler.flat(10).forEach((handler2) => handler2?.(event, anchorRef.value)); - } else { - handler?.(event, anchorRef.value); - } +async function prefetchRoute(url, prefetchData, probability = 0.8, manifestHash) { + if (!isBrowser) { + return; + } + try { + const loadedRoute = await loadRoute( + qwikRouterConfig.routes, + qwikRouterConfig.cacheModules, + url.pathname + ); + if (!loadedRoute) { + return; } - if (!isDev && anchorRef.value) { - handlePrefetch?.(void 0, anchorRef.value); + let routeName = loadedRoute.$routeName$; + routeName = routeName.endsWith('/') ? routeName : routeName + '/'; + if (routeName.length > 1 && routeName.startsWith('/')) { + routeName = routeName.slice(1); } - }); - return /* @__PURE__ */ jsx('a', { - ref: anchorRef, - ...{ 'q:link': !!clientNavPath }, - ...linkProps, - onClick$: [ - preventDefault, - handlePreload, - // needs to be in between preventDefault and onClick$ to ensure it starts asap. - onClick$, - handleClientSideNavigation, - ], - 'data-prefetch': prefetchData, - onMouseOver$: [linkProps.onMouseOver$, handlePrefetch], - onFocus$: [linkProps.onFocus$, handlePrefetch], - onQVisible$: [], - children: /* @__PURE__ */ jsx(Slot, {}), - }); -}); - -const resolveHead = (endpoint, routeLocation, contentModules, locale, defaults) => - withLocale(locale, () => { - const head = createDocumentHead(defaults); - const getData = (loaderOrAction) => { - const id = loaderOrAction.__id; - if (loaderOrAction.__brand === 'server_loader') { - if (!(id in endpoint.loaders)) { - throw new Error( - 'You can not get the returned data of a loader that has not been executed for this request.' - ); + p(routeName, probability); + if (!prefetchData || !manifestHash) { + return; + } + if (loadedRoute.$loaders$?.length && loadedRoute.$loaderPaths$) { + const basePath = qwikRouterConfig.basePathname ?? '/'; + for (const hash of loadedRoute.$loaders$) { + let loaderPath = loadedRoute.$loaderPaths$?.[hash]; + if (!loaderPath) { + continue; } - } - const data = endpoint.loaders[id]; - if (isPromise(data)) { - throw new Error('Loaders returning a promise can not be resolved for the head function.'); - } - return data; - }; - const fns = []; - for (const contentModule of contentModules) { - const contentModuleHead = contentModule?.head; - if (contentModuleHead) { - if (typeof contentModuleHead === 'function') { - fns.unshift(contentModuleHead); - } else if (typeof contentModuleHead === 'object') { - resolveDocumentHead(head, contentModuleHead); + if (basePath !== '/' && !loaderPath.startsWith(basePath)) { + loaderPath = basePath + loaderPath.slice(1); } + const pathBase = loaderPath.endsWith('/') ? loaderPath : loaderPath + '/'; + const fetchUrl = `${pathBase}q-loader-${hash}.${manifestHash}.json`; + fetch(fetchUrl) + .then((r) => r.blob()) + .catch(() => {}); } } - if (fns.length) { - const headProps = { - head, - withLocale: (fn) => fn(), - resolveValue: getData, - ...routeLocation, - }; - for (const fn of fns) { - resolveDocumentHead(head, fn(headProps)); - } - } - return head; - }); -const resolveDocumentHead = (resolvedHead, updatedHead) => { - if (typeof updatedHead.title === 'string') { - resolvedHead.title = updatedHead.title; - } - mergeArray(resolvedHead.meta, updatedHead.meta); - mergeArray(resolvedHead.links, updatedHead.links); - mergeArray(resolvedHead.styles, updatedHead.styles); - mergeArray(resolvedHead.scripts, updatedHead.scripts); - Object.assign(resolvedHead.frontmatter, updatedHead.frontmatter); + } catch {} +} + +const Link = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((props) => { + const nav = useNavigate(); + const loc = useLocation(); + const head = useDocumentHead(); + const originalHref = props.href; + const anchorRef = useSignal(); + const { + onClick$, + prefetch: prefetchProp, + reload, + replaceState, + scroll, + ...linkProps + } = /* @__PURE__ */ (() => props)(); + const clientNavPath = untrack( + getClientNavPath, + { + ...linkProps, + reload, + }, + loc + ); + linkProps.href = clientNavPath || originalHref; + const prefetchData = + (!!clientNavPath && prefetchProp !== false && prefetchProp !== 'js') || void 0; + const prefetch = + prefetchData || + (!!clientNavPath && prefetchProp !== false && untrack(shouldPreload, clientNavPath, loc)); + const handlePrefetch = prefetch + ? /* @__PURE__ */ inlinedQrl( + (_, elm) => { + const head2 = _captures[0]; + if (navigator.connection?.saveData) { + return; + } + if (elm && elm.href) { + const url = new URL(elm.href); + prefetchRoute(url, elm.hasAttribute('data-prefetch'), 0.8, head2.manifestHash); + } + }, + 'Link_component_handlePrefetch_AGvVXzXKbms', + [head] + ) + : void 0; + const preventDefault = clientNavPath + ? _qrlSync((event) => { + if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) { + event.preventDefault(); + } + }, 'event=>{if(!(event.metaKey||event.ctrlKey||event.shiftKey||event.altKey)){event.preventDefault();}}') + : void 0; + const handleClientSideNavigation = clientNavPath + ? /* @__PURE__ */ inlinedQrl( + (event, elm) => { + const nav2 = _captures[0], + reload2 = _captures[1], + replaceState2 = _captures[2], + scroll2 = _captures[3]; + if (event.defaultPrevented) { + if (elm.href) { + elm.setAttribute('aria-pressed', 'true'); + nav2(elm.href, { + forceReload: reload2, + replaceState: replaceState2, + scroll: scroll2, + }).then(() => { + elm.removeAttribute('aria-pressed'); + }); + } + } + }, + 'Link_component_handleClientSideNavigation_h3qenoGeI6M', + [nav, reload, replaceState, scroll] + ) + : void 0; + const handlePreload = /* @__PURE__ */ inlinedQrl((_, elm) => { + const url = new URL(elm.href); + prefetchRoute(url.pathname, false, 1); + }, 'Link_component_handlePreload_AAemwtuBjsE'); + useVisibleTaskQrl( + /* @__PURE__ */ inlinedQrl( + ({ track }) => { + const anchorRef2 = _captures[0], + handlePrefetch2 = _captures[1], + linkProps2 = _captures[2], + loc2 = _captures[3]; + track(() => loc2.url.pathname); + const handler = linkProps2.onQVisible$; + if (handler) { + const event = new CustomEvent('qvisible'); + if (Array.isArray(handler)) { + handler.flat(10).forEach((handler2) => handler2?.(event, anchorRef2.value)); + } else { + handler?.(event, anchorRef2.value); + } + } + if (!isDev && anchorRef2.value) { + handlePrefetch2?.(void 0, anchorRef2.value); + } + }, + 'Link_component_useVisibleTask_xKeuRmnoNSA', + [anchorRef, handlePrefetch, linkProps, loc] + ) + ); + return /* @__PURE__ */ _jsxSplit( + 'a', + { + ref: anchorRef, + 'q:link': !!clientNavPath, + ..._getVarProps(linkProps), + ..._getConstProps(linkProps), + 'q-e:click': [preventDefault, handlePreload, onClick$, handleClientSideNavigation], + 'data-prefetch': prefetchData, + 'q-e:mouseover': [linkProps.onMouseOver$, handlePrefetch], + 'q-e:focus': [linkProps.onFocus$, handlePrefetch], + }, + { + // We need to prevent the onQVisible$ from being called twice since it is handled in the visible task + 'q-e:qvisible': [], + }, + /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, 'jO_0'), + 0, + 'jO_1' + ); + }, 'Link_component_VPmar9tb3t4') +); + +const newScrollState = () => { + return { + x: 0, + y: 0, + w: 0, + h: 0, + }; }; -const mergeArray = (existingArr, newArr) => { - if (Array.isArray(newArr)) { - for (const newItem of newArr) { - if (typeof newItem.key === 'string') { - const existingIndex = existingArr.findIndex((i) => i.key === newItem.key); - if (existingIndex > -1) { - existingArr[existingIndex] = newItem; - continue; - } +const clientNavigate = (win, navType, fromURL, toURL, replaceState = false) => { + if (navType !== 'popstate') { + const samePath = isSamePath(fromURL, toURL); + const sameHash = fromURL.hash === toURL.hash; + if (!samePath || !sameHash) { + const newState = { + _qRouterScroll: newScrollState(), + }; + if (replaceState) { + win.history.replaceState(newState, '', toPath(toURL)); + } else { + win.history.pushState(newState, '', toPath(toURL)); } - existingArr.push(newItem); } } }; -const createDocumentHead = (defaults) => ({ - title: defaults?.title || '', - meta: [...(defaults?.meta || [])], - links: [...(defaults?.links || [])], - styles: [...(defaults?.styles || [])], - scripts: [...(defaults?.scripts || [])], - frontmatter: { ...defaults?.frontmatter }, -}); const transitionCss = '@layer qwik{@supports selector(html:active-view-transition-type(type)){html:active-view-transition-type(qwik-navigation){:root{view-transition-name:none}}}@supports not selector(html:active-view-transition-type(type)){:root{view-transition-name:none}}}'; @@ -288,15 +330,6 @@ function callRestoreScrollOnDocument() { document.__q_scroll_restore__ = void 0; } } -const restoreScroll = (type, toUrl, fromUrl, scroller, scrollState) => { - if (type === 'popstate' && scrollState) { - scroller.scrollTo(scrollState.x, scrollState.y); - } else if (type === 'link' || type === 'form') { - if (!hashScroll(toUrl, fromUrl)) { - scroller.scrollTo(0, 0); - } - } -}; const hashScroll = (toUrl, fromUrl) => { const elmId = toUrl.hash.slice(1); const elm = elmId && document.getElementById(elmId); @@ -308,6 +341,15 @@ const hashScroll = (toUrl, fromUrl) => { } return false; }; +const restoreScroll = (type, toUrl, fromUrl, scroller, scrollState) => { + if (type === 'popstate' && scrollState) { + scroller.scrollTo(scrollState.x, scrollState.y); + } else if (type === 'link' || type === 'form') { + if (!hashScroll(toUrl, fromUrl)) { + scroller.scrollTo(0, 0); + } + } +}; const currentScrollState = (elm) => { return { x: elm.scrollLeft, @@ -326,151 +368,209 @@ const saveScrollHistory = (scrollState) => { history.replaceState(state, ''); }; -const spaInit = event$((_, el) => { - if (!window._qRouterSPA && !window._qRouterInitPopstate) { - const currentPath = location.pathname + location.search; - const checkAndScroll = (scrollState) => { - if (scrollState) { - window.scrollTo(scrollState.x, scrollState.y); - } - }; - const currentScrollState = () => { - const elm = document.documentElement; - return { - x: elm.scrollLeft, - y: elm.scrollTop, - w: Math.max(elm.scrollWidth, elm.clientWidth), - h: Math.max(elm.scrollHeight, elm.clientHeight), - }; - }; - const saveScrollState = (scrollState) => { - const state = history.state || {}; - state._qRouterScroll = scrollState || currentScrollState(); - history.replaceState(state, ''); - }; - saveScrollState(); - window._qRouterInitPopstate = () => { - if (window._qRouterSPA) { - return; - } - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); - if (currentPath !== location.pathname + location.search) { - const getContainer = (el2) => - el2.closest('[q\\:container]:not([q\\:container=html]):not([q\\:container=text])'); - const container = getContainer(el); - const domContainer = container.qContainer; - const hostElement = domContainer.vNodeLocate(el); - const nav = domContainer?.resolveContext(hostElement, { - id: 'qc--n', - }); - if (nav) { - nav(location.href, { type: 'popstate' }); - } else { - location.reload(); +const spaInit = eventQrl( + /* @__PURE__ */ inlinedQrl((_, el) => { + if (!window._qRouterSPA && !window._qRouterInitPopstate) { + const currentPath = location.pathname + location.search; + const checkAndScroll = (scrollState) => { + if (scrollState) { + window.scrollTo(scrollState.x, scrollState.y); } - } else { - if (history.scrollRestoration === 'manual') { - const scrollState = history.state?._qRouterScroll; - checkAndScroll(scrollState); - window._qRouterScrollEnabled = true; + }; + const currentScrollState = () => { + const elm = document.documentElement; + return { + x: elm.scrollLeft, + y: elm.scrollTop, + w: Math.max(elm.scrollWidth, elm.clientWidth), + h: Math.max(elm.scrollHeight, elm.clientHeight), + }; + }; + const saveScrollState = (scrollState) => { + const state = history.state || {}; + state._qRouterScroll = scrollState || currentScrollState(); + history.replaceState(state, ''); + }; + saveScrollState(); + window._qRouterInitPopstate = () => { + if (window._qRouterSPA) { + return; } - } - }; - if (!window._qRouterHistoryPatch) { - window._qRouterHistoryPatch = true; - const pushState = history.pushState; - const replaceState = history.replaceState; - const prepareState = (state) => { - if (state === null || typeof state === 'undefined') { - state = {}; - } else if (state?.constructor !== Object) { - state = { _data: state }; - if (isDev) { - console.warn( - 'In a Qwik SPA context, `history.state` is used to store scroll state. Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. We need to be able to automatically attach the scroll state to your state object. A new state object has been created, your data has been moved to: `history.state._data`' - ); + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + if (currentPath !== location.pathname + location.search) { + const getContainer = (el2) => + el2.closest('[q\\:container]:not([q\\:container=html]):not([q\\:container=text])'); + const container = getContainer(el); + const domContainer = container.qContainer; + const hostElement = domContainer.vNodeLocate(el); + const nav = domContainer?.resolveContext(hostElement, { + id: 'qr-n', + }); + if (nav) { + nav(location.href, { + type: 'popstate', + }); + } else { + location.reload(); + } + } else { + if (history.scrollRestoration === 'manual') { + const scrollState = history.state?._qRouterScroll; + checkAndScroll(scrollState); + window._qRouterScrollEnabled = true; } } - state._qRouterScroll = state._qRouterScroll || currentScrollState(); - return state; - }; - history.pushState = (state, title, url) => { - state = prepareState(state); - return pushState.call(history, state, title, url); }; - history.replaceState = (state, title, url) => { - state = prepareState(state); - return replaceState.call(history, state, title, url); - }; - } - window._qRouterInitAnchors = (event) => { - if (window._qRouterSPA || event.defaultPrevented) { - return; - } - const target = event.target.closest('a[href]'); - if (target && !target.hasAttribute('preventdefault:click')) { - const href = target.getAttribute('href'); - const prev = new URL(location.href); - const dest = new URL(href, prev); - const sameOrigin = dest.origin === prev.origin; - const samePath = dest.pathname + dest.search === prev.pathname + prev.search; - if (sameOrigin && samePath) { - event.preventDefault(); - if (dest.href !== prev.href) { - history.pushState(null, '', dest); + if (!window._qRouterHistoryPatch) { + window._qRouterHistoryPatch = true; + const pushState = history.pushState; + const replaceState = history.replaceState; + const prepareState = (state) => { + if (state === null || typeof state === 'undefined') { + state = {}; + } else if (state?.constructor !== Object) { + state = { + _data: state, + }; + if (isDev) { + console.warn( + 'In a Qwik SPA context, `history.state` is used to store scroll state. Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. We need to be able to automatically attach the scroll state to your state object. A new state object has been created, your data has been moved to: `history.state._data`' + ); + } } - if (!dest.hash) { - if (dest.href.endsWith('#')) { - window.scrollTo(0, 0); - } else { - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); - saveScrollState({ ...currentScrollState(), x: 0, y: 0 }); - location.reload(); + state._qRouterScroll = state._qRouterScroll || currentScrollState(); + return state; + }; + history.pushState = (state, title, url) => { + state = prepareState(state); + return pushState.call(history, state, title, url); + }; + history.replaceState = (state, title, url) => { + state = prepareState(state); + return replaceState.call(history, state, title, url); + }; + } + window._qRouterInitAnchors = (event) => { + if (window._qRouterSPA || event.defaultPrevented) { + return; + } + const target = event.target.closest('a[href]'); + if (target && !target.hasAttribute('preventdefault:click')) { + const href = target.getAttribute('href'); + const prev = new URL(location.href); + const dest = new URL(href, prev); + const sameOrigin = dest.origin === prev.origin; + const samePath = dest.pathname + dest.search === prev.pathname + prev.search; + if (sameOrigin && samePath) { + event.preventDefault(); + if (dest.href !== prev.href) { + history.pushState(null, '', dest); } - } else { - const elmId = dest.hash.slice(1); - const elm = document.getElementById(elmId); - if (elm) { - elm.scrollIntoView(); + if (!dest.hash) { + if (dest.href.endsWith('#')) { + window.scrollTo(0, 0); + } else { + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + saveScrollState({ + ...currentScrollState(), + x: 0, + y: 0, + }); + location.reload(); + } + } else { + const elmId = dest.hash.slice(1); + const elm = document.getElementById(elmId); + if (elm) { + elm.scrollIntoView(); + } } } } - } + }; + window._qRouterInitVisibility = () => { + if ( + !window._qRouterSPA && + window._qRouterScrollEnabled && + document.visibilityState === 'hidden' + ) { + saveScrollState(); + } + }; + window._qRouterInitScroll = () => { + if (window._qRouterSPA || !window._qRouterScrollEnabled) { + return; + } + clearTimeout(window._qRouterScrollDebounce); + window._qRouterScrollDebounce = setTimeout(() => { + saveScrollState(); + window._qRouterScrollDebounce = void 0; + }, 200); + }; + window._qRouterScrollEnabled = true; + setTimeout(() => { + window.addEventListener('popstate', window._qRouterInitPopstate); + window.addEventListener('scroll', window._qRouterInitScroll, { + passive: true, + }); + document.addEventListener('click', window._qRouterInitAnchors); + if (!window.navigation) { + document.addEventListener('visibilitychange', window._qRouterInitVisibility, { + passive: true, + }); + } + }, 0); + } + }, 'spa_init_event_igI1pUsax0E') +); + +async function submitAction(action, routePath) { + const pathBase = routePath.endsWith('/') ? routePath : routePath + '/'; + const url = `${pathBase}?${QACTION_KEY}=${encodeURIComponent(action.id)}`; + const actionData = action.data; + let fetchOptions; + if (actionData instanceof FormData) { + fetchOptions = { + method: 'POST', + body: actionData, + headers: { + Accept: 'application/json', + }, }; - window._qRouterInitVisibility = () => { - if ( - !window._qRouterSPA && - window._qRouterScrollEnabled && - document.visibilityState === 'hidden' - ) { - saveScrollState(); - } + } else { + fetchOptions = { + method: 'POST', + body: JSON.stringify(actionData), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Accept: 'application/json', + }, }; - window._qRouterInitScroll = () => { - if (window._qRouterSPA || !window._qRouterScrollEnabled) { - return; - } - clearTimeout(window._qRouterScrollDebounce); - window._qRouterScrollDebounce = setTimeout(() => { - saveScrollState(); - window._qRouterScrollDebounce = void 0; - }, 200); + } + const response = await fetch(url, fetchOptions); + if (response.redirected) { + const redirectedURL = new URL(response.url); + if (redirectedURL.origin !== location.origin) { + location.href = redirectedURL.href; + return void 0; + } + location.href = redirectedURL.href; + return void 0; + } + if ((response.headers.get('content-type') || '').includes('json')) { + const text = await response.text(); + const data = _deserialize(text); + return { + status: response.status, + result: data?.result, + loaderHashes: data?.loaderHashes, + loaderValues: data?.loaders, }; - window._qRouterScrollEnabled = true; - setTimeout(() => { - window.addEventListener('popstate', window._qRouterInitPopstate); - window.addEventListener('scroll', window._qRouterInitScroll, { passive: true }); - document.addEventListener('click', window._qRouterInitAnchors); - if (!window.navigation) { - document.addEventListener('visibilitychange', window._qRouterInitVisibility, { - passive: true, - }); - } - }, 0); } -}); + return void 0; +} const startViewTransition = (params) => { if (!params.update) { @@ -483,7 +583,9 @@ const startViewTransition = (params) => { } catch { transition = document.startViewTransition(params.update); } - const event = new CustomEvent('qviewtransition', { detail: transition }); + const event = new CustomEvent('qviewtransition', { + detail: transition, + }); document.dispatchEvent(event); return transition; } else { @@ -494,14 +596,19 @@ const startViewTransition = (params) => { const QWIK_CITY_SCROLLER = '_qCityScroller'; const QWIK_ROUTER_SCROLLER = '_qRouterScroller'; const preventNav = {}; -const internalState = { navCount: 0 }; +const internalState = { + navCount: 0, + redirectCount: 0, +}; const useQwikRouter = (props) => { if (!isServer) { throw new Error( 'useQwikRouter can only run during SSR on the server. If you are seeing this, it means you are re-rendering the root of your application. Fix that or use the component around the root of your application.' ); } - useStyles$(transitionCss); + useStylesQrl( + /* @__PURE__ */ inlinedQrl(transitionCss, 'qwik_view_transition_css_inline_vNfd9raIMI0') + ); const env = useQwikRouterEnv(); if (!env?.params) { throw new Error( @@ -513,6 +620,7 @@ const useQwikRouter = (props) => { throw new Error(`Missing Qwik URL Env Data`); } const serverHead = useServerData('documentHead'); + const manifestHash = useServerData('containerAttributes')?.['q:manifest-hash']; if ( env.ev.originalUrl.pathname !== env.ev.url.pathname && !__EXPERIMENTAL__.enableRequestRewrite @@ -528,47 +636,44 @@ const useQwikRouter = (props) => { isNavigating: false, prevUrl: void 0, }; - const routeLocation = useStore(routeLocationTarget, { deep: false }); + const routeLocation = useStore(routeLocationTarget, { + deep: false, + }); const navResolver = {}; - const container = _getContextContainer(); - const getSerializationStrategy = (loaderId) => { - return ( - env.response.loadersSerializationStrategy.get(loaderId) || - DEFAULT_LOADERS_SERIALIZATION_STRATEGY - ); - }; - const loadersObject = {}; - const loaderState = {}; - for (const [key, value] of Object.entries(env.response.loaders)) { - loadersObject[key] = value; - loaderState[key] = createLoaderSignal( - loadersObject, - key, - url, - getSerializationStrategy(key), - container - ); - } - loadersObject[SerializerSymbol] = (obj) => { - const loadersSerializationObject = {}; - for (const [k, v] of Object.entries(obj)) { - loadersSerializationObject[k] = getSerializationStrategy(k) === 'always' ? v : _UNINITIALIZED; + env.routeLoaderCtx.manifestHash = manifestHash || ''; + env.routeLoaderCtx.pageUrl = url; + const routeLoaderCtx = useStore(env.routeLoaderCtx); + const loaderState = useStore( + {}, + { + deep: false, } - return loadersSerializationObject; - }; + ); + const contentModulesForInit = env.loadedRoute.$mods$; + const loaders = ensureRouteLoaderSignals(contentModulesForInit, loaderState, routeLoaderCtx); + for (const loader of loaders) { + const value = env.loaderValues[loader.__id]; + if (value !== void 0) { + setLoaderSignalValue(loaderState[loader.__id], value); + } + } const routeInternal = useSignal({ type: 'initial', dest: url, scroll: true, }); - const documentHead = useStore(() => createDocumentHead(serverHead)); + const documentHead = useStore(() => createDocumentHead(serverHead, manifestHash)); const content = useStore({ headings: void 0, menu: void 0, }); const contentInternal = useSignal(); + const httpStatus = useSignal({ + status: env.response.status, + message: env.loadedRoute.$notFound$ ? 'Not Found' : (env.response.statusMessage ?? ''), + }); const currentActionId = env.response.action; - const currentAction = currentActionId ? env.response.loaders[currentActionId] : void 0; + const currentAction = currentActionId ? env.response.actionResult : void 0; const actionState = useSignal( currentAction ? { @@ -581,7 +686,7 @@ const useQwikRouter = (props) => { } : void 0 ); - const registerPreventNav = $((fn$) => { + const registerPreventNav = /* @__PURE__ */ inlinedQrl((fn$) => { if (!isBrowser) { return; } @@ -610,171 +715,270 @@ const useQwikRouter = (props) => { } } }; - }); - const goto = $(async (path, opt) => { - const { - type = 'link', - forceReload = path === void 0, - // Hack for nav() because this API is already set. - replaceState = false, - scroll = true, - } = typeof opt === 'object' ? opt : { forceReload: opt }; - internalState.navCount++; - if (isBrowser && type === 'link' && routeInternal.value.type === 'initial') { - const url2 = new URL(window.location.href); - routeInternal.value.dest = url2; - routeLocation.url = url2; - } - const lastDest = routeInternal.value.dest; - const dest = - path === void 0 ? lastDest : typeof path === 'number' ? path : toUrl(path, routeLocation.url); - if ( - preventNav.$cbs$ && - (forceReload || - typeof dest === 'number' || - !isSamePath(dest, lastDest) || - !isSameOrigin(dest, lastDest)) - ) { - const ourNavId = internalState.navCount; - const prevents = await Promise.all([...preventNav.$cbs$.values()].map((cb) => cb(dest))); - if (ourNavId !== internalState.navCount || prevents.some(Boolean)) { - if (ourNavId === internalState.navCount && type === 'popstate') { - history.pushState(null, '', lastDest); - } - return; + }, 'useQwikRouter_registerPreventNav_69B0DK0eZJc'); + const getScroller = /* @__PURE__ */ inlinedQrl(() => { + let scroller = document.getElementById(QWIK_ROUTER_SCROLLER); + if (!scroller) { + scroller = document.getElementById(QWIK_CITY_SCROLLER); + if (scroller && isDev) { + console.warn( + `Please update your scroller ID to "${QWIK_ROUTER_SCROLLER}" as "${QWIK_CITY_SCROLLER}" is deprecated and will be removed in V3` + ); } } - if (typeof dest === 'number') { - if (isBrowser) { - history.go(dest); + return scroller ?? document.documentElement; + }, 'useQwikRouter_getScroller_0UhDFwlxeFQ'); + const goto = /* @__PURE__ */ inlinedQrl( + async (path, opt) => { + const actionState2 = _captures[0], + getScroller2 = _captures[1], + manifestHash2 = _captures[2], + navResolver2 = _captures[3], + routeInternal2 = _captures[4], + routeLocation2 = _captures[5]; + const { + type = 'link', + forceReload = path === void 0, + replaceState = false, + scroll = true, + } = typeof opt === 'object' + ? opt + : { + forceReload: opt, + }; + internalState.navCount++; + if (isBrowser && type === 'link' && routeInternal2.value.type === 'initial') { + const url2 = new URL(window.location.href); + routeInternal2.value.dest = url2; + routeLocation2.url = url2; } - return; - } - if (!isSameOrigin(dest, lastDest)) { - if (isBrowser) { - location.href = dest.href; + const lastDest = routeInternal2.value.dest; + const dest = + path === void 0 + ? lastDest + : typeof path === 'number' + ? path + : toUrl(path, routeLocation2.url); + if ( + preventNav.$cbs$ && + (forceReload || + typeof dest === 'number' || + !isSamePath(dest, lastDest) || + !isSameOrigin(dest, lastDest)) + ) { + const ourNavId = internalState.navCount; + const prevents = await Promise.all([...preventNav.$cbs$.values()].map((cb) => cb(dest))); + if (ourNavId !== internalState.navCount || prevents.some(Boolean)) { + if (ourNavId === internalState.navCount && type === 'popstate') { + history.pushState(null, '', lastDest); + } + return; + } } - return; - } - if (!forceReload && isSamePath(dest, lastDest)) { - if (isBrowser) { - if (type === 'link' && dest.href !== location.href) { - history.pushState(null, '', dest); + if (typeof dest === 'number') { + if (isBrowser) { + history.go(dest); } - let scroller = document.getElementById(QWIK_ROUTER_SCROLLER); - if (!scroller) { - scroller = document.getElementById(QWIK_CITY_SCROLLER); - if (scroller && isDev) { - console.warn( - `Please update your scroller ID to "${QWIK_ROUTER_SCROLLER}" as "${QWIK_CITY_SCROLLER}" is deprecated and will be removed in V3` - ); - } + return; + } + if (!isSameOrigin(dest, lastDest)) { + if (isBrowser) { + location.href = dest.href; } - if (!scroller) { - scroller = document.documentElement; + return; + } + if (!forceReload && isSamePath(dest, lastDest)) { + if (isBrowser) { + if (type === 'link' && dest.href !== location.href) { + history.pushState(null, '', dest); + } + const scroller = await getScroller2(); + restoreScroll(type, dest, new URL(location.href), scroller, getScrollHistory()); + if (type === 'popstate') { + window._qRouterScrollEnabled = true; + } } - restoreScroll(type, dest, new URL(location.href), scroller, getScrollHistory()); - if (type === 'popstate') { - window._qRouterScrollEnabled = true; + if (dest.href !== routeLocation2.url.href) { + const newUrl = new URL(dest.href); + routeInternal2.value.dest = newUrl; + routeLocation2.url = newUrl; } + return; } - return; - } - routeInternal.value = { - type, - dest, - forceReload, - replaceState, - scroll, - }; - if (isBrowser) { - loadClientData(dest); - loadRoute( - qwikRouterConfig.routes, - qwikRouterConfig.menus, - qwikRouterConfig.cacheModules, - dest.pathname - ); - } - actionState.value = void 0; - routeLocation.isNavigating = true; - return new Promise((resolve) => { - navResolver.r = resolve; - }); - }); + let historyUpdated = false; + if (isBrowser && type === 'link' && !forceReload) { + const scroller = await getScroller2(); + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + const scrollState = currentScrollState(scroller); + saveScrollHistory(scrollState); + clientNavigate(window, type, new URL(location.href), dest, replaceState); + historyUpdated = true; + } + routeInternal2.value = { + type, + dest, + forceReload, + replaceState, + scroll, + historyUpdated, + }; + if (isBrowser) { + prefetchRoute(dest, true, 0.8, manifestHash2); + } + actionState2.value = void 0; + routeLocation2.isNavigating = true; + return new Promise((resolve) => { + navResolver2.r = resolve; + }); + }, + 'useQwikRouter_goto_8j8Vrz2yUIM', + [actionState, getScroller, manifestHash, navResolver, routeInternal, routeLocation] + ); useContextProvider(ContentContext, content); useContextProvider(ContentInternalContext, contentInternal); useContextProvider(DocumentHeadContext, documentHead); + useContextProvider(HttpStatusContext, httpStatus); useContextProvider(RouteLocationContext, routeLocation); useContextProvider(RouteNavigateContext, goto); useContextProvider(RouteStateContext, loaderState); + useContextProvider(RouteLoaderCtxContext, routeLoaderCtx); + routeLoaderCtx.goto = goto; useContextProvider(RouteActionContext, actionState); useContextProvider(RoutePreventNavigateContext, registerPreventNav); - useTask$(({ track }) => { - async function run() { - const navigation = track(routeInternal); - const action = track(actionState); - const locale = getLocale(''); - const prevUrl = routeLocation.url; - const navType = action ? 'form' : navigation.type; - const replaceState = navigation.replaceState; - let trackUrl; - let clientPageData; - let loadedRoute = null; - let container2; - if (isServer) { - trackUrl = new URL(navigation.dest, routeLocation.url); - loadedRoute = env.loadedRoute; - clientPageData = env.response; - } else { - trackUrl = new URL(navigation.dest, location); - if (trackUrl.pathname.endsWith('/')) { - if (globalThis.__NO_TRAILING_SLASH__) { - trackUrl.pathname = trackUrl.pathname.slice(0, -1); - } - } else if (!globalThis.__NO_TRAILING_SLASH__) { - trackUrl.pathname += '/'; - } - let loadRoutePromise = loadRoute( - qwikRouterConfig.routes, - qwikRouterConfig.menus, - qwikRouterConfig.cacheModules, - trackUrl.pathname - ); - container2 = _getContextContainer(); - const pageData = (clientPageData = await loadClientData(trackUrl, { - action, - clearCache: true, - })); - if (!pageData) { - routeInternal.untrackedValue = { type: navType, dest: trackUrl }; - return; - } - const newHref = pageData.href; - const newURL = new URL(newHref, trackUrl); - if (!isSamePath(newURL, trackUrl)) { - if (!pageData.isRewrite) { - trackUrl = newURL; + useTaskQrl( + /* @__PURE__ */ inlinedQrl( + async ({ track }) => { + const actionState2 = _captures[0], + content2 = _captures[1], + contentInternal2 = _captures[2], + documentHead2 = _captures[3], + env2 = _captures[4], + getScroller2 = _captures[5], + goto2 = _captures[6], + httpStatus2 = _captures[7], + loaderState2 = _captures[8], + navResolver2 = _captures[9], + props2 = _captures[10], + routeInternal2 = _captures[11], + routeLoaderCtx2 = _captures[12], + routeLocation2 = _captures[13], + routeLocationTarget2 = _captures[14], + serverHead2 = _captures[15]; + const container = _getContextContainer(); + const navigation = track(routeInternal2); + const action = track(actionState2); + const locale = getLocale(''); + const prevUrl = routeLocation2.url; + const navType = action ? 'form' : navigation.type; + const replaceState = navigation.replaceState; + let trackUrl; + let endpointResponse; + let actionData; + let loadedRoute; + if (isServer) { + trackUrl = new URL(navigation.dest, routeLocation2.url); + loadedRoute = env2.loadedRoute; + endpointResponse = env2.response; + actionData = endpointResponse; + } else { + trackUrl = new URL(navigation.dest, location); + if (trackUrl.pathname.endsWith('/')) { + if (globalThis.__NO_TRAILING_SLASH__) { + trackUrl.pathname = trackUrl.pathname.slice(0, -1); + } + } else if (!globalThis.__NO_TRAILING_SLASH__) { + trackUrl.pathname += '/'; } - loadRoutePromise = loadRoute( + const loadRoutePromise = loadRoute( qwikRouterConfig.routes, - qwikRouterConfig.menus, qwikRouterConfig.cacheModules, - newURL.pathname - // Load the actual required path. + trackUrl.pathname ); + try { + loadedRoute = await loadRoutePromise; + } catch (e) { + console.error(e); + window.location.href = trackUrl.href; + return; + } + if (action) { + const result = await submitAction(action, trackUrl.pathname); + if (!result) { + routeInternal2.untrackedValue = { + type: navType, + dest: trackUrl, + }; + return; + } + actionData = { + status: result.status, + action: action.id, + actionResult: result.result, + }; + if (action.resolve) { + action.resolve({ + status: result.status, + result: result.result, + }); + } + if (result.loaderValues && Object.keys(result.loaderValues).length > 0) { + for (const [id, value] of Object.entries(result.loaderValues)) { + const signal = loaderState2[id]; + if (signal) { + setLoaderSignalValue(signal, value); + } + } + } + if (result.loaderHashes) { + for (const hash of result.loaderHashes) { + loaderState2[hash]?.invalidate(true); + } + } + } } - try { - loadedRoute = await loadRoutePromise; - } catch (e) { - console.error(e); - window.location.href = newHref; + const { $routeName$, $params$, $mods$, $menu$, $notFound$ } = loadedRoute; + const contentModules = $mods$; + updateRouteLoaderCtx(routeLoaderCtx2, loadedRoute.$loaderPaths$, trackUrl); + const routeLoaders = ensureRouteLoaderSignals( + contentModules, + loaderState2, + routeLoaderCtx2 + ); + const navCountBefore = internalState.navCount; + if (!isServer && routeLoaders.length > 0) { + await Promise.all(routeLoaders.map((loader) => loaderState2[loader.__id]?.promise())); + } + if (internalState.navCount !== navCountBefore) { + if (++internalState.redirectCount > 20) { + console.error('Too many redirects, aborting navigation'); + internalState.redirectCount = 0; + return; + } return; } - } - if (loadedRoute) { - const [routeName, params, mods, menu] = loadedRoute; - const contentModules = mods; + internalState.redirectCount = 0; + if ($notFound$) { + httpStatus2.value = { + status: 404, + message: 'Not Found', + }; + } else if (endpointResponse) { + httpStatus2.value = { + status: endpointResponse.status, + message: endpointResponse.statusMessage ?? 'OK', + }; + } else if (actionData) { + httpStatus2.value = { + status: actionData.status, + message: 'OK', + }; + } else { + httpStatus2.value = { + status: 200, + message: 'OK', + }; + } const pageModule = contentModules[contentModules.length - 1]; if (navigation.dest.search && !!isSamePath(trackUrl, prevUrl)) { trackUrl.search = navigation.dest.search; @@ -783,84 +987,61 @@ const useQwikRouter = (props) => { let shouldForceUrl = false; let shouldForceParams = false; if (!isSamePath(trackUrl, prevUrl)) { - if (_hasStoreEffects(routeLocation, 'prevUrl')) { + if (_hasStoreEffects(routeLocation2, 'prevUrl')) { shouldForcePrevUrl = true; } - routeLocationTarget.prevUrl = prevUrl; + routeLocationTarget2.prevUrl = prevUrl; } - if (routeLocationTarget.url !== trackUrl) { - if (_hasStoreEffects(routeLocation, 'url')) { + if (routeLocationTarget2.url !== trackUrl) { + if (_hasStoreEffects(routeLocation2, 'url')) { shouldForceUrl = true; } - routeLocationTarget.url = trackUrl; + routeLocationTarget2.url = trackUrl; } - if (routeLocationTarget.params !== params) { - if (_hasStoreEffects(routeLocation, 'params')) { + if (routeLocationTarget2.params !== $params$) { + if (_hasStoreEffects(routeLocation2, 'params')) { shouldForceParams = true; } - routeLocationTarget.params = params; + routeLocationTarget2.params = $params$; } - routeInternal.untrackedValue = { type: navType, dest: trackUrl }; - const resolvedHead = resolveHead( - clientPageData, - routeLocation, - contentModules, - locale, - serverHead + routeInternal2.untrackedValue = { + type: navType, + dest: trackUrl, + }; + const resolvedHead = await _retryOnPromise(() => + resolveHead(actionData, loaderState2, routeLocation2, contentModules, locale, serverHead2) ); - content.headings = pageModule.headings; - content.menu = menu; - contentInternal.untrackedValue = noSerialize(contentModules); - documentHead.links = resolvedHead.links; - documentHead.meta = resolvedHead.meta; - documentHead.styles = resolvedHead.styles; - documentHead.scripts = resolvedHead.scripts; - documentHead.title = resolvedHead.title; - documentHead.frontmatter = resolvedHead.frontmatter; + content2.headings = pageModule.headings; + content2.menu = $menu$; + contentInternal2.untrackedValue = noSerialize(contentModules); + documentHead2.links = resolvedHead.links; + documentHead2.meta = resolvedHead.meta; + documentHead2.styles = resolvedHead.styles; + documentHead2.scripts = resolvedHead.scripts; + documentHead2.title = resolvedHead.title; + documentHead2.frontmatter = resolvedHead.frontmatter; if (isBrowser) { let scrollState; if (navType === 'popstate') { scrollState = getScrollHistory(); } - const scroller = - document.getElementById(QWIK_ROUTER_SCROLLER) ?? document.documentElement; + const scroller = await getScroller2(); if ( (navigation.scroll && (!navigation.forceReload || !isSamePath(trackUrl, prevUrl)) && - (navType === 'link' || navType === 'popstate')) || // Action might have responded with a redirect. + (navType === 'link' || navType === 'popstate')) || (navType === 'form' && !isSamePath(trackUrl, prevUrl)) ) { document.__q_scroll_restore__ = () => restoreScroll(navType, trackUrl, prevUrl, scroller, scrollState); } - const loaders = clientPageData?.loaders; - if (loaders) { - const container3 = _getContextContainer(); - for (const [key, value] of Object.entries(loaders)) { - const signal = loaderState[key]; - const awaitedValue = await value; - loadersObject[key] = awaitedValue; - if (!signal) { - loaderState[key] = createLoaderSignal( - loadersObject, - key, - trackUrl, - DEFAULT_LOADERS_SERIALIZATION_STRATEGY, - container3 - ); - } else { - signal.invalidate(); - } - } - } - CLIENT_DATA_CACHE.clear(); if (!window._qRouterSPA) { window._qRouterSPA = true; history.scrollRestoration = 'manual'; window.addEventListener('popstate', () => { window._qRouterScrollEnabled = false; clearTimeout(window._qRouterScrollDebounce); - goto(location.href, { + goto2(location.href, { type: 'popstate', }); }); @@ -874,7 +1055,9 @@ const useQwikRouter = (props) => { if (state === null || typeof state === 'undefined') { state = {}; } else if (state?.constructor !== Object) { - state = { _data: state }; + state = { + _data: state, + }; if (isDev) { console.warn( 'In a Qwik SPA context, `history.state` is used to store scroll state. Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. We need to be able to automatically attach the scroll state to your state object. A new state object has been created, your data has been moved to: `history.state._data`' @@ -918,7 +1101,7 @@ const useQwikRouter = (props) => { location.reload(); return; } - goto(target.getAttribute('href')); + goto2(target.getAttribute('href')); } } }); @@ -941,7 +1124,9 @@ const useQwikRouter = (props) => { saveScrollHistory(scrollState2); } }, - { passive: true } + { + passive: true, + } ); document.removeEventListener('visibilitychange', window._qRouterInitVisibility); window._qRouterInitVisibility = void 0; @@ -959,7 +1144,9 @@ const useQwikRouter = (props) => { window._qRouterScrollDebounce = void 0; }, 200); }, - { passive: true } + { + passive: true, + } ); removeEventListener('scroll', window._qRouterInitScroll); window._qRouterInitScroll = void 0; @@ -968,16 +1155,26 @@ const useQwikRouter = (props) => { if (navType !== 'popstate') { window._qRouterScrollEnabled = false; clearTimeout(window._qRouterScrollDebounce); - const scrollState2 = currentScrollState(scroller); - saveScrollHistory(scrollState2); + if (!navigation.historyUpdated) { + const scrollState2 = currentScrollState(scroller); + saveScrollHistory(scrollState2); + } } const navigate = () => { - clientNavigate(window, navType, prevUrl, trackUrl, replaceState); - contentInternal.trigger(); - return _waitUntilRendered(container2); + if (navigation.historyUpdated) { + const currentPath = location.pathname + location.search + location.hash; + const nextPath = toPath(trackUrl); + if (currentPath !== nextPath) { + history.replaceState(history.state, '', nextPath); + } + } else { + clientNavigate(window, navType, prevUrl, trackUrl, replaceState); + } + contentInternal2.trigger(); + return _waitUntilRendered(container); }; const _waitNextPage = () => { - if (isServer || props?.viewTransition === false) { + if (isServer || props2?.viewTransition === false) { return navigate(); } else { const viewTransition = startViewTransition({ @@ -996,7 +1193,7 @@ const useQwikRouter = (props) => { throw err; }) .finally(() => { - container2.element.setAttribute?.(Q_ROUTE, routeName); + container.element.setAttribute?.(Q_ROUTE, $routeName$); const scrollState2 = currentScrollState(scroller); saveScrollHistory(scrollState2); window._qRouterScrollEnabled = true; @@ -1004,31 +1201,51 @@ const useQwikRouter = (props) => { callRestoreScrollOnDocument(); } if (shouldForcePrevUrl) { - forceStoreEffects(routeLocation, 'prevUrl'); + forceStoreEffects(routeLocation2, 'prevUrl'); } if (shouldForceUrl) { - forceStoreEffects(routeLocation, 'url'); + forceStoreEffects(routeLocation2, 'url'); } if (shouldForceParams) { - forceStoreEffects(routeLocation, 'params'); + forceStoreEffects(routeLocation2, 'params'); } - routeLocation.isNavigating = false; - navResolver.r?.(); + routeLocation2.isNavigating = false; + navResolver2.r?.(); }); } - } - } - if (isServer) { - return run(); - } else { - run(); + }, + 'useQwikRouter_useTask_XpalYii770E', + [ + actionState, + content, + contentInternal, + documentHead, + env, + getScroller, + goto, + httpStatus, + loaderState, + navResolver, + props, + routeInternal, + routeLoaderCtx, + routeLocation, + routeLocationTarget, + serverHead, + ] + ), + // We should only wait for navigation to complete on the server + { + deferUpdates: isServer, } - }); + ); }; -const QwikRouterProvider = component$((props) => { - useQwikRouter(props); - return /* @__PURE__ */ jsx(Slot, {}); -}); +const QwikRouterProvider = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((props) => { + useQwikRouter(props); + return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, '5y_0'); + }, 'QwikRouterProvider_component_6Kjfa79mqlY') +); const QwikCityProvider = QwikRouterProvider; const useQwikMockRouter = (props) => { const urlEnv = props.url ?? 'http://localhost/'; @@ -1040,234 +1257,153 @@ const useQwikMockRouter = (props) => { isNavigating: false, prevUrl: void 0, }, - { deep: false } + { + deep: false, + } ); const loadersData = props.loaders?.reduce((acc, { loader, data }) => { acc[loader.__id] = data; return acc; }, {}); - const loaderState = useStore(loadersData ?? {}, { deep: false }); + const loaderState = useStore( + {}, + { + deep: false, + } + ); + for (const [loaderId, data] of Object.entries(loadersData ?? {})) { + loaderState[loaderId] ||= createAsyncQrl( + /* @__PURE__ */ inlinedQrl( + async () => { + const data2 = _captures[0]; + return data2; + }, + 'useQwikMockRouter_createAsync_clbxpuXqpEU', + [data] + ), + { + initial: data, + } + ); + } const goto = props.goto ?? - $(async () => { + /* @__PURE__ */ inlinedQrl(async () => { console.warn('QwikRouterMockProvider: goto not provided'); - }); - const documentHead = useStore(createDocumentHead, { deep: false }); + }, 'useQwikMockRouter_goto_aViHFxQ1a3s'); + const documentHead = useStore(createDocumentHead, { + deep: false, + }); const content = useStore( { headings: void 0, menu: void 0, }, - { deep: false } + { + deep: false, + } ); const contentInternal = useSignal(); const actionState = useSignal(); - useContextProvider(ContentContext, content); - useContextProvider(ContentInternalContext, contentInternal); - useContextProvider(DocumentHeadContext, documentHead); - useContextProvider(RouteLocationContext, routeLocation); - useContextProvider(RouteNavigateContext, goto); - useContextProvider(RouteStateContext, loaderState); - useContextProvider(RouteActionContext, actionState); - const actionsMocks = props.actions?.reduce((acc, { action, handler }) => { - acc[action.__id] = handler; - return acc; - }, {}); - useTask$(async ({ track }) => { - const action = track(actionState); - if (!action?.resolve) { - return; - } - const mock = actionsMocks?.[action.id]; - if (mock) { - const actionResult = await mock(action.data); - action.resolve(actionResult); - } + const httpStatus = useSignal({ + status: 200, + message: '', }); -}; -const QwikRouterMockProvider = component$((props) => { - useQwikMockRouter(props); - return /* @__PURE__ */ jsx(Slot, {}); -}); -const QwikCityMockProvider = QwikRouterMockProvider; - -const RouterOutlet = component$(() => { - const serverData = useServerData('containerAttributes'); - if (!serverData) { - throw new Error('PrefetchServiceWorker component must be rendered on the server.'); - } - const internalContext = useContext(ContentInternalContext); - const contents = internalContext.value; - if (contents && contents.length > 0) { - const contentsLen = contents.length; - let cmp = null; - for (let i = contentsLen - 1; i >= 0; i--) { - if (contents[i].default) { - cmp = jsx$1(contents[i].default, { - children: cmp, - }); - } - } - return /* @__PURE__ */ jsxs(Fragment, { - children: [ - cmp, - !__EXPERIMENTAL__.noSPA && - /* @__PURE__ */ jsx('script', { - 'document:onQCInit$': spaInit, - 'document:onQInit$': sync$(() => { - ((w, h) => { - if (!w._qcs && h.scrollRestoration === 'manual') { - w._qcs = true; - const s = h.state?._qRouterScroll; - if (s) { - w.scrollTo(s.x, s.y); - } - document.dispatchEvent(new Event('qcinit')); - } - })(window, history); - }), - }), - ], - }); - } - return SkipRender; -}); - -const routeActionQrl = (actionQrl, ...rest) => { - const { id, validators } = getValidators(rest, actionQrl); - function action() { - const loc = useLocation(); - const currentAction = useAction(); - const initialState = { - actionPath: `?${QACTION_KEY}=${id}`, - submitted: false, - isRunning: false, - status: void 0, - value: void 0, - formData: void 0, - }; - const state = useStore(() => { - const value = currentAction.value; - if (value && value?.id === id) { - const data = value.data; - if (data instanceof FormData) { - initialState.formData = data; + useContextProvider(ContentContext, content); + useContextProvider(ContentInternalContext, contentInternal); + useContextProvider(DocumentHeadContext, documentHead); + useContextProvider(HttpStatusContext, httpStatus); + useContextProvider(RouteLocationContext, routeLocation); + useContextProvider(RouteNavigateContext, goto); + useContextProvider(RouteStateContext, loaderState); + useContextProvider(RouteActionContext, actionState); + const actionsMocks = props.actions?.reduce((acc, { action, handler }) => { + acc[action.__id] = handler; + return acc; + }, {}); + useTaskQrl( + /* @__PURE__ */ inlinedQrl( + async ({ track }) => { + const actionState2 = _captures[0], + actionsMocks2 = _captures[1]; + const action = track(actionState2); + if (!action?.resolve) { + return; } - if (value.output) { - const { status, result } = value.output; - initialState.status = status; - initialState.value = result; + const mock = actionsMocks2?.[action.id]; + if (mock) { + const actionResult = await mock(action.data); + action.resolve(actionResult); } - } - return initialState; - }); - const submit = $((input = {}) => { - if (isServer) { - throw new Error(`Actions can not be invoked within the server during SSR. -Action.run() can only be called on the browser, for example when a user clicks a button, or submits a form.`); - } - let data; - let form; - if (input instanceof SubmitEvent) { - form = input.target; - data = new FormData(form); - if ( - (input.submitter instanceof HTMLInputElement || - input.submitter instanceof HTMLButtonElement) && - input.submitter.name - ) { - if (input.submitter.name) { - data.append(input.submitter.name, input.submitter.value); - } + }, + 'useQwikMockRouter_useTask_tXTLR4tzCy0', + [actionState, actionsMocks] + ) + ); +}; +const QwikRouterMockProvider = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((props) => { + useQwikMockRouter(props); + return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, '5y_1'); + }, 'QwikRouterMockProvider_component_IN4dVpT0x74') +); +const QwikCityMockProvider = QwikRouterMockProvider; + +const RouterOutlet = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl(() => { + const serverData = useServerData('containerAttributes'); + if (!serverData) { + throw new Error('PrefetchServiceWorker component must be rendered on the server.'); + } + const internalContext = useContext(ContentInternalContext); + const contents = internalContext.value; + if (contents && contents.length > 0) { + const contentsLen = contents.length; + let cmp = null; + for (let i = contentsLen - 1; i >= 0; i--) { + if (contents[i].default) { + cmp = _jsxSorted(contents[i].default, null, null, cmp, 1, 'Fn_0'); } - } else { - data = input; } - return new Promise((resolve) => { - if (data instanceof FormData) { - state.formData = data; - } - state.submitted = true; - state.isRunning = true; - loc.isNavigating = true; - currentAction.value = { - data, - id, - resolve: noSerialize(resolve), - }; - }).then(({ result, status }) => { - state.isRunning = false; - state.status = status; - state.value = result; - if (form) { - if (form.getAttribute('data-spa-reset') === 'true') { - form.reset(); - } - const detail = { status, value: result }; - form.dispatchEvent( - new CustomEvent('submitcompleted', { - bubbles: false, - cancelable: false, - composed: false, - detail, - }) - ); - } - return { - status, - value: result, - }; - }); - }); - initialState.submit = submit; - return state; - } - action.__brand = 'server_action'; - action.__validators = validators; - action.__qrl = actionQrl; - action.__id = id; - Object.freeze(action); - return action; -}; -const globalActionQrl = (actionQrl, ...rest) => { - const action = routeActionQrl(actionQrl, ...rest); - if (isServer) { - if (typeof globalThis._qwikActionsMap === 'undefined') { - globalThis._qwikActionsMap = /* @__PURE__ */ new Map(); + return /* @__PURE__ */ _jsxSorted( + Fragment, + null, + null, + [ + cmp, + !__EXPERIMENTAL__.noSPA && + /* @__PURE__ */ _jsxSorted( + 'script', + { + 'q-d:qinit': _qrlSync(() => { + ((w, h) => { + if (!w._qcs && h.scrollRestoration === 'manual') { + w._qcs = true; + const s = h.state?._qRouterScroll; + if (s) { + w.scrollTo(s.x, s.y); + } + document.dispatchEvent(new Event('qcinit')); + } + })(window, history); + }, '()=>{((w,h)=>{if(!w._qcs&&h.scrollRestoration==="manual"){w._qcs=!0;const s=h.state?._qRouterScroll;if(s){w.scrollTo(s.x,s.y);}document.dispatchEvent(new Event("qcinit"));}})(window,history);}'), + }, + { + 'q-d:qcinit': spaInit, + }, + null, + 2, + 'Fn_1' + ), + ], + 1, + 'Fn_2' + ); } - globalThis._qwikActionsMap.set(action.__id, action); - } - return action; -}; -const routeAction$ = /* @__PURE__ */ implicit$FirstArg(routeActionQrl); -const globalAction$ = /* @__PURE__ */ implicit$FirstArg(globalActionQrl); -const getValue = (obj) => obj.value; -const routeLoaderQrl = (loaderQrl, ...rest) => { - const { id, validators, serializationStrategy } = getValidators(rest, loaderQrl); - function loader() { - const state = _resolveContextWithoutSequentialScope(RouteStateContext); - if (!(id in state)) { - throw new Error(`routeLoader$ "${loaderQrl.getSymbol()}" was invoked in a route where it was not declared. - This is because the routeLoader$ was not exported in a 'layout.tsx' or 'index.tsx' file of the existing route. - For more information check: https://qwik.dev/docs/route-loader/ + return SkipRender; + }, 'RouterOutlet_component_QwONcWD5gIg') +); - If your are managing reusable logic or a library it is essential that this function is re-exported from within 'layout.tsx' or 'index.tsx file of the existing route otherwise it will not run or throw exception. - For more information check: https://qwik.dev/docs/re-exporting-loaders/`); - } - const loaderData = state[id]; - untrack(getValue, loaderData); - return loaderData; - } - loader.__brand = 'server_loader'; - loader.__qrl = loaderQrl; - loader.__validators = validators; - loader.__id = id; - loader.__serializationStrategy = serializationStrategy; - loader.__expires = -1; - Object.freeze(loader); - return loader; -}; -const routeLoader$ = /* @__PURE__ */ implicit$FirstArg(routeLoaderQrl); const validatorQrl = (validator) => { if (isServer) { return { @@ -1400,113 +1536,9 @@ const zodQrl = (qrl) => { return void 0; }; const zod$ = /* @__PURE__ */ implicit$FirstArg(zodQrl); -const serverQrl = (qrl, options) => { - if (isServer) { - const captured = qrl.getCaptured(); - if (captured && captured.length > 0 && !_getContextHostElement()) { - throw new Error('For security reasons, we cannot serialize QRLs that capture lexical scope.'); - } - } - const method = options?.method?.toUpperCase?.() || 'POST'; - const headers = options?.headers || {}; - const origin = options?.origin || ''; - const fetchOptions = options?.fetchOptions || {}; - return $(async function (...args) { - const abortSignal = args.length > 0 && args[0] instanceof AbortSignal ? args.shift() : void 0; - if (isServer) { - let requestEvent = _asyncRequestStore?.getStore(); - if (!requestEvent) { - const contexts = [useQwikRouterEnv()?.ev, this, _getContextEvent()]; - requestEvent = contexts.find( - (v2) => - v2 && - Object.prototype.hasOwnProperty.call(v2, 'sharedMap') && - Object.prototype.hasOwnProperty.call(v2, 'cookie') - ); - } - return qrl.apply(requestEvent, args); - } else { - let filteredArgs = args.map((arg) => { - if (arg instanceof SubmitEvent && arg.target instanceof HTMLFormElement) { - return new FormData(arg.target); - } else if (arg instanceof Event) { - return null; - } else if (arg instanceof Node) { - return null; - } - return arg; - }); - if (!filteredArgs.length) { - filteredArgs = void 0; - } - const qrlHash = qrl.getHash(); - let query = ''; - const config = { - ...fetchOptions, - method, - headers: { - ...headers, - 'Content-Type': 'application/qwik-json', - Accept: 'application/json, application/qwik-json, text/qwik-json-stream, text/plain', - // Required so we don't call accidentally - 'X-QRL': qrlHash, - }, - signal: abortSignal, - }; - const captured = qrl.getCaptured(); - let toSend = [filteredArgs]; - if (captured?.length) { - toSend = [filteredArgs, ...captured]; - } else { - toSend = filteredArgs ? [filteredArgs] : []; - } - const body = await _serialize(toSend); - if (method === 'GET') { - query += `&${QDATA_KEY}=${encodeURIComponent(body)}`; - } else { - config.body = body; - } - const res = await fetch(`${origin}?${QFN_KEY}=${qrlHash}${query}`, config); - const contentType = res.headers.get('Content-Type'); - if (res.ok && contentType === 'text/qwik-json-stream' && res.body) { - return (async function* () { - try { - for await (const result of deserializeStream(res.body, abortSignal)) { - yield result; - } - } finally { - if (!abortSignal?.aborted) { - await res.body.cancel(); - } - } - })(); - } else if (contentType === 'application/qwik-json') { - const str = await res.text(); - const obj = _deserialize(str); - if (res.status >= 400) { - throw obj; - } - return obj; - } else if (contentType === 'application/json') { - const obj = await res.json(); - if (res.status >= 400) { - throw obj; - } - return obj; - } else if (contentType === 'text/plain' || contentType === 'text/html') { - const str = await res.text(); - if (res.status >= 400) { - throw str; - } - return str; - } - } - }); -}; -const server$ = /* @__PURE__ */ implicit$FirstArg(serverQrl); const getValidators = (rest, qrl) => { let id; - let serializationStrategy = DEFAULT_LOADERS_SERIALIZATION_STRATEGY; + let invalidate; const validators = []; if (rest.length === 1) { const options = rest[0]; @@ -1515,12 +1547,12 @@ const getValidators = (rest, qrl) => { validators.push(options); } else { id = options.id; - if (options.serializationStrategy) { - serializationStrategy = options.serializationStrategy; - } if (options.validation) { validators.push(...options.validation); } + if (options.invalidate) { + invalidate = options.invalidate.map((loader) => loader.__id); + } } } } else if (rest.length > 1) { @@ -1539,9 +1571,129 @@ const getValidators = (rest, qrl) => { return { validators: validators.reverse(), id, - serializationStrategy, + invalidate, }; }; +const routeActionQrl = (actionQrl, ...rest) => { + const { id, validators, invalidate } = getValidators(rest, actionQrl); + function action() { + const loc = useLocation(); + const currentAction = useAction(); + const initialState = { + actionPath: `?${QACTION_KEY}=${id}`, + submitted: false, + isRunning: false, + status: void 0, + value: void 0, + formData: void 0, + }; + const state = useStore(() => { + const value = currentAction.value; + if (value && value?.id === id) { + const data = value.data; + if (data instanceof FormData) { + initialState.formData = data; + } + if (value.output) { + const { status, result } = value.output; + initialState.status = status; + initialState.value = result; + } + } + return initialState; + }); + const submit = /* @__PURE__ */ inlinedQrl( + (input = {}) => { + const currentAction2 = _captures[0], + id2 = _captures[1], + loc2 = _captures[2], + state2 = _captures[3]; + if (isServer) { + throw new Error(`Actions can not be invoked within the server during SSR. +Action.run() can only be called on the browser, for example when a user clicks a button, or submits a form.`); + } + let data; + let form; + if (input instanceof SubmitEvent) { + form = input.target; + data = new FormData(form); + if ( + (input.submitter instanceof HTMLInputElement || + input.submitter instanceof HTMLButtonElement) && + input.submitter.name + ) { + if (input.submitter.name) { + data.append(input.submitter.name, input.submitter.value); + } + } + } else { + data = input; + } + return new Promise((resolve) => { + if (data instanceof FormData) { + state2.formData = data; + } + state2.submitted = true; + state2.isRunning = true; + loc2.isNavigating = true; + currentAction2.value = { + data, + id: id2, + resolve: noSerialize(resolve), + }; + }).then((_rawProps) => { + state2.isRunning = false; + state2.status = _rawProps.status; + state2.value = _rawProps.result; + if (form) { + if (form.getAttribute('data-spa-reset') === 'true') { + form.reset(); + } + const detail = { + status: _rawProps.status, + value: _rawProps.result, + }; + form.dispatchEvent( + new CustomEvent('submitcompleted', { + bubbles: false, + cancelable: false, + composed: false, + detail, + }) + ); + } + return { + status: _rawProps.status, + value: _rawProps.result, + }; + }); + }, + 'routeActionQrl_action_submit_YuS5bpdQ360', + [currentAction, id, loc, state] + ); + initialState.submit = submit; + return state; + } + action.__brand = 'server_action'; + action.__validators = validators; + action.__qrl = actionQrl; + action.__id = id; + action.__invalidate = invalidate; + Object.freeze(action); + return action; +}; +const globalActionQrl = (actionQrl, ...rest) => { + const action = routeActionQrl(actionQrl, ...rest); + if (isServer) { + if (typeof globalThis._qwikActionsMap === 'undefined') { + globalThis._qwikActionsMap = /* @__PURE__ */ new Map(); + } + globalThis._qwikActionsMap.set(action.__id, action); + } + return action; +}; +const routeAction$ = /* @__PURE__ */ implicit$FirstArg(routeActionQrl); +const globalAction$ = /* @__PURE__ */ implicit$FirstArg(globalActionQrl); const deserializeStream = async function* (stream, abortSignal) { const reader = stream.getReader(); try { @@ -1552,7 +1704,9 @@ const deserializeStream = async function* (stream, abortSignal) { if (result.done) { break; } - buffer += decoder.decode(result.value, { stream: true }); + buffer += decoder.decode(result.value, { + stream: true, + }); const lines = buffer.split(/\n/); buffer = lines.pop(); for (const line of lines) { @@ -1564,60 +1718,246 @@ const deserializeStream = async function* (stream, abortSignal) { reader.releaseLock(); } }; +const serverQrl = (qrl, options) => { + if (isServer) { + const captured = qrl.getCaptured(); + if (captured && captured.length > 0 && !_getContextHostElement()) { + throw new Error('For security reasons, we cannot serialize QRLs that capture lexical scope.'); + } + } + const method = options?.method?.toUpperCase?.() || 'POST'; + const headers = options?.headers || {}; + const origin = options?.origin || ''; + const fetchOptions = options?.fetchOptions || {}; + return /* @__PURE__ */ inlinedQrl( + async function (...args) { + const fetchOptions2 = _captures[0], + headers2 = _captures[1], + method2 = _captures[2], + origin2 = _captures[3], + qrl2 = _captures[4]; + const abortSignal = args.length > 0 && args[0] instanceof AbortSignal ? args.shift() : void 0; + if (isServer) { + return qrl2.apply(getRequestEvent(this), args); + } else { + let filteredArgs = args.map((arg) => { + if (arg instanceof SubmitEvent && arg.target instanceof HTMLFormElement) { + return new FormData(arg.target); + } else if (arg instanceof Event) { + return null; + } else if (arg instanceof Node) { + return null; + } + return arg; + }); + if (!filteredArgs.length) { + filteredArgs = void 0; + } + const qrlHash = qrl2.getHash(); + let query = ''; + const config = { + ...fetchOptions2, + method: method2, + headers: { + ...headers2, + 'Content-Type': 'application/qwik-json', + Accept: 'application/json, application/qwik-json, text/qwik-json-stream, text/plain', + // Required so we don't call accidentally + 'X-QRL': qrlHash, + }, + signal: abortSignal, + }; + const captured = qrl2.getCaptured(); + let toSend = [filteredArgs]; + if (captured?.length) { + toSend = [filteredArgs, ...captured]; + } else { + toSend = filteredArgs ? [filteredArgs] : []; + } + const body = await _serialize(toSend); + if (method2 === 'GET') { + query += `&${QDATA_KEY}=${encodeURIComponent(body)}`; + } else { + config.body = body; + } + const res = await fetch(`${origin2}?${QFN_KEY}=${qrlHash}${query}`, config); + const contentType = res.headers.get('Content-Type'); + if (res.ok && contentType === 'text/qwik-json-stream' && res.body) { + return (async function* () { + try { + for await (const result of deserializeStream(res.body, abortSignal)) { + yield result; + } + } finally { + if (!abortSignal?.aborted) { + await res.body.cancel(); + } + } + })(); + } else if (contentType === 'application/qwik-json') { + const str = await res.text(); + const obj = _deserialize(str); + if (res.status >= 400) { + throw obj; + } + return obj; + } else if (contentType === 'application/json') { + const obj = await res.json(); + if (res.status >= 400) { + throw obj; + } + return obj; + } else if (contentType === 'text/plain' || contentType === 'text/html') { + const str = await res.text(); + if (res.status >= 400) { + throw str; + } + return str; + } + } + }, + 'serverQrl_w03grD0Ag68', + [fetchOptions, headers, method, origin, qrl] + ); +}; +const server$ = /* @__PURE__ */ implicit$FirstArg(serverQrl); const ServiceWorkerRegister = (props) => - /* @__PURE__ */ jsx('script', { - type: 'module', - dangerouslySetInnerHTML: swRegister, - nonce: props.nonce, - }); + /* @__PURE__ */ _jsxSorted( + 'script', + { + nonce: _wrapProp(props, 'nonce'), + }, + { + type: 'module', + dangerouslySetInnerHTML: swRegister, + }, + null, + 3, + '1x_0' + ); +const _hf0 = (p0) => !p0.reloadDocument; +const _hf0_str = '!p0.reloadDocument'; +const _hf1 = (p0) => (p0.spaReset ? 'true' : void 0); +const _hf1_str = 'p0.spaReset?"true":undefined'; +const GetForm = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((_rawProps) => { + const rest = _restProps(_rawProps, ['action', 'spaReset', 'reloadDocument', 'onSubmit$']); + const nav = useNavigate(); + return /* @__PURE__ */ _jsxSplit( + 'form', + { + action: 'get', + 'preventdefault:submit': _fnSignal(_hf0, [_rawProps], _hf0_str), + 'data-spa-reset': _fnSignal(_hf1, [_rawProps], _hf1_str), + ..._getVarProps(rest), + ..._getConstProps(rest), + 'q-e:submit': [ + ...(Array.isArray(_rawProps.onSubmit$) ? _rawProps.onSubmit$ : [_rawProps.onSubmit$]), + /* @__PURE__ */ inlinedQrl( + async (_evt, form) => { + const nav2 = _captures[0]; + const formData = new FormData(form); + const params = new URLSearchParams(); + formData.forEach((value, key) => { + if (typeof value === 'string') { + params.append(key, value); + } + }); + await nav2('?' + params.toString(), { + type: 'form', + forceReload: true, + }); + }, + 'GetForm_component_form_q_e_submit_r3dkP9d2cF8', + [nav] + ), + /* @__PURE__ */ inlinedQrl((_evt, form) => { + if (form.getAttribute('data-spa-reset') === 'true') { + form.reset(); + } + form.dispatchEvent( + new CustomEvent('submitcompleted', { + bubbles: false, + cancelable: false, + composed: false, + detail: { + status: 200, + }, + }) + ); + }, 'GetForm_component_form_q_e_submit_1_cuYklZAOHrA'), + ], + }, + null, + /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, 'Q4_0'), + 0, + 'Q4_1' + ); + }, 'GetForm_component_2U5Z2Z8ryc0') +); const Form = ({ action, spaReset, reloadDocument, onSubmit$, ...rest }, key) => { if (action) { const isArrayApi = Array.isArray(onSubmit$); if (isArrayApi) { - return jsx$1( + return _jsxSplit( 'form', { - ...rest, - action: action.actionPath, + ..._getVarProps(rest), + ..._getConstProps(rest), + action: _wrapProp(action, 'actionPath'), 'preventdefault:submit': !reloadDocument, - onSubmit$: [ + 'q-e:submit': [ ...onSubmit$, // action.submit "submitcompleted" event for onSubmitCompleted$ events !reloadDocument - ? $((evt) => { - if (!action.submitted) { - return action.submit(evt); - } - }) + ? /* @__PURE__ */ inlinedQrl( + (evt) => { + const action2 = _captures[0]; + if (!action2.submitted) { + return action2.submit(evt); + } + }, + 'Form_form_q_e_submit_6i0Jq5q8JFg', + [action] + ) : void 0, ], - method: 'post', ['data-spa-reset']: spaReset ? 'true' : void 0, }, + { + method: 'post', + }, + null, + 0, key ); } - return jsx$1( + return _jsxSplit( 'form', { - ...rest, - action: action.actionPath, + ..._getVarProps(rest), + ..._getConstProps(rest), + action: _wrapProp(action, 'actionPath'), 'preventdefault:submit': !reloadDocument, - onSubmit$: [ + 'q-e:submit': [ // Since v2, this fires before the action is executed so it can be prevented onSubmit$, // action.submit "submitcompleted" event for onSubmitCompleted$ events !reloadDocument ? action.submit : void 0, ], - method: 'post', ['data-spa-reset']: spaReset ? 'true' : void 0, }, + { + method: 'post', + }, + null, + 0, key ); } else { - return /* @__PURE__ */ jsx( + return /* @__PURE__ */ _jsxSplit( GetForm, { spaReset, @@ -1625,49 +1965,13 @@ const Form = ({ action, spaReset, reloadDocument, onSubmit$, ...rest }, key) => onSubmit$, ...rest, }, + null, + null, + 0, key ); } }; -const GetForm = component$(({ action: _0, spaReset, reloadDocument, onSubmit$, ...rest }) => { - const nav = useNavigate(); - return /* @__PURE__ */ jsx('form', { - action: 'get', - 'preventdefault:submit': !reloadDocument, - 'data-spa-reset': spaReset ? 'true' : void 0, - ...rest, - onSubmit$: [ - ...(Array.isArray(onSubmit$) ? onSubmit$ : [onSubmit$]), - $(async (_evt, form) => { - const formData = new FormData(form); - const params = new URLSearchParams(); - formData.forEach((value, key) => { - if (typeof value === 'string') { - params.append(key, value); - } - }); - await nav('?' + params.toString(), { type: 'form', forceReload: true }); - }), - $((_evt, form) => { - if (form.getAttribute('data-spa-reset') === 'true') { - form.reset(); - } - form.dispatchEvent( - new CustomEvent('submitcompleted', { - bubbles: false, - cancelable: false, - composed: false, - detail: { - status: 200, - }, - }) - ); - }), - // end of array - ], - children: /* @__PURE__ */ jsx(Slot, {}), - }); -}); const untypedAppUrl = function appUrl(route, params, paramsPrefix = '') { const path = route.split('/'); @@ -1713,35 +2017,67 @@ const createRenderer = (getOptions) => { }; }; -const DocumentHeadTags = component$((props) => { - let head = useDocumentHead(); - if (props) { - head = { ...head, ...props }; - } - return /* @__PURE__ */ jsxs(Fragment, { - children: [ - head.title && /* @__PURE__ */ jsx('title', { children: head.title }), - head.meta.map((m) => /* @__PURE__ */ jsx('meta', { ...m })), - head.links.map((l) => /* @__PURE__ */ jsx('link', { ...l })), - head.styles.map((s) => { - const props2 = s.props || s; - return /* @__PURE__ */ createElement('style', { - ...props2, - dangerouslySetInnerHTML: s.style || props2.dangerouslySetInnerHTML, - key: s.key, - }); - }), - head.scripts.map((s) => { - const props2 = s.props || s; - return /* @__PURE__ */ createElement('script', { - ...props2, - dangerouslySetInnerHTML: s.script || props2.dangerouslySetInnerHTML, - key: s.key, - }); - }), - ], - }); -}); +const DocumentHeadTags = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((props) => { + let head = useDocumentHead(); + if (props) { + head = { + ...head, + ...props, + }; + } + return /* @__PURE__ */ _jsxSorted( + Fragment, + null, + null, + [ + head.title && /* @__PURE__ */ _jsxSorted('title', null, null, head.title, 1, 'r5_0'), + head.meta.map((m) => + /* @__PURE__ */ _jsxSplit( + 'meta', + { + ..._getVarProps(m), + }, + _getConstProps(m), + null, + 0, + 'r5_1' + ) + ), + head.links.map((l) => + /* @__PURE__ */ _jsxSplit( + 'link', + { + ..._getVarProps(l), + }, + _getConstProps(l), + null, + 0, + 'r5_2' + ) + ), + head.styles.map((s) => { + const props2 = s.props || s; + return /* @__PURE__ */ createElement('style', { + ...props2, + dangerouslySetInnerHTML: s.style || props2.dangerouslySetInnerHTML, + key: s.key, + }); + }), + head.scripts.map((s) => { + const props2 = s.props || s; + return /* @__PURE__ */ createElement('script', { + ...props2, + dangerouslySetInnerHTML: s.script || props2.dangerouslySetInnerHTML, + key: s.key, + }); + }), + ], + 1, + 'r5_3' + ); + }, 'DocumentHeadTags_component_9CrWYOoCpgY') +); export { DocumentHeadTags, @@ -1762,17 +2098,12 @@ export { omitProps, routeAction$, routeActionQrl, - routeLoader$, - routeLoaderQrl, server$, serverQrl, untypedAppUrl, - useContent, useDocumentHead, useLocation, useNavigate, - usePreventNavigate$, - usePreventNavigateQrl, useQwikRouter, valibot$, valibotQrl, diff --git a/packages/optimizer/core/src/snapshots/qwik_core__test__example_qwik_router_client.snap b/packages/optimizer/core/src/snapshots/qwik_core__test__example_qwik_router_client.snap index 7d751eb715c..651390b93c5 100644 --- a/packages/optimizer/core/src/snapshots/qwik_core__test__example_qwik_router_client.snap +++ b/packages/optimizer/core/src/snapshots/qwik_core__test__example_qwik_router_client.snap @@ -5,286 +5,328 @@ expression: output --- ==INPUT== -import { jsx, Fragment, jsxs } from '@qwik.dev/core/jsx-runtime'; import { - component$, + componentQrl, + inlinedQrl, useErrorBoundary, useOnWindow, - $, + _captures, + _jsxSorted, Slot, - createContextId, - useContext, - implicit$FirstArg, - noSerialize, - useVisibleTask$, - useServerData, + isBrowser, useSignal, untrack, - sync$, + _qrlSync, + useVisibleTaskQrl, isDev, - withLocale, - event$, + _jsxSplit, + _getConstProps, + _getVarProps, + eventQrl, isServer, - useStyles$, + useStylesQrl, + useServerData, useStore, - isBrowser, useContextProvider, - useTask$, + useTaskQrl, getLocale, - jsx as jsx$1, + noSerialize, + useContext, SkipRender, + implicit$FirstArg, + withLocale, + _wrapProp, + _restProps, + _fnSignal, createElement, } from '@qwik.dev/core'; +import { Fragment } from '@qwik.dev/core/jsx-runtime'; +import * as qwikRouterConfig from '@qwik-router-config'; +import { p } from '@qwik.dev/core/preloader'; import { + l as loadRoute, g as getClientNavPath, s as shouldPreload, - p as preloadRouteBundles, - l as loadClientData, - i as isPromise, - a as isSamePath, - c as createLoaderSignal, - t as toUrl, - b as isSameOrigin, - d as loadRoute, - D as DEFAULT_LOADERS_SERIALIZATION_STRATEGY, - C as CLIENT_DATA_CACHE, - Q as Q_ROUTE, - e as clientNavigate, - f as QFN_KEY, - h as QACTION_KEY, - j as QDATA_KEY, -} from './chunks/routing.qwik.mjs'; -import * as qwikRouterConfig from '@qwik-router-config'; + i as isSamePath, + t as toPath, + c as createDocumentHead, + r as resolveHead, + a as isSameOrigin, + b as toUrl, +} from './chunks/head.qwik.mjs'; +import { + u as useNavigate, + a as useLocation, + b as useDocumentHead, + c as useQwikRouterEnv, + d as useAction, +} from './chunks/use-functions.qwik.mjs'; +export { + e as useContent, + f as useHttpStatus, + g as usePreventNavigate$, + h as usePreventNavigateQrl, +} from './chunks/use-functions.qwik.mjs'; import { + _deserialize, _getContextContainer, - SerializerSymbol, - _UNINITIALIZED, _hasStoreEffects, + _retryOnPromise, forceStoreEffects, + createAsyncQrl, _waitUntilRendered, _getContextHostElement, - _getContextEvent, _serialize, - _deserialize, - _resolveContextWithoutSequentialScope, } from '@qwik.dev/core/internal'; -import { _asyncRequestStore } from '@qwik.dev/router/middleware/request-handler'; +import { + Q as QACTION_KEY, + e as ensureRouteLoaderSignals, + s as setLoaderSignalValue, + C as ContentContext, + a as ContentInternalContext, + D as DocumentHeadContext, + H as HttpStatusContext, + R as RouteLocationContext, + b as RouteNavigateContext, + c as RouteStateContext, + d as RouteLoaderCtxContext, + f as RouteActionContext, + g as RoutePreventNavigateContext, + u as updateRouteLoaderCtx, + h as Q_ROUTE, + i as getRequestEvent, + j as QFN_KEY, + k as QDATA_KEY, +} from './chunks/route-loaders.qwik.mjs'; +export { r as routeLoader$, l as routeLoaderQrl } from './chunks/route-loaders.qwik.mjs'; import * as v from 'valibot'; import * as z from 'zod'; export { z } from 'zod'; import swRegister from '@qwik-router-sw-register'; import { renderToStream } from '@qwik.dev/core/server'; -import '@qwik.dev/core/preloader'; -import './chunks/types.qwik.mjs'; - -const ErrorBoundary = component$((props) => { - const store = useErrorBoundary(); - useOnWindow( - 'qerror', - $((e) => { - store.error = e.detail.error; - }) - ); - if (store.error && props.fallback$) { - return /* @__PURE__ */ jsx(Fragment, { children: props.fallback$(store.error) }); - } - return /* @__PURE__ */ jsx(Slot, {}); -}); -const RouteStateContext = /* @__PURE__ */ createContextId('qc-s'); -const ContentContext = /* @__PURE__ */ createContextId('qc-c'); -const ContentInternalContext = /* @__PURE__ */ createContextId('qc-ic'); -const DocumentHeadContext = /* @__PURE__ */ createContextId('qc-h'); -const RouteLocationContext = /* @__PURE__ */ createContextId('qc-l'); -const RouteNavigateContext = /* @__PURE__ */ createContextId('qc-n'); -const RouteActionContext = /* @__PURE__ */ createContextId('qc-a'); -const RoutePreventNavigateContext = /* @__PURE__ */ createContextId('qc-p'); - -const useContent = () => useContext(ContentContext); -const useDocumentHead = () => useContext(DocumentHeadContext); -const useLocation = () => useContext(RouteLocationContext); -const useNavigate = () => useContext(RouteNavigateContext); -const usePreventNavigateQrl = (fn) => { - if (!__EXPERIMENTAL__.preventNavigate) { - throw new Error( - 'usePreventNavigate$ is experimental and must be enabled with `experimental: ["preventNavigate"]` in the `qwikVite` plugin.' +const ErrorBoundary = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((props) => { + const store = useErrorBoundary(); + useOnWindow( + 'qerror', + /* @__PURE__ */ inlinedQrl( + (e) => { + const store2 = _captures[0]; + store2.error = e.detail.error; + }, + 'ErrorBoundary_component_useOnWindow_G0jFRpoNY0M', + [store] + ) ); - } - const registerPreventNav = useContext(RoutePreventNavigateContext); - useVisibleTask$(() => registerPreventNav(fn)); -}; -const usePreventNavigate$ = implicit$FirstArg(usePreventNavigateQrl); -const useAction = () => useContext(RouteActionContext); -const useQwikRouterEnv = () => noSerialize(useServerData('qwikrouter')); + if (store.error && props.fallback$) { + return /* @__PURE__ */ _jsxSorted( + Fragment, + null, + null, + props.fallback$(store.error), + 1, + 'bA_0' + ); + } + return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, 'bA_1'); + }, 'ErrorBoundary_component_pOa6vjtC7ik') +); -const Link = component$((props) => { - const nav = useNavigate(); - const loc = useLocation(); - const originalHref = props.href; - const anchorRef = useSignal(); - const { - onClick$, - prefetch: prefetchProp, - reload, - replaceState, - scroll, - ...linkProps - } = /* @__PURE__ */ (() => props)(); - const clientNavPath = untrack(getClientNavPath, { ...linkProps, reload }, loc); - linkProps.href = clientNavPath || originalHref; - const prefetchData = - (!!clientNavPath && prefetchProp !== false && prefetchProp !== 'js') || void 0; - const prefetch = - prefetchData || - (!!clientNavPath && prefetchProp !== false && untrack(shouldPreload, clientNavPath, loc)); - const handlePrefetch = prefetch - ? $((_, elm) => { - if (navigator.connection?.saveData) { - return; - } - if (elm && elm.href) { - const url = new URL(elm.href); - preloadRouteBundles(url.pathname); - if (elm.hasAttribute('data-prefetch')) { - loadClientData(url, { - preloadRouteBundles: false, - isPrefetch: true, - }); - } - } - }) - : void 0; - const preventDefault = clientNavPath - ? sync$((event) => { - if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) { - event.preventDefault(); - } - }) - : void 0; - const handleClientSideNavigation = clientNavPath - ? $((event, elm) => { - if (event.defaultPrevented) { - if (elm.href) { - elm.setAttribute('aria-pressed', 'true'); - nav(elm.href, { forceReload: reload, replaceState, scroll }).then(() => { - elm.removeAttribute('aria-pressed'); - }); - } - } - }) - : void 0; - const handlePreload = $((_, elm) => { - const url = new URL(elm.href); - preloadRouteBundles(url.pathname, 1); - }); - useVisibleTask$(({ track }) => { - track(() => loc.url.pathname); - const handler = linkProps.onQVisible$; - if (handler) { - const event = new CustomEvent('qvisible'); - if (Array.isArray(handler)) { - handler.flat(10).forEach((handler2) => handler2?.(event, anchorRef.value)); - } else { - handler?.(event, anchorRef.value); - } +async function prefetchRoute(url, prefetchData, probability = 0.8, manifestHash) { + if (!isBrowser) { + return; + } + try { + const loadedRoute = await loadRoute( + qwikRouterConfig.routes, + qwikRouterConfig.cacheModules, + url.pathname + ); + if (!loadedRoute) { + return; } - if (!isDev && anchorRef.value) { - handlePrefetch?.(void 0, anchorRef.value); + let routeName = loadedRoute.$routeName$; + routeName = routeName.endsWith('/') ? routeName : routeName + '/'; + if (routeName.length > 1 && routeName.startsWith('/')) { + routeName = routeName.slice(1); } - }); - return /* @__PURE__ */ jsx('a', { - ref: anchorRef, - ...{ 'q:link': !!clientNavPath }, - ...linkProps, - onClick$: [ - preventDefault, - handlePreload, - // needs to be in between preventDefault and onClick$ to ensure it starts asap. - onClick$, - handleClientSideNavigation, - ], - 'data-prefetch': prefetchData, - onMouseOver$: [linkProps.onMouseOver$, handlePrefetch], - onFocus$: [linkProps.onFocus$, handlePrefetch], - onQVisible$: [], - children: /* @__PURE__ */ jsx(Slot, {}), - }); -}); - -const resolveHead = (endpoint, routeLocation, contentModules, locale, defaults) => - withLocale(locale, () => { - const head = createDocumentHead(defaults); - const getData = (loaderOrAction) => { - const id = loaderOrAction.__id; - if (loaderOrAction.__brand === 'server_loader') { - if (!(id in endpoint.loaders)) { - throw new Error( - 'You can not get the returned data of a loader that has not been executed for this request.' - ); + p(routeName, probability); + if (!prefetchData || !manifestHash) { + return; + } + if (loadedRoute.$loaders$?.length && loadedRoute.$loaderPaths$) { + const basePath = qwikRouterConfig.basePathname ?? '/'; + for (const hash of loadedRoute.$loaders$) { + let loaderPath = loadedRoute.$loaderPaths$?.[hash]; + if (!loaderPath) { + continue; } - } - const data = endpoint.loaders[id]; - if (isPromise(data)) { - throw new Error('Loaders returning a promise can not be resolved for the head function.'); - } - return data; - }; - const fns = []; - for (const contentModule of contentModules) { - const contentModuleHead = contentModule?.head; - if (contentModuleHead) { - if (typeof contentModuleHead === 'function') { - fns.unshift(contentModuleHead); - } else if (typeof contentModuleHead === 'object') { - resolveDocumentHead(head, contentModuleHead); + if (basePath !== '/' && !loaderPath.startsWith(basePath)) { + loaderPath = basePath + loaderPath.slice(1); } + const pathBase = loaderPath.endsWith('/') ? loaderPath : loaderPath + '/'; + const fetchUrl = `${pathBase}q-loader-${hash}.${manifestHash}.json`; + fetch(fetchUrl) + .then((r) => r.blob()) + .catch(() => {}); } } - if (fns.length) { - const headProps = { - head, - withLocale: (fn) => fn(), - resolveValue: getData, - ...routeLocation, - }; - for (const fn of fns) { - resolveDocumentHead(head, fn(headProps)); - } - } - return head; - }); -const resolveDocumentHead = (resolvedHead, updatedHead) => { - if (typeof updatedHead.title === 'string') { - resolvedHead.title = updatedHead.title; - } - mergeArray(resolvedHead.meta, updatedHead.meta); - mergeArray(resolvedHead.links, updatedHead.links); - mergeArray(resolvedHead.styles, updatedHead.styles); - mergeArray(resolvedHead.scripts, updatedHead.scripts); - Object.assign(resolvedHead.frontmatter, updatedHead.frontmatter); + } catch {} +} + +const Link = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((props) => { + const nav = useNavigate(); + const loc = useLocation(); + const head = useDocumentHead(); + const originalHref = props.href; + const anchorRef = useSignal(); + const { + onClick$, + prefetch: prefetchProp, + reload, + replaceState, + scroll, + ...linkProps + } = /* @__PURE__ */ (() => props)(); + const clientNavPath = untrack( + getClientNavPath, + { + ...linkProps, + reload, + }, + loc + ); + linkProps.href = clientNavPath || originalHref; + const prefetchData = + (!!clientNavPath && prefetchProp !== false && prefetchProp !== 'js') || void 0; + const prefetch = + prefetchData || + (!!clientNavPath && prefetchProp !== false && untrack(shouldPreload, clientNavPath, loc)); + const handlePrefetch = prefetch + ? /* @__PURE__ */ inlinedQrl( + (_, elm) => { + const head2 = _captures[0]; + if (navigator.connection?.saveData) { + return; + } + if (elm && elm.href) { + const url = new URL(elm.href); + prefetchRoute(url, elm.hasAttribute('data-prefetch'), 0.8, head2.manifestHash); + } + }, + 'Link_component_handlePrefetch_AGvVXzXKbms', + [head] + ) + : void 0; + const preventDefault = clientNavPath + ? _qrlSync((event) => { + if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) { + event.preventDefault(); + } + }, 'event=>{if(!(event.metaKey||event.ctrlKey||event.shiftKey||event.altKey)){event.preventDefault();}}') + : void 0; + const handleClientSideNavigation = clientNavPath + ? /* @__PURE__ */ inlinedQrl( + (event, elm) => { + const nav2 = _captures[0], + reload2 = _captures[1], + replaceState2 = _captures[2], + scroll2 = _captures[3]; + if (event.defaultPrevented) { + if (elm.href) { + elm.setAttribute('aria-pressed', 'true'); + nav2(elm.href, { + forceReload: reload2, + replaceState: replaceState2, + scroll: scroll2, + }).then(() => { + elm.removeAttribute('aria-pressed'); + }); + } + } + }, + 'Link_component_handleClientSideNavigation_h3qenoGeI6M', + [nav, reload, replaceState, scroll] + ) + : void 0; + const handlePreload = /* @__PURE__ */ inlinedQrl((_, elm) => { + const url = new URL(elm.href); + prefetchRoute(url.pathname, false, 1); + }, 'Link_component_handlePreload_AAemwtuBjsE'); + useVisibleTaskQrl( + /* @__PURE__ */ inlinedQrl( + ({ track }) => { + const anchorRef2 = _captures[0], + handlePrefetch2 = _captures[1], + linkProps2 = _captures[2], + loc2 = _captures[3]; + track(() => loc2.url.pathname); + const handler = linkProps2.onQVisible$; + if (handler) { + const event = new CustomEvent('qvisible'); + if (Array.isArray(handler)) { + handler.flat(10).forEach((handler2) => handler2?.(event, anchorRef2.value)); + } else { + handler?.(event, anchorRef2.value); + } + } + if (!isDev && anchorRef2.value) { + handlePrefetch2?.(void 0, anchorRef2.value); + } + }, + 'Link_component_useVisibleTask_xKeuRmnoNSA', + [anchorRef, handlePrefetch, linkProps, loc] + ) + ); + return /* @__PURE__ */ _jsxSplit( + 'a', + { + ref: anchorRef, + 'q:link': !!clientNavPath, + ..._getVarProps(linkProps), + ..._getConstProps(linkProps), + 'q-e:click': [preventDefault, handlePreload, onClick$, handleClientSideNavigation], + 'data-prefetch': prefetchData, + 'q-e:mouseover': [linkProps.onMouseOver$, handlePrefetch], + 'q-e:focus': [linkProps.onFocus$, handlePrefetch], + }, + { + // We need to prevent the onQVisible$ from being called twice since it is handled in the visible task + 'q-e:qvisible': [], + }, + /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, 'jO_0'), + 0, + 'jO_1' + ); + }, 'Link_component_VPmar9tb3t4') +); + +const newScrollState = () => { + return { + x: 0, + y: 0, + w: 0, + h: 0, + }; }; -const mergeArray = (existingArr, newArr) => { - if (Array.isArray(newArr)) { - for (const newItem of newArr) { - if (typeof newItem.key === 'string') { - const existingIndex = existingArr.findIndex((i) => i.key === newItem.key); - if (existingIndex > -1) { - existingArr[existingIndex] = newItem; - continue; - } +const clientNavigate = (win, navType, fromURL, toURL, replaceState = false) => { + if (navType !== 'popstate') { + const samePath = isSamePath(fromURL, toURL); + const sameHash = fromURL.hash === toURL.hash; + if (!samePath || !sameHash) { + const newState = { + _qRouterScroll: newScrollState(), + }; + if (replaceState) { + win.history.replaceState(newState, '', toPath(toURL)); + } else { + win.history.pushState(newState, '', toPath(toURL)); } - existingArr.push(newItem); } } }; -const createDocumentHead = (defaults) => ({ - title: defaults?.title || '', - meta: [...(defaults?.meta || [])], - links: [...(defaults?.links || [])], - styles: [...(defaults?.styles || [])], - scripts: [...(defaults?.scripts || [])], - frontmatter: { ...defaults?.frontmatter }, -}); const transitionCss = '@layer qwik{@supports selector(html:active-view-transition-type(type)){html:active-view-transition-type(qwik-navigation){:root{view-transition-name:none}}}@supports not selector(html:active-view-transition-type(type)){:root{view-transition-name:none}}}'; @@ -295,15 +337,6 @@ function callRestoreScrollOnDocument() { document.__q_scroll_restore__ = void 0; } } -const restoreScroll = (type, toUrl, fromUrl, scroller, scrollState) => { - if (type === 'popstate' && scrollState) { - scroller.scrollTo(scrollState.x, scrollState.y); - } else if (type === 'link' || type === 'form') { - if (!hashScroll(toUrl, fromUrl)) { - scroller.scrollTo(0, 0); - } - } -}; const hashScroll = (toUrl, fromUrl) => { const elmId = toUrl.hash.slice(1); const elm = elmId && document.getElementById(elmId); @@ -315,6 +348,15 @@ const hashScroll = (toUrl, fromUrl) => { } return false; }; +const restoreScroll = (type, toUrl, fromUrl, scroller, scrollState) => { + if (type === 'popstate' && scrollState) { + scroller.scrollTo(scrollState.x, scrollState.y); + } else if (type === 'link' || type === 'form') { + if (!hashScroll(toUrl, fromUrl)) { + scroller.scrollTo(0, 0); + } + } +}; const currentScrollState = (elm) => { return { x: elm.scrollLeft, @@ -333,151 +375,209 @@ const saveScrollHistory = (scrollState) => { history.replaceState(state, ''); }; -const spaInit = event$((_, el) => { - if (!window._qRouterSPA && !window._qRouterInitPopstate) { - const currentPath = location.pathname + location.search; - const checkAndScroll = (scrollState) => { - if (scrollState) { - window.scrollTo(scrollState.x, scrollState.y); - } - }; - const currentScrollState = () => { - const elm = document.documentElement; - return { - x: elm.scrollLeft, - y: elm.scrollTop, - w: Math.max(elm.scrollWidth, elm.clientWidth), - h: Math.max(elm.scrollHeight, elm.clientHeight), - }; - }; - const saveScrollState = (scrollState) => { - const state = history.state || {}; - state._qRouterScroll = scrollState || currentScrollState(); - history.replaceState(state, ''); - }; - saveScrollState(); - window._qRouterInitPopstate = () => { - if (window._qRouterSPA) { - return; - } - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); - if (currentPath !== location.pathname + location.search) { - const getContainer = (el2) => - el2.closest('[q\\:container]:not([q\\:container=html]):not([q\\:container=text])'); - const container = getContainer(el); - const domContainer = container.qContainer; - const hostElement = domContainer.vNodeLocate(el); - const nav = domContainer?.resolveContext(hostElement, { - id: 'qc--n', - }); - if (nav) { - nav(location.href, { type: 'popstate' }); - } else { - location.reload(); +const spaInit = eventQrl( + /* @__PURE__ */ inlinedQrl((_, el) => { + if (!window._qRouterSPA && !window._qRouterInitPopstate) { + const currentPath = location.pathname + location.search; + const checkAndScroll = (scrollState) => { + if (scrollState) { + window.scrollTo(scrollState.x, scrollState.y); } - } else { - if (history.scrollRestoration === 'manual') { - const scrollState = history.state?._qRouterScroll; - checkAndScroll(scrollState); - window._qRouterScrollEnabled = true; + }; + const currentScrollState = () => { + const elm = document.documentElement; + return { + x: elm.scrollLeft, + y: elm.scrollTop, + w: Math.max(elm.scrollWidth, elm.clientWidth), + h: Math.max(elm.scrollHeight, elm.clientHeight), + }; + }; + const saveScrollState = (scrollState) => { + const state = history.state || {}; + state._qRouterScroll = scrollState || currentScrollState(); + history.replaceState(state, ''); + }; + saveScrollState(); + window._qRouterInitPopstate = () => { + if (window._qRouterSPA) { + return; } - } - }; - if (!window._qRouterHistoryPatch) { - window._qRouterHistoryPatch = true; - const pushState = history.pushState; - const replaceState = history.replaceState; - const prepareState = (state) => { - if (state === null || typeof state === 'undefined') { - state = {}; - } else if (state?.constructor !== Object) { - state = { _data: state }; - if (isDev) { - console.warn( - 'In a Qwik SPA context, `history.state` is used to store scroll state. Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. We need to be able to automatically attach the scroll state to your state object. A new state object has been created, your data has been moved to: `history.state._data`' - ); + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + if (currentPath !== location.pathname + location.search) { + const getContainer = (el2) => + el2.closest('[q\\:container]:not([q\\:container=html]):not([q\\:container=text])'); + const container = getContainer(el); + const domContainer = container.qContainer; + const hostElement = domContainer.vNodeLocate(el); + const nav = domContainer?.resolveContext(hostElement, { + id: 'qr-n', + }); + if (nav) { + nav(location.href, { + type: 'popstate', + }); + } else { + location.reload(); + } + } else { + if (history.scrollRestoration === 'manual') { + const scrollState = history.state?._qRouterScroll; + checkAndScroll(scrollState); + window._qRouterScrollEnabled = true; } } - state._qRouterScroll = state._qRouterScroll || currentScrollState(); - return state; - }; - history.pushState = (state, title, url) => { - state = prepareState(state); - return pushState.call(history, state, title, url); }; - history.replaceState = (state, title, url) => { - state = prepareState(state); - return replaceState.call(history, state, title, url); - }; - } - window._qRouterInitAnchors = (event) => { - if (window._qRouterSPA || event.defaultPrevented) { - return; - } - const target = event.target.closest('a[href]'); - if (target && !target.hasAttribute('preventdefault:click')) { - const href = target.getAttribute('href'); - const prev = new URL(location.href); - const dest = new URL(href, prev); - const sameOrigin = dest.origin === prev.origin; - const samePath = dest.pathname + dest.search === prev.pathname + prev.search; - if (sameOrigin && samePath) { - event.preventDefault(); - if (dest.href !== prev.href) { - history.pushState(null, '', dest); + if (!window._qRouterHistoryPatch) { + window._qRouterHistoryPatch = true; + const pushState = history.pushState; + const replaceState = history.replaceState; + const prepareState = (state) => { + if (state === null || typeof state === 'undefined') { + state = {}; + } else if (state?.constructor !== Object) { + state = { + _data: state, + }; + if (isDev) { + console.warn( + 'In a Qwik SPA context, `history.state` is used to store scroll state. Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. We need to be able to automatically attach the scroll state to your state object. A new state object has been created, your data has been moved to: `history.state._data`' + ); + } } - if (!dest.hash) { - if (dest.href.endsWith('#')) { - window.scrollTo(0, 0); - } else { - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); - saveScrollState({ ...currentScrollState(), x: 0, y: 0 }); - location.reload(); + state._qRouterScroll = state._qRouterScroll || currentScrollState(); + return state; + }; + history.pushState = (state, title, url) => { + state = prepareState(state); + return pushState.call(history, state, title, url); + }; + history.replaceState = (state, title, url) => { + state = prepareState(state); + return replaceState.call(history, state, title, url); + }; + } + window._qRouterInitAnchors = (event) => { + if (window._qRouterSPA || event.defaultPrevented) { + return; + } + const target = event.target.closest('a[href]'); + if (target && !target.hasAttribute('preventdefault:click')) { + const href = target.getAttribute('href'); + const prev = new URL(location.href); + const dest = new URL(href, prev); + const sameOrigin = dest.origin === prev.origin; + const samePath = dest.pathname + dest.search === prev.pathname + prev.search; + if (sameOrigin && samePath) { + event.preventDefault(); + if (dest.href !== prev.href) { + history.pushState(null, '', dest); } - } else { - const elmId = dest.hash.slice(1); - const elm = document.getElementById(elmId); - if (elm) { - elm.scrollIntoView(); + if (!dest.hash) { + if (dest.href.endsWith('#')) { + window.scrollTo(0, 0); + } else { + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + saveScrollState({ + ...currentScrollState(), + x: 0, + y: 0, + }); + location.reload(); + } + } else { + const elmId = dest.hash.slice(1); + const elm = document.getElementById(elmId); + if (elm) { + elm.scrollIntoView(); + } } } } - } + }; + window._qRouterInitVisibility = () => { + if ( + !window._qRouterSPA && + window._qRouterScrollEnabled && + document.visibilityState === 'hidden' + ) { + saveScrollState(); + } + }; + window._qRouterInitScroll = () => { + if (window._qRouterSPA || !window._qRouterScrollEnabled) { + return; + } + clearTimeout(window._qRouterScrollDebounce); + window._qRouterScrollDebounce = setTimeout(() => { + saveScrollState(); + window._qRouterScrollDebounce = void 0; + }, 200); + }; + window._qRouterScrollEnabled = true; + setTimeout(() => { + window.addEventListener('popstate', window._qRouterInitPopstate); + window.addEventListener('scroll', window._qRouterInitScroll, { + passive: true, + }); + document.addEventListener('click', window._qRouterInitAnchors); + if (!window.navigation) { + document.addEventListener('visibilitychange', window._qRouterInitVisibility, { + passive: true, + }); + } + }, 0); + } + }, 'spa_init_event_igI1pUsax0E') +); + +async function submitAction(action, routePath) { + const pathBase = routePath.endsWith('/') ? routePath : routePath + '/'; + const url = `${pathBase}?${QACTION_KEY}=${encodeURIComponent(action.id)}`; + const actionData = action.data; + let fetchOptions; + if (actionData instanceof FormData) { + fetchOptions = { + method: 'POST', + body: actionData, + headers: { + Accept: 'application/json', + }, }; - window._qRouterInitVisibility = () => { - if ( - !window._qRouterSPA && - window._qRouterScrollEnabled && - document.visibilityState === 'hidden' - ) { - saveScrollState(); - } + } else { + fetchOptions = { + method: 'POST', + body: JSON.stringify(actionData), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Accept: 'application/json', + }, }; - window._qRouterInitScroll = () => { - if (window._qRouterSPA || !window._qRouterScrollEnabled) { - return; - } - clearTimeout(window._qRouterScrollDebounce); - window._qRouterScrollDebounce = setTimeout(() => { - saveScrollState(); - window._qRouterScrollDebounce = void 0; - }, 200); + } + const response = await fetch(url, fetchOptions); + if (response.redirected) { + const redirectedURL = new URL(response.url); + if (redirectedURL.origin !== location.origin) { + location.href = redirectedURL.href; + return void 0; + } + location.href = redirectedURL.href; + return void 0; + } + if ((response.headers.get('content-type') || '').includes('json')) { + const text = await response.text(); + const data = _deserialize(text); + return { + status: response.status, + result: data?.result, + loaderHashes: data?.loaderHashes, + loaderValues: data?.loaders, }; - window._qRouterScrollEnabled = true; - setTimeout(() => { - window.addEventListener('popstate', window._qRouterInitPopstate); - window.addEventListener('scroll', window._qRouterInitScroll, { passive: true }); - document.addEventListener('click', window._qRouterInitAnchors); - if (!window.navigation) { - document.addEventListener('visibilitychange', window._qRouterInitVisibility, { - passive: true, - }); - } - }, 0); } -}); + return void 0; +} const startViewTransition = (params) => { if (!params.update) { @@ -490,7 +590,9 @@ const startViewTransition = (params) => { } catch { transition = document.startViewTransition(params.update); } - const event = new CustomEvent('qviewtransition', { detail: transition }); + const event = new CustomEvent('qviewtransition', { + detail: transition, + }); document.dispatchEvent(event); return transition; } else { @@ -501,14 +603,19 @@ const startViewTransition = (params) => { const QWIK_CITY_SCROLLER = '_qCityScroller'; const QWIK_ROUTER_SCROLLER = '_qRouterScroller'; const preventNav = {}; -const internalState = { navCount: 0 }; +const internalState = { + navCount: 0, + redirectCount: 0, +}; const useQwikRouter = (props) => { if (!isServer) { throw new Error( 'useQwikRouter can only run during SSR on the server. If you are seeing this, it means you are re-rendering the root of your application. Fix that or use the component around the root of your application.' ); } - useStyles$(transitionCss); + useStylesQrl( + /* @__PURE__ */ inlinedQrl(transitionCss, 'qwik_view_transition_css_inline_vNfd9raIMI0') + ); const env = useQwikRouterEnv(); if (!env?.params) { throw new Error( @@ -520,6 +627,7 @@ const useQwikRouter = (props) => { throw new Error(`Missing Qwik URL Env Data`); } const serverHead = useServerData('documentHead'); + const manifestHash = useServerData('containerAttributes')?.['q:manifest-hash']; if ( env.ev.originalUrl.pathname !== env.ev.url.pathname && !__EXPERIMENTAL__.enableRequestRewrite @@ -535,47 +643,44 @@ const useQwikRouter = (props) => { isNavigating: false, prevUrl: void 0, }; - const routeLocation = useStore(routeLocationTarget, { deep: false }); + const routeLocation = useStore(routeLocationTarget, { + deep: false, + }); const navResolver = {}; - const container = _getContextContainer(); - const getSerializationStrategy = (loaderId) => { - return ( - env.response.loadersSerializationStrategy.get(loaderId) || - DEFAULT_LOADERS_SERIALIZATION_STRATEGY - ); - }; - const loadersObject = {}; - const loaderState = {}; - for (const [key, value] of Object.entries(env.response.loaders)) { - loadersObject[key] = value; - loaderState[key] = createLoaderSignal( - loadersObject, - key, - url, - getSerializationStrategy(key), - container - ); - } - loadersObject[SerializerSymbol] = (obj) => { - const loadersSerializationObject = {}; - for (const [k, v] of Object.entries(obj)) { - loadersSerializationObject[k] = getSerializationStrategy(k) === 'always' ? v : _UNINITIALIZED; + env.routeLoaderCtx.manifestHash = manifestHash || ''; + env.routeLoaderCtx.pageUrl = url; + const routeLoaderCtx = useStore(env.routeLoaderCtx); + const loaderState = useStore( + {}, + { + deep: false, } - return loadersSerializationObject; - }; + ); + const contentModulesForInit = env.loadedRoute.$mods$; + const loaders = ensureRouteLoaderSignals(contentModulesForInit, loaderState, routeLoaderCtx); + for (const loader of loaders) { + const value = env.loaderValues[loader.__id]; + if (value !== void 0) { + setLoaderSignalValue(loaderState[loader.__id], value); + } + } const routeInternal = useSignal({ type: 'initial', dest: url, scroll: true, }); - const documentHead = useStore(() => createDocumentHead(serverHead)); + const documentHead = useStore(() => createDocumentHead(serverHead, manifestHash)); const content = useStore({ headings: void 0, menu: void 0, }); const contentInternal = useSignal(); + const httpStatus = useSignal({ + status: env.response.status, + message: env.loadedRoute.$notFound$ ? 'Not Found' : (env.response.statusMessage ?? ''), + }); const currentActionId = env.response.action; - const currentAction = currentActionId ? env.response.loaders[currentActionId] : void 0; + const currentAction = currentActionId ? env.response.actionResult : void 0; const actionState = useSignal( currentAction ? { @@ -588,7 +693,7 @@ const useQwikRouter = (props) => { } : void 0 ); - const registerPreventNav = $((fn$) => { + const registerPreventNav = /* @__PURE__ */ inlinedQrl((fn$) => { if (!isBrowser) { return; } @@ -617,171 +722,270 @@ const useQwikRouter = (props) => { } } }; - }); - const goto = $(async (path, opt) => { - const { - type = 'link', - forceReload = path === void 0, - // Hack for nav() because this API is already set. - replaceState = false, - scroll = true, - } = typeof opt === 'object' ? opt : { forceReload: opt }; - internalState.navCount++; - if (isBrowser && type === 'link' && routeInternal.value.type === 'initial') { - const url2 = new URL(window.location.href); - routeInternal.value.dest = url2; - routeLocation.url = url2; + }, 'useQwikRouter_registerPreventNav_69B0DK0eZJc'); + const getScroller = /* @__PURE__ */ inlinedQrl(() => { + let scroller = document.getElementById(QWIK_ROUTER_SCROLLER); + if (!scroller) { + scroller = document.getElementById(QWIK_CITY_SCROLLER); + if (scroller && isDev) { + console.warn( + `Please update your scroller ID to "${QWIK_ROUTER_SCROLLER}" as "${QWIK_CITY_SCROLLER}" is deprecated and will be removed in V3` + ); + } } - const lastDest = routeInternal.value.dest; - const dest = - path === void 0 ? lastDest : typeof path === 'number' ? path : toUrl(path, routeLocation.url); - if ( - preventNav.$cbs$ && - (forceReload || - typeof dest === 'number' || - !isSamePath(dest, lastDest) || - !isSameOrigin(dest, lastDest)) - ) { - const ourNavId = internalState.navCount; - const prevents = await Promise.all([...preventNav.$cbs$.values()].map((cb) => cb(dest))); - if (ourNavId !== internalState.navCount || prevents.some(Boolean)) { - if (ourNavId === internalState.navCount && type === 'popstate') { - history.pushState(null, '', lastDest); + return scroller ?? document.documentElement; + }, 'useQwikRouter_getScroller_0UhDFwlxeFQ'); + const goto = /* @__PURE__ */ inlinedQrl( + async (path, opt) => { + const actionState2 = _captures[0], + getScroller2 = _captures[1], + manifestHash2 = _captures[2], + navResolver2 = _captures[3], + routeInternal2 = _captures[4], + routeLocation2 = _captures[5]; + const { + type = 'link', + forceReload = path === void 0, + replaceState = false, + scroll = true, + } = typeof opt === 'object' + ? opt + : { + forceReload: opt, + }; + internalState.navCount++; + if (isBrowser && type === 'link' && routeInternal2.value.type === 'initial') { + const url2 = new URL(window.location.href); + routeInternal2.value.dest = url2; + routeLocation2.url = url2; + } + const lastDest = routeInternal2.value.dest; + const dest = + path === void 0 + ? lastDest + : typeof path === 'number' + ? path + : toUrl(path, routeLocation2.url); + if ( + preventNav.$cbs$ && + (forceReload || + typeof dest === 'number' || + !isSamePath(dest, lastDest) || + !isSameOrigin(dest, lastDest)) + ) { + const ourNavId = internalState.navCount; + const prevents = await Promise.all([...preventNav.$cbs$.values()].map((cb) => cb(dest))); + if (ourNavId !== internalState.navCount || prevents.some(Boolean)) { + if (ourNavId === internalState.navCount && type === 'popstate') { + history.pushState(null, '', lastDest); + } + return; + } + } + if (typeof dest === 'number') { + if (isBrowser) { + history.go(dest); } return; } - } - if (typeof dest === 'number') { - if (isBrowser) { - history.go(dest); - } - return; - } - if (!isSameOrigin(dest, lastDest)) { - if (isBrowser) { - location.href = dest.href; - } - return; - } - if (!forceReload && isSamePath(dest, lastDest)) { - if (isBrowser) { - if (type === 'link' && dest.href !== location.href) { - history.pushState(null, '', dest); + if (!isSameOrigin(dest, lastDest)) { + if (isBrowser) { + location.href = dest.href; } - let scroller = document.getElementById(QWIK_ROUTER_SCROLLER); - if (!scroller) { - scroller = document.getElementById(QWIK_CITY_SCROLLER); - if (scroller && isDev) { - console.warn( - `Please update your scroller ID to "${QWIK_ROUTER_SCROLLER}" as "${QWIK_CITY_SCROLLER}" is deprecated and will be removed in V3` - ); + return; + } + if (!forceReload && isSamePath(dest, lastDest)) { + if (isBrowser) { + if (type === 'link' && dest.href !== location.href) { + history.pushState(null, '', dest); + } + const scroller = await getScroller2(); + restoreScroll(type, dest, new URL(location.href), scroller, getScrollHistory()); + if (type === 'popstate') { + window._qRouterScrollEnabled = true; } } - if (!scroller) { - scroller = document.documentElement; - } - restoreScroll(type, dest, new URL(location.href), scroller, getScrollHistory()); - if (type === 'popstate') { - window._qRouterScrollEnabled = true; + if (dest.href !== routeLocation2.url.href) { + const newUrl = new URL(dest.href); + routeInternal2.value.dest = newUrl; + routeLocation2.url = newUrl; } + return; } - return; - } - routeInternal.value = { - type, - dest, - forceReload, - replaceState, - scroll, - }; - if (isBrowser) { - loadClientData(dest); - loadRoute( - qwikRouterConfig.routes, - qwikRouterConfig.menus, - qwikRouterConfig.cacheModules, - dest.pathname - ); - } - actionState.value = void 0; - routeLocation.isNavigating = true; - return new Promise((resolve) => { - navResolver.r = resolve; - }); - }); + let historyUpdated = false; + if (isBrowser && type === 'link' && !forceReload) { + const scroller = await getScroller2(); + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + const scrollState = currentScrollState(scroller); + saveScrollHistory(scrollState); + clientNavigate(window, type, new URL(location.href), dest, replaceState); + historyUpdated = true; + } + routeInternal2.value = { + type, + dest, + forceReload, + replaceState, + scroll, + historyUpdated, + }; + if (isBrowser) { + prefetchRoute(dest, true, 0.8, manifestHash2); + } + actionState2.value = void 0; + routeLocation2.isNavigating = true; + return new Promise((resolve) => { + navResolver2.r = resolve; + }); + }, + 'useQwikRouter_goto_8j8Vrz2yUIM', + [actionState, getScroller, manifestHash, navResolver, routeInternal, routeLocation] + ); useContextProvider(ContentContext, content); useContextProvider(ContentInternalContext, contentInternal); useContextProvider(DocumentHeadContext, documentHead); + useContextProvider(HttpStatusContext, httpStatus); useContextProvider(RouteLocationContext, routeLocation); useContextProvider(RouteNavigateContext, goto); useContextProvider(RouteStateContext, loaderState); + useContextProvider(RouteLoaderCtxContext, routeLoaderCtx); + routeLoaderCtx.goto = goto; useContextProvider(RouteActionContext, actionState); useContextProvider(RoutePreventNavigateContext, registerPreventNav); - useTask$(({ track }) => { - async function run() { - const navigation = track(routeInternal); - const action = track(actionState); - const locale = getLocale(''); - const prevUrl = routeLocation.url; - const navType = action ? 'form' : navigation.type; - const replaceState = navigation.replaceState; - let trackUrl; - let clientPageData; - let loadedRoute = null; - let container2; - if (isServer) { - trackUrl = new URL(navigation.dest, routeLocation.url); - loadedRoute = env.loadedRoute; - clientPageData = env.response; - } else { - trackUrl = new URL(navigation.dest, location); - if (trackUrl.pathname.endsWith('/')) { - if (globalThis.__NO_TRAILING_SLASH__) { - trackUrl.pathname = trackUrl.pathname.slice(0, -1); - } - } else if (!globalThis.__NO_TRAILING_SLASH__) { - trackUrl.pathname += '/'; - } - let loadRoutePromise = loadRoute( - qwikRouterConfig.routes, - qwikRouterConfig.menus, - qwikRouterConfig.cacheModules, - trackUrl.pathname - ); - container2 = _getContextContainer(); - const pageData = (clientPageData = await loadClientData(trackUrl, { - action, - clearCache: true, - })); - if (!pageData) { - routeInternal.untrackedValue = { type: navType, dest: trackUrl }; - return; - } - const newHref = pageData.href; - const newURL = new URL(newHref, trackUrl); - if (!isSamePath(newURL, trackUrl)) { - if (!pageData.isRewrite) { - trackUrl = newURL; + useTaskQrl( + /* @__PURE__ */ inlinedQrl( + async ({ track }) => { + const actionState2 = _captures[0], + content2 = _captures[1], + contentInternal2 = _captures[2], + documentHead2 = _captures[3], + env2 = _captures[4], + getScroller2 = _captures[5], + goto2 = _captures[6], + httpStatus2 = _captures[7], + loaderState2 = _captures[8], + navResolver2 = _captures[9], + props2 = _captures[10], + routeInternal2 = _captures[11], + routeLoaderCtx2 = _captures[12], + routeLocation2 = _captures[13], + routeLocationTarget2 = _captures[14], + serverHead2 = _captures[15]; + const container = _getContextContainer(); + const navigation = track(routeInternal2); + const action = track(actionState2); + const locale = getLocale(''); + const prevUrl = routeLocation2.url; + const navType = action ? 'form' : navigation.type; + const replaceState = navigation.replaceState; + let trackUrl; + let endpointResponse; + let actionData; + let loadedRoute; + if (isServer) { + trackUrl = new URL(navigation.dest, routeLocation2.url); + loadedRoute = env2.loadedRoute; + endpointResponse = env2.response; + actionData = endpointResponse; + } else { + trackUrl = new URL(navigation.dest, location); + if (trackUrl.pathname.endsWith('/')) { + if (globalThis.__NO_TRAILING_SLASH__) { + trackUrl.pathname = trackUrl.pathname.slice(0, -1); + } + } else if (!globalThis.__NO_TRAILING_SLASH__) { + trackUrl.pathname += '/'; } - loadRoutePromise = loadRoute( + const loadRoutePromise = loadRoute( qwikRouterConfig.routes, - qwikRouterConfig.menus, qwikRouterConfig.cacheModules, - newURL.pathname - // Load the actual required path. + trackUrl.pathname ); + try { + loadedRoute = await loadRoutePromise; + } catch (e) { + console.error(e); + window.location.href = trackUrl.href; + return; + } + if (action) { + const result = await submitAction(action, trackUrl.pathname); + if (!result) { + routeInternal2.untrackedValue = { + type: navType, + dest: trackUrl, + }; + return; + } + actionData = { + status: result.status, + action: action.id, + actionResult: result.result, + }; + if (action.resolve) { + action.resolve({ + status: result.status, + result: result.result, + }); + } + if (result.loaderValues && Object.keys(result.loaderValues).length > 0) { + for (const [id, value] of Object.entries(result.loaderValues)) { + const signal = loaderState2[id]; + if (signal) { + setLoaderSignalValue(signal, value); + } + } + } + if (result.loaderHashes) { + for (const hash of result.loaderHashes) { + loaderState2[hash]?.invalidate(true); + } + } + } } - try { - loadedRoute = await loadRoutePromise; - } catch (e) { - console.error(e); - window.location.href = newHref; + const { $routeName$, $params$, $mods$, $menu$, $notFound$ } = loadedRoute; + const contentModules = $mods$; + updateRouteLoaderCtx(routeLoaderCtx2, loadedRoute.$loaderPaths$, trackUrl); + const routeLoaders = ensureRouteLoaderSignals( + contentModules, + loaderState2, + routeLoaderCtx2 + ); + const navCountBefore = internalState.navCount; + if (!isServer && routeLoaders.length > 0) { + await Promise.all(routeLoaders.map((loader) => loaderState2[loader.__id]?.promise())); + } + if (internalState.navCount !== navCountBefore) { + if (++internalState.redirectCount > 20) { + console.error('Too many redirects, aborting navigation'); + internalState.redirectCount = 0; + return; + } return; } - } - if (loadedRoute) { - const [routeName, params, mods, menu] = loadedRoute; - const contentModules = mods; + internalState.redirectCount = 0; + if ($notFound$) { + httpStatus2.value = { + status: 404, + message: 'Not Found', + }; + } else if (endpointResponse) { + httpStatus2.value = { + status: endpointResponse.status, + message: endpointResponse.statusMessage ?? 'OK', + }; + } else if (actionData) { + httpStatus2.value = { + status: actionData.status, + message: 'OK', + }; + } else { + httpStatus2.value = { + status: 200, + message: 'OK', + }; + } const pageModule = contentModules[contentModules.length - 1]; if (navigation.dest.search && !!isSamePath(trackUrl, prevUrl)) { trackUrl.search = navigation.dest.search; @@ -790,84 +994,61 @@ const useQwikRouter = (props) => { let shouldForceUrl = false; let shouldForceParams = false; if (!isSamePath(trackUrl, prevUrl)) { - if (_hasStoreEffects(routeLocation, 'prevUrl')) { + if (_hasStoreEffects(routeLocation2, 'prevUrl')) { shouldForcePrevUrl = true; } - routeLocationTarget.prevUrl = prevUrl; + routeLocationTarget2.prevUrl = prevUrl; } - if (routeLocationTarget.url !== trackUrl) { - if (_hasStoreEffects(routeLocation, 'url')) { + if (routeLocationTarget2.url !== trackUrl) { + if (_hasStoreEffects(routeLocation2, 'url')) { shouldForceUrl = true; } - routeLocationTarget.url = trackUrl; + routeLocationTarget2.url = trackUrl; } - if (routeLocationTarget.params !== params) { - if (_hasStoreEffects(routeLocation, 'params')) { + if (routeLocationTarget2.params !== $params$) { + if (_hasStoreEffects(routeLocation2, 'params')) { shouldForceParams = true; } - routeLocationTarget.params = params; + routeLocationTarget2.params = $params$; } - routeInternal.untrackedValue = { type: navType, dest: trackUrl }; - const resolvedHead = resolveHead( - clientPageData, - routeLocation, - contentModules, - locale, - serverHead + routeInternal2.untrackedValue = { + type: navType, + dest: trackUrl, + }; + const resolvedHead = await _retryOnPromise(() => + resolveHead(actionData, loaderState2, routeLocation2, contentModules, locale, serverHead2) ); - content.headings = pageModule.headings; - content.menu = menu; - contentInternal.untrackedValue = noSerialize(contentModules); - documentHead.links = resolvedHead.links; - documentHead.meta = resolvedHead.meta; - documentHead.styles = resolvedHead.styles; - documentHead.scripts = resolvedHead.scripts; - documentHead.title = resolvedHead.title; - documentHead.frontmatter = resolvedHead.frontmatter; + content2.headings = pageModule.headings; + content2.menu = $menu$; + contentInternal2.untrackedValue = noSerialize(contentModules); + documentHead2.links = resolvedHead.links; + documentHead2.meta = resolvedHead.meta; + documentHead2.styles = resolvedHead.styles; + documentHead2.scripts = resolvedHead.scripts; + documentHead2.title = resolvedHead.title; + documentHead2.frontmatter = resolvedHead.frontmatter; if (isBrowser) { let scrollState; if (navType === 'popstate') { scrollState = getScrollHistory(); } - const scroller = - document.getElementById(QWIK_ROUTER_SCROLLER) ?? document.documentElement; + const scroller = await getScroller2(); if ( (navigation.scroll && (!navigation.forceReload || !isSamePath(trackUrl, prevUrl)) && - (navType === 'link' || navType === 'popstate')) || // Action might have responded with a redirect. + (navType === 'link' || navType === 'popstate')) || (navType === 'form' && !isSamePath(trackUrl, prevUrl)) ) { document.__q_scroll_restore__ = () => restoreScroll(navType, trackUrl, prevUrl, scroller, scrollState); } - const loaders = clientPageData?.loaders; - if (loaders) { - const container3 = _getContextContainer(); - for (const [key, value] of Object.entries(loaders)) { - const signal = loaderState[key]; - const awaitedValue = await value; - loadersObject[key] = awaitedValue; - if (!signal) { - loaderState[key] = createLoaderSignal( - loadersObject, - key, - trackUrl, - DEFAULT_LOADERS_SERIALIZATION_STRATEGY, - container3 - ); - } else { - signal.invalidate(); - } - } - } - CLIENT_DATA_CACHE.clear(); if (!window._qRouterSPA) { window._qRouterSPA = true; history.scrollRestoration = 'manual'; window.addEventListener('popstate', () => { window._qRouterScrollEnabled = false; clearTimeout(window._qRouterScrollDebounce); - goto(location.href, { + goto2(location.href, { type: 'popstate', }); }); @@ -881,7 +1062,9 @@ const useQwikRouter = (props) => { if (state === null || typeof state === 'undefined') { state = {}; } else if (state?.constructor !== Object) { - state = { _data: state }; + state = { + _data: state, + }; if (isDev) { console.warn( 'In a Qwik SPA context, `history.state` is used to store scroll state. Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. We need to be able to automatically attach the scroll state to your state object. A new state object has been created, your data has been moved to: `history.state._data`' @@ -925,7 +1108,7 @@ const useQwikRouter = (props) => { location.reload(); return; } - goto(target.getAttribute('href')); + goto2(target.getAttribute('href')); } } }); @@ -948,7 +1131,9 @@ const useQwikRouter = (props) => { saveScrollHistory(scrollState2); } }, - { passive: true } + { + passive: true, + } ); document.removeEventListener('visibilitychange', window._qRouterInitVisibility); window._qRouterInitVisibility = void 0; @@ -966,7 +1151,9 @@ const useQwikRouter = (props) => { window._qRouterScrollDebounce = void 0; }, 200); }, - { passive: true } + { + passive: true, + } ); removeEventListener('scroll', window._qRouterInitScroll); window._qRouterInitScroll = void 0; @@ -975,16 +1162,26 @@ const useQwikRouter = (props) => { if (navType !== 'popstate') { window._qRouterScrollEnabled = false; clearTimeout(window._qRouterScrollDebounce); - const scrollState2 = currentScrollState(scroller); - saveScrollHistory(scrollState2); + if (!navigation.historyUpdated) { + const scrollState2 = currentScrollState(scroller); + saveScrollHistory(scrollState2); + } } const navigate = () => { - clientNavigate(window, navType, prevUrl, trackUrl, replaceState); - contentInternal.trigger(); - return _waitUntilRendered(container2); + if (navigation.historyUpdated) { + const currentPath = location.pathname + location.search + location.hash; + const nextPath = toPath(trackUrl); + if (currentPath !== nextPath) { + history.replaceState(history.state, '', nextPath); + } + } else { + clientNavigate(window, navType, prevUrl, trackUrl, replaceState); + } + contentInternal2.trigger(); + return _waitUntilRendered(container); }; const _waitNextPage = () => { - if (isServer || props?.viewTransition === false) { + if (isServer || props2?.viewTransition === false) { return navigate(); } else { const viewTransition = startViewTransition({ @@ -1003,7 +1200,7 @@ const useQwikRouter = (props) => { throw err; }) .finally(() => { - container2.element.setAttribute?.(Q_ROUTE, routeName); + container.element.setAttribute?.(Q_ROUTE, $routeName$); const scrollState2 = currentScrollState(scroller); saveScrollHistory(scrollState2); window._qRouterScrollEnabled = true; @@ -1011,31 +1208,51 @@ const useQwikRouter = (props) => { callRestoreScrollOnDocument(); } if (shouldForcePrevUrl) { - forceStoreEffects(routeLocation, 'prevUrl'); + forceStoreEffects(routeLocation2, 'prevUrl'); } if (shouldForceUrl) { - forceStoreEffects(routeLocation, 'url'); + forceStoreEffects(routeLocation2, 'url'); } if (shouldForceParams) { - forceStoreEffects(routeLocation, 'params'); + forceStoreEffects(routeLocation2, 'params'); } - routeLocation.isNavigating = false; - navResolver.r?.(); + routeLocation2.isNavigating = false; + navResolver2.r?.(); }); } - } - } - if (isServer) { - return run(); - } else { - run(); + }, + 'useQwikRouter_useTask_XpalYii770E', + [ + actionState, + content, + contentInternal, + documentHead, + env, + getScroller, + goto, + httpStatus, + loaderState, + navResolver, + props, + routeInternal, + routeLoaderCtx, + routeLocation, + routeLocationTarget, + serverHead, + ] + ), + // We should only wait for navigation to complete on the server + { + deferUpdates: isServer, } - }); + ); }; -const QwikRouterProvider = component$((props) => { - useQwikRouter(props); - return /* @__PURE__ */ jsx(Slot, {}); -}); +const QwikRouterProvider = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((props) => { + useQwikRouter(props); + return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, '5y_0'); + }, 'QwikRouterProvider_component_6Kjfa79mqlY') +); const QwikCityProvider = QwikRouterProvider; const useQwikMockRouter = (props) => { const urlEnv = props.url ?? 'http://localhost/'; @@ -1047,31 +1264,62 @@ const useQwikMockRouter = (props) => { isNavigating: false, prevUrl: void 0, }, - { deep: false } + { + deep: false, + } ); const loadersData = props.loaders?.reduce((acc, { loader, data }) => { acc[loader.__id] = data; return acc; }, {}); - const loaderState = useStore(loadersData ?? {}, { deep: false }); + const loaderState = useStore( + {}, + { + deep: false, + } + ); + for (const [loaderId, data] of Object.entries(loadersData ?? {})) { + loaderState[loaderId] ||= createAsyncQrl( + /* @__PURE__ */ inlinedQrl( + async () => { + const data2 = _captures[0]; + return data2; + }, + 'useQwikMockRouter_createAsync_clbxpuXqpEU', + [data] + ), + { + initial: data, + } + ); + } const goto = props.goto ?? - $(async () => { + /* @__PURE__ */ inlinedQrl(async () => { console.warn('QwikRouterMockProvider: goto not provided'); - }); - const documentHead = useStore(createDocumentHead, { deep: false }); + }, 'useQwikMockRouter_goto_aViHFxQ1a3s'); + const documentHead = useStore(createDocumentHead, { + deep: false, + }); const content = useStore( { headings: void 0, menu: void 0, }, - { deep: false } + { + deep: false, + } ); const contentInternal = useSignal(); const actionState = useSignal(); + const httpStatus = useSignal({ + status: 200, + message: '', + }); useContextProvider(ContentContext, content); useContextProvider(ContentInternalContext, contentInternal); useContextProvider(DocumentHeadContext, documentHead); + useContextProvider(HttpStatusContext, httpStatus); useContextProvider(RouteLocationContext, routeLocation); useContextProvider(RouteNavigateContext, goto); useContextProvider(RouteStateContext, loaderState); @@ -1080,201 +1328,89 @@ const useQwikMockRouter = (props) => { acc[action.__id] = handler; return acc; }, {}); - useTask$(async ({ track }) => { - const action = track(actionState); - if (!action?.resolve) { - return; - } - const mock = actionsMocks?.[action.id]; - if (mock) { - const actionResult = await mock(action.data); - action.resolve(actionResult); - } - }); + useTaskQrl( + /* @__PURE__ */ inlinedQrl( + async ({ track }) => { + const actionState2 = _captures[0], + actionsMocks2 = _captures[1]; + const action = track(actionState2); + if (!action?.resolve) { + return; + } + const mock = actionsMocks2?.[action.id]; + if (mock) { + const actionResult = await mock(action.data); + action.resolve(actionResult); + } + }, + 'useQwikMockRouter_useTask_tXTLR4tzCy0', + [actionState, actionsMocks] + ) + ); }; -const QwikRouterMockProvider = component$((props) => { - useQwikMockRouter(props); - return /* @__PURE__ */ jsx(Slot, {}); -}); +const QwikRouterMockProvider = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((props) => { + useQwikMockRouter(props); + return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, '5y_1'); + }, 'QwikRouterMockProvider_component_IN4dVpT0x74') +); const QwikCityMockProvider = QwikRouterMockProvider; -const RouterOutlet = component$(() => { - const serverData = useServerData('containerAttributes'); - if (!serverData) { - throw new Error('PrefetchServiceWorker component must be rendered on the server.'); - } - const internalContext = useContext(ContentInternalContext); - const contents = internalContext.value; - if (contents && contents.length > 0) { - const contentsLen = contents.length; - let cmp = null; - for (let i = contentsLen - 1; i >= 0; i--) { - if (contents[i].default) { - cmp = jsx$1(contents[i].default, { - children: cmp, - }); - } +const RouterOutlet = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl(() => { + const serverData = useServerData('containerAttributes'); + if (!serverData) { + throw new Error('PrefetchServiceWorker component must be rendered on the server.'); } - return /* @__PURE__ */ jsxs(Fragment, { - children: [ - cmp, - !__EXPERIMENTAL__.noSPA && - /* @__PURE__ */ jsx('script', { - 'document:onQCInit$': spaInit, - 'document:onQInit$': sync$(() => { - ((w, h) => { - if (!w._qcs && h.scrollRestoration === 'manual') { - w._qcs = true; - const s = h.state?._qRouterScroll; - if (s) { - w.scrollTo(s.x, s.y); - } - document.dispatchEvent(new Event('qcinit')); - } - })(window, history); - }), - }), - ], - }); - } - return SkipRender; -}); - -const routeActionQrl = (actionQrl, ...rest) => { - const { id, validators } = getValidators(rest, actionQrl); - function action() { - const loc = useLocation(); - const currentAction = useAction(); - const initialState = { - actionPath: `?${QACTION_KEY}=${id}`, - submitted: false, - isRunning: false, - status: void 0, - value: void 0, - formData: void 0, - }; - const state = useStore(() => { - const value = currentAction.value; - if (value && value?.id === id) { - const data = value.data; - if (data instanceof FormData) { - initialState.formData = data; - } - if (value.output) { - const { status, result } = value.output; - initialState.status = status; - initialState.value = result; - } - } - return initialState; - }); - const submit = $((input = {}) => { - if (isServer) { - throw new Error(`Actions can not be invoked within the server during SSR. -Action.run() can only be called on the browser, for example when a user clicks a button, or submits a form.`); - } - let data; - let form; - if (input instanceof SubmitEvent) { - form = input.target; - data = new FormData(form); - if ( - (input.submitter instanceof HTMLInputElement || - input.submitter instanceof HTMLButtonElement) && - input.submitter.name - ) { - if (input.submitter.name) { - data.append(input.submitter.name, input.submitter.value); - } + const internalContext = useContext(ContentInternalContext); + const contents = internalContext.value; + if (contents && contents.length > 0) { + const contentsLen = contents.length; + let cmp = null; + for (let i = contentsLen - 1; i >= 0; i--) { + if (contents[i].default) { + cmp = _jsxSorted(contents[i].default, null, null, cmp, 1, 'Fn_0'); } - } else { - data = input; } - return new Promise((resolve) => { - if (data instanceof FormData) { - state.formData = data; - } - state.submitted = true; - state.isRunning = true; - loc.isNavigating = true; - currentAction.value = { - data, - id, - resolve: noSerialize(resolve), - }; - }).then(({ result, status }) => { - state.isRunning = false; - state.status = status; - state.value = result; - if (form) { - if (form.getAttribute('data-spa-reset') === 'true') { - form.reset(); - } - const detail = { status, value: result }; - form.dispatchEvent( - new CustomEvent('submitcompleted', { - bubbles: false, - cancelable: false, - composed: false, - detail, - }) - ); - } - return { - status, - value: result, - }; - }); - }); - initialState.submit = submit; - return state; - } - action.__brand = 'server_action'; - action.__validators = validators; - action.__qrl = actionQrl; - action.__id = id; - Object.freeze(action); - return action; -}; -const globalActionQrl = (actionQrl, ...rest) => { - const action = routeActionQrl(actionQrl, ...rest); - if (isServer) { - if (typeof globalThis._qwikActionsMap === 'undefined') { - globalThis._qwikActionsMap = /* @__PURE__ */ new Map(); + return /* @__PURE__ */ _jsxSorted( + Fragment, + null, + null, + [ + cmp, + !__EXPERIMENTAL__.noSPA && + /* @__PURE__ */ _jsxSorted( + 'script', + { + 'q-d:qinit': _qrlSync(() => { + ((w, h) => { + if (!w._qcs && h.scrollRestoration === 'manual') { + w._qcs = true; + const s = h.state?._qRouterScroll; + if (s) { + w.scrollTo(s.x, s.y); + } + document.dispatchEvent(new Event('qcinit')); + } + })(window, history); + }, '()=>{((w,h)=>{if(!w._qcs&&h.scrollRestoration==="manual"){w._qcs=!0;const s=h.state?._qRouterScroll;if(s){w.scrollTo(s.x,s.y);}document.dispatchEvent(new Event("qcinit"));}})(window,history);}'), + }, + { + 'q-d:qcinit': spaInit, + }, + null, + 2, + 'Fn_1' + ), + ], + 1, + 'Fn_2' + ); } - globalThis._qwikActionsMap.set(action.__id, action); - } - return action; -}; -const routeAction$ = /* @__PURE__ */ implicit$FirstArg(routeActionQrl); -const globalAction$ = /* @__PURE__ */ implicit$FirstArg(globalActionQrl); -const getValue = (obj) => obj.value; -const routeLoaderQrl = (loaderQrl, ...rest) => { - const { id, validators, serializationStrategy } = getValidators(rest, loaderQrl); - function loader() { - const state = _resolveContextWithoutSequentialScope(RouteStateContext); - if (!(id in state)) { - throw new Error(`routeLoader$ "${loaderQrl.getSymbol()}" was invoked in a route where it was not declared. - This is because the routeLoader$ was not exported in a 'layout.tsx' or 'index.tsx' file of the existing route. - For more information check: https://qwik.dev/docs/route-loader/ + return SkipRender; + }, 'RouterOutlet_component_QwONcWD5gIg') +); - If your are managing reusable logic or a library it is essential that this function is re-exported from within 'layout.tsx' or 'index.tsx file of the existing route otherwise it will not run or throw exception. - For more information check: https://qwik.dev/docs/re-exporting-loaders/`); - } - const loaderData = state[id]; - untrack(getValue, loaderData); - return loaderData; - } - loader.__brand = 'server_loader'; - loader.__qrl = loaderQrl; - loader.__validators = validators; - loader.__id = id; - loader.__serializationStrategy = serializationStrategy; - loader.__expires = -1; - Object.freeze(loader); - return loader; -}; -const routeLoader$ = /* @__PURE__ */ implicit$FirstArg(routeLoaderQrl); const validatorQrl = (validator) => { if (isServer) { return { @@ -1407,113 +1543,9 @@ const zodQrl = (qrl) => { return void 0; }; const zod$ = /* @__PURE__ */ implicit$FirstArg(zodQrl); -const serverQrl = (qrl, options) => { - if (isServer) { - const captured = qrl.getCaptured(); - if (captured && captured.length > 0 && !_getContextHostElement()) { - throw new Error('For security reasons, we cannot serialize QRLs that capture lexical scope.'); - } - } - const method = options?.method?.toUpperCase?.() || 'POST'; - const headers = options?.headers || {}; - const origin = options?.origin || ''; - const fetchOptions = options?.fetchOptions || {}; - return $(async function (...args) { - const abortSignal = args.length > 0 && args[0] instanceof AbortSignal ? args.shift() : void 0; - if (isServer) { - let requestEvent = _asyncRequestStore?.getStore(); - if (!requestEvent) { - const contexts = [useQwikRouterEnv()?.ev, this, _getContextEvent()]; - requestEvent = contexts.find( - (v2) => - v2 && - Object.prototype.hasOwnProperty.call(v2, 'sharedMap') && - Object.prototype.hasOwnProperty.call(v2, 'cookie') - ); - } - return qrl.apply(requestEvent, args); - } else { - let filteredArgs = args.map((arg) => { - if (arg instanceof SubmitEvent && arg.target instanceof HTMLFormElement) { - return new FormData(arg.target); - } else if (arg instanceof Event) { - return null; - } else if (arg instanceof Node) { - return null; - } - return arg; - }); - if (!filteredArgs.length) { - filteredArgs = void 0; - } - const qrlHash = qrl.getHash(); - let query = ''; - const config = { - ...fetchOptions, - method, - headers: { - ...headers, - 'Content-Type': 'application/qwik-json', - Accept: 'application/json, application/qwik-json, text/qwik-json-stream, text/plain', - // Required so we don't call accidentally - 'X-QRL': qrlHash, - }, - signal: abortSignal, - }; - const captured = qrl.getCaptured(); - let toSend = [filteredArgs]; - if (captured?.length) { - toSend = [filteredArgs, ...captured]; - } else { - toSend = filteredArgs ? [filteredArgs] : []; - } - const body = await _serialize(toSend); - if (method === 'GET') { - query += `&${QDATA_KEY}=${encodeURIComponent(body)}`; - } else { - config.body = body; - } - const res = await fetch(`${origin}?${QFN_KEY}=${qrlHash}${query}`, config); - const contentType = res.headers.get('Content-Type'); - if (res.ok && contentType === 'text/qwik-json-stream' && res.body) { - return (async function* () { - try { - for await (const result of deserializeStream(res.body, abortSignal)) { - yield result; - } - } finally { - if (!abortSignal?.aborted) { - await res.body.cancel(); - } - } - })(); - } else if (contentType === 'application/qwik-json') { - const str = await res.text(); - const obj = _deserialize(str); - if (res.status >= 400) { - throw obj; - } - return obj; - } else if (contentType === 'application/json') { - const obj = await res.json(); - if (res.status >= 400) { - throw obj; - } - return obj; - } else if (contentType === 'text/plain' || contentType === 'text/html') { - const str = await res.text(); - if (res.status >= 400) { - throw str; - } - return str; - } - } - }); -}; -const server$ = /* @__PURE__ */ implicit$FirstArg(serverQrl); const getValidators = (rest, qrl) => { let id; - let serializationStrategy = DEFAULT_LOADERS_SERIALIZATION_STRATEGY; + let invalidate; const validators = []; if (rest.length === 1) { const options = rest[0]; @@ -1522,12 +1554,12 @@ const getValidators = (rest, qrl) => { validators.push(options); } else { id = options.id; - if (options.serializationStrategy) { - serializationStrategy = options.serializationStrategy; - } if (options.validation) { validators.push(...options.validation); } + if (options.invalidate) { + invalidate = options.invalidate.map((loader) => loader.__id); + } } } } else if (rest.length > 1) { @@ -1546,9 +1578,129 @@ const getValidators = (rest, qrl) => { return { validators: validators.reverse(), id, - serializationStrategy, + invalidate, }; }; +const routeActionQrl = (actionQrl, ...rest) => { + const { id, validators, invalidate } = getValidators(rest, actionQrl); + function action() { + const loc = useLocation(); + const currentAction = useAction(); + const initialState = { + actionPath: `?${QACTION_KEY}=${id}`, + submitted: false, + isRunning: false, + status: void 0, + value: void 0, + formData: void 0, + }; + const state = useStore(() => { + const value = currentAction.value; + if (value && value?.id === id) { + const data = value.data; + if (data instanceof FormData) { + initialState.formData = data; + } + if (value.output) { + const { status, result } = value.output; + initialState.status = status; + initialState.value = result; + } + } + return initialState; + }); + const submit = /* @__PURE__ */ inlinedQrl( + (input = {}) => { + const currentAction2 = _captures[0], + id2 = _captures[1], + loc2 = _captures[2], + state2 = _captures[3]; + if (isServer) { + throw new Error(`Actions can not be invoked within the server during SSR. +Action.run() can only be called on the browser, for example when a user clicks a button, or submits a form.`); + } + let data; + let form; + if (input instanceof SubmitEvent) { + form = input.target; + data = new FormData(form); + if ( + (input.submitter instanceof HTMLInputElement || + input.submitter instanceof HTMLButtonElement) && + input.submitter.name + ) { + if (input.submitter.name) { + data.append(input.submitter.name, input.submitter.value); + } + } + } else { + data = input; + } + return new Promise((resolve) => { + if (data instanceof FormData) { + state2.formData = data; + } + state2.submitted = true; + state2.isRunning = true; + loc2.isNavigating = true; + currentAction2.value = { + data, + id: id2, + resolve: noSerialize(resolve), + }; + }).then((_rawProps) => { + state2.isRunning = false; + state2.status = _rawProps.status; + state2.value = _rawProps.result; + if (form) { + if (form.getAttribute('data-spa-reset') === 'true') { + form.reset(); + } + const detail = { + status: _rawProps.status, + value: _rawProps.result, + }; + form.dispatchEvent( + new CustomEvent('submitcompleted', { + bubbles: false, + cancelable: false, + composed: false, + detail, + }) + ); + } + return { + status: _rawProps.status, + value: _rawProps.result, + }; + }); + }, + 'routeActionQrl_action_submit_YuS5bpdQ360', + [currentAction, id, loc, state] + ); + initialState.submit = submit; + return state; + } + action.__brand = 'server_action'; + action.__validators = validators; + action.__qrl = actionQrl; + action.__id = id; + action.__invalidate = invalidate; + Object.freeze(action); + return action; +}; +const globalActionQrl = (actionQrl, ...rest) => { + const action = routeActionQrl(actionQrl, ...rest); + if (isServer) { + if (typeof globalThis._qwikActionsMap === 'undefined') { + globalThis._qwikActionsMap = /* @__PURE__ */ new Map(); + } + globalThis._qwikActionsMap.set(action.__id, action); + } + return action; +}; +const routeAction$ = /* @__PURE__ */ implicit$FirstArg(routeActionQrl); +const globalAction$ = /* @__PURE__ */ implicit$FirstArg(globalActionQrl); const deserializeStream = async function* (stream, abortSignal) { const reader = stream.getReader(); try { @@ -1559,7 +1711,9 @@ const deserializeStream = async function* (stream, abortSignal) { if (result.done) { break; } - buffer += decoder.decode(result.value, { stream: true }); + buffer += decoder.decode(result.value, { + stream: true, + }); const lines = buffer.split(/\n/); buffer = lines.pop(); for (const line of lines) { @@ -1571,60 +1725,246 @@ const deserializeStream = async function* (stream, abortSignal) { reader.releaseLock(); } }; +const serverQrl = (qrl, options) => { + if (isServer) { + const captured = qrl.getCaptured(); + if (captured && captured.length > 0 && !_getContextHostElement()) { + throw new Error('For security reasons, we cannot serialize QRLs that capture lexical scope.'); + } + } + const method = options?.method?.toUpperCase?.() || 'POST'; + const headers = options?.headers || {}; + const origin = options?.origin || ''; + const fetchOptions = options?.fetchOptions || {}; + return /* @__PURE__ */ inlinedQrl( + async function (...args) { + const fetchOptions2 = _captures[0], + headers2 = _captures[1], + method2 = _captures[2], + origin2 = _captures[3], + qrl2 = _captures[4]; + const abortSignal = args.length > 0 && args[0] instanceof AbortSignal ? args.shift() : void 0; + if (isServer) { + return qrl2.apply(getRequestEvent(this), args); + } else { + let filteredArgs = args.map((arg) => { + if (arg instanceof SubmitEvent && arg.target instanceof HTMLFormElement) { + return new FormData(arg.target); + } else if (arg instanceof Event) { + return null; + } else if (arg instanceof Node) { + return null; + } + return arg; + }); + if (!filteredArgs.length) { + filteredArgs = void 0; + } + const qrlHash = qrl2.getHash(); + let query = ''; + const config = { + ...fetchOptions2, + method: method2, + headers: { + ...headers2, + 'Content-Type': 'application/qwik-json', + Accept: 'application/json, application/qwik-json, text/qwik-json-stream, text/plain', + // Required so we don't call accidentally + 'X-QRL': qrlHash, + }, + signal: abortSignal, + }; + const captured = qrl2.getCaptured(); + let toSend = [filteredArgs]; + if (captured?.length) { + toSend = [filteredArgs, ...captured]; + } else { + toSend = filteredArgs ? [filteredArgs] : []; + } + const body = await _serialize(toSend); + if (method2 === 'GET') { + query += `&${QDATA_KEY}=${encodeURIComponent(body)}`; + } else { + config.body = body; + } + const res = await fetch(`${origin2}?${QFN_KEY}=${qrlHash}${query}`, config); + const contentType = res.headers.get('Content-Type'); + if (res.ok && contentType === 'text/qwik-json-stream' && res.body) { + return (async function* () { + try { + for await (const result of deserializeStream(res.body, abortSignal)) { + yield result; + } + } finally { + if (!abortSignal?.aborted) { + await res.body.cancel(); + } + } + })(); + } else if (contentType === 'application/qwik-json') { + const str = await res.text(); + const obj = _deserialize(str); + if (res.status >= 400) { + throw obj; + } + return obj; + } else if (contentType === 'application/json') { + const obj = await res.json(); + if (res.status >= 400) { + throw obj; + } + return obj; + } else if (contentType === 'text/plain' || contentType === 'text/html') { + const str = await res.text(); + if (res.status >= 400) { + throw str; + } + return str; + } + } + }, + 'serverQrl_w03grD0Ag68', + [fetchOptions, headers, method, origin, qrl] + ); +}; +const server$ = /* @__PURE__ */ implicit$FirstArg(serverQrl); const ServiceWorkerRegister = (props) => - /* @__PURE__ */ jsx('script', { - type: 'module', - dangerouslySetInnerHTML: swRegister, - nonce: props.nonce, - }); + /* @__PURE__ */ _jsxSorted( + 'script', + { + nonce: _wrapProp(props, 'nonce'), + }, + { + type: 'module', + dangerouslySetInnerHTML: swRegister, + }, + null, + 3, + '1x_0' + ); +const _hf0 = (p0) => !p0.reloadDocument; +const _hf0_str = '!p0.reloadDocument'; +const _hf1 = (p0) => (p0.spaReset ? 'true' : void 0); +const _hf1_str = 'p0.spaReset?"true":undefined'; +const GetForm = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((_rawProps) => { + const rest = _restProps(_rawProps, ['action', 'spaReset', 'reloadDocument', 'onSubmit$']); + const nav = useNavigate(); + return /* @__PURE__ */ _jsxSplit( + 'form', + { + action: 'get', + 'preventdefault:submit': _fnSignal(_hf0, [_rawProps], _hf0_str), + 'data-spa-reset': _fnSignal(_hf1, [_rawProps], _hf1_str), + ..._getVarProps(rest), + ..._getConstProps(rest), + 'q-e:submit': [ + ...(Array.isArray(_rawProps.onSubmit$) ? _rawProps.onSubmit$ : [_rawProps.onSubmit$]), + /* @__PURE__ */ inlinedQrl( + async (_evt, form) => { + const nav2 = _captures[0]; + const formData = new FormData(form); + const params = new URLSearchParams(); + formData.forEach((value, key) => { + if (typeof value === 'string') { + params.append(key, value); + } + }); + await nav2('?' + params.toString(), { + type: 'form', + forceReload: true, + }); + }, + 'GetForm_component_form_q_e_submit_r3dkP9d2cF8', + [nav] + ), + /* @__PURE__ */ inlinedQrl((_evt, form) => { + if (form.getAttribute('data-spa-reset') === 'true') { + form.reset(); + } + form.dispatchEvent( + new CustomEvent('submitcompleted', { + bubbles: false, + cancelable: false, + composed: false, + detail: { + status: 200, + }, + }) + ); + }, 'GetForm_component_form_q_e_submit_1_cuYklZAOHrA'), + ], + }, + null, + /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, 'Q4_0'), + 0, + 'Q4_1' + ); + }, 'GetForm_component_2U5Z2Z8ryc0') +); const Form = ({ action, spaReset, reloadDocument, onSubmit$, ...rest }, key) => { if (action) { const isArrayApi = Array.isArray(onSubmit$); if (isArrayApi) { - return jsx$1( + return _jsxSplit( 'form', { - ...rest, - action: action.actionPath, + ..._getVarProps(rest), + ..._getConstProps(rest), + action: _wrapProp(action, 'actionPath'), 'preventdefault:submit': !reloadDocument, - onSubmit$: [ + 'q-e:submit': [ ...onSubmit$, // action.submit "submitcompleted" event for onSubmitCompleted$ events !reloadDocument - ? $((evt) => { - if (!action.submitted) { - return action.submit(evt); - } - }) + ? /* @__PURE__ */ inlinedQrl( + (evt) => { + const action2 = _captures[0]; + if (!action2.submitted) { + return action2.submit(evt); + } + }, + 'Form_form_q_e_submit_6i0Jq5q8JFg', + [action] + ) : void 0, ], - method: 'post', ['data-spa-reset']: spaReset ? 'true' : void 0, }, + { + method: 'post', + }, + null, + 0, key ); } - return jsx$1( + return _jsxSplit( 'form', { - ...rest, - action: action.actionPath, + ..._getVarProps(rest), + ..._getConstProps(rest), + action: _wrapProp(action, 'actionPath'), 'preventdefault:submit': !reloadDocument, - onSubmit$: [ + 'q-e:submit': [ // Since v2, this fires before the action is executed so it can be prevented onSubmit$, // action.submit "submitcompleted" event for onSubmitCompleted$ events !reloadDocument ? action.submit : void 0, ], - method: 'post', ['data-spa-reset']: spaReset ? 'true' : void 0, }, + { + method: 'post', + }, + null, + 0, key ); } else { - return /* @__PURE__ */ jsx( + return /* @__PURE__ */ _jsxSplit( GetForm, { spaReset, @@ -1632,49 +1972,13 @@ const Form = ({ action, spaReset, reloadDocument, onSubmit$, ...rest }, key) => onSubmit$, ...rest, }, + null, + null, + 0, key ); } }; -const GetForm = component$(({ action: _0, spaReset, reloadDocument, onSubmit$, ...rest }) => { - const nav = useNavigate(); - return /* @__PURE__ */ jsx('form', { - action: 'get', - 'preventdefault:submit': !reloadDocument, - 'data-spa-reset': spaReset ? 'true' : void 0, - ...rest, - onSubmit$: [ - ...(Array.isArray(onSubmit$) ? onSubmit$ : [onSubmit$]), - $(async (_evt, form) => { - const formData = new FormData(form); - const params = new URLSearchParams(); - formData.forEach((value, key) => { - if (typeof value === 'string') { - params.append(key, value); - } - }); - await nav('?' + params.toString(), { type: 'form', forceReload: true }); - }), - $((_evt, form) => { - if (form.getAttribute('data-spa-reset') === 'true') { - form.reset(); - } - form.dispatchEvent( - new CustomEvent('submitcompleted', { - bubbles: false, - cancelable: false, - composed: false, - detail: { - status: 200, - }, - }) - ); - }), - // end of array - ], - children: /* @__PURE__ */ jsx(Slot, {}), - }); -}); const untypedAppUrl = function appUrl(route, params, paramsPrefix = '') { const path = route.split('/'); @@ -1720,35 +2024,67 @@ const createRenderer = (getOptions) => { }; }; -const DocumentHeadTags = component$((props) => { - let head = useDocumentHead(); - if (props) { - head = { ...head, ...props }; - } - return /* @__PURE__ */ jsxs(Fragment, { - children: [ - head.title && /* @__PURE__ */ jsx('title', { children: head.title }), - head.meta.map((m) => /* @__PURE__ */ jsx('meta', { ...m })), - head.links.map((l) => /* @__PURE__ */ jsx('link', { ...l })), - head.styles.map((s) => { - const props2 = s.props || s; - return /* @__PURE__ */ createElement('style', { - ...props2, - dangerouslySetInnerHTML: s.style || props2.dangerouslySetInnerHTML, - key: s.key, - }); - }), - head.scripts.map((s) => { - const props2 = s.props || s; - return /* @__PURE__ */ createElement('script', { - ...props2, - dangerouslySetInnerHTML: s.script || props2.dangerouslySetInnerHTML, - key: s.key, - }); - }), - ], - }); -}); +const DocumentHeadTags = /* @__PURE__ */ componentQrl( + /* @__PURE__ */ inlinedQrl((props) => { + let head = useDocumentHead(); + if (props) { + head = { + ...head, + ...props, + }; + } + return /* @__PURE__ */ _jsxSorted( + Fragment, + null, + null, + [ + head.title && /* @__PURE__ */ _jsxSorted('title', null, null, head.title, 1, 'r5_0'), + head.meta.map((m) => + /* @__PURE__ */ _jsxSplit( + 'meta', + { + ..._getVarProps(m), + }, + _getConstProps(m), + null, + 0, + 'r5_1' + ) + ), + head.links.map((l) => + /* @__PURE__ */ _jsxSplit( + 'link', + { + ..._getVarProps(l), + }, + _getConstProps(l), + null, + 0, + 'r5_2' + ) + ), + head.styles.map((s) => { + const props2 = s.props || s; + return /* @__PURE__ */ createElement('style', { + ...props2, + dangerouslySetInnerHTML: s.style || props2.dangerouslySetInnerHTML, + key: s.key, + }); + }), + head.scripts.map((s) => { + const props2 = s.props || s; + return /* @__PURE__ */ createElement('script', { + ...props2, + dangerouslySetInnerHTML: s.script || props2.dangerouslySetInnerHTML, + key: s.key, + }); + }), + ], + 1, + 'r5_3' + ); + }, 'DocumentHeadTags_component_9CrWYOoCpgY') +); export { DocumentHeadTags, @@ -1769,17 +2105,12 @@ export { omitProps, routeAction$, routeActionQrl, - routeLoader$, - routeLoaderQrl, server$, serverQrl, untypedAppUrl, - useContent, useDocumentHead, useLocation, useNavigate, - usePreventNavigate$, - usePreventNavigateQrl, useQwikRouter, valibot$, valibotQrl, @@ -1789,106 +2120,73 @@ export { zodQrl, }; -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_GetForm_component_form_q_e_submit_1_0WWF0MwldwA.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_ErrorBoundary_component_useOnWindow_G0jFRpoNY0M.mjs (ENTRY POINT)== -export const GetForm_component_form_q_e_submit_1_0WWF0MwldwA = (_evt, form)=>{ - if (form.getAttribute('data-spa-reset') === 'true') form.reset(); - form.dispatchEvent(new CustomEvent('submitcompleted', { - bubbles: false, - cancelable: false, - composed: false, - detail: { - status: 200 - } - })); +import { _captures } from "@qwik.dev/core"; +// +export const ErrorBoundary_component_useOnWindow_G0jFRpoNY0M = (e)=>{ + const store2 = _captures[0]; + store2.error = e.detail.error; }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"+DAknDQ,CAAC,MAAM;IACP,IAAI,KAAK,YAAY,CAAC,sBAAsB,QAC1C,KAAK,KAAK;IAEZ,KAAK,aAAa,CAChB,IAAI,YAAY,mBAAmB;QACjC,SAAS;QACT,YAAY;QACZ,UAAU;QACV,QAAQ;YACN,QAAQ;QACV;IACF;AAEJ\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;+DA0GQ,CAAC;IACC,MAAM,SAAS,SAAS,CAAC,EAAE;IAC3B,OAAO,KAAK,GAAG,EAAE,MAAM,CAAC,KAAK;AAC/B\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "GetForm_component_form_q_e_submit_1_0WWF0MwldwA", + "name": "ErrorBoundary_component_useOnWindow_G0jFRpoNY0M", "entry": null, - "displayName": "index.qwik.mjs_GetForm_component_form_q_e_submit_1", - "hash": "0WWF0MwldwA", - "canonicalFilename": "index.qwik.mjs_GetForm_component_form_q_e_submit_1_0WWF0MwldwA", + "displayName": "index.qwik.mjs_ErrorBoundary_component_useOnWindow", + "hash": "G0jFRpoNY0M", + "canonicalFilename": "index.qwik.mjs_ErrorBoundary_component_useOnWindow_G0jFRpoNY0M", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", - "parent": "GetForm_component_OIWHwJ5eKxg", + "parent": "ErrorBoundary_component_pOa6vjtC7ik", "ctxKind": "function", - "ctxName": "$", - "captures": false, + "ctxName": "useOnWindow", + "captures": true, "loc": [ - 55126, - 55498 + 2433, + 2531 ], "paramNames": [ - "_evt", - "form" + "e" + ], + "captureNames": [ + "store" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_GetForm_component_OIWHwJ5eKxg.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_ErrorBoundary_component_pOa6vjtC7ik.mjs (ENTRY POINT)== -import { useNavigate } from "./index.qwik.mjs"; +import { Fragment } from "@qwik.dev/core/jsx-runtime"; import { Slot } from "@qwik.dev/core"; -import { _fnSignal } from "@qwik.dev/core"; -import { _getConstProps } from "@qwik.dev/core"; -import { _getVarProps } from "@qwik.dev/core"; import { _jsxSorted } from "@qwik.dev/core"; -import { _jsxSplit } from "@qwik.dev/core"; -import { _restProps } from "@qwik.dev/core"; import { qrl } from "@qwik.dev/core"; +import { useErrorBoundary } from "@qwik.dev/core"; +import { useOnWindow } from "@qwik.dev/core"; // -const _hf0 = (p0)=>!p0.reloadDocument; -const _hf0_str = "!p0.reloadDocument"; -const _hf1 = (p0)=>p0.spaReset ? 'true' : void 0; -const _hf1_str = 'p0.spaReset?"true":void 0'; -// -const q_GetForm_component_form_q_e_submit_1_0WWF0MwldwA = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_GetForm_component_form_q_e_submit_1_0WWF0MwldwA.mjs"), "GetForm_component_form_q_e_submit_1_0WWF0MwldwA"); -const q_GetForm_component_form_q_e_submit_D0PAP3eJ0Ng = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_GetForm_component_form_q_e_submit_D0PAP3eJ0Ng.mjs"), "GetForm_component_form_q_e_submit_D0PAP3eJ0Ng"); +const q_ErrorBoundary_component_useOnWindow_G0jFRpoNY0M = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_ErrorBoundary_component_useOnWindow_G0jFRpoNY0M.mjs"), "ErrorBoundary_component_useOnWindow_G0jFRpoNY0M"); // -export const GetForm_component_OIWHwJ5eKxg = (_rawProps)=>{ - const rest = _restProps(_rawProps, [ - "action", - "spaReset", - "reloadDocument", - "onSubmit$" - ]); - const nav = useNavigate(); - return /* @__PURE__ */ _jsxSplit('form', { - action: 'get', - 'preventdefault:submit': _fnSignal(_hf0, [ - _rawProps - ], _hf0_str), - 'data-spa-reset': _fnSignal(_hf1, [ - _rawProps - ], _hf1_str), - ..._getVarProps(rest), - ..._getConstProps(rest), - "q-e:submit": [ - ...Array.isArray(_rawProps.onSubmit$) ? _rawProps.onSubmit$ : [ - _rawProps.onSubmit$ - ], - q_GetForm_component_form_q_e_submit_D0PAP3eJ0Ng.w([ - nav - ]), - q_GetForm_component_form_q_e_submit_1_0WWF0MwldwA - ] - }, null, /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, "0K_10"), 0, "0K_11"); -}; - +export const ErrorBoundary_component_pOa6vjtC7ik = (props)=>{ + const store = useErrorBoundary(); + useOnWindow('qerror', q_ErrorBoundary_component_useOnWindow_G0jFRpoNY0M.w([ + store + ])); + if (store.error && props.fallback$) return /* @__PURE__ */ _jsxSorted(Fragment, null, null, props.fallback$(store.error), 1, 'bA_0'); + return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, 'bA_1'); +}; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;;mBAmmD6B,IAJuB;;mBAK9B,GALoB,WAKT,SAAS,KAAK;;;;;;6CALpB;;;;;;;IACzB,MAAM,MAAM;IACZ,OAAO,aAAa,GAAG,UAAI;QACzB,QAAQ;QACR,uBAAuB;;;QACvB,gBAAgB;;;wBACb;0BAAA;QACH,cAAW;eACL,MAAM,OAAO,WAR6C,uBAAA,YAQnB;0BARmB;aAQR;;;;;SA2BvD;aACS,aAAa,GAAG,WAAI;AAElC\"}") + +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;mDAqG6B,CAAC;IAC1B,MAAM,QAAQ;IACd,YACE;;;IAUF,IAAI,MAAM,KAAK,IAAI,MAAM,SAAS,EAChC,OAAO,aAAa,GAAG,WACrB,UACA,MACA,MACA,MAAM,SAAS,CAAC,MAAM,KAAK,GAC3B,GACA;IAGJ,OAAO,aAAa,GAAG,WAAW,MAAM,MAAM,MAAM,MAAM,GAAG;AAC/D\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "GetForm_component_OIWHwJ5eKxg", + "name": "ErrorBoundary_component_pOa6vjtC7ik", "entry": null, - "displayName": "index.qwik.mjs_GetForm_component", - "hash": "OIWHwJ5eKxg", - "canonicalFilename": "index.qwik.mjs_GetForm_component_OIWHwJ5eKxg", + "displayName": "index.qwik.mjs_ErrorBoundary_component", + "hash": "pOa6vjtC7ik", + "canonicalFilename": "index.qwik.mjs_ErrorBoundary_component_pOa6vjtC7ik", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, @@ -1896,76 +2194,180 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/ind "ctxName": "component$", "captures": false, "loc": [ - 54411, - 55582 + 2307, + 2908 ], "paramNames": [ - "_rawProps" + "props" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikRouter_useStyles_XNocHv0sxCQ.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Link_component_handlePrefetch_AGvVXzXKbms.mjs (ENTRY POINT)== -export const useQwikRouter_useStyles_XNocHv0sxCQ = '@layer qwik{@supports selector(html:active-view-transition-type(type)){html:active-view-transition-type(qwik-navigation){:root{view-transition-name:none}}}@supports not selector(html:active-view-transition-type(type)){:root{view-transition-name:none}}}'; +import { _auto_prefetchRoute as prefetchRoute } from "./index.qwik.mjs"; +import { _captures } from "@qwik.dev/core"; +// +export const Link_component_handlePrefetch_AGvVXzXKbms = (_, elm)=>{ + const head2 = _captures[0]; + if (navigator.connection?.saveData) return; + if (elm && elm.href) { + const url = new URL(elm.href); + prefetchRoute(url, elm.hasAttribute('data-prefetch'), 0.8, head2.manifestHash); + } +}; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"mDA0RE\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;yDAyMU,CAAC,GAAG;IACF,MAAM,QAAQ,SAAS,CAAC,EAAE;IAC1B,IAAI,UAAU,UAAU,EAAE,UACxB;IAEF,IAAI,OAAO,IAAI,IAAI,EAAE;QACnB,MAAM,MAAM,IAAI,IAAI,IAAI,IAAI;QAC5B,cAAc,KAAK,IAAI,YAAY,CAAC,kBAAkB,KAAK,MAAM,YAAY;IAC/E;AACF\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "useQwikRouter_useStyles_XNocHv0sxCQ", + "name": "Link_component_handlePrefetch_AGvVXzXKbms", "entry": null, - "displayName": "index.qwik.mjs_useQwikRouter_useStyles", - "hash": "XNocHv0sxCQ", - "canonicalFilename": "index.qwik.mjs_useQwikRouter_useStyles_XNocHv0sxCQ", + "displayName": "index.qwik.mjs_Link_component_handlePrefetch", + "hash": "AGvVXzXKbms", + "canonicalFilename": "index.qwik.mjs_Link_component_handlePrefetch_AGvVXzXKbms", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", - "parent": null, + "parent": "Link_component_VPmar9tb3t4", "ctxKind": "function", - "ctxName": "useStyles$", + "ctxName": "handlePrefetch", + "captures": true, + "loc": [ + 5216, + 5555 + ], + "paramNames": [ + "_", + "elm" + ], + "captureNames": [ + "head" + ] +} +*/ +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Link_component_handleClientSideNavigation_h3qenoGeI6M.mjs (ENTRY POINT)== + +import { _captures } from "@qwik.dev/core"; +// +export const Link_component_handleClientSideNavigation_h3qenoGeI6M = (event, elm)=>{ + const nav2 = _captures[0], reload2 = _captures[1], replaceState2 = _captures[2], scroll2 = _captures[3]; + if (event.defaultPrevented) { + if (elm.href) { + elm.setAttribute('aria-pressed', 'true'); + nav2(elm.href, { + forceReload: reload2, + replaceState: replaceState2, + scroll: scroll2 + }).then(()=>{ + elm.removeAttribute('aria-pressed'); + }); + } + } +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;qEAgOU,CAAC,OAAO;IACN,MAAM,OAAO,SAAS,CAAC,EAAE,EACvB,UAAU,SAAS,CAAC,EAAE,EACtB,gBAAgB,SAAS,CAAC,EAAE,EAC5B,UAAU,SAAS,CAAC,EAAE;IACxB,IAAI,MAAM,gBAAgB,EACxB;QAAA,IAAI,IAAI,IAAI,EAAE;YACZ,IAAI,YAAY,CAAC,gBAAgB;YACjC,KAAK,IAAI,IAAI,EAAE;gBACb,aAAa;gBACb,cAAc;gBACd,QAAQ;YACV,GAAG,IAAI,CAAC;gBACN,IAAI,eAAe,CAAC;YACtB;QACF;IAAA;AAEJ\"}") +/* +{ + "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", + "name": "Link_component_handleClientSideNavigation_h3qenoGeI6M", + "entry": null, + "displayName": "index.qwik.mjs_Link_component_handleClientSideNavigation", + "hash": "h3qenoGeI6M", + "canonicalFilename": "index.qwik.mjs_Link_component_handleClientSideNavigation_h3qenoGeI6M", + "path": "../node_modules/@qwik.dev/router", + "extension": "mjs", + "parent": "Link_component_VPmar9tb3t4", + "ctxKind": "function", + "ctxName": "handleClientSideNavigation", + "captures": true, + "loc": [ + 6088, + 6698 + ], + "paramNames": [ + "event", + "elm" + ], + "captureNames": [ + "nav", + "reload", + "replaceState", + "scroll" + ] +} +*/ +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Link_component_handlePreload_AAemwtuBjsE.mjs (ENTRY POINT)== + +import { _auto_prefetchRoute as prefetchRoute } from "./index.qwik.mjs"; +// +export const Link_component_handlePreload_AAemwtuBjsE = (_, elm)=>{ + const url = new URL(elm.href); + prefetchRoute(url.pathname, false, 1); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;wDAsPqD,CAAC,GAAG;IACnD,MAAM,MAAM,IAAI,IAAI,IAAI,IAAI;IAC5B,cAAc,IAAI,QAAQ,EAAE,OAAO;AACrC\"}") +/* +{ + "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", + "name": "Link_component_handlePreload_AAemwtuBjsE", + "entry": null, + "displayName": "index.qwik.mjs_Link_component_handlePreload", + "hash": "AAemwtuBjsE", + "canonicalFilename": "index.qwik.mjs_Link_component_handlePreload_AAemwtuBjsE", + "path": "../node_modules/@qwik.dev/router", + "extension": "mjs", + "parent": "Link_component_VPmar9tb3t4", + "ctxKind": "function", + "ctxName": "handlePreload", "captures": false, "loc": [ - 8723, - 8977 + 6892, + 6993 + ], + "paramNames": [ + "_", + "elm" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Link_component_useVisibleTask_6K6z063D0C4.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Link_component_useVisibleTask_xKeuRmnoNSA.mjs (ENTRY POINT)== import { _captures } from "@qwik.dev/core"; import { isDev } from "@qwik.dev/core"; // -export const Link_component_useVisibleTask_6K6z063D0C4 = ({ track })=>{ - const anchorRef = _captures[0], handlePrefetch = _captures[1], linkProps = _captures[2], loc = _captures[3]; - track(()=>loc.url.pathname); - const handler = linkProps.onQVisible$; +export const Link_component_useVisibleTask_xKeuRmnoNSA = ({ track })=>{ + const anchorRef2 = _captures[0], handlePrefetch2 = _captures[1], linkProps2 = _captures[2], loc2 = _captures[3]; + track(()=>loc2.url.pathname); + const handler = linkProps2.onQVisible$; if (handler) { const event = new CustomEvent('qvisible'); - if (Array.isArray(handler)) handler.flat(10).forEach((handler2)=>handler2?.(event, anchorRef.value)); - else handler?.(event, anchorRef.value); + if (Array.isArray(handler)) handler.flat(10).forEach((handler2)=>handler2?.(event, anchorRef2.value)); + else handler?.(event, anchorRef2.value); } - if (!isDev && anchorRef.value) handlePrefetch?.(void 0, anchorRef.value); + if (!isDev && anchorRef2.value) handlePrefetch2?.(void 0, anchorRef2.value); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;yDA4KkB,CAAC,EAAE,KAAK,EAAE;;IACxB,MAAM,IAAM,IAAI,GAAG,CAAC,QAAQ;IAC5B,MAAM,UAAU,UAAU,WAAW;IACrC,IAAI,SAAS;QACX,MAAM,QAAQ,IAAI,YAAY;QAC9B,IAAI,MAAM,OAAO,CAAC,UAChB,QAAQ,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,WAAa,WAAW,OAAO,UAAU,KAAK;aAExE,UAAU,OAAO,UAAU,KAAK;IAEpC;IACA,IAAI,CAAC,SAAS,UAAU,KAAK,EAC3B,iBAAiB,KAAK,GAAG,UAAU,KAAK\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;yDA4PQ,CAAC,EAAE,KAAK,EAAE;IACR,MAAM,aAAa,SAAS,CAAC,EAAE,EAC7B,kBAAkB,SAAS,CAAC,EAAE,EAC9B,aAAa,SAAS,CAAC,EAAE,EACzB,OAAO,SAAS,CAAC,EAAE;IACrB,MAAM,IAAM,KAAK,GAAG,CAAC,QAAQ;IAC7B,MAAM,UAAU,WAAW,WAAW;IACtC,IAAI,SAAS;QACX,MAAM,QAAQ,IAAI,YAAY;QAC9B,IAAI,MAAM,OAAO,CAAC,UAChB,QAAQ,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,WAAa,WAAW,OAAO,WAAW,KAAK;aAEzE,UAAU,OAAO,WAAW,KAAK;IAErC;IACA,IAAI,CAAC,SAAS,WAAW,KAAK,EAC5B,kBAAkB,KAAK,GAAG,WAAW,KAAK;AAE9C\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "Link_component_useVisibleTask_6K6z063D0C4", + "name": "Link_component_useVisibleTask_xKeuRmnoNSA", "entry": null, "displayName": "index.qwik.mjs_Link_component_useVisibleTask", - "hash": "6K6z063D0C4", - "canonicalFilename": "index.qwik.mjs_Link_component_useVisibleTask_6K6z063D0C4", + "hash": "xKeuRmnoNSA", + "canonicalFilename": "index.qwik.mjs_Link_component_useVisibleTask_xKeuRmnoNSA", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", - "parent": "Link_component_nhj84CU1784", + "parent": "Link_component_VPmar9tb3t4", "ctxKind": "function", "ctxName": "useVisibleTask$", "captures": true, "loc": [ - 5200, - 5650 + 7105, + 7805 ], "paramNames": [ "{track}" @@ -1978,57 +2380,98 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/ind ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_RouterOutlet_component_hKWOEO9aIDM.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Link_component_VPmar9tb3t4.mjs (ENTRY POINT)== -import { _auto_ContentInternalContext as ContentInternalContext } from "./index.qwik.mjs"; -import { _auto_spaInit as spaInit } from "./index.qwik.mjs"; -import { Fragment } from "@qwik.dev/core/jsx-runtime"; -import { SkipRender } from "@qwik.dev/core"; +import { Slot } from "@qwik.dev/core"; +import { _getConstProps } from "@qwik.dev/core"; +import { _getVarProps } from "@qwik.dev/core"; import { _jsxSorted } from "@qwik.dev/core"; +import { _jsxSplit } from "@qwik.dev/core"; import { _qrlSync } from "@qwik.dev/core"; -import { useContext } from "@qwik.dev/core"; -import { useServerData } from "@qwik.dev/core"; +import { g as getClientNavPath } from "./chunks/head.qwik.mjs"; +import { qrl } from "@qwik.dev/core"; +import { s as shouldPreload } from "./chunks/head.qwik.mjs"; +import { untrack } from "@qwik.dev/core"; +import { b as useDocumentHead } from "./chunks/use-functions.qwik.mjs"; +import { a as useLocation } from "./chunks/use-functions.qwik.mjs"; +import { u as useNavigate } from "./chunks/use-functions.qwik.mjs"; +import { useSignal } from "@qwik.dev/core"; +import { useVisibleTaskQrl } from "@qwik.dev/core"; // -export const RouterOutlet_component_hKWOEO9aIDM = ()=>{ - const serverData = useServerData('containerAttributes'); - if (!serverData) throw new Error('PrefetchServiceWorker component must be rendered on the server.'); - const internalContext = useContext(ContentInternalContext); - const contents = internalContext.value; - if (contents && contents.length > 0) { - const contentsLen = contents.length; - let cmp = null; - for(let i = contentsLen - 1; i >= 0; i--)if (contents[i].default) cmp = _jsxSorted(contents[i].default, null, null, cmp, 1, "0K_6"); - return /* @__PURE__ */ _jsxSorted(Fragment, null, null, [ - cmp, - !__EXPERIMENTAL__.noSPA && /* @__PURE__ */ _jsxSorted('script', { - "q-d:qinit": _qrlSync(()=>{ - ((w, h)=>{ - if (!w._qcs && h.scrollRestoration === 'manual') { - w._qcs = true; - const s = h.state?._qRouterScroll; - if (s) w.scrollTo(s.x, s.y); - document.dispatchEvent(new Event('qcinit')); - } - })(window, history); - }, '()=>{((w,h)=>{if(!w._qcs&&h.scrollRestoration==="manual"){w._qcs=true;const s=h.state?._qRouterScroll;if(s){w.scrollTo(s.x,s.y);}document.dispatchEvent(new Event("qcinit"));}})(window,history);}') - }, { - "q-d:qcinit": spaInit - }, null, 2, "0K_7") - ], 1, "0K_8"); - } - return SkipRender; +const q_Link_component_handleClientSideNavigation_h3qenoGeI6M = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Link_component_handleClientSideNavigation_h3qenoGeI6M.mjs"), "Link_component_handleClientSideNavigation_h3qenoGeI6M"); +const q_Link_component_handlePrefetch_AGvVXzXKbms = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Link_component_handlePrefetch_AGvVXzXKbms.mjs"), "Link_component_handlePrefetch_AGvVXzXKbms"); +const q_Link_component_handlePreload_AAemwtuBjsE = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Link_component_handlePreload_AAemwtuBjsE.mjs"), "Link_component_handlePreload_AAemwtuBjsE"); +const q_Link_component_useVisibleTask_xKeuRmnoNSA = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Link_component_useVisibleTask_xKeuRmnoNSA.mjs"), "Link_component_useVisibleTask_xKeuRmnoNSA"); +// +export const Link_component_VPmar9tb3t4 = (props)=>{ + const nav = useNavigate(); + const loc = useLocation(); + const head = useDocumentHead(); + const originalHref = props.href; + const anchorRef = useSignal(); + const { onClick$, prefetch: prefetchProp, reload, replaceState, scroll, ...linkProps } = /* @__PURE__ */ (()=>props)(); + const clientNavPath = untrack(getClientNavPath, { + ...linkProps, + reload + }, loc); + linkProps.href = clientNavPath || originalHref; + const prefetchData = !!clientNavPath && prefetchProp !== false && prefetchProp !== 'js' || void 0; + const prefetch = prefetchData || !!clientNavPath && prefetchProp !== false && untrack(shouldPreload, clientNavPath, loc); + const handlePrefetch = prefetch ? q_Link_component_handlePrefetch_AGvVXzXKbms.w([ + head + ]) : void 0; + const preventDefault = clientNavPath ? _qrlSync((event)=>{ + if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) event.preventDefault(); + }, 'event=>{if(!(event.metaKey||event.ctrlKey||event.shiftKey||event.altKey)){event.preventDefault();}}') : void 0; + const handleClientSideNavigation = clientNavPath ? q_Link_component_handleClientSideNavigation_h3qenoGeI6M.w([ + nav, + reload, + replaceState, + scroll + ]) : void 0; + const handlePreload = q_Link_component_handlePreload_AAemwtuBjsE; + useVisibleTaskQrl(q_Link_component_useVisibleTask_xKeuRmnoNSA.w([ + anchorRef, + handlePrefetch, + linkProps, + loc + ])); + return /* @__PURE__ */ _jsxSplit('a', { + ref: anchorRef, + 'q:link': !!clientNavPath, + ..._getVarProps(linkProps), + ..._getConstProps(linkProps), + 'q-e:click': [ + preventDefault, + handlePreload, + onClick$, + handleClientSideNavigation + ], + 'data-prefetch': prefetchData, + 'q-e:mouseover': [ + linkProps.onMouseOver$, + handlePrefetch + ], + 'q-e:focus': [ + linkProps.onFocus$, + handlePrefetch + ] + }, { + // We need to prevent the onQVisible$ from being called twice since it is handled in the visible task + 'q-e:qvisible': [] + }, /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, 'jO_0'), 0, 'jO_1'); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;kDAqkCgC;IAC9B,MAAM,aAAa,cAAc;IACjC,IAAI,CAAC,YACH,MAAM,IAAI,MAAM;IAElB,MAAM,kBAAkB,WAAW;IACnC,MAAM,WAAW,gBAAgB,KAAK;IACtC,IAAI,YAAY,SAAS,MAAM,GAAG,GAAG;QACnC,MAAM,cAAc,SAAS,MAAM;QACnC,IAAI,MAAM;QACV,IAAK,IAAI,IAAI,cAAc,GAAG,KAAK,GAAG,IACpC,IAAI,QAAQ,CAAC,EAAE,CAAC,OAAO,EACrB,MAAM,WAAM,QAAQ,CAAC,EAAE,CAAC,OAAO,cACnB;QAIhB,OAAO,aAAa,GAAG,WAAK,sBAChB;YACR;YACA,CAAC,iBAAiB,KAAK,IACrB,aAAa,GAAG,WAAI;gBAElB,WAAmB,WAAQ;oBACzB,CAAC,CAAC,GAAG;wBACH,IAAI,CAAC,EAAE,IAAI,IAAI,EAAE,iBAAiB,KAAK,UAAU;4BAC/C,EAAE,IAAI,GAAG;4BACT,MAAM,IAAI,EAAE,KAAK,EAAE;4BACnB,IAAI,GACF,EAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;4BAErB,SAAS,aAAa,CAAC,IAAI,MAAM;wBACnC;oBACF,CAAC,EAAE,QAAQ;gBACb;;gBAZA,cAAsB;;SAc3B;IAEL;IACA,OAAO;AACT\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;;;;;;;;0CA2K6B,CAAC;IAC1B,MAAM,MAAM;IACZ,MAAM,MAAM;IACZ,MAAM,OAAO;IACb,MAAM,eAAe,MAAM,IAAI;IAC/B,MAAM,YAAY;IAClB,MAAM,EACJ,QAAQ,EACR,UAAU,YAAY,EACtB,MAAM,EACN,YAAY,EACZ,MAAM,EACN,GAAG,WACJ,GAAG,aAAa,GAAG,CAAC,IAAM,KAAK;IAChC,MAAM,gBAAgB,QACpB,kBACA;QACE,GAAG,SAAS;QACZ;IACF,GACA;IAEF,UAAU,IAAI,GAAG,iBAAiB;IAClC,MAAM,eACJ,AAAC,CAAC,CAAC,iBAAiB,iBAAiB,SAAS,iBAAiB,QAAS,KAAK;IAC/E,MAAM,WACJ,gBACC,CAAC,CAAC,iBAAiB,iBAAiB,SAAS,QAAQ,eAAe,eAAe;IACtF,MAAM,iBAAiB;;SAenB,KAAK;IACT,MAAM,iBAAiB,gBACnB,SAAS,CAAC;QACR,IAAI,CAAC,CAAC,MAAM,OAAO,IAAI,MAAM,OAAO,IAAI,MAAM,QAAQ,IAAI,MAAM,MAAM,GACpE,MAAM,cAAc;IAExB,GAAG,yGACH,KAAK;IACT,MAAM,6BAA6B;;;;;SAuB/B,KAAK;IACT,MAAM;IAIN;;;;;;IAyBA,OAAO,aAAa,GAAG,UACrB,KACA;QACE,KAAK;QACL,UAAU,CAAC,CAAC;QACZ,GAAG,aAAa,UAAU;QAC1B,GAAG,eAAe,UAAU;QAC5B,aAAa;YAAC;YAAgB;YAAe;YAAU;SAA2B;QAClF,iBAAiB;QACjB,iBAAiB;YAAC,UAAU,YAAY;YAAE;SAAe;QACzD,aAAa;YAAC,UAAU,QAAQ;YAAE;SAAe;IACnD,GACA;QACE,qGAAqG;QACrG,gBAAgB,EAAE;IACpB,GACA,aAAa,GAAG,WAAW,MAAM,MAAM,MAAM,MAAM,GAAG,SACtD,GACA;AAEJ\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "RouterOutlet_component_hKWOEO9aIDM", + "name": "Link_component_VPmar9tb3t4", "entry": null, - "displayName": "index.qwik.mjs_RouterOutlet_component", - "hash": "hKWOEO9aIDM", - "canonicalFilename": "index.qwik.mjs_RouterOutlet_component_hKWOEO9aIDM", + "displayName": "index.qwik.mjs_Link_component", + "hash": "VPmar9tb3t4", + "canonicalFilename": "index.qwik.mjs_Link_component_VPmar9tb3t4", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, @@ -2036,879 +2479,833 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/ind "ctxName": "component$", "captures": false, "loc": [ - 37647, - 38909 + 4351, + 8639 + ], + "paramNames": [ + "props" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs == +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_spa_init_event_igI1pUsax0E.mjs (ENTRY POINT)== -import { componentQrl } from "@qwik.dev/core"; -import { qrl } from "@qwik.dev/core"; -import { _jsxSorted } from "@qwik.dev/core"; -import { useVisibleTaskQrl } from "@qwik.dev/core"; -import { _getVarProps } from "@qwik.dev/core"; -import { _getConstProps } from "@qwik.dev/core"; -import { _jsxSplit } from "@qwik.dev/core"; -import { eventQrl } from "@qwik.dev/core"; -import { useStylesQrl } from "@qwik.dev/core"; -import { useTaskQrl } from "@qwik.dev/core"; -import { _wrapProp } from "@qwik.dev/core"; -import { createContextId, useContext, implicit$FirstArg, noSerialize, useServerData, useSignal, untrack, isDev, withLocale, isServer, useStore, useContextProvider } from '@qwik.dev/core'; -import { a as isSamePath, c as createLoaderSignal, D as DEFAULT_LOADERS_SERIALIZATION_STRATEGY, h as QACTION_KEY } from './chunks/routing.qwik.mjs'; -import { _getContextContainer, SerializerSymbol, _UNINITIALIZED, _getContextHostElement, _resolveContextWithoutSequentialScope } from '@qwik.dev/core/internal'; -import * as v from 'valibot'; -import * as z from 'zod'; -import swRegister from '@qwik-router-sw-register'; -import { renderToStream } from '@qwik.dev/core/server'; -import '@qwik.dev/core/preloader'; -import './chunks/types.qwik.mjs'; -// -const q_DocumentHeadTags_component_LaCcLS5Bz4c = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_DocumentHeadTags_component_LaCcLS5Bz4c.mjs"), "DocumentHeadTags_component_LaCcLS5Bz4c"); -const q_ErrorBoundary_component_yTCHi5s1o00 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_ErrorBoundary_component_yTCHi5s1o00.mjs"), "ErrorBoundary_component_yTCHi5s1o00"); -const q_Form_form_q_e_submit_iIfbMzzXpIA = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Form_form_q_e_submit_iIfbMzzXpIA.mjs"), "Form_form_q_e_submit_iIfbMzzXpIA"); -const q_GetForm_component_OIWHwJ5eKxg = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_GetForm_component_OIWHwJ5eKxg.mjs"), "GetForm_component_OIWHwJ5eKxg"); -const q_Link_component_nhj84CU1784 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Link_component_nhj84CU1784.mjs"), "Link_component_nhj84CU1784"); -const q_QwikRouterMockProvider_component_kN7AQXV0aXo = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_QwikRouterMockProvider_component_kN7AQXV0aXo.mjs"), "QwikRouterMockProvider_component_kN7AQXV0aXo"); -const q_QwikRouterProvider_component_lCQXGdS0iZM = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_QwikRouterProvider_component_lCQXGdS0iZM.mjs"), "QwikRouterProvider_component_lCQXGdS0iZM"); -const q_RouterOutlet_component_hKWOEO9aIDM = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_RouterOutlet_component_hKWOEO9aIDM.mjs"), "RouterOutlet_component_hKWOEO9aIDM"); -const q_routeActionQrl_action_submit_JY3C42B1B08 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_routeActionQrl_action_submit_JY3C42B1B08.mjs"), "routeActionQrl_action_submit_JY3C42B1B08"); -const q_serverQrl_RA3PmZ4Oyak = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_serverQrl_RA3PmZ4Oyak.mjs"), "serverQrl_RA3PmZ4Oyak"); -const q_spaInit_event_Js1cotabL5I = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_spaInit_event_Js1cotabL5I.mjs"), "spaInit_event_Js1cotabL5I"); -const q_usePreventNavigateQrl_useVisibleTask_no0bm2fybZo = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_usePreventNavigateQrl_useVisibleTask_no0bm2fybZo.mjs"), "usePreventNavigateQrl_useVisibleTask_no0bm2fybZo"); -// -qrl(()=>import("./index.qwik.mjs_useQwikMockRouter_goto_ojVznvSDqoM.mjs"), "useQwikMockRouter_goto_ojVznvSDqoM"); -qrl(()=>import("./index.qwik.mjs_useQwikMockRouter_useTask_oml2hW1aK6I.mjs"), "useQwikMockRouter_useTask_oml2hW1aK6I"); -// -const q_useQwikRouter_goto_OSnb99dm7Ow = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikRouter_goto_OSnb99dm7Ow.mjs"), "useQwikRouter_goto_OSnb99dm7Ow"); -const q_useQwikRouter_registerPreventNav_W0LIs8PUJoA = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikRouter_registerPreventNav_W0LIs8PUJoA.mjs"), "useQwikRouter_registerPreventNav_W0LIs8PUJoA"); -const q_useQwikRouter_useStyles_XNocHv0sxCQ = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikRouter_useStyles_XNocHv0sxCQ.mjs"), "useQwikRouter_useStyles_XNocHv0sxCQ"); -const q_useQwikRouter_useTask_omhKiQfdzZU = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikRouter_useTask_omhKiQfdzZU.mjs"), "useQwikRouter_useTask_omhKiQfdzZU"); +import { isDev } from "@qwik.dev/core"; // -export { z } from 'zod'; -const ErrorBoundary = /*#__PURE__*/ componentQrl(q_ErrorBoundary_component_yTCHi5s1o00); -const RouteStateContext = /* @__PURE__ */ createContextId('qc-s'); -const ContentContext = /* @__PURE__ */ createContextId('qc-c'); -const ContentInternalContext = /* @__PURE__ */ createContextId('qc-ic'); -const DocumentHeadContext = /* @__PURE__ */ createContextId('qc-h'); -const RouteLocationContext = /* @__PURE__ */ createContextId('qc-l'); -const RouteNavigateContext = /* @__PURE__ */ createContextId('qc-n'); -const RouteActionContext = /* @__PURE__ */ createContextId('qc-a'); -const RoutePreventNavigateContext = /* @__PURE__ */ createContextId('qc-p'); -const useContent = ()=>useContext(ContentContext); -const useDocumentHead = ()=>useContext(DocumentHeadContext); -const useLocation = ()=>useContext(RouteLocationContext); -const useNavigate = ()=>useContext(RouteNavigateContext); -const usePreventNavigateQrl = (fn)=>{ - if (!__EXPERIMENTAL__.preventNavigate) throw new Error('usePreventNavigate$ is experimental and must be enabled with `experimental: ["preventNavigate"]` in the `qwikVite` plugin.'); - const registerPreventNav = useContext(RoutePreventNavigateContext); - useVisibleTaskQrl(q_usePreventNavigateQrl_useVisibleTask_no0bm2fybZo.w([ - fn, - registerPreventNav - ])); -}; -const usePreventNavigate$ = implicit$FirstArg(usePreventNavigateQrl); -const useAction = ()=>useContext(RouteActionContext); -const useQwikRouterEnv = ()=>noSerialize(useServerData('qwikrouter')); -const Link = /*#__PURE__*/ componentQrl(q_Link_component_nhj84CU1784); -const createDocumentHead = (defaults)=>({ - title: defaults?.title || '', - meta: [ - ...defaults?.meta || [] - ], - links: [ - ...defaults?.links || [] - ], - styles: [ - ...defaults?.styles || [] - ], - scripts: [ - ...defaults?.scripts || [] - ], - frontmatter: { - ...defaults?.frontmatter +export const spa_init_event_igI1pUsax0E = (_, el)=>{ + if (!window._qRouterSPA && !window._qRouterInitPopstate) { + const currentPath = location.pathname + location.search; + const checkAndScroll = (scrollState)=>{ + if (scrollState) window.scrollTo(scrollState.x, scrollState.y); + }; + const currentScrollState = ()=>{ + const elm = document.documentElement; + return { + x: elm.scrollLeft, + y: elm.scrollTop, + w: Math.max(elm.scrollWidth, elm.clientWidth), + h: Math.max(elm.scrollHeight, elm.clientHeight) + }; + }; + const saveScrollState = (scrollState)=>{ + const state = history.state || {}; + state._qRouterScroll = scrollState || currentScrollState(); + history.replaceState(state, ''); + }; + saveScrollState(); + window._qRouterInitPopstate = ()=>{ + if (window._qRouterSPA) return; + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + if (currentPath !== location.pathname + location.search) { + const getContainer = (el2)=>el2.closest('[q\\:container]:not([q\\:container=html]):not([q\\:container=text])'); + const container = getContainer(el); + const domContainer = container.qContainer; + const hostElement = domContainer.vNodeLocate(el); + const nav = domContainer?.resolveContext(hostElement, { + id: 'qr-n' + }); + if (nav) nav(location.href, { + type: 'popstate' + }); + else location.reload(); + } else if (history.scrollRestoration === 'manual') { + const scrollState = history.state?._qRouterScroll; + checkAndScroll(scrollState); + window._qRouterScrollEnabled = true; + } + }; + if (!window._qRouterHistoryPatch) { + window._qRouterHistoryPatch = true; + const pushState = history.pushState; + const replaceState = history.replaceState; + const prepareState = (state)=>{ + if (state === null || typeof state === 'undefined') state = {}; + else if (state?.constructor !== Object) { + state = { + _data: state + }; + if (isDev) console.warn('In a Qwik SPA context, `history.state` is used to store scroll state. Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. We need to be able to automatically attach the scroll state to your state object. A new state object has been created, your data has been moved to: `history.state._data`'); + } + state._qRouterScroll = state._qRouterScroll || currentScrollState(); + return state; + }; + history.pushState = (state, title, url)=>{ + state = prepareState(state); + return pushState.call(history, state, title, url); + }; + history.replaceState = (state, title, url)=>{ + state = prepareState(state); + return replaceState.call(history, state, title, url); + }; } - }); -const hashScroll = (toUrl, fromUrl)=>{ - const elmId = toUrl.hash.slice(1); - const elm = elmId && document.getElementById(elmId); - if (elm) { - elm.scrollIntoView(); - return true; - } else if (!elm && toUrl.hash && isSamePath(toUrl, fromUrl)) return true; - return false; -}; -const restoreScroll = (type, toUrl, fromUrl, scroller, scrollState)=>{ - if (type === 'popstate' && scrollState) scroller.scrollTo(scrollState.x, scrollState.y); - else if (type === 'link' || type === 'form') { - if (!hashScroll(toUrl, fromUrl)) scroller.scrollTo(0, 0); + window._qRouterInitAnchors = (event)=>{ + if (window._qRouterSPA || event.defaultPrevented) return; + const target = event.target.closest('a[href]'); + if (target && !target.hasAttribute('preventdefault:click')) { + const href = target.getAttribute('href'); + const prev = new URL(location.href); + const dest = new URL(href, prev); + const sameOrigin = dest.origin === prev.origin; + const samePath = dest.pathname + dest.search === prev.pathname + prev.search; + if (sameOrigin && samePath) { + event.preventDefault(); + if (dest.href !== prev.href) history.pushState(null, '', dest); + if (!dest.hash) { + if (dest.href.endsWith('#')) window.scrollTo(0, 0); + else { + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + saveScrollState({ + ...currentScrollState(), + x: 0, + y: 0 + }); + location.reload(); + } + } else { + const elmId = dest.hash.slice(1); + const elm = document.getElementById(elmId); + if (elm) elm.scrollIntoView(); + } + } + } + }; + window._qRouterInitVisibility = ()=>{ + if (!window._qRouterSPA && window._qRouterScrollEnabled && document.visibilityState === 'hidden') saveScrollState(); + }; + window._qRouterInitScroll = ()=>{ + if (window._qRouterSPA || !window._qRouterScrollEnabled) return; + clearTimeout(window._qRouterScrollDebounce); + window._qRouterScrollDebounce = setTimeout(()=>{ + saveScrollState(); + window._qRouterScrollDebounce = void 0; + }, 200); + }; + window._qRouterScrollEnabled = true; + setTimeout(()=>{ + window.addEventListener('popstate', window._qRouterInitPopstate); + window.addEventListener('scroll', window._qRouterInitScroll, { + passive: true + }); + document.addEventListener('click', window._qRouterInitAnchors); + if (!window.navigation) document.addEventListener('visibilitychange', window._qRouterInitVisibility, { + passive: true + }); + }, 0); } }; -const getScrollHistory = ()=>{ - const state = history.state; - return state?._qRouterScroll; -}; -const spaInit = eventQrl(q_spaInit_event_Js1cotabL5I); -const QWIK_CITY_SCROLLER = '_qCityScroller'; -const QWIK_ROUTER_SCROLLER = '_qRouterScroller'; -const preventNav = {}; -const internalState = { - navCount: 0 -}; -const useQwikRouter = (props)=>{ - if (!isServer) throw new Error('useQwikRouter can only run during SSR on the server. If you are seeing this, it means you are re-rendering the root of your application. Fix that or use the component around the root of your application.'); - useStylesQrl(q_useQwikRouter_useStyles_XNocHv0sxCQ); - const env = useQwikRouterEnv(); - if (!env?.params) throw new Error(`Missing Qwik Router Env Data for help visit https://github.com/QwikDev/qwik/issues/6237`); - const urlEnv = useServerData('url'); - if (!urlEnv) throw new Error(`Missing Qwik URL Env Data`); - const serverHead = useServerData('documentHead'); - if (env.ev.originalUrl.pathname !== env.ev.url.pathname && !__EXPERIMENTAL__.enableRequestRewrite) throw new Error(`enableRequestRewrite is an experimental feature and is not enabled. Please enable the feature flag by adding \`experimental: ["enableRequestRewrite"]\` to your qwikVite plugin options.`); - const url = new URL(urlEnv); - const routeLocationTarget = { - url, - params: env.params, - isNavigating: false, - prevUrl: void 0 - }; - const routeLocation = useStore(routeLocationTarget, { - deep: false - }); - const navResolver = {}; - const container = _getContextContainer(); - const getSerializationStrategy = (loaderId)=>{ - return env.response.loadersSerializationStrategy.get(loaderId) || DEFAULT_LOADERS_SERIALIZATION_STRATEGY; - }; - const loadersObject = {}; - const loaderState = {}; - for (const [key, value] of Object.entries(env.response.loaders)){ - loadersObject[key] = value; - loaderState[key] = createLoaderSignal(loadersObject, key, url, getSerializationStrategy(key), container); - } - loadersObject[SerializerSymbol] = (obj)=>{ - const loadersSerializationObject = {}; - for (const [k, v] of Object.entries(obj))loadersSerializationObject[k] = getSerializationStrategy(k) === 'always' ? v : _UNINITIALIZED; - return loadersSerializationObject; - }; - const routeInternal = useSignal({ - type: 'initial', - dest: url, - scroll: true - }); - const documentHead = useStore(()=>createDocumentHead(serverHead)); - const content = useStore({ - headings: void 0, - menu: void 0 - }); - const contentInternal = useSignal(); - const currentActionId = env.response.action; - const currentAction = currentActionId ? env.response.loaders[currentActionId] : void 0; - const actionState = useSignal(currentAction ? { - id: currentActionId, - data: env.response.formData, - output: { - result: currentAction, - status: env.response.status + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"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\"}") +/* +{ + "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", + "name": "spa_init_event_igI1pUsax0E", + "entry": null, + "displayName": "index.qwik.mjs_spa_init_event", + "hash": "igI1pUsax0E", + "canonicalFilename": "index.qwik.mjs_spa_init_event_igI1pUsax0E", + "path": "../node_modules/@qwik.dev/router", + "extension": "mjs", + "parent": null, + "ctxKind": "function", + "ctxName": "event$", + "captures": false, + "loc": [ + 10838, + 16625 + ], + "paramNames": [ + "_", + "el" + ] +} +*/ +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_qwik_view_transition_css_inline_vNfd9raIMI0.mjs (ENTRY POINT)== + +export const qwik_view_transition_css_inline_vNfd9raIMI0 = '@layer qwik{@supports selector(html:active-view-transition-type(type)){html:active-view-transition-type(qwik-navigation){:root{view-transition-name:none}}}@supports not selector(html:active-view-transition-type(type)){:root{view-transition-name:none}}}'; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"2DAoUE\"}") +/* +{ + "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", + "name": "qwik_view_transition_css_inline_vNfd9raIMI0", + "entry": null, + "displayName": "index.qwik.mjs_qwik_view_transition_css_inline", + "hash": "vNfd9raIMI0", + "canonicalFilename": "index.qwik.mjs_qwik_view_transition_css_inline_vNfd9raIMI0", + "path": "../node_modules/@qwik.dev/router", + "extension": "mjs", + "parent": null, + "ctxKind": "function", + "ctxName": "useStyles$", + "captures": false, + "loc": [ + 18986, + 18999 + ] +} +*/ +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikRouter_registerPreventNav_69B0DK0eZJc.mjs (ENTRY POINT)== + +import { _auto_internalState as internalState } from "./index.qwik.mjs"; +import { _auto_preventNav as preventNav } from "./index.qwik.mjs"; +import { isBrowser } from "@qwik.dev/core"; +// +export const useQwikRouter_registerPreventNav_69B0DK0eZJc = (fn$)=>{ + if (!isBrowser) return; + preventNav.$handler$ ||= (event)=>{ + internalState.navCount++; + if (!preventNav.$cbs$) return; + const prevents = [ + ...preventNav.$cbs$.values() + ].map((cb)=>cb.resolved ? cb.resolved() : cb()); + if (prevents.some(Boolean)) { + event.preventDefault(); + event.returnValue = true; } - } : void 0); - const registerPreventNav = q_useQwikRouter_registerPreventNav_W0LIs8PUJoA; - const goto = q_useQwikRouter_goto_OSnb99dm7Ow.w([ - actionState, - navResolver, - routeInternal, - routeLocation - ]); - useContextProvider(ContentContext, content); - useContextProvider(ContentInternalContext, contentInternal); - useContextProvider(DocumentHeadContext, documentHead); - useContextProvider(RouteLocationContext, routeLocation); - useContextProvider(RouteNavigateContext, goto); - useContextProvider(RouteStateContext, loaderState); - useContextProvider(RouteActionContext, actionState); - useContextProvider(RoutePreventNavigateContext, registerPreventNav); - useTaskQrl(q_useQwikRouter_useTask_omhKiQfdzZU.w([ - actionState, - content, - contentInternal, - documentHead, - env, - goto, - loaderState, - loadersObject, - navResolver, - props, - routeInternal, - routeLocation, - routeLocationTarget, - serverHead - ])); -}; -const QwikRouterProvider = /*#__PURE__*/ componentQrl(q_QwikRouterProvider_component_lCQXGdS0iZM); -const QwikCityProvider = QwikRouterProvider; -const QwikRouterMockProvider = /*#__PURE__*/ componentQrl(q_QwikRouterMockProvider_component_kN7AQXV0aXo); -const QwikCityMockProvider = QwikRouterMockProvider; -const RouterOutlet = /*#__PURE__*/ componentQrl(q_RouterOutlet_component_hKWOEO9aIDM); -const getValue = (obj)=>obj.value; -const validatorQrl = (validator)=>{ - if (isServer) return { - validate: validator }; - return void 0; -}; -const validator$ = /* @__PURE__ */ implicit$FirstArg(validatorQrl); -const flattenValibotIssues = (issues)=>{ - return issues.reduce((acc, issue)=>{ - if (issue.path) { - const hasArrayType = issue.path.some((path)=>path.type === 'array'); - if (hasArrayType) { - const keySuffix = issue.expected === 'Array' ? '[]' : ''; - const key = issue.path.map((item)=>item.type === 'array' ? '*' : item.key).join('.').replace(/\.\*/g, '[]') + keySuffix; - acc[key] = acc[key] || []; - if (Array.isArray(acc[key])) acc[key].push(issue.message); - return acc; - } else acc[issue.path.map((item)=>item.key).join('.')] = issue.message; - } - return acc; - }, {}); -}; -const valibotQrl = (qrl)=>{ - if (!__EXPERIMENTAL__.valibot) throw new Error('Valibot is an experimental feature and is not enabled. Please enable the feature flag by adding `experimental: ["valibot"]` to your qwikVite plugin options.'); - if (isServer) return { - __brand: 'valibot', - async validate (ev, inputData) { - const schema = await qrl.resolve().then((obj)=>typeof obj === 'function' ? obj(ev) : obj); - const data = inputData ?? await ev.parseBody(); - const result = await v.safeParseAsync(schema, data); - if (result.success) return { - success: true, - data: result.output - }; - else { - if (isDev) console.error('ERROR: Valibot validation failed', result.issues); - return { - success: false, - status: 400, - error: { - formErrors: v.flatten(result.issues).root ?? [], - fieldErrors: flattenValibotIssues(result.issues) - } - }; + (preventNav.$cbs$ ||= /* @__PURE__ */ new Set()).add(fn$); + fn$.resolve(); + window.addEventListener('beforeunload', preventNav.$handler$); + return ()=>{ + if (preventNav.$cbs$) { + preventNav.$cbs$.delete(fn$); + if (!preventNav.$cbs$.size) { + preventNav.$cbs$ = void 0; + window.removeEventListener('beforeunload', preventNav.$handler$); } } }; - return void 0; -}; -const valibot$ = /* @__PURE__ */ implicit$FirstArg(valibotQrl); -const flattenZodIssues = (issues)=>{ - issues = Array.isArray(issues) ? issues : [ - issues - ]; - return issues.reduce((acc, issue)=>{ - const isExpectingArray = 'expected' in issue && issue.expected === 'array'; - const hasArrayType = issue.path.some((path)=>typeof path === 'number') || isExpectingArray; - if (hasArrayType) { - const keySuffix = 'expected' in issue && issue.expected === 'array' ? '[]' : ''; - const key = issue.path.map((path)=>typeof path === 'number' ? '*' : path).join('.').replace(/\.\*/g, '[]') + keySuffix; - acc[key] = acc[key] || []; - if (Array.isArray(acc[key])) acc[key].push(issue.message); - return acc; - } else acc[issue.path.join('.')] = issue.message; - return acc; - }, {}); -}; -const zodQrl = (qrl)=>{ - if (isServer) return { - __brand: 'zod', - async validate (ev, inputData) { - const schema = await qrl.resolve().then((obj)=>{ - if (typeof obj === 'function') obj = obj(z, ev); - if (obj instanceof z.Schema) return obj; - else return z.object(obj); - }); - const data = inputData ?? await ev.parseBody(); - const result = await withLocale(ev.locale(), ()=>schema.safeParseAsync(data)); - if (result.success) return result; - else { - if (isDev) console.error('ERROR: Zod validation failed', result.error.issues); - return { - success: false, - status: 400, - error: { - formErrors: result.error.flatten().formErrors, - fieldErrors: flattenZodIssues(result.error.issues) - } - }; - } - } - }; - return void 0; -}; -const zod$ = /* @__PURE__ */ implicit$FirstArg(zodQrl); -const serverQrl = (qrl, options)=>{ - if (isServer) { - const captured = qrl.getCaptured(); - if (captured && captured.length > 0 && !_getContextHostElement()) throw new Error('For security reasons, we cannot serialize QRLs that capture lexical scope.'); - } - const method = options?.method?.toUpperCase?.() || 'POST'; - const headers = options?.headers || {}; - const origin = options?.origin || ''; - const fetchOptions = options?.fetchOptions || {}; - return q_serverQrl_RA3PmZ4Oyak.w([ - fetchOptions, - headers, - method, - origin, - qrl - ]); -}; -const server$ = /* @__PURE__ */ implicit$FirstArg(serverQrl); -const getValidators = (rest, qrl)=>{ - let id; - let serializationStrategy = DEFAULT_LOADERS_SERIALIZATION_STRATEGY; - const validators = []; - if (rest.length === 1) { - const options = rest[0]; - if (options && typeof options === 'object') { - if ('validate' in options) validators.push(options); - else { - id = options.id; - if (options.serializationStrategy) serializationStrategy = options.serializationStrategy; - if (options.validation) validators.push(...options.validation); - } - } - } else if (rest.length > 1) validators.push(...rest.filter((v2)=>!!v2)); - if (typeof id === 'string') { - if (isDev) { - if (!/^[\w/.-]+$/.test(id)) throw new Error(`Invalid id: ${id}, id can only contain [a-zA-Z0-9_.-]`); - } - id = `id_${id}`; - } else id = qrl.getHash(); - return { - validators: validators.reverse(), - id, - serializationStrategy - }; -}; -const routeActionQrl = (actionQrl, ...rest)=>{ - const { id, validators } = getValidators(rest, actionQrl); - function action() { - const loc = useLocation(); - const currentAction = useAction(); - const initialState = { - actionPath: `?${QACTION_KEY}=${id}`, - submitted: false, - isRunning: false, - status: void 0, - value: void 0, - formData: void 0 - }; - const state = useStore(()=>{ - const value = currentAction.value; - if (value && value?.id === id) { - const data = value.data; - if (data instanceof FormData) initialState.formData = data; - if (value.output) { - const { status, result } = value.output; - initialState.status = status; - initialState.value = result; - } - } - return initialState; - }); - const submit = q_routeActionQrl_action_submit_JY3C42B1B08.w([ - currentAction, - id, - loc, - state - ]); - initialState.submit = submit; - return state; - } - action.__brand = 'server_action'; - action.__validators = validators; - action.__qrl = actionQrl; - action.__id = id; - Object.freeze(action); - return action; -}; -const globalActionQrl = (actionQrl, ...rest)=>{ - const action = routeActionQrl(actionQrl, ...rest); - if (isServer) { - if (typeof globalThis._qwikActionsMap === 'undefined') globalThis._qwikActionsMap = /* @__PURE__ */ new Map(); - globalThis._qwikActionsMap.set(action.__id, action); - } - return action; -}; -const routeAction$ = /* @__PURE__ */ implicit$FirstArg(routeActionQrl); -const globalAction$ = /* @__PURE__ */ implicit$FirstArg(globalActionQrl); -const routeLoaderQrl = (loaderQrl, ...rest)=>{ - const { id, validators, serializationStrategy } = getValidators(rest, loaderQrl); - function loader() { - const state = _resolveContextWithoutSequentialScope(RouteStateContext); - if (!(id in state)) throw new Error(`routeLoader$ "${loaderQrl.getSymbol()}" was invoked in a route where it was not declared. - This is because the routeLoader$ was not exported in a 'layout.tsx' or 'index.tsx' file of the existing route. - For more information check: https://qwik.dev/docs/route-loader/ - - If your are managing reusable logic or a library it is essential that this function is re-exported from within 'layout.tsx' or 'index.tsx file of the existing route otherwise it will not run or throw exception. - For more information check: https://qwik.dev/docs/re-exporting-loaders/`); - const loaderData = state[id]; - untrack(getValue, loaderData); - return loaderData; - } - loader.__brand = 'server_loader'; - loader.__qrl = loaderQrl; - loader.__validators = validators; - loader.__id = id; - loader.__serializationStrategy = serializationStrategy; - loader.__expires = -1; - Object.freeze(loader); - return loader; -}; -const routeLoader$ = /* @__PURE__ */ implicit$FirstArg(routeLoaderQrl); -const ServiceWorkerRegister = (props)=>/* @__PURE__ */ _jsxSorted('script', { - nonce: _wrapProp(props, "nonce") - }, { - type: 'module', - dangerouslySetInnerHTML: swRegister - }, null, 3, "0K_9"); -const GetForm = /*#__PURE__*/ componentQrl(q_GetForm_component_OIWHwJ5eKxg); -const Form = ({ action, spaReset, reloadDocument, onSubmit$, ...rest }, key)=>{ - if (action) { - const isArrayApi = Array.isArray(onSubmit$); - if (isArrayApi) return _jsxSplit('form', { - ..._getVarProps(rest), - ..._getConstProps(rest), - action: _wrapProp(action, "actionPath"), - 'preventdefault:submit': !reloadDocument, - "q-e:submit": [ - ...onSubmit$, - // action.submit "submitcompleted" event for onSubmitCompleted$ events - !reloadDocument ? q_Form_form_q_e_submit_iIfbMzzXpIA.w([ - action - ]) : void 0 - ], - ['data-spa-reset']: spaReset ? 'true' : void 0 - }, { - method: 'post' - }, null, 0, key); - return _jsxSplit('form', { - ..._getVarProps(rest), - ..._getConstProps(rest), - action: _wrapProp(action, "actionPath"), - 'preventdefault:submit': !reloadDocument, - "q-e:submit": [ - // Since v2, this fires before the action is executed so it can be prevented - onSubmit$, - // action.submit "submitcompleted" event for onSubmitCompleted$ events - !reloadDocument ? action.submit : void 0 - ], - ['data-spa-reset']: spaReset ? 'true' : void 0 - }, { - method: 'post' - }, null, 0, key); - } else return /* @__PURE__ */ _jsxSplit(GetForm, { - spaReset, - reloadDocument, - onSubmit$, - ..._getVarProps(rest) - }, _getConstProps(rest), null, 0, key); -}; -const untypedAppUrl = function appUrl(route, params, paramsPrefix = '') { - const path = route.split('/'); - for(let i = 0; i < path.length; i++){ - const segment = path[i]; - if (segment.startsWith('[') && segment.endsWith(']')) { - const isSpread = segment.startsWith('[...'); - const key = segment.substring(segment.startsWith('[...') ? 4 : 1, segment.length - 1); - const value = params ? params[paramsPrefix + key] || params[key] : ''; - path[i] = isSpread ? value : encodeURIComponent(value); - } - if (segment.startsWith('(') && segment.endsWith(')')) path.splice(i, 1); - } - let url = path.join('/'); - let baseURL = '/'; - if (baseURL) { - if (!baseURL.endsWith('/')) baseURL += '/'; - while(url.startsWith('/'))url = url.substring(1); - url = baseURL + url; - } - return url; -}; -function omitProps(obj, keys) { - const omittedObj = {}; - for(const key in obj)if (!key.startsWith('param:') && !keys.includes(key)) omittedObj[key] = obj[key]; - return omittedObj; -} -const createRenderer = (getOptions)=>{ - return (opts)=>{ - const { jsx, options } = getOptions(opts); - return renderToStream(jsx, options); - }; -}; -const DocumentHeadTags = /*#__PURE__*/ componentQrl(q_DocumentHeadTags_component_LaCcLS5Bz4c); -export { DocumentHeadTags, ErrorBoundary, Form, Link, QWIK_CITY_SCROLLER, QWIK_ROUTER_SCROLLER, QwikCityMockProvider, QwikCityProvider, QwikRouterMockProvider, QwikRouterProvider, RouterOutlet, ServiceWorkerRegister, createRenderer, globalAction$, globalActionQrl, omitProps, routeAction$, routeActionQrl, routeLoader$, routeLoaderQrl, server$, serverQrl, untypedAppUrl, useContent, useDocumentHead, useLocation, useNavigate, usePreventNavigate$, usePreventNavigateQrl, useQwikRouter, valibot$, valibotQrl, validator$, validatorQrl, zod$, zodQrl }; -export { ContentInternalContext as _auto_ContentInternalContext }; -export { getScrollHistory as _auto_getScrollHistory }; -export { internalState as _auto_internalState }; -export { preventNav as _auto_preventNav }; -export { restoreScroll as _auto_restoreScroll }; -export { spaInit as _auto_spaInit }; -export { useQwikRouterEnv as _auto_useQwikRouterEnv }; -export { ContentContext as _auto_ContentContext }; -export { DocumentHeadContext as _auto_DocumentHeadContext }; -export { RouteActionContext as _auto_RouteActionContext }; -export { RouteLocationContext as _auto_RouteLocationContext }; -export { RouteNavigateContext as _auto_RouteNavigateContext }; -export { RouteStateContext as _auto_RouteStateContext }; -export { createDocumentHead as _auto_createDocumentHead }; - - -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"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\"}") -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Link_component_handlePreload_MhXmSxzp4GE.mjs (ENTRY POINT)== - -import { p as preloadRouteBundles } from "./chunks/routing.qwik.mjs"; -// -export const Link_component_handlePreload_MhXmSxzp4GE = (_, elm)=>{ - const url = new URL(elm.href); - preloadRouteBundles(url.pathname, 1); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;wDAwK0B,CAAC,GAAG;IAC1B,MAAM,MAAM,IAAI,IAAI,IAAI,IAAI;IAC5B,oBAAoB,IAAI,QAAQ,EAAE;AACpC\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;4DAgrBwD,CAAC;IACrD,IAAI,CAAC,WACH;IAEF,WAAW,SAAS,KAAK,CAAC;QACxB,cAAc,QAAQ;QACtB,IAAI,CAAC,WAAW,KAAK,EACnB;QAEF,MAAM,WAAW;eAAI,WAAW,KAAK,CAAC,MAAM;SAAG,CAAC,GAAG,CAAC,CAAC,KACnD,GAAG,QAAQ,GAAG,GAAG,QAAQ,KAAK;QAEhC,IAAI,SAAS,IAAI,CAAC,UAAU;YAC1B,MAAM,cAAc;YACpB,MAAM,WAAW,GAAG;QACtB;IACF;IACA,CAAC,WAAW,KAAK,KAAK,aAAa,GAAG,IAAI,KAAK,EAAE,GAAG,CAAC;IACrD,IAAI,OAAO;IACX,OAAO,gBAAgB,CAAC,gBAAgB,WAAW,SAAS;IAC5D,OAAO;QACL,IAAI,WAAW,KAAK,EAAE;YACpB,WAAW,KAAK,CAAC,MAAM,CAAC;YACxB,IAAI,CAAC,WAAW,KAAK,CAAC,IAAI,EAAE;gBAC1B,WAAW,KAAK,GAAG,KAAK;gBACxB,OAAO,mBAAmB,CAAC,gBAAgB,WAAW,SAAS;YACjE;QACF;IACF;AACF\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "Link_component_handlePreload_MhXmSxzp4GE", + "name": "useQwikRouter_registerPreventNav_69B0DK0eZJc", "entry": null, - "displayName": "index.qwik.mjs_Link_component_handlePreload", - "hash": "MhXmSxzp4GE", - "canonicalFilename": "index.qwik.mjs_Link_component_handlePreload_MhXmSxzp4GE", + "displayName": "index.qwik.mjs_useQwikRouter_registerPreventNav", + "hash": "69B0DK0eZJc", + "canonicalFilename": "index.qwik.mjs_useQwikRouter_registerPreventNav_69B0DK0eZJc", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", - "parent": "Link_component_nhj84CU1784", + "parent": null, "ctxKind": "function", - "ctxName": "$", + "ctxName": "registerPreventNav", "captures": false, "loc": [ - 5085, - 5179 + 21540, + 22372 ], "paramNames": [ - "_", - "elm" + "fn$" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_ErrorBoundary_component_useOnWindow_GYhPAutMLGk.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikRouter_getScroller_0UhDFwlxeFQ.mjs (ENTRY POINT)== -import { _captures } from "@qwik.dev/core"; +import { _auto_QWIK_CITY_SCROLLER as QWIK_CITY_SCROLLER } from "./index.qwik.mjs"; +import { _auto_QWIK_ROUTER_SCROLLER as QWIK_ROUTER_SCROLLER } from "./index.qwik.mjs"; +import { isDev } from "@qwik.dev/core"; // -export const ErrorBoundary_component_useOnWindow_GYhPAutMLGk = (e)=>{ - const store = _captures[0]; - store.error = e.detail.error; +export const useQwikRouter_getScroller_0UhDFwlxeFQ = ()=>{ + let scroller = document.getElementById(QWIK_ROUTER_SCROLLER); + if (!scroller) { + scroller = document.getElementById(QWIK_CITY_SCROLLER); + if (scroller && isDev) console.warn(`Please update your scroller ID to "${QWIK_ROUTER_SCROLLER}" as "${QWIK_CITY_SCROLLER}" is deprecated and will be removed in V3`); + } + return scroller ?? document.documentElement; }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;+DA4EM,CAAC;;IACD,MAAM,KAAK,GAAG,EAAE,MAAM,CAAC,KAAK\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;qDA8sBiD;IAC7C,IAAI,WAAW,SAAS,cAAc,CAAC;IACvC,IAAI,CAAC,UAAU;QACb,WAAW,SAAS,cAAc,CAAC;QACnC,IAAI,YAAY,OACd,QAAQ,IAAI,CACV,CAAC,mCAAmC,EAAE,qBAAqB,MAAM,EAAE,mBAAmB,yCAAyC,CAAC;IAGtI;IACA,OAAO,YAAY,SAAS,eAAe;AAC7C\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "ErrorBoundary_component_useOnWindow_GYhPAutMLGk", + "name": "useQwikRouter_getScroller_0UhDFwlxeFQ", "entry": null, - "displayName": "index.qwik.mjs_ErrorBoundary_component_useOnWindow", - "hash": "GYhPAutMLGk", - "canonicalFilename": "index.qwik.mjs_ErrorBoundary_component_useOnWindow_GYhPAutMLGk", + "displayName": "index.qwik.mjs_useQwikRouter_getScroller", + "hash": "0UhDFwlxeFQ", + "canonicalFilename": "index.qwik.mjs_useQwikRouter_getScroller_0UhDFwlxeFQ", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", - "parent": "ErrorBoundary_component_yTCHi5s1o00", + "parent": null, "ctxKind": "function", - "ctxName": "$", - "captures": true, + "ctxName": "getScroller", + "captures": false, "loc": [ - 1704, - 1754 - ], - "paramNames": [ - "e" - ], - "captureNames": [ - "store" + 22472, + 22898 ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_QwikRouterMockProvider_component_kN7AQXV0aXo.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikRouter_goto_8j8Vrz2yUIM.mjs (ENTRY POINT)== -import { _auto_ContentContext as ContentContext } from "./index.qwik.mjs"; -import { _auto_ContentInternalContext as ContentInternalContext } from "./index.qwik.mjs"; -import { _auto_DocumentHeadContext as DocumentHeadContext } from "./index.qwik.mjs"; -import { _auto_RouteActionContext as RouteActionContext } from "./index.qwik.mjs"; -import { _auto_RouteLocationContext as RouteLocationContext } from "./index.qwik.mjs"; -import { _auto_RouteNavigateContext as RouteNavigateContext } from "./index.qwik.mjs"; -import { _auto_RouteStateContext as RouteStateContext } from "./index.qwik.mjs"; -import { _auto_createDocumentHead as createDocumentHead } from "./index.qwik.mjs"; -import { Slot } from "@qwik.dev/core"; -import { _jsxSorted } from "@qwik.dev/core"; -import { qrl } from "@qwik.dev/core"; -import { useContextProvider } from "@qwik.dev/core"; -import { useSignal } from "@qwik.dev/core"; -import { useStore } from "@qwik.dev/core"; -import { useTaskQrl } from "@qwik.dev/core"; -// -const q_useQwikMockRouter_goto_ojVznvSDqoM = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikMockRouter_goto_ojVznvSDqoM.mjs"), "useQwikMockRouter_goto_ojVznvSDqoM"); -const q_useQwikMockRouter_useTask_oml2hW1aK6I = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikMockRouter_useTask_oml2hW1aK6I.mjs"), "useQwikMockRouter_useTask_oml2hW1aK6I"); +import { _auto_clientNavigate as clientNavigate } from "./index.qwik.mjs"; +import { _auto_currentScrollState as currentScrollState } from "./index.qwik.mjs"; +import { _auto_getScrollHistory as getScrollHistory } from "./index.qwik.mjs"; +import { _auto_internalState as internalState } from "./index.qwik.mjs"; +import { _auto_prefetchRoute as prefetchRoute } from "./index.qwik.mjs"; +import { _auto_preventNav as preventNav } from "./index.qwik.mjs"; +import { _auto_restoreScroll as restoreScroll } from "./index.qwik.mjs"; +import { _auto_saveScrollHistory as saveScrollHistory } from "./index.qwik.mjs"; +import { _captures } from "@qwik.dev/core"; +import { isBrowser } from "@qwik.dev/core"; +import { a as isSameOrigin } from "./chunks/head.qwik.mjs"; +import { i as isSamePath } from "./chunks/head.qwik.mjs"; +import { b as toUrl } from "./chunks/head.qwik.mjs"; // -const useQwikMockRouter = (props)=>{ - const urlEnv = props.url ?? 'http://localhost/'; - const url = new URL(urlEnv); - const routeLocation = useStore({ - url, - params: props.params ?? {}, - isNavigating: false, - prevUrl: void 0 - }, { - deep: false - }); - const loadersData = props.loaders?.reduce((acc, { loader, data })=>{ - acc[loader.__id] = data; - return acc; - }, {}); - const loaderState = useStore(loadersData ?? {}, { - deep: false - }); - const goto = props.goto ?? q_useQwikMockRouter_goto_ojVznvSDqoM; - const documentHead = useStore(createDocumentHead, { - deep: false - }); - const content = useStore({ - headings: void 0, - menu: void 0 - }, { - deep: false +export const useQwikRouter_goto_8j8Vrz2yUIM = async (path, opt)=>{ + const actionState2 = _captures[0], getScroller2 = _captures[1], manifestHash2 = _captures[2], navResolver2 = _captures[3], routeInternal2 = _captures[4], routeLocation2 = _captures[5]; + const { type = 'link', forceReload = path === void 0, replaceState = false, scroll = true } = typeof opt === 'object' ? opt : { + forceReload: opt + }; + internalState.navCount++; + if (isBrowser && type === 'link' && routeInternal2.value.type === 'initial') { + const url2 = new URL(window.location.href); + routeInternal2.value.dest = url2; + routeLocation2.url = url2; + } + const lastDest = routeInternal2.value.dest; + const dest = path === void 0 ? lastDest : typeof path === 'number' ? path : toUrl(path, routeLocation2.url); + if (preventNav.$cbs$ && (forceReload || typeof dest === 'number' || !isSamePath(dest, lastDest) || !isSameOrigin(dest, lastDest))) { + const ourNavId = internalState.navCount; + const prevents = await Promise.all([ + ...preventNav.$cbs$.values() + ].map((cb)=>cb(dest))); + if (ourNavId !== internalState.navCount || prevents.some(Boolean)) { + if (ourNavId === internalState.navCount && type === 'popstate') history.pushState(null, '', lastDest); + return; + } + } + if (typeof dest === 'number') { + if (isBrowser) history.go(dest); + return; + } + if (!isSameOrigin(dest, lastDest)) { + if (isBrowser) location.href = dest.href; + return; + } + if (!forceReload && isSamePath(dest, lastDest)) { + if (isBrowser) { + if (type === 'link' && dest.href !== location.href) history.pushState(null, '', dest); + const scroller = await getScroller2(); + restoreScroll(type, dest, new URL(location.href), scroller, getScrollHistory()); + if (type === 'popstate') window._qRouterScrollEnabled = true; + } + if (dest.href !== routeLocation2.url.href) { + const newUrl = new URL(dest.href); + routeInternal2.value.dest = newUrl; + routeLocation2.url = newUrl; + } + return; + } + let historyUpdated = false; + if (isBrowser && type === 'link' && !forceReload) { + const scroller = await getScroller2(); + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + const scrollState = currentScrollState(scroller); + saveScrollHistory(scrollState); + clientNavigate(window, type, new URL(location.href), dest, replaceState); + historyUpdated = true; + } + routeInternal2.value = { + type, + dest, + forceReload, + replaceState, + scroll, + historyUpdated + }; + if (isBrowser) prefetchRoute(dest, true, 0.8, manifestHash2); + actionState2.value = void 0; + routeLocation2.isNavigating = true; + return new Promise((resolve)=>{ + navResolver2.r = resolve; }); - const contentInternal = useSignal(); - const actionState = useSignal(); - useContextProvider(ContentContext, content); - useContextProvider(ContentInternalContext, contentInternal); - useContextProvider(DocumentHeadContext, documentHead); - useContextProvider(RouteLocationContext, routeLocation); - useContextProvider(RouteNavigateContext, goto); - useContextProvider(RouteStateContext, loaderState); - useContextProvider(RouteActionContext, actionState); - const actionsMocks = props.actions?.reduce((acc, { action, handler })=>{ - acc[action.__id] = handler; - return acc; - }, {}); - useTaskQrl(q_useQwikMockRouter_useTask_oml2hW1aK6I.w([ - actionState, - actionsMocks - ])); -}; -export const QwikRouterMockProvider_component_kN7AQXV0aXo = (props)=>{ - useQwikMockRouter(props); - return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, "0K_5"); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;;;;;;MAwgCM,oBAAoB,CAAC;IACzB,MAAM,SAAS,MAAM,GAAG,IAAI;IAC5B,MAAM,MAAM,IAAI,IAAI;IACpB,MAAM,gBAAgB,SACpB;QACE;QACA,QAAQ,MAAM,MAAM,IAAI,CAAC;QACzB,cAAc;QACd,SAAS,KAAK;IAChB,GACA;QAAE,MAAM;IAAM;IAEhB,MAAM,cAAc,MAAM,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE;QAC9D,GAAG,CAAC,OAAO,IAAI,CAAC,GAAG;QACnB,OAAO;IACT,GAAG,CAAC;IACJ,MAAM,cAAc,SAAS,eAAe,CAAC,GAAG;QAAE,MAAM;IAAM;IAC9D,MAAM,OACJ,MAAM,IAAI;IAIZ,MAAM,eAAe,SAAS,oBAAoB;QAAE,MAAM;IAAM;IAChE,MAAM,UAAU,SACd;QACE,UAAU,KAAK;QACf,MAAM,KAAK;IACb,GACA;QAAE,MAAM;IAAM;IAEhB,MAAM,kBAAkB;IACxB,MAAM,cAAc;IACpB,mBAAmB,gBAAgB;IACnC,mBAAmB,wBAAwB;IAC3C,mBAAmB,qBAAqB;IACxC,mBAAmB,sBAAsB;IACzC,mBAAmB,sBAAsB;IACzC,mBAAmB,mBAAmB;IACtC,mBAAmB,oBAAoB;IACvC,MAAM,eAAe,MAAM,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE;QAClE,GAAG,CAAC,OAAO,IAAI,CAAC,GAAG;QACnB,OAAO;IACT,GAAG,CAAC;IACJ;;;;AAWF;4DAC0C,CAAC;IACzC,kBAAkB;IAClB,OAAO,aAAa,GAAG,WAAI;AAC7B\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;8CA2tBI,OAAO,MAAM;IACX,MAAM,eAAe,SAAS,CAAC,EAAE,EAC/B,eAAe,SAAS,CAAC,EAAE,EAC3B,gBAAgB,SAAS,CAAC,EAAE,EAC5B,eAAe,SAAS,CAAC,EAAE,EAC3B,iBAAiB,SAAS,CAAC,EAAE,EAC7B,iBAAiB,SAAS,CAAC,EAAE;IAC/B,MAAM,EACJ,OAAO,MAAM,EACb,cAAc,SAAS,KAAK,CAAC,EAC7B,eAAe,KAAK,EACpB,SAAS,IAAI,EACd,GAAG,OAAO,QAAQ,WACf,MACA;QACE,aAAa;IACf;IACJ,cAAc,QAAQ;IACtB,IAAI,aAAa,SAAS,UAAU,eAAe,KAAK,CAAC,IAAI,KAAK,WAAW;QAC3E,MAAM,OAAO,IAAI,IAAI,OAAO,QAAQ,CAAC,IAAI;QACzC,eAAe,KAAK,CAAC,IAAI,GAAG;QAC5B,eAAe,GAAG,GAAG;IACvB;IACA,MAAM,WAAW,eAAe,KAAK,CAAC,IAAI;IAC1C,MAAM,OACJ,SAAS,KAAK,IACV,WACA,OAAO,SAAS,WACd,OACA,MAAM,MAAM,eAAe,GAAG;IACtC,IACE,WAAW,KAAK,IAChB,CAAC,eACC,OAAO,SAAS,YAChB,CAAC,WAAW,MAAM,aAClB,CAAC,aAAa,MAAM,SAAS,GAC/B;QACA,MAAM,WAAW,cAAc,QAAQ;QACvC,MAAM,WAAW,MAAM,QAAQ,GAAG,CAAC;eAAI,WAAW,KAAK,CAAC,MAAM;SAAG,CAAC,GAAG,CAAC,CAAC,KAAO,GAAG;QACjF,IAAI,aAAa,cAAc,QAAQ,IAAI,SAAS,IAAI,CAAC,UAAU;YACjE,IAAI,aAAa,cAAc,QAAQ,IAAI,SAAS,YAClD,QAAQ,SAAS,CAAC,MAAM,IAAI;YAE9B;QACF;IACF;IACA,IAAI,OAAO,SAAS,UAAU;QAC5B,IAAI,WACF,QAAQ,EAAE,CAAC;QAEb;IACF;IACA,IAAI,CAAC,aAAa,MAAM,WAAW;QACjC,IAAI,WACF,SAAS,IAAI,GAAG,KAAK,IAAI;QAE3B;IACF;IACA,IAAI,CAAC,eAAe,WAAW,MAAM,WAAW;QAC9C,IAAI,WAAW;YACb,IAAI,SAAS,UAAU,KAAK,IAAI,KAAK,SAAS,IAAI,EAChD,QAAQ,SAAS,CAAC,MAAM,IAAI;YAE9B,MAAM,WAAW,MAAM;YACvB,cAAc,MAAM,MAAM,IAAI,IAAI,SAAS,IAAI,GAAG,UAAU;YAC5D,IAAI,SAAS,YACX,OAAO,qBAAqB,GAAG;QAEnC;QACA,IAAI,KAAK,IAAI,KAAK,eAAe,GAAG,CAAC,IAAI,EAAE;YACzC,MAAM,SAAS,IAAI,IAAI,KAAK,IAAI;YAChC,eAAe,KAAK,CAAC,IAAI,GAAG;YAC5B,eAAe,GAAG,GAAG;QACvB;QACA;IACF;IACA,IAAI,iBAAiB;IACrB,IAAI,aAAa,SAAS,UAAU,CAAC,aAAa;QAChD,MAAM,WAAW,MAAM;QACvB,OAAO,qBAAqB,GAAG;QAC/B,aAAa,OAAO,sBAAsB;QAC1C,MAAM,cAAc,mBAAmB;QACvC,kBAAkB;QAClB,eAAe,QAAQ,MAAM,IAAI,IAAI,SAAS,IAAI,GAAG,MAAM;QAC3D,iBAAiB;IACnB;IACA,eAAe,KAAK,GAAG;QACrB;QACA;QACA;QACA;QACA;QACA;IACF;IACA,IAAI,WACF,cAAc,MAAM,MAAM,KAAK;IAEjC,aAAa,KAAK,GAAG,KAAK;IAC1B,eAAe,YAAY,GAAG;IAC9B,OAAO,IAAI,QAAQ,CAAC;QAClB,aAAa,CAAC,GAAG;IACnB;AACF\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "QwikRouterMockProvider_component_kN7AQXV0aXo", + "name": "useQwikRouter_goto_8j8Vrz2yUIM", "entry": null, - "displayName": "index.qwik.mjs_QwikRouterMockProvider_component", - "hash": "kN7AQXV0aXo", - "canonicalFilename": "index.qwik.mjs_QwikRouterMockProvider_component_kN7AQXV0aXo", + "displayName": "index.qwik.mjs_useQwikRouter_goto", + "hash": "8j8Vrz2yUIM", + "canonicalFilename": "index.qwik.mjs_useQwikRouter_goto_8j8Vrz2yUIM", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, "ctxKind": "function", - "ctxName": "component$", - "captures": false, + "ctxName": "goto", + "captures": true, "loc": [ - 37476, - 37558 + 22989, + 26331 ], "paramNames": [ - "props" + "path", + "opt" + ], + "captureNames": [ + "actionState", + "getScroller", + "manifestHash", + "navResolver", + "routeInternal", + "routeLocation" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikRouter_registerPreventNav_W0LIs8PUJoA.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikRouter_useTask_XpalYii770E.mjs (ENTRY POINT)== +import { _auto_clientNavigate as clientNavigate } from "./index.qwik.mjs"; +import { _auto_currentScrollState as currentScrollState } from "./index.qwik.mjs"; +import { _auto_getScrollHistory as getScrollHistory } from "./index.qwik.mjs"; import { _auto_internalState as internalState } from "./index.qwik.mjs"; -import { _auto_preventNav as preventNav } from "./index.qwik.mjs"; +import { _auto_restoreScroll as restoreScroll } from "./index.qwik.mjs"; +import { _auto_saveScrollHistory as saveScrollHistory } from "./index.qwik.mjs"; +import { _auto_spaInit as spaInit } from "./index.qwik.mjs"; +import { Q as QACTION_KEY } from "./chunks/route-loaders.qwik.mjs"; +import { h as Q_ROUTE } from "./chunks/route-loaders.qwik.mjs"; +import { _captures } from "@qwik.dev/core"; +import { _deserialize } from "@qwik.dev/core/internal"; +import { _getContextContainer } from "@qwik.dev/core/internal"; +import { _hasStoreEffects } from "@qwik.dev/core/internal"; +import { _retryOnPromise } from "@qwik.dev/core/internal"; +import { _waitUntilRendered } from "@qwik.dev/core/internal"; +import { e as ensureRouteLoaderSignals } from "./chunks/route-loaders.qwik.mjs"; +import { forceStoreEffects } from "@qwik.dev/core/internal"; +import { getLocale } from "@qwik.dev/core"; import { isBrowser } from "@qwik.dev/core"; +import { isDev } from "@qwik.dev/core"; +import { a as isSameOrigin } from "./chunks/head.qwik.mjs"; +import { i as isSamePath } from "./chunks/head.qwik.mjs"; +import { isServer } from "@qwik.dev/core"; +import { l as loadRoute } from "./chunks/head.qwik.mjs"; +import { noSerialize } from "@qwik.dev/core"; +import * as qwikRouterConfig from "@qwik-router-config"; +import { r as resolveHead } from "./chunks/head.qwik.mjs"; +import { s as setLoaderSignalValue } from "./chunks/route-loaders.qwik.mjs"; +import { t as toPath } from "./chunks/head.qwik.mjs"; +import { u as updateRouteLoaderCtx } from "./chunks/route-loaders.qwik.mjs"; // -export const useQwikRouter_registerPreventNav_W0LIs8PUJoA = (fn$)=>{ - if (!isBrowser) return; - preventNav.$handler$ ||= (event)=>{ - internalState.navCount++; - if (!preventNav.$cbs$) return; - const prevents = [ - ...preventNav.$cbs$.values() - ].map((cb)=>cb.resolved ? cb.resolved() : cb()); - if (prevents.some(Boolean)) { - event.preventDefault(); - event.returnValue = true; +function callRestoreScrollOnDocument() { + if (document.__q_scroll_restore__) { + document.__q_scroll_restore__(); + document.__q_scroll_restore__ = void 0; + } +} +async function submitAction(action, routePath) { + const pathBase = routePath.endsWith('/') ? routePath : routePath + '/'; + const url = `${pathBase}?${QACTION_KEY}=${encodeURIComponent(action.id)}`; + const actionData = action.data; + let fetchOptions; + if (actionData instanceof FormData) fetchOptions = { + method: 'POST', + body: actionData, + headers: { + Accept: 'application/json' } }; - (preventNav.$cbs$ ||= /* @__PURE__ */ new Set()).add(fn$); - fn$.resolve(); - window.addEventListener('beforeunload', preventNav.$handler$); - return ()=>{ - if (preventNav.$cbs$) { - preventNav.$cbs$.delete(fn$); - if (!preventNav.$cbs$.size) { - preventNav.$cbs$ = void 0; - window.removeEventListener('beforeunload', preventNav.$handler$); + else fetchOptions = { + method: 'POST', + body: JSON.stringify(actionData), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + Accept: 'application/json' + } + }; + const response = await fetch(url, fetchOptions); + if (response.redirected) { + const redirectedURL = new URL(response.url); + if (redirectedURL.origin !== location.origin) { + location.href = redirectedURL.href; + return void 0; + } + location.href = redirectedURL.href; + return void 0; + } + if ((response.headers.get('content-type') || '').includes('json')) { + const text = await response.text(); + const data = _deserialize(text); + return { + status: response.status, + result: data?.result, + loaderHashes: data?.loaderHashes, + loaderValues: data?.loaders + }; + } + return void 0; +} +const startViewTransition = (params)=>{ + if (!params.update) return; + if ('startViewTransition' in document) { + let transition; + try { + transition = document.startViewTransition(params); + } catch { + transition = document.startViewTransition(params.update); + } + const event = new CustomEvent('qviewtransition', { + detail: transition + }); + document.dispatchEvent(event); + return transition; + } else params.update?.(); +}; +export const useQwikRouter_useTask_XpalYii770E = async ({ track })=>{ + const actionState2 = _captures[0], content2 = _captures[1], contentInternal2 = _captures[2], documentHead2 = _captures[3], env2 = _captures[4], getScroller2 = _captures[5], goto2 = _captures[6], httpStatus2 = _captures[7], loaderState2 = _captures[8], navResolver2 = _captures[9], props2 = _captures[10], routeInternal2 = _captures[11], routeLoaderCtx2 = _captures[12], routeLocation2 = _captures[13], routeLocationTarget2 = _captures[14], serverHead2 = _captures[15]; + const container = _getContextContainer(); + const navigation = track(routeInternal2); + const action = track(actionState2); + const locale = getLocale(''); + const prevUrl = routeLocation2.url; + const navType = action ? 'form' : navigation.type; + const replaceState = navigation.replaceState; + let trackUrl; + let endpointResponse; + let actionData; + let loadedRoute; + if (isServer) { + trackUrl = new URL(navigation.dest, routeLocation2.url); + loadedRoute = env2.loadedRoute; + endpointResponse = env2.response; + actionData = endpointResponse; + } else { + trackUrl = new URL(navigation.dest, location); + if (trackUrl.pathname.endsWith('/')) { + if (globalThis.__NO_TRAILING_SLASH__) trackUrl.pathname = trackUrl.pathname.slice(0, -1); + } else if (!globalThis.__NO_TRAILING_SLASH__) trackUrl.pathname += '/'; + const loadRoutePromise = loadRoute(qwikRouterConfig.routes, qwikRouterConfig.cacheModules, trackUrl.pathname); + try { + loadedRoute = await loadRoutePromise; + } catch (e) { + console.error(e); + window.location.href = trackUrl.href; + return; + } + if (action) { + const result = await submitAction(action, trackUrl.pathname); + if (!result) { + routeInternal2.untrackedValue = { + type: navType, + dest: trackUrl + }; + return; + } + actionData = { + status: result.status, + action: action.id, + actionResult: result.result + }; + if (action.resolve) action.resolve({ + status: result.status, + result: result.result + }); + if (result.loaderValues && Object.keys(result.loaderValues).length > 0) for (const [id, value] of Object.entries(result.loaderValues)){ + const signal = loaderState2[id]; + if (signal) setLoaderSignalValue(signal, value); } + if (result.loaderHashes) for (const hash of result.loaderHashes)loaderState2[hash]?.invalidate(true); + } + } + const { $routeName$, $params$, $mods$, $menu$, $notFound$ } = loadedRoute; + const contentModules = $mods$; + updateRouteLoaderCtx(routeLoaderCtx2, loadedRoute.$loaderPaths$, trackUrl); + const routeLoaders = ensureRouteLoaderSignals(contentModules, loaderState2, routeLoaderCtx2); + const navCountBefore = internalState.navCount; + if (!isServer && routeLoaders.length > 0) await Promise.all(routeLoaders.map((loader)=>loaderState2[loader.__id]?.promise())); + if (internalState.navCount !== navCountBefore) { + if (++internalState.redirectCount > 20) { + console.error('Too many redirects, aborting navigation'); + internalState.redirectCount = 0; + return; } + return; + } + internalState.redirectCount = 0; + if ($notFound$) httpStatus2.value = { + status: 404, + message: 'Not Found' + }; + else if (endpointResponse) httpStatus2.value = { + status: endpointResponse.status, + message: endpointResponse.statusMessage ?? 'OK' + }; + else if (actionData) httpStatus2.value = { + status: actionData.status, + message: 'OK' + }; + else httpStatus2.value = { + status: 200, + message: 'OK' + }; + const pageModule = contentModules[contentModules.length - 1]; + if (navigation.dest.search && !!isSamePath(trackUrl, prevUrl)) trackUrl.search = navigation.dest.search; + let shouldForcePrevUrl = false; + let shouldForceUrl = false; + let shouldForceParams = false; + if (!isSamePath(trackUrl, prevUrl)) { + if (_hasStoreEffects(routeLocation2, 'prevUrl')) shouldForcePrevUrl = true; + routeLocationTarget2.prevUrl = prevUrl; + } + if (routeLocationTarget2.url !== trackUrl) { + if (_hasStoreEffects(routeLocation2, 'url')) shouldForceUrl = true; + routeLocationTarget2.url = trackUrl; + } + if (routeLocationTarget2.params !== $params$) { + if (_hasStoreEffects(routeLocation2, 'params')) shouldForceParams = true; + routeLocationTarget2.params = $params$; + } + routeInternal2.untrackedValue = { + type: navType, + dest: trackUrl }; + const resolvedHead = await _retryOnPromise(()=>resolveHead(actionData, loaderState2, routeLocation2, contentModules, locale, serverHead2)); + content2.headings = pageModule.headings; + content2.menu = $menu$; + contentInternal2.untrackedValue = noSerialize(contentModules); + documentHead2.links = resolvedHead.links; + documentHead2.meta = resolvedHead.meta; + documentHead2.styles = resolvedHead.styles; + documentHead2.scripts = resolvedHead.scripts; + documentHead2.title = resolvedHead.title; + documentHead2.frontmatter = resolvedHead.frontmatter; + if (isBrowser) { + let scrollState; + if (navType === 'popstate') scrollState = getScrollHistory(); + const scroller = await getScroller2(); + if (navigation.scroll && (!navigation.forceReload || !isSamePath(trackUrl, prevUrl)) && (navType === 'link' || navType === 'popstate') || navType === 'form' && !isSamePath(trackUrl, prevUrl)) document.__q_scroll_restore__ = ()=>restoreScroll(navType, trackUrl, prevUrl, scroller, scrollState); + if (!window._qRouterSPA) { + window._qRouterSPA = true; + history.scrollRestoration = 'manual'; + window.addEventListener('popstate', ()=>{ + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + goto2(location.href, { + type: 'popstate' + }); + }); + window.removeEventListener('popstate', window._qRouterInitPopstate); + window._qRouterInitPopstate = void 0; + if (!window._qRouterHistoryPatch) { + window._qRouterHistoryPatch = true; + const pushState = history.pushState; + const replaceState2 = history.replaceState; + const prepareState = (state)=>{ + if (state === null || typeof state === 'undefined') state = {}; + else if (state?.constructor !== Object) { + state = { + _data: state + }; + if (isDev) console.warn('In a Qwik SPA context, `history.state` is used to store scroll state. Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. We need to be able to automatically attach the scroll state to your state object. A new state object has been created, your data has been moved to: `history.state._data`'); + } + state._qRouterScroll = state._qRouterScroll || currentScrollState(scroller); + return state; + }; + history.pushState = (state, title, url2)=>{ + state = prepareState(state); + return pushState.call(history, state, title, url2); + }; + history.replaceState = (state, title, url2)=>{ + state = prepareState(state); + return replaceState2.call(history, state, title, url2); + }; + } + document.addEventListener('click', (event)=>{ + if (event.defaultPrevented) return; + const target = event.target.closest('a[href]'); + if (target && !target.hasAttribute('preventdefault:click')) { + const href = target.getAttribute('href'); + const prev = new URL(location.href); + const dest = new URL(href, prev); + if (isSameOrigin(dest, prev) && isSamePath(dest, prev)) { + event.preventDefault(); + if (!dest.hash && !dest.href.endsWith('#')) { + if (dest.href !== prev.href) history.pushState(null, '', dest); + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + saveScrollHistory({ + ...currentScrollState(scroller), + x: 0, + y: 0 + }); + location.reload(); + return; + } + goto2(target.getAttribute('href')); + } + } + }); + document.removeEventListener('click', window._qRouterInitAnchors); + window._qRouterInitAnchors = void 0; + if (!window.navigation) { + document.addEventListener('visibilitychange', ()=>{ + if ((window._qRouterScrollEnabled || window._qCityScrollEnabled) && document.visibilityState === 'hidden') { + if (window._qCityScrollEnabled) console.warn('"_qCityScrollEnabled" is deprecated. Use "_qRouterScrollEnabled" instead.'); + const scrollState2 = currentScrollState(scroller); + saveScrollHistory(scrollState2); + } + }, { + passive: true + }); + document.removeEventListener('visibilitychange', window._qRouterInitVisibility); + window._qRouterInitVisibility = void 0; + } + window.addEventListener('scroll', ()=>{ + if (!window._qRouterScrollEnabled && !window._qCityScrollEnabled) return; + clearTimeout(window._qRouterScrollDebounce); + window._qRouterScrollDebounce = setTimeout(()=>{ + const scrollState2 = currentScrollState(scroller); + saveScrollHistory(scrollState2); + window._qRouterScrollDebounce = void 0; + }, 200); + }, { + passive: true + }); + removeEventListener('scroll', window._qRouterInitScroll); + window._qRouterInitScroll = void 0; + spaInit.resolve(); + } + if (navType !== 'popstate') { + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + if (!navigation.historyUpdated) { + const scrollState2 = currentScrollState(scroller); + saveScrollHistory(scrollState2); + } + } + const navigate = ()=>{ + if (navigation.historyUpdated) { + const currentPath = location.pathname + location.search + location.hash; + const nextPath = toPath(trackUrl); + if (currentPath !== nextPath) history.replaceState(history.state, '', nextPath); + } else clientNavigate(window, navType, prevUrl, trackUrl, replaceState); + contentInternal2.trigger(); + return _waitUntilRendered(container); + }; + const _waitNextPage = ()=>{ + if (isServer || props2?.viewTransition === false) return navigate(); + else { + const viewTransition = startViewTransition({ + update: navigate, + types: [ + 'qwik-navigation' + ] + }); + if (!viewTransition) return Promise.resolve(); + return viewTransition.ready; + } + }; + _waitNextPage().catch((err)=>{ + navigate(); + throw err; + }).finally(()=>{ + container.element.setAttribute?.(Q_ROUTE, $routeName$); + const scrollState2 = currentScrollState(scroller); + saveScrollHistory(scrollState2); + window._qRouterScrollEnabled = true; + if (isBrowser) callRestoreScrollOnDocument(); + if (shouldForcePrevUrl) forceStoreEffects(routeLocation2, 'prevUrl'); + if (shouldForceUrl) forceStoreEffects(routeLocation2, 'url'); + if (shouldForceParams) forceStoreEffects(routeLocation2, 'params'); + routeLocation2.isNavigating = false; + navResolver2.r?.(); + }); + } }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;4DAukB+B,CAAC;IAC5B,IAAI,CAAC,WACH;IAEF,WAAW,SAAS,KAAK,CAAC;QACxB,cAAc,QAAQ;QACtB,IAAI,CAAC,WAAW,KAAK,EACnB;QAEF,MAAM,WAAW;eAAI,WAAW,KAAK,CAAC,MAAM;SAAG,CAAC,GAAG,CAAC,CAAC,KACnD,GAAG,QAAQ,GAAG,GAAG,QAAQ,KAAK;QAEhC,IAAI,SAAS,IAAI,CAAC,UAAU;YAC1B,MAAM,cAAc;YACpB,MAAM,WAAW,GAAG;QACtB;IACF;IACA,CAAC,WAAW,KAAK,KAAK,aAAa,GAAG,IAAI,KAAK,EAAE,GAAG,CAAC;IACrD,IAAI,OAAO;IACX,OAAO,gBAAgB,CAAC,gBAAgB,WAAW,SAAS;IAC5D,OAAO;QACL,IAAI,WAAW,KAAK,EAAE;YACpB,WAAW,KAAK,CAAC,MAAM,CAAC;YACxB,IAAI,CAAC,WAAW,KAAK,CAAC,IAAI,EAAE;gBAC1B,WAAW,KAAK,GAAG,KAAK;gBACxB,OAAO,mBAAmB,CAAC,gBAAgB,WAAW,SAAS;YACjE;QACF;IACF;AACF\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"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\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "useQwikRouter_registerPreventNav_W0LIs8PUJoA", + "name": "useQwikRouter_useTask_XpalYii770E", "entry": null, - "displayName": "index.qwik.mjs_useQwikRouter_registerPreventNav", - "hash": "W0LIs8PUJoA", - "canonicalFilename": "index.qwik.mjs_useQwikRouter_registerPreventNav_W0LIs8PUJoA", + "displayName": "index.qwik.mjs_useQwikRouter_useTask", + "hash": "XpalYii770E", + "canonicalFilename": "index.qwik.mjs_useQwikRouter_useTask_XpalYii770E", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, "ctxKind": "function", - "ctxName": "$", - "captures": false, + "ctxName": "useTask$", + "captures": true, "loc": [ - 19044, - 19876 + 27116, + 41631 ], "paramNames": [ - "fn$" - ] -} -*/ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Form_form_q_e_submit_iIfbMzzXpIA.mjs (ENTRY POINT)== - -import { _captures } from "@qwik.dev/core"; -// -export const Form_form_q_e_submit_iIfbMzzXpIA = (evt)=>{ - const action = _captures[0]; - if (!action.submitted) return action.submit(evt); -}; - - -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;gDAojDkB,CAAC;;IACD,IAAI,CAAC,OAAO,SAAS,EACnB,OAAO,OAAO,MAAM,CAAC\"}") -/* -{ - "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "Form_form_q_e_submit_iIfbMzzXpIA", - "entry": null, - "displayName": "index.qwik.mjs_Form_form_q_e_submit", - "hash": "iIfbMzzXpIA", - "canonicalFilename": "index.qwik.mjs_Form_form_q_e_submit_iIfbMzzXpIA", - "path": "../node_modules/@qwik.dev/router", - "extension": "mjs", - "parent": null, - "ctxKind": "function", - "ctxName": "$", - "captures": true, - "loc": [ - 53387, - 53525 - ], - "paramNames": [ - "evt" + "{track}" ], "captureNames": [ - "action" - ] -} -*/ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikMockRouter_goto_ojVznvSDqoM.mjs (ENTRY POINT)== - -export const useQwikMockRouter_goto_ojVznvSDqoM = async ()=>{ - console.warn('QwikRouterMockProvider: goto not provided'); -}; - - -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"kDA2hCM;IACA,QAAQ,IAAI,CAAC;AACf\"}") -/* -{ - "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "useQwikMockRouter_goto_ojVznvSDqoM", - "entry": null, - "displayName": "index.qwik.mjs_useQwikMockRouter_goto", - "hash": "ojVznvSDqoM", - "canonicalFilename": "index.qwik.mjs_useQwikMockRouter_goto_ojVznvSDqoM", - "path": "../node_modules/@qwik.dev/router", - "extension": "mjs", - "parent": null, - "ctxKind": "function", - "ctxName": "$", - "captures": false, - "loc": [ - 36289, - 36373 + "actionState", + "content", + "contentInternal", + "documentHead", + "env", + "getScroller", + "goto", + "httpStatus", + "loaderState", + "navResolver", + "props", + "routeInternal", + "routeLoaderCtx", + "routeLocation", + "routeLocationTarget", + "serverHead" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_DocumentHeadTags_component_LaCcLS5Bz4c.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_QwikRouterProvider_component_6Kjfa79mqlY.mjs (ENTRY POINT)== -import { useDocumentHead } from "./index.qwik.mjs"; -import { Fragment } from "@qwik.dev/core/jsx-runtime"; -import { _getConstProps } from "@qwik.dev/core"; -import { _getVarProps } from "@qwik.dev/core"; +import { _auto_useQwikRouter as useQwikRouter } from "./index.qwik.mjs"; +import { Slot } from "@qwik.dev/core"; import { _jsxSorted } from "@qwik.dev/core"; -import { _jsxSplit } from "@qwik.dev/core"; -import { createElement } from "@qwik.dev/core"; // -export const DocumentHeadTags_component_LaCcLS5Bz4c = (props)=>{ - let head = useDocumentHead(); - if (props) head = { - ...head, - ...props - }; - return /* @__PURE__ */ _jsxSorted(Fragment, null, null, [ - head.title && /* @__PURE__ */ _jsxSorted('title', null, null, head.title, 1, "0K_12"), - head.meta.map((m)=>/* @__PURE__ */ _jsxSplit('meta', { - ..._getVarProps(m) - }, _getConstProps(m), null, 0, "0K_13")), - head.links.map((l)=>/* @__PURE__ */ _jsxSplit('link', { - ..._getVarProps(l) - }, _getConstProps(l), null, 0, "0K_14")), - head.styles.map((s)=>{ - const props2 = s.props || s; - return /* @__PURE__ */ createElement('style', { - ...props2, - dangerouslySetInnerHTML: s.style || props2.dangerouslySetInnerHTML, - key: s.key - }); - }), - head.scripts.map((s)=>{ - const props2 = s.props || s; - return /* @__PURE__ */ createElement('script', { - ...props2, - dangerouslySetInnerHTML: s.script || props2.dangerouslySetInnerHTML, - key: s.key - }); - }) - ], 1, "0K_15"); +export const QwikRouterProvider_component_6Kjfa79mqlY = (props)=>{ + useQwikRouter(props); + return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, '5y_0'); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;sDAmrDoC,CAAC;IACnC,IAAI,OAAO;IACX,IAAI,OACF,OAAO;QAAE,GAAG,IAAI;QAAE,GAAG,KAAK;IAAC;IAE7B,OAAO,aAAa,GAAG,WAAK,sBAChB;QACR,KAAK,KAAK,IAAI,aAAa,GAAG,WAAI,qBAAqB,KAAK,KAAK;QACjE,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,IAAM,aAAa,GAAG,UAAI;gCAAa;8BAAA;QACtD,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC,IAAM,aAAa,GAAG,UAAI;gCAAa;8BAAA;QACvD,KAAK,MAAM,CAAC,GAAG,CAAC,CAAC;YACf,MAAM,SAAS,EAAE,KAAK,IAAI;YAC1B,OAAO,aAAa,GAAG,cAAc,SAAS;gBAC5C,GAAG,MAAM;gBACT,yBAAyB,EAAE,KAAK,IAAI,OAAO,uBAAuB;gBAClE,KAAK,EAAE,GAAG;YACZ;QACF;QACA,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;YAChB,MAAM,SAAS,EAAE,KAAK,IAAI;YAC1B,OAAO,aAAa,GAAG,cAAc,UAAU;gBAC7C,GAAG,MAAM;gBACT,yBAAyB,EAAE,MAAM,IAAI,OAAO,uBAAuB;gBACnE,KAAK,EAAE,GAAG;YACZ;QACF;KACD;AAEL\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;wDA2tC6B,CAAC;IAC1B,cAAc;IACd,OAAO,aAAa,GAAG,WAAW,MAAM,MAAM,MAAM,MAAM,GAAG;AAC/D\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "DocumentHeadTags_component_LaCcLS5Bz4c", + "name": "QwikRouterProvider_component_6Kjfa79mqlY", "entry": null, - "displayName": "index.qwik.mjs_DocumentHeadTags_component", - "hash": "LaCcLS5Bz4c", - "canonicalFilename": "index.qwik.mjs_DocumentHeadTags_component_LaCcLS5Bz4c", + "displayName": "index.qwik.mjs_QwikRouterProvider_component", + "hash": "6Kjfa79mqlY", + "canonicalFilename": "index.qwik.mjs_QwikRouterProvider_component_6Kjfa79mqlY", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, @@ -2916,731 +3313,383 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/ind "ctxName": "component$", "captures": false, "loc": [ - 56859, - 57777 + 42232, + 42348 ], "paramNames": [ "props" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_spaInit_event_Js1cotabL5I.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikMockRouter_createAsync_clbxpuXqpEU.mjs (ENTRY POINT)== -import { isDev } from "@qwik.dev/core"; +import { _captures } from "@qwik.dev/core"; // -export const spaInit_event_Js1cotabL5I = (_, el)=>{ - if (!window._qRouterSPA && !window._qRouterInitPopstate) { - const currentPath = location.pathname + location.search; - const checkAndScroll = (scrollState)=>{ - if (scrollState) window.scrollTo(scrollState.x, scrollState.y); - }; - const currentScrollState = ()=>{ - const elm = document.documentElement; - return { - x: elm.scrollLeft, - y: elm.scrollTop, - w: Math.max(elm.scrollWidth, elm.clientWidth), - h: Math.max(elm.scrollHeight, elm.clientHeight) - }; - }; - const saveScrollState = (scrollState)=>{ - const state = history.state || {}; - state._qRouterScroll = scrollState || currentScrollState(); - history.replaceState(state, ''); - }; - saveScrollState(); - window._qRouterInitPopstate = ()=>{ - if (window._qRouterSPA) return; - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); - if (currentPath !== location.pathname + location.search) { - const getContainer = (el2)=>el2.closest('[q\\:container]:not([q\\:container=html]):not([q\\:container=text])'); - const container = getContainer(el); - const domContainer = container.qContainer; - const hostElement = domContainer.vNodeLocate(el); - const nav = domContainer?.resolveContext(hostElement, { - id: 'qc--n' - }); - if (nav) nav(location.href, { - type: 'popstate' - }); - else location.reload(); - } else if (history.scrollRestoration === 'manual') { - const scrollState = history.state?._qRouterScroll; - checkAndScroll(scrollState); - window._qRouterScrollEnabled = true; - } - }; - if (!window._qRouterHistoryPatch) { - window._qRouterHistoryPatch = true; - const pushState = history.pushState; - const replaceState = history.replaceState; - const prepareState = (state)=>{ - if (state === null || typeof state === 'undefined') state = {}; - else if (state?.constructor !== Object) { - state = { - _data: state - }; - if (isDev) console.warn('In a Qwik SPA context, `history.state` is used to store scroll state. Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. We need to be able to automatically attach the scroll state to your state object. A new state object has been created, your data has been moved to: `history.state._data`'); - } - state._qRouterScroll = state._qRouterScroll || currentScrollState(); - return state; - }; - history.pushState = (state, title, url)=>{ - state = prepareState(state); - return pushState.call(history, state, title, url); - }; - history.replaceState = (state, title, url)=>{ - state = prepareState(state); - return replaceState.call(history, state, title, url); - }; - } - window._qRouterInitAnchors = (event)=>{ - if (window._qRouterSPA || event.defaultPrevented) return; - const target = event.target.closest('a[href]'); - if (target && !target.hasAttribute('preventdefault:click')) { - const href = target.getAttribute('href'); - const prev = new URL(location.href); - const dest = new URL(href, prev); - const sameOrigin = dest.origin === prev.origin; - const samePath = dest.pathname + dest.search === prev.pathname + prev.search; - if (sameOrigin && samePath) { - event.preventDefault(); - if (dest.href !== prev.href) history.pushState(null, '', dest); - if (!dest.hash) { - if (dest.href.endsWith('#')) window.scrollTo(0, 0); - else { - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); - saveScrollState({ - ...currentScrollState(), - x: 0, - y: 0 - }); - location.reload(); - } - } else { - const elmId = dest.hash.slice(1); - const elm = document.getElementById(elmId); - if (elm) elm.scrollIntoView(); - } - } - } - }; - window._qRouterInitVisibility = ()=>{ - if (!window._qRouterSPA && window._qRouterScrollEnabled && document.visibilityState === 'hidden') saveScrollState(); - }; - window._qRouterInitScroll = ()=>{ - if (window._qRouterSPA || !window._qRouterScrollEnabled) return; - clearTimeout(window._qRouterScrollDebounce); - window._qRouterScrollDebounce = setTimeout(()=>{ - saveScrollState(); - window._qRouterScrollDebounce = void 0; - }, 200); - }; - window._qRouterScrollEnabled = true; - setTimeout(()=>{ - window.addEventListener('popstate', window._qRouterInitPopstate); - window.addEventListener('scroll', window._qRouterInitScroll, { - passive: true - }); - document.addEventListener('click', window._qRouterInitAnchors); - if (!window.navigation) document.addEventListener('visibilitychange', window._qRouterInitVisibility, { - passive: true - }); - }, 0); - } +export const useQwikMockRouter_createAsync_clbxpuXqpEU = async ()=>{ + const data2 = _captures[0]; + return data2; }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"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\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;yDA4vCQ;IACE,MAAM,QAAQ,SAAS,CAAC,EAAE;IAC1B,OAAO;AACT\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "spaInit_event_Js1cotabL5I", + "name": "useQwikMockRouter_createAsync_clbxpuXqpEU", "entry": null, - "displayName": "index.qwik.mjs_spaInit_event", - "hash": "Js1cotabL5I", - "canonicalFilename": "index.qwik.mjs_spaInit_event_Js1cotabL5I", + "displayName": "index.qwik.mjs_useQwikMockRouter_createAsync", + "hash": "clbxpuXqpEU", + "canonicalFilename": "index.qwik.mjs_useQwikMockRouter_createAsync_clbxpuXqpEU", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, "ctxKind": "function", - "ctxName": "event$", - "captures": false, + "ctxName": "createAsync$", + "captures": true, "loc": [ - 10232, - 15588 + 43103, + 43188 ], - "paramNames": [ - "_", - "el" + "captureNames": [ + "data" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_QwikRouterProvider_component_lCQXGdS0iZM.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikMockRouter_goto_aViHFxQ1a3s.mjs (ENTRY POINT)== -import { useQwikRouter } from "./index.qwik.mjs"; -import { Slot } from "@qwik.dev/core"; -import { _jsxSorted } from "@qwik.dev/core"; -// -export const QwikRouterProvider_component_lCQXGdS0iZM = (props)=>{ - useQwikRouter(props); - return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, "0K_4"); +export const useQwikMockRouter_goto_aViHFxQ1a3s = async ()=>{ + console.warn('QwikRouterMockProvider: goto not provided'); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;wDAmgCsC,CAAC;IACrC,cAAc;IACd,OAAO,aAAa,GAAG,WAAI;AAC7B\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"kDA0wC+B;IACzB,QAAQ,IAAI,CAAC;AACf\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "QwikRouterProvider_component_lCQXGdS0iZM", + "name": "useQwikMockRouter_goto_aViHFxQ1a3s", "entry": null, - "displayName": "index.qwik.mjs_QwikRouterProvider_component", - "hash": "lCQXGdS0iZM", - "canonicalFilename": "index.qwik.mjs_QwikRouterProvider_component_lCQXGdS0iZM", + "displayName": "index.qwik.mjs_useQwikMockRouter_goto", + "hash": "aViHFxQ1a3s", + "canonicalFilename": "index.qwik.mjs_useQwikMockRouter_goto_aViHFxQ1a3s", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, "ctxKind": "function", - "ctxName": "component$", + "ctxName": "goto", "captures": false, "loc": [ - 35640, - 35718 - ], - "paramNames": [ - "props" + 43381, + 43465 ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Link_component_handlePrefetch_Evus9ZlzXpQ.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikMockRouter_useTask_tXTLR4tzCy0.mjs (ENTRY POINT)== -import { l as loadClientData } from "./chunks/routing.qwik.mjs"; -import { p as preloadRouteBundles } from "./chunks/routing.qwik.mjs"; +import { _captures } from "@qwik.dev/core"; // -export const Link_component_handlePrefetch_Evus9ZlzXpQ = (_, elm)=>{ - if (navigator.connection?.saveData) return; - if (elm && elm.href) { - const url = new URL(elm.href); - preloadRouteBundles(url.pathname); - if (elm.hasAttribute('data-prefetch')) loadClientData(url, { - preloadRouteBundles: false, - isPrefetch: true - }); +export const useQwikMockRouter_useTask_tXTLR4tzCy0 = async ({ track })=>{ + const actionState2 = _captures[0], actionsMocks2 = _captures[1]; + const action = track(actionState2); + if (!action?.resolve) return; + const mock = actionsMocks2?.[action.id]; + if (mock) { + const actionResult = await mock(action.data); + action.resolve(actionResult); } }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;yDAqIQ,CAAC,GAAG;IACJ,IAAI,UAAU,UAAU,EAAE,UACxB;IAEF,IAAI,OAAO,IAAI,IAAI,EAAE;QACnB,MAAM,MAAM,IAAI,IAAI,IAAI,IAAI;QAC5B,oBAAoB,IAAI,QAAQ;QAChC,IAAI,IAAI,YAAY,CAAC,kBACnB,eAAe,KAAK;YAClB,qBAAqB;YACrB,YAAY;QACd;IAEJ;AACF\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;qDA6yCM,OAAO,EAAE,KAAK,EAAE;IACd,MAAM,eAAe,SAAS,CAAC,EAAE,EAC/B,gBAAgB,SAAS,CAAC,EAAE;IAC9B,MAAM,SAAS,MAAM;IACrB,IAAI,CAAC,QAAQ,SACX;IAEF,MAAM,OAAO,eAAe,CAAC,OAAO,EAAE,CAAC;IACvC,IAAI,MAAM;QACR,MAAM,eAAe,MAAM,KAAK,OAAO,IAAI;QAC3C,OAAO,OAAO,CAAC;IACjB;AACF\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "Link_component_handlePrefetch_Evus9ZlzXpQ", + "name": "useQwikMockRouter_useTask_tXTLR4tzCy0", "entry": null, - "displayName": "index.qwik.mjs_Link_component_handlePrefetch", - "hash": "Evus9ZlzXpQ", - "canonicalFilename": "index.qwik.mjs_Link_component_handlePrefetch_Evus9ZlzXpQ", + "displayName": "index.qwik.mjs_useQwikMockRouter_useTask", + "hash": "tXTLR4tzCy0", + "canonicalFilename": "index.qwik.mjs_useQwikMockRouter_useTask_tXTLR4tzCy0", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", - "parent": "Link_component_nhj84CU1784", + "parent": null, "ctxKind": "function", - "ctxName": "$", - "captures": false, + "ctxName": "useTask$", + "captures": true, "loc": [ - 4027, - 4436 + 44476, + 44868 ], "paramNames": [ - "_", - "elm" + "{track}" + ], + "captureNames": [ + "actionState", + "actionsMocks" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikRouter_useTask_omhKiQfdzZU.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_QwikRouterMockProvider_component_IN4dVpT0x74.mjs (ENTRY POINT)== -import { _captures } from "@qwik.dev/core"; -import { QWIK_ROUTER_SCROLLER } from "./index.qwik.mjs"; -import { _auto_getScrollHistory as getScrollHistory } from "./index.qwik.mjs"; -import { _auto_restoreScroll as restoreScroll } from "./index.qwik.mjs"; -import { _auto_spaInit as spaInit } from "./index.qwik.mjs"; -import { _auto_createDocumentHead as createDocumentHead } from "./index.qwik.mjs"; -import { C as CLIENT_DATA_CACHE } from "./chunks/routing.qwik.mjs"; -import { D as DEFAULT_LOADERS_SERIALIZATION_STRATEGY } from "./chunks/routing.qwik.mjs"; -import { Q as Q_ROUTE } from "./chunks/routing.qwik.mjs"; -import { _getContextContainer } from "@qwik.dev/core/internal"; -import { _hasStoreEffects } from "@qwik.dev/core/internal"; -import { _waitUntilRendered } from "@qwik.dev/core/internal"; -import { e as clientNavigate } from "./chunks/routing.qwik.mjs"; -import { c as createLoaderSignal } from "./chunks/routing.qwik.mjs"; -import { forceStoreEffects } from "@qwik.dev/core/internal"; -import { getLocale } from "@qwik.dev/core"; -import { isBrowser } from "@qwik.dev/core"; -import { isDev } from "@qwik.dev/core"; -import { i as isPromise } from "./chunks/routing.qwik.mjs"; -import { b as isSameOrigin } from "./chunks/routing.qwik.mjs"; -import { a as isSamePath } from "./chunks/routing.qwik.mjs"; -import { isServer } from "@qwik.dev/core"; -import { l as loadClientData } from "./chunks/routing.qwik.mjs"; -import { d as loadRoute } from "./chunks/routing.qwik.mjs"; -import { noSerialize } from "@qwik.dev/core"; -import * as qwikRouterConfig from "@qwik-router-config"; -import { withLocale } from "@qwik.dev/core"; +import { C as ContentContext } from "./chunks/route-loaders.qwik.mjs"; +import { a as ContentInternalContext } from "./chunks/route-loaders.qwik.mjs"; +import { D as DocumentHeadContext } from "./chunks/route-loaders.qwik.mjs"; +import { H as HttpStatusContext } from "./chunks/route-loaders.qwik.mjs"; +import { f as RouteActionContext } from "./chunks/route-loaders.qwik.mjs"; +import { R as RouteLocationContext } from "./chunks/route-loaders.qwik.mjs"; +import { b as RouteNavigateContext } from "./chunks/route-loaders.qwik.mjs"; +import { c as RouteStateContext } from "./chunks/route-loaders.qwik.mjs"; +import { Slot } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { createAsyncQrl } from "@qwik.dev/core/internal"; +import { c as createDocumentHead } from "./chunks/head.qwik.mjs"; +import { qrl } from "@qwik.dev/core"; +import { useContextProvider } from "@qwik.dev/core"; +import { useSignal } from "@qwik.dev/core"; +import { useStore } from "@qwik.dev/core"; +import { useTaskQrl } from "@qwik.dev/core"; // -const mergeArray = (existingArr, newArr)=>{ - if (Array.isArray(newArr)) for (const newItem of newArr){ - if (typeof newItem.key === 'string') { - const existingIndex = existingArr.findIndex((i)=>i.key === newItem.key); - if (existingIndex > -1) { - existingArr[existingIndex] = newItem; - continue; - } - } - existingArr.push(newItem); - } -}; -const resolveDocumentHead = (resolvedHead, updatedHead)=>{ - if (typeof updatedHead.title === 'string') resolvedHead.title = updatedHead.title; - mergeArray(resolvedHead.meta, updatedHead.meta); - mergeArray(resolvedHead.links, updatedHead.links); - mergeArray(resolvedHead.styles, updatedHead.styles); - mergeArray(resolvedHead.scripts, updatedHead.scripts); - Object.assign(resolvedHead.frontmatter, updatedHead.frontmatter); -}; -const resolveHead = (endpoint, routeLocation, contentModules, locale, defaults)=>withLocale(locale, ()=>{ - const head = createDocumentHead(defaults); - const getData = (loaderOrAction)=>{ - const id = loaderOrAction.__id; - if (loaderOrAction.__brand === 'server_loader') { - if (!(id in endpoint.loaders)) throw new Error('You can not get the returned data of a loader that has not been executed for this request.'); - } - const data = endpoint.loaders[id]; - if (isPromise(data)) throw new Error('Loaders returning a promise can not be resolved for the head function.'); - return data; - }; - const fns = []; - for (const contentModule of contentModules){ - const contentModuleHead = contentModule?.head; - if (contentModuleHead) { - if (typeof contentModuleHead === 'function') fns.unshift(contentModuleHead); - else if (typeof contentModuleHead === 'object') resolveDocumentHead(head, contentModuleHead); - } - } - if (fns.length) { - const headProps = { - head, - withLocale: (fn)=>fn(), - resolveValue: getData, - ...routeLocation - }; - for (const fn of fns)resolveDocumentHead(head, fn(headProps)); - } - return head; +const q_useQwikMockRouter_createAsync_clbxpuXqpEU = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikMockRouter_createAsync_clbxpuXqpEU.mjs"), "useQwikMockRouter_createAsync_clbxpuXqpEU"); +const q_useQwikMockRouter_goto_aViHFxQ1a3s = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikMockRouter_goto_aViHFxQ1a3s.mjs"), "useQwikMockRouter_goto_aViHFxQ1a3s"); +const q_useQwikMockRouter_useTask_tXTLR4tzCy0 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikMockRouter_useTask_tXTLR4tzCy0.mjs"), "useQwikMockRouter_useTask_tXTLR4tzCy0"); +// +const useQwikMockRouter = (props)=>{ + const urlEnv = props.url ?? 'http://localhost/'; + const url = new URL(urlEnv); + const routeLocation = useStore({ + url, + params: props.params ?? {}, + isNavigating: false, + prevUrl: void 0 + }, { + deep: false }); -function callRestoreScrollOnDocument() { - if (document.__q_scroll_restore__) { - document.__q_scroll_restore__(); - document.__q_scroll_restore__ = void 0; - } -} -const currentScrollState = (elm)=>{ - return { - x: elm.scrollLeft, - y: elm.scrollTop, - w: Math.max(elm.scrollWidth, elm.clientWidth), - h: Math.max(elm.scrollHeight, elm.clientHeight) - }; -}; -const saveScrollHistory = (scrollState)=>{ - const state = history.state || {}; - state._qRouterScroll = scrollState; - history.replaceState(state, ''); -}; -const startViewTransition = (params)=>{ - if (!params.update) return; - if ('startViewTransition' in document) { - let transition; - try { - transition = document.startViewTransition(params); - } catch { - transition = document.startViewTransition(params.update); - } - const event = new CustomEvent('qviewtransition', { - detail: transition - }); - document.dispatchEvent(event); - return transition; - } else params.update?.(); + const loadersData = props.loaders?.reduce((acc, { loader, data })=>{ + acc[loader.__id] = data; + return acc; + }, {}); + const loaderState = useStore({}, { + deep: false + }); + for (const [loaderId, data] of Object.entries(loadersData ?? {}))loaderState[loaderId] ||= createAsyncQrl(q_useQwikMockRouter_createAsync_clbxpuXqpEU.w([ + data + ]), { + initial: data + }); + const goto = props.goto ?? q_useQwikMockRouter_goto_aViHFxQ1a3s; + const documentHead = useStore(createDocumentHead, { + deep: false + }); + const content = useStore({ + headings: void 0, + menu: void 0 + }, { + deep: false + }); + const contentInternal = useSignal(); + const actionState = useSignal(); + const httpStatus = useSignal({ + status: 200, + message: '' + }); + useContextProvider(ContentContext, content); + useContextProvider(ContentInternalContext, contentInternal); + useContextProvider(DocumentHeadContext, documentHead); + useContextProvider(HttpStatusContext, httpStatus); + useContextProvider(RouteLocationContext, routeLocation); + useContextProvider(RouteNavigateContext, goto); + useContextProvider(RouteStateContext, loaderState); + useContextProvider(RouteActionContext, actionState); + const actionsMocks = props.actions?.reduce((acc, { action, handler })=>{ + acc[action.__id] = handler; + return acc; + }, {}); + useTaskQrl(q_useQwikMockRouter_useTask_tXTLR4tzCy0.w([ + actionState, + actionsMocks + ])); }; -export const useQwikRouter_useTask_omhKiQfdzZU = ({ track })=>{ - const actionState = _captures[0], content = _captures[1], contentInternal = _captures[2], documentHead = _captures[3], env = _captures[4], goto = _captures[5], loaderState = _captures[6], loadersObject = _captures[7], navResolver = _captures[8], props = _captures[9], routeInternal = _captures[10], routeLocation = _captures[11], routeLocationTarget = _captures[12], serverHead = _captures[13]; - async function run() { - const navigation = track(routeInternal); - const action = track(actionState); - const locale = getLocale(''); - const prevUrl = routeLocation.url; - const navType = action ? 'form' : navigation.type; - const replaceState = navigation.replaceState; - let trackUrl; - let clientPageData; - let loadedRoute = null; - let container2; - if (isServer) { - trackUrl = new URL(navigation.dest, routeLocation.url); - loadedRoute = env.loadedRoute; - clientPageData = env.response; - } else { - trackUrl = new URL(navigation.dest, location); - if (trackUrl.pathname.endsWith('/')) { - if (globalThis.__NO_TRAILING_SLASH__) trackUrl.pathname = trackUrl.pathname.slice(0, -1); - } else if (!globalThis.__NO_TRAILING_SLASH__) trackUrl.pathname += '/'; - let loadRoutePromise = loadRoute(qwikRouterConfig.routes, qwikRouterConfig.menus, qwikRouterConfig.cacheModules, trackUrl.pathname); - container2 = _getContextContainer(); - const pageData = clientPageData = await loadClientData(trackUrl, { - action, - clearCache: true - }); - if (!pageData) { - routeInternal.untrackedValue = { - type: navType, - dest: trackUrl - }; - return; - } - const newHref = pageData.href; - const newURL = new URL(newHref, trackUrl); - if (!isSamePath(newURL, trackUrl)) { - if (!pageData.isRewrite) trackUrl = newURL; - loadRoutePromise = loadRoute(qwikRouterConfig.routes, qwikRouterConfig.menus, qwikRouterConfig.cacheModules, newURL.pathname); - } - try { - loadedRoute = await loadRoutePromise; - } catch (e) { - console.error(e); - window.location.href = newHref; - return; - } - } - if (loadedRoute) { - const [routeName, params, mods, menu] = loadedRoute; - const contentModules = mods; - const pageModule = contentModules[contentModules.length - 1]; - if (navigation.dest.search && !!isSamePath(trackUrl, prevUrl)) trackUrl.search = navigation.dest.search; - let shouldForcePrevUrl = false; - let shouldForceUrl = false; - let shouldForceParams = false; - if (!isSamePath(trackUrl, prevUrl)) { - if (_hasStoreEffects(routeLocation, 'prevUrl')) shouldForcePrevUrl = true; - routeLocationTarget.prevUrl = prevUrl; - } - if (routeLocationTarget.url !== trackUrl) { - if (_hasStoreEffects(routeLocation, 'url')) shouldForceUrl = true; - routeLocationTarget.url = trackUrl; - } - if (routeLocationTarget.params !== params) { - if (_hasStoreEffects(routeLocation, 'params')) shouldForceParams = true; - routeLocationTarget.params = params; - } - routeInternal.untrackedValue = { - type: navType, - dest: trackUrl - }; - const resolvedHead = resolveHead(clientPageData, routeLocation, contentModules, locale, serverHead); - content.headings = pageModule.headings; - content.menu = menu; - contentInternal.untrackedValue = noSerialize(contentModules); - documentHead.links = resolvedHead.links; - documentHead.meta = resolvedHead.meta; - documentHead.styles = resolvedHead.styles; - documentHead.scripts = resolvedHead.scripts; - documentHead.title = resolvedHead.title; - documentHead.frontmatter = resolvedHead.frontmatter; - if (isBrowser) { - let scrollState; - if (navType === 'popstate') scrollState = getScrollHistory(); - const scroller = document.getElementById(QWIK_ROUTER_SCROLLER) ?? document.documentElement; - if (navigation.scroll && (!navigation.forceReload || !isSamePath(trackUrl, prevUrl)) && (navType === 'link' || navType === 'popstate') || navType === 'form' && !isSamePath(trackUrl, prevUrl)) document.__q_scroll_restore__ = ()=>restoreScroll(navType, trackUrl, prevUrl, scroller, scrollState); - const loaders = clientPageData?.loaders; - if (loaders) { - const container3 = _getContextContainer(); - for (const [key, value] of Object.entries(loaders)){ - const signal = loaderState[key]; - const awaitedValue = await value; - loadersObject[key] = awaitedValue; - if (!signal) loaderState[key] = createLoaderSignal(loadersObject, key, trackUrl, DEFAULT_LOADERS_SERIALIZATION_STRATEGY, container3); - else signal.invalidate(); - } - } - CLIENT_DATA_CACHE.clear(); - if (!window._qRouterSPA) { - window._qRouterSPA = true; - history.scrollRestoration = 'manual'; - window.addEventListener('popstate', ()=>{ - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); - goto(location.href, { - type: 'popstate' - }); - }); - window.removeEventListener('popstate', window._qRouterInitPopstate); - window._qRouterInitPopstate = void 0; - if (!window._qRouterHistoryPatch) { - window._qRouterHistoryPatch = true; - const pushState = history.pushState; - const replaceState2 = history.replaceState; - const prepareState = (state)=>{ - if (state === null || typeof state === 'undefined') state = {}; - else if (state?.constructor !== Object) { - state = { - _data: state - }; - if (isDev) console.warn('In a Qwik SPA context, `history.state` is used to store scroll state. Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. We need to be able to automatically attach the scroll state to your state object. A new state object has been created, your data has been moved to: `history.state._data`'); - } - state._qRouterScroll = state._qRouterScroll || currentScrollState(scroller); - return state; - }; - history.pushState = (state, title, url2)=>{ - state = prepareState(state); - return pushState.call(history, state, title, url2); - }; - history.replaceState = (state, title, url2)=>{ - state = prepareState(state); - return replaceState2.call(history, state, title, url2); - }; - } - document.addEventListener('click', (event)=>{ - if (event.defaultPrevented) return; - const target = event.target.closest('a[href]'); - if (target && !target.hasAttribute('preventdefault:click')) { - const href = target.getAttribute('href'); - const prev = new URL(location.href); - const dest = new URL(href, prev); - if (isSameOrigin(dest, prev) && isSamePath(dest, prev)) { - event.preventDefault(); - if (!dest.hash && !dest.href.endsWith('#')) { - if (dest.href !== prev.href) history.pushState(null, '', dest); - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); - saveScrollHistory({ - ...currentScrollState(scroller), - x: 0, - y: 0 - }); - location.reload(); - return; - } - goto(target.getAttribute('href')); - } - } - }); - document.removeEventListener('click', window._qRouterInitAnchors); - window._qRouterInitAnchors = void 0; - if (!window.navigation) { - document.addEventListener('visibilitychange', ()=>{ - if ((window._qRouterScrollEnabled || window._qCityScrollEnabled) && document.visibilityState === 'hidden') { - if (window._qCityScrollEnabled) console.warn('"_qCityScrollEnabled" is deprecated. Use "_qRouterScrollEnabled" instead.'); - const scrollState2 = currentScrollState(scroller); - saveScrollHistory(scrollState2); - } - }, { - passive: true - }); - document.removeEventListener('visibilitychange', window._qRouterInitVisibility); - window._qRouterInitVisibility = void 0; - } - window.addEventListener('scroll', ()=>{ - if (!window._qRouterScrollEnabled && !window._qCityScrollEnabled) return; - clearTimeout(window._qRouterScrollDebounce); - window._qRouterScrollDebounce = setTimeout(()=>{ - const scrollState2 = currentScrollState(scroller); - saveScrollHistory(scrollState2); - window._qRouterScrollDebounce = void 0; - }, 200); - }, { - passive: true - }); - removeEventListener('scroll', window._qRouterInitScroll); - window._qRouterInitScroll = void 0; - spaInit.resolve(); - } - if (navType !== 'popstate') { - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); - const scrollState2 = currentScrollState(scroller); - saveScrollHistory(scrollState2); - } - const navigate = ()=>{ - clientNavigate(window, navType, prevUrl, trackUrl, replaceState); - contentInternal.trigger(); - return _waitUntilRendered(container2); - }; - const _waitNextPage = ()=>{ - if (isServer || props?.viewTransition === false) return navigate(); - else { - const viewTransition = startViewTransition({ - update: navigate, - types: [ - 'qwik-navigation' - ] - }); - if (!viewTransition) return Promise.resolve(); - return viewTransition.ready; - } - }; - _waitNextPage().catch((err)=>{ - navigate(); - throw err; - }).finally(()=>{ - container2.element.setAttribute?.(Q_ROUTE, routeName); - const scrollState2 = currentScrollState(scroller); - saveScrollHistory(scrollState2); - window._qRouterScrollEnabled = true; - if (isBrowser) callRestoreScrollOnDocument(); - if (shouldForcePrevUrl) forceStoreEffects(routeLocation, 'prevUrl'); - if (shouldForceUrl) forceStoreEffects(routeLocation, 'url'); - if (shouldForceParams) forceStoreEffects(routeLocation, 'params'); - routeLocation.isNavigating = false; - navResolver.r?.(); - }); - } - } - } - if (isServer) return run(); - else run(); +export const QwikRouterMockProvider_component_IN4dVpT0x74 = (props)=>{ + useQwikMockRouter(props); + return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, '5y_1'); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;;;;;;;;;;;;;;;MAkQM,aAAa,CAAC,aAAa;IAC/B,IAAI,MAAM,OAAO,CAAC,SAChB,KAAK,MAAM,WAAW,OAAQ;QAC5B,IAAI,OAAO,QAAQ,GAAG,KAAK,UAAU;YACnC,MAAM,gBAAgB,YAAY,SAAS,CAAC,CAAC,IAAM,EAAE,GAAG,KAAK,QAAQ,GAAG;YACxE,IAAI,gBAAgB,IAAI;gBACtB,WAAW,CAAC,cAAc,GAAG;gBAC7B;YACF;QACF;QACA,YAAY,IAAI,CAAC;IACnB;AAEJ;MAvBM,sBAAsB,CAAC,cAAc;IACzC,IAAI,OAAO,YAAY,KAAK,KAAK,UAC/B,aAAa,KAAK,GAAG,YAAY,KAAK;IAExC,WAAW,aAAa,IAAI,EAAE,YAAY,IAAI;IAC9C,WAAW,aAAa,KAAK,EAAE,YAAY,KAAK;IAChD,WAAW,aAAa,MAAM,EAAE,YAAY,MAAM;IAClD,WAAW,aAAa,OAAO,EAAE,YAAY,OAAO;IACpD,OAAO,MAAM,CAAC,aAAa,WAAW,EAAE,YAAY,WAAW;AACjE;MAnDM,cAAc,CAAC,UAAU,eAAe,gBAAgB,QAAQ,WACpE,WAAW,QAAQ;QACjB,MAAM,OAAO,mBAAmB;QAChC,MAAM,UAAU,CAAC;YACf,MAAM,KAAK,eAAe,IAAI;YAC9B,IAAI,eAAe,OAAO,KAAK,iBAAiB;gBAC9C,IAAI,CAAC,CAAC,MAAM,SAAS,OAAO,GAC1B,MAAM,IAAI,MACR;YAGN;YACA,MAAM,OAAO,SAAS,OAAO,CAAC,GAAG;YACjC,IAAI,UAAU,OACZ,MAAM,IAAI,MAAM;YAElB,OAAO;QACT;QACA,MAAM,MAAM,EAAE;QACd,KAAK,MAAM,iBAAiB,eAAgB;YAC1C,MAAM,oBAAoB,eAAe;YACzC,IAAI,mBAAmB;gBACrB,IAAI,OAAO,sBAAsB,YAC/B,IAAI,OAAO,CAAC;qBACP,IAAI,OAAO,sBAAsB,UACtC,oBAAoB,MAAM;YAE9B;QACF;QACA,IAAI,IAAI,MAAM,EAAE;YACd,MAAM,YAAY;gBAChB;gBACA,YAAY,CAAC,KAAO;gBACpB,cAAc;gBACd,GAAG,aAAa;YAClB;YACA,KAAK,MAAM,MAAM,IACf,oBAAoB,MAAM,GAAG;QAEjC;QACA,OAAO;IACT;AAqCF,SAAS;IACP,IAAI,SAAS,oBAAoB,EAAE;QACjC,SAAS,oBAAoB;QAC7B,SAAS,oBAAoB,GAAG,KAAK;IACvC;AACF;MAqBM,qBAAqB,CAAC;IAC1B,OAAO;QACL,GAAG,IAAI,UAAU;QACjB,GAAG,IAAI,SAAS;QAChB,GAAG,KAAK,GAAG,CAAC,IAAI,WAAW,EAAE,IAAI,WAAW;QAC5C,GAAG,KAAK,GAAG,CAAC,IAAI,YAAY,EAAE,IAAI,YAAY;IAChD;AACF;MAKM,oBAAoB,CAAC;IACzB,MAAM,QAAQ,QAAQ,KAAK,IAAI,CAAC;IAChC,MAAM,cAAc,GAAG;IACvB,QAAQ,YAAY,CAAC,OAAO;AAC9B;MAoJM,sBAAsB,CAAC;IAC3B,IAAI,CAAC,OAAO,MAAM,EAChB;IAEF,IAAI,yBAAyB,UAAU;QACrC,IAAI;QACJ,IAAI;YACF,aAAa,SAAS,mBAAmB,CAAC;QAC5C,EAAE,OAAM;YACN,aAAa,SAAS,mBAAmB,CAAC,OAAO,MAAM;QACzD;QACA,MAAM,QAAQ,IAAI,YAAY,mBAAmB;YAAE,QAAQ;QAAW;QACtE,SAAS,aAAa,CAAC;QACvB,OAAO;IACT,OACE,OAAO,MAAM;AAEjB;iDA6NW,CAAC,EAAE,KAAK,EAAE;;IACjB,eAAe;QACb,MAAM,aAAa,MAAM;QACzB,MAAM,SAAS,MAAM;QACrB,MAAM,SAAS,UAAU;QACzB,MAAM,UAAU,cAAc,GAAG;QACjC,MAAM,UAAU,SAAS,SAAS,WAAW,IAAI;QACjD,MAAM,eAAe,WAAW,YAAY;QAC5C,IAAI;QACJ,IAAI;QACJ,IAAI,cAAc;QAClB,IAAI;QACJ,IAAI,UAAU;YACZ,WAAW,IAAI,IAAI,WAAW,IAAI,EAAE,cAAc,GAAG;YACrD,cAAc,IAAI,WAAW;YAC7B,iBAAiB,IAAI,QAAQ;QAC/B,OAAO;YACL,WAAW,IAAI,IAAI,WAAW,IAAI,EAAE;YACpC,IAAI,SAAS,QAAQ,CAAC,QAAQ,CAAC,MAC7B;gBAAA,IAAI,WAAW,qBAAqB,EAClC,SAAS,QAAQ,GAAG,SAAS,QAAQ,CAAC,KAAK,CAAC,GAAG;YACjD,OACK,IAAI,CAAC,WAAW,qBAAqB,EAC1C,SAAS,QAAQ,IAAI;YAEvB,IAAI,mBAAmB,UACrB,iBAAiB,MAAM,EACvB,iBAAiB,KAAK,EACtB,iBAAiB,YAAY,EAC7B,SAAS,QAAQ;YAEnB,aAAa;YACb,MAAM,WAAY,iBAAiB,MAAM,eAAe,UAAU;gBAChE;gBACA,YAAY;YACd;YACA,IAAI,CAAC,UAAU;gBACb,cAAc,cAAc,GAAG;oBAAE,MAAM;oBAAS,MAAM;gBAAS;gBAC/D;YACF;YACA,MAAM,UAAU,SAAS,IAAI;YAC7B,MAAM,SAAS,IAAI,IAAI,SAAS;YAChC,IAAI,CAAC,WAAW,QAAQ,WAAW;gBACjC,IAAI,CAAC,SAAS,SAAS,EACrB,WAAW;gBAEb,mBAAmB,UACjB,iBAAiB,MAAM,EACvB,iBAAiB,KAAK,EACtB,iBAAiB,YAAY,EAC7B,OAAO,QAAQ;YAGnB;YACA,IAAI;gBACF,cAAc,MAAM;YACtB,EAAE,OAAO,GAAG;gBACV,QAAQ,KAAK,CAAC;gBACd,OAAO,QAAQ,CAAC,IAAI,GAAG;gBACvB;YACF;QACF;QACA,IAAI,aAAa;YACf,MAAM,CAAC,WAAW,QAAQ,MAAM,KAAK,GAAG;YACxC,MAAM,iBAAiB;YACvB,MAAM,aAAa,cAAc,CAAC,eAAe,MAAM,GAAG,EAAE;YAC5D,IAAI,WAAW,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,WAAW,UAAU,UACnD,SAAS,MAAM,GAAG,WAAW,IAAI,CAAC,MAAM;YAE1C,IAAI,qBAAqB;YACzB,IAAI,iBAAiB;YACrB,IAAI,oBAAoB;YACxB,IAAI,CAAC,WAAW,UAAU,UAAU;gBAClC,IAAI,iBAAiB,eAAe,YAClC,qBAAqB;gBAEvB,oBAAoB,OAAO,GAAG;YAChC;YACA,IAAI,oBAAoB,GAAG,KAAK,UAAU;gBACxC,IAAI,iBAAiB,eAAe,QAClC,iBAAiB;gBAEnB,oBAAoB,GAAG,GAAG;YAC5B;YACA,IAAI,oBAAoB,MAAM,KAAK,QAAQ;gBACzC,IAAI,iBAAiB,eAAe,WAClC,oBAAoB;gBAEtB,oBAAoB,MAAM,GAAG;YAC/B;YACA,cAAc,cAAc,GAAG;gBAAE,MAAM;gBAAS,MAAM;YAAS;YAC/D,MAAM,eAAe,YACnB,gBACA,eACA,gBACA,QACA;YAEF,QAAQ,QAAQ,GAAG,WAAW,QAAQ;YACtC,QAAQ,IAAI,GAAG;YACf,gBAAgB,cAAc,GAAG,YAAY;YAC7C,aAAa,KAAK,GAAG,aAAa,KAAK;YACvC,aAAa,IAAI,GAAG,aAAa,IAAI;YACrC,aAAa,MAAM,GAAG,aAAa,MAAM;YACzC,aAAa,OAAO,GAAG,aAAa,OAAO;YAC3C,aAAa,KAAK,GAAG,aAAa,KAAK;YACvC,aAAa,WAAW,GAAG,aAAa,WAAW;YACnD,IAAI,WAAW;gBACb,IAAI;gBACJ,IAAI,YAAY,YACd,cAAc;gBAEhB,MAAM,WACJ,SAAS,cAAc,CAAC,yBAAyB,SAAS,eAAe;gBAC3E,IACE,AAAC,WAAW,MAAM,IAChB,CAAC,CAAC,WAAW,WAAW,IAAI,CAAC,WAAW,UAAU,QAAQ,KAC1D,CAAC,YAAY,UAAU,YAAY,UAAU,KAC9C,YAAY,UAAU,CAAC,WAAW,UAAU,UAE7C,SAAS,oBAAoB,GAAG,IAC9B,cAAc,SAAS,UAAU,SAAS,UAAU;gBAExD,MAAM,UAAU,gBAAgB;gBAChC,IAAI,SAAS;oBACX,MAAM,aAAa;oBACnB,KAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,OAAO,CAAC,SAAU;wBAClD,MAAM,SAAS,WAAW,CAAC,IAAI;wBAC/B,MAAM,eAAe,MAAM;wBAC3B,aAAa,CAAC,IAAI,GAAG;wBACrB,IAAI,CAAC,QACH,WAAW,CAAC,IAAI,GAAG,mBACjB,eACA,KACA,UACA,wCACA;6BAGF,OAAO,UAAU;oBAErB;gBACF;gBACA,kBAAkB,KAAK;gBACvB,IAAI,CAAC,OAAO,WAAW,EAAE;oBACvB,OAAO,WAAW,GAAG;oBACrB,QAAQ,iBAAiB,GAAG;oBAC5B,OAAO,gBAAgB,CAAC,YAAY;wBAClC,OAAO,qBAAqB,GAAG;wBAC/B,aAAa,OAAO,sBAAsB;wBAC1C,KAAK,SAAS,IAAI,EAAE;4BAClB,MAAM;wBACR;oBACF;oBACA,OAAO,mBAAmB,CAAC,YAAY,OAAO,oBAAoB;oBAClE,OAAO,oBAAoB,GAAG,KAAK;oBACnC,IAAI,CAAC,OAAO,oBAAoB,EAAE;wBAChC,OAAO,oBAAoB,GAAG;wBAC9B,MAAM,YAAY,QAAQ,SAAS;wBACnC,MAAM,gBAAgB,QAAQ,YAAY;wBAC1C,MAAM,eAAe,CAAC;4BACpB,IAAI,UAAU,QAAQ,OAAO,UAAU,aACrC,QAAQ,CAAC;iCACJ,IAAI,OAAO,gBAAgB,QAAQ;gCACxC,QAAQ;oCAAE,OAAO;gCAAM;gCACvB,IAAI,OACF,QAAQ,IAAI,CACV;4BAGN;4BACA,MAAM,cAAc,GAAG,MAAM,cAAc,IAAI,mBAAmB;4BAClE,OAAO;wBACT;wBACA,QAAQ,SAAS,GAAG,CAAC,OAAO,OAAO;4BACjC,QAAQ,aAAa;4BACrB,OAAO,UAAU,IAAI,CAAC,SAAS,OAAO,OAAO;wBAC/C;wBACA,QAAQ,YAAY,GAAG,CAAC,OAAO,OAAO;4BACpC,QAAQ,aAAa;4BACrB,OAAO,cAAc,IAAI,CAAC,SAAS,OAAO,OAAO;wBACnD;oBACF;oBACA,SAAS,gBAAgB,CAAC,SAAS,CAAC;wBAClC,IAAI,MAAM,gBAAgB,EACxB;wBAEF,MAAM,SAAS,MAAM,MAAM,CAAC,OAAO,CAAC;wBACpC,IAAI,UAAU,CAAC,OAAO,YAAY,CAAC,yBAAyB;4BAC1D,MAAM,OAAO,OAAO,YAAY,CAAC;4BACjC,MAAM,OAAO,IAAI,IAAI,SAAS,IAAI;4BAClC,MAAM,OAAO,IAAI,IAAI,MAAM;4BAC3B,IAAI,aAAa,MAAM,SAAS,WAAW,MAAM,OAAO;gCACtD,MAAM,cAAc;gCACpB,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,MAAM;oCAC1C,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,EACzB,QAAQ,SAAS,CAAC,MAAM,IAAI;oCAE9B,OAAO,qBAAqB,GAAG;oCAC/B,aAAa,OAAO,sBAAsB;oCAC1C,kBAAkB;wCAChB,GAAG,mBAAmB,SAAS;wCAC/B,GAAG;wCACH,GAAG;oCACL;oCACA,SAAS,MAAM;oCACf;gCACF;gCACA,KAAK,OAAO,YAAY,CAAC;4BAC3B;wBACF;oBACF;oBACA,SAAS,mBAAmB,CAAC,SAAS,OAAO,mBAAmB;oBAChE,OAAO,mBAAmB,GAAG,KAAK;oBAClC,IAAI,CAAC,OAAO,UAAU,EAAE;wBACtB,SAAS,gBAAgB,CACvB,oBACA;4BACE,IACE,CAAC,OAAO,qBAAqB,IAAI,OAAO,mBAAmB,KAC3D,SAAS,eAAe,KAAK,UAC7B;gCACA,IAAI,OAAO,mBAAmB,EAC5B,QAAQ,IAAI,CACV;gCAGJ,MAAM,eAAe,mBAAmB;gCACxC,kBAAkB;4BACpB;wBACF,GACA;4BAAE,SAAS;wBAAK;wBAElB,SAAS,mBAAmB,CAAC,oBAAoB,OAAO,sBAAsB;wBAC9E,OAAO,sBAAsB,GAAG,KAAK;oBACvC;oBACA,OAAO,gBAAgB,CACrB,UACA;wBACE,IAAI,CAAC,OAAO,qBAAqB,IAAI,CAAC,OAAO,mBAAmB,EAC9D;wBAEF,aAAa,OAAO,sBAAsB;wBAC1C,OAAO,sBAAsB,GAAG,WAAW;4BACzC,MAAM,eAAe,mBAAmB;4BACxC,kBAAkB;4BAClB,OAAO,sBAAsB,GAAG,KAAK;wBACvC,GAAG;oBACL,GACA;wBAAE,SAAS;oBAAK;oBAElB,oBAAoB,UAAU,OAAO,kBAAkB;oBACvD,OAAO,kBAAkB,GAAG,KAAK;oBACjC,QAAQ,OAAO;gBACjB;gBACA,IAAI,YAAY,YAAY;oBAC1B,OAAO,qBAAqB,GAAG;oBAC/B,aAAa,OAAO,sBAAsB;oBAC1C,MAAM,eAAe,mBAAmB;oBACxC,kBAAkB;gBACpB;gBACA,MAAM,WAAW;oBACf,eAAe,QAAQ,SAAS,SAAS,UAAU;oBACnD,gBAAgB,OAAO;oBACvB,OAAO,mBAAmB;gBAC5B;gBACA,MAAM,gBAAgB;oBACpB,IAAI,YAAY,OAAO,mBAAmB,OACxC,OAAO;yBACF;wBACL,MAAM,iBAAiB,oBAAoB;4BACzC,QAAQ;4BACR,OAAO;gCAAC;6BAAkB;wBAC5B;wBACA,IAAI,CAAC,gBACH,OAAO,QAAQ,OAAO;wBAExB,OAAO,eAAe,KAAK;oBAC7B;gBACF;gBACA,gBACG,KAAK,CAAC,CAAC;oBACN;oBACA,MAAM;gBACR,GACC,OAAO,CAAC;oBACP,WAAW,OAAO,CAAC,YAAY,GAAG,SAAS;oBAC3C,MAAM,eAAe,mBAAmB;oBACxC,kBAAkB;oBAClB,OAAO,qBAAqB,GAAG;oBAC/B,IAAI,WACF;oBAEF,IAAI,oBACF,kBAAkB,eAAe;oBAEnC,IAAI,gBACF,kBAAkB,eAAe;oBAEnC,IAAI,mBACF,kBAAkB,eAAe;oBAEnC,cAAc,YAAY,GAAG;oBAC7B,YAAY,CAAC;gBACf;YACJ;QACF;IACF;IACA,IAAI,UACF,OAAO;SAEP\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;;;;;;;;;MAiuCM,oBAAoB,CAAC;IACzB,MAAM,SAAS,MAAM,GAAG,IAAI;IAC5B,MAAM,MAAM,IAAI,IAAI;IACpB,MAAM,gBAAgB,SACpB;QACE;QACA,QAAQ,MAAM,MAAM,IAAI,CAAC;QACzB,cAAc;QACd,SAAS,KAAK;IAChB,GACA;QACE,MAAM;IACR;IAEF,MAAM,cAAc,MAAM,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE;QAC9D,GAAG,CAAC,OAAO,IAAI,CAAC,GAAG;QACnB,OAAO;IACT,GAAG,CAAC;IACJ,MAAM,cAAc,SAClB,CAAC,GACD;QACE,MAAM;IACR;IAEF,KAAK,MAAM,CAAC,UAAU,KAAK,IAAI,OAAO,OAAO,CAAC,eAAe,CAAC,GAC5D,WAAW,CAAC,SAAS,KAAK;;QASxB;QACE,SAAS;IACX;IAGJ,MAAM,OACJ,MAAM,IAAI;IAIZ,MAAM,eAAe,SAAS,oBAAoB;QAChD,MAAM;IACR;IACA,MAAM,UAAU,SACd;QACE,UAAU,KAAK;QACf,MAAM,KAAK;IACb,GACA;QACE,MAAM;IACR;IAEF,MAAM,kBAAkB;IACxB,MAAM,cAAc;IACpB,MAAM,aAAa,UAAU;QAC3B,QAAQ;QACR,SAAS;IACX;IACA,mBAAmB,gBAAgB;IACnC,mBAAmB,wBAAwB;IAC3C,mBAAmB,qBAAqB;IACxC,mBAAmB,mBAAmB;IACtC,mBAAmB,sBAAsB;IACzC,mBAAmB,sBAAsB;IACzC,mBAAmB,mBAAmB;IACtC,mBAAmB,oBAAoB;IACvC,MAAM,eAAe,MAAM,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE;QAClE,GAAG,CAAC,OAAO,IAAI,CAAC,GAAG;QACnB,OAAO;IACT,GAAG,CAAC;IACJ;;;;AAmBF;4DAE6B,CAAC;IAC1B,kBAAkB;IAClB,OAAO,aAAa,GAAG,WAAW,MAAM,MAAM,MAAM,MAAM,GAAG;AAC/D\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "useQwikRouter_useTask_omhKiQfdzZU", + "name": "QwikRouterMockProvider_component_IN4dVpT0x74", "entry": null, - "displayName": "index.qwik.mjs_useQwikRouter_useTask", - "hash": "omhKiQfdzZU", - "canonicalFilename": "index.qwik.mjs_useQwikRouter_useTask_omhKiQfdzZU", + "displayName": "index.qwik.mjs_QwikRouterMockProvider_component", + "hash": "IN4dVpT0x74", + "canonicalFilename": "index.qwik.mjs_QwikRouterMockProvider_component_IN4dVpT0x74", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, "ctxKind": "function", - "ctxName": "useTask$", - "captures": true, + "ctxName": "component$", + "captures": false, "loc": [ - 23188, - 35596 + 45055, + 45175 ], "paramNames": [ - "{track}" - ], - "captureNames": [ - "actionState", - "content", - "contentInternal", - "documentHead", - "env", - "goto", - "loaderState", - "loadersObject", - "navResolver", - "props", - "routeInternal", - "routeLocation", - "routeLocationTarget", - "serverHead" + "props" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_usePreventNavigateQrl_useVisibleTask_no0bm2fybZo.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_RouterOutlet_component_QwONcWD5gIg.mjs (ENTRY POINT)== -import { _captures } from "@qwik.dev/core"; +import { _auto_spaInit as spaInit } from "./index.qwik.mjs"; +import { a as ContentInternalContext } from "./chunks/route-loaders.qwik.mjs"; +import { Fragment } from "@qwik.dev/core/jsx-runtime"; +import { SkipRender } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { _qrlSync } from "@qwik.dev/core"; +import { useContext } from "@qwik.dev/core"; +import { useServerData } from "@qwik.dev/core"; // -export const usePreventNavigateQrl_useVisibleTask_no0bm2fybZo = ()=>{ - const fn = _captures[0], registerPreventNav = _captures[1]; - return registerPreventNav(fn); +export const RouterOutlet_component_QwONcWD5gIg = ()=>{ + const serverData = useServerData('containerAttributes'); + if (!serverData) throw new Error('PrefetchServiceWorker component must be rendered on the server.'); + const internalContext = useContext(ContentInternalContext); + const contents = internalContext.value; + if (contents && contents.length > 0) { + const contentsLen = contents.length; + let cmp = null; + for(let i = contentsLen - 1; i >= 0; i--)if (contents[i].default) cmp = _jsxSorted(contents[i].default, null, null, cmp, 1, 'Fn_0'); + return /* @__PURE__ */ _jsxSorted(Fragment, null, null, [ + cmp, + !__EXPERIMENTAL__.noSPA && /* @__PURE__ */ _jsxSorted('script', { + 'q-d:qinit': _qrlSync(()=>{ + ((w, h)=>{ + if (!w._qcs && h.scrollRestoration === 'manual') { + w._qcs = true; + const s = h.state?._qRouterScroll; + if (s) w.scrollTo(s.x, s.y); + document.dispatchEvent(new Event('qcinit')); + } + })(window, history); + }, '()=>{((w,h)=>{if(!w._qcs&&h.scrollRestoration==="manual"){w._qcs=!0;const s=h.state?._qRouterScroll;if(s){w.scrollTo(s.x,s.y);}document.dispatchEvent(new Event("qcinit"));}})(window,history);}') + }, { + 'q-d:qcinit': spaInit + }, null, 2, 'Fn_1') + ], 1, 'Fn_2'); + } + return SkipRender; }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;gEA0GkB;;WAAM,mBAAmB\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;kDAw0C6B;IACzB,MAAM,aAAa,cAAc;IACjC,IAAI,CAAC,YACH,MAAM,IAAI,MAAM;IAElB,MAAM,kBAAkB,WAAW;IACnC,MAAM,WAAW,gBAAgB,KAAK;IACtC,IAAI,YAAY,SAAS,MAAM,GAAG,GAAG;QACnC,MAAM,cAAc,SAAS,MAAM;QACnC,IAAI,MAAM;QACV,IAAK,IAAI,IAAI,cAAc,GAAG,KAAK,GAAG,IACpC,IAAI,QAAQ,CAAC,EAAE,CAAC,OAAO,EACrB,MAAM,WAAW,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,MAAM,KAAK,GAAG;QAG9D,OAAO,aAAa,GAAG,WACrB,UACA,MACA,MACA;YACE;YACA,CAAC,iBAAiB,KAAK,IACrB,aAAa,GAAG,WACd,UACA;gBACE,aAAa,SAAS;oBACpB,CAAC,CAAC,GAAG;wBACH,IAAI,CAAC,EAAE,IAAI,IAAI,EAAE,iBAAiB,KAAK,UAAU;4BAC/C,EAAE,IAAI,GAAG;4BACT,MAAM,IAAI,EAAE,KAAK,EAAE;4BACnB,IAAI,GACF,EAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;4BAErB,SAAS,aAAa,CAAC,IAAI,MAAM;wBACnC;oBACF,CAAC,EAAE,QAAQ;gBACb,GAAG;YACL,GACA;gBACE,cAAc;YAChB,GACA,MACA,GACA;SAEL,EACD,GACA;IAEJ;IACA,OAAO;AACT\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "usePreventNavigateQrl_useVisibleTask_no0bm2fybZo", + "name": "RouterOutlet_component_QwONcWD5gIg", "entry": null, - "displayName": "index.qwik.mjs_usePreventNavigateQrl_useVisibleTask", - "hash": "no0bm2fybZo", - "canonicalFilename": "index.qwik.mjs_usePreventNavigateQrl_useVisibleTask_no0bm2fybZo", + "displayName": "index.qwik.mjs_RouterOutlet_component", + "hash": "QwONcWD5gIg", + "canonicalFilename": "index.qwik.mjs_RouterOutlet_component_QwONcWD5gIg", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, "ctxKind": "function", - "ctxName": "useVisibleTask$", - "captures": true, + "ctxName": "component$", + "captures": false, "loc": [ - 3065, - 3093 - ], - "captureNames": [ - "fn", - "registerPreventNav" + 45362, + 47103 ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikMockRouter_useTask_oml2hW1aK6I.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_routeActionQrl_action_submit_YuS5bpdQ360.mjs (ENTRY POINT)== import { _captures } from "@qwik.dev/core"; +import { isServer } from "@qwik.dev/core"; +import { noSerialize } from "@qwik.dev/core"; // -export const useQwikMockRouter_useTask_oml2hW1aK6I = async ({ track })=>{ - const actionState = _captures[0], actionsMocks = _captures[1]; - const action = track(actionState); - if (!action?.resolve) return; - const mock = actionsMocks?.[action.id]; - if (mock) { - const actionResult = await mock(action.data); - action.resolve(actionResult); - } +export const routeActionQrl_action_submit_YuS5bpdQ360 = (input = {})=>{ + const currentAction2 = _captures[0], id2 = _captures[1], loc2 = _captures[2], state2 = _captures[3]; + if (isServer) throw new Error(`Actions can not be invoked within the server during SSR. +Action.run() can only be called on the browser, for example when a user clicks a button, or submits a form.`); + let data; + let form; + if (input instanceof SubmitEvent) { + form = input.target; + data = new FormData(form); + if ((input.submitter instanceof HTMLInputElement || input.submitter instanceof HTMLButtonElement) && input.submitter.name) { + if (input.submitter.name) data.append(input.submitter.name, input.submitter.value); + } + } else data = input; + return new Promise((resolve)=>{ + if (data instanceof FormData) state2.formData = data; + state2.submitted = true; + state2.isRunning = true; + loc2.isNavigating = true; + currentAction2.value = { + data, + id: id2, + resolve: noSerialize(resolve) + }; + }).then((_rawProps)=>{ + state2.isRunning = false; + state2.status = _rawProps.status; + state2.value = _rawProps.result; + if (form) { + if (form.getAttribute('data-spa-reset') === 'true') form.reset(); + const detail = { + status: _rawProps.status, + value: _rawProps.result + }; + form.dispatchEvent(new CustomEvent('submitcompleted', { + bubbles: false, + cancelable: false, + composed: false, + detail + })); + } + return { + status: _rawProps.status, + value: _rawProps.result + }; + }); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;qDAmjCW,OAAO,EAAE,KAAK,EAAE;;IACvB,MAAM,SAAS,MAAM;IACrB,IAAI,CAAC,QAAQ,SACX;IAEF,MAAM,OAAO,cAAc,CAAC,OAAO,EAAE,CAAC;IACtC,IAAI,MAAM;QACR,MAAM,eAAe,MAAM,KAAK,OAAO,IAAI;QAC3C,OAAO,OAAO,CAAC;IACjB\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;wDAqkDM,CAAC,QAAQ,CAAC,CAAC;IACT,MAAM,iBAAiB,SAAS,CAAC,EAAE,EACjC,MAAM,SAAS,CAAC,EAAE,EAClB,OAAO,SAAS,CAAC,EAAE,EACnB,SAAS,SAAS,CAAC,EAAE;IACvB,IAAI,UACF,MAAM,IAAI,MAAM,CAAC;2GACgF,CAAC;IAEpG,IAAI;IACJ,IAAI;IACJ,IAAI,iBAAiB,aAAa;QAChC,OAAO,MAAM,MAAM;QACnB,OAAO,IAAI,SAAS;QACpB,IACE,CAAC,MAAM,SAAS,YAAY,oBAC1B,MAAM,SAAS,YAAY,iBAAiB,KAC9C,MAAM,SAAS,CAAC,IAAI,EAEpB;YAAA,IAAI,MAAM,SAAS,CAAC,IAAI,EACtB,KAAK,MAAM,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,MAAM,SAAS,CAAC,KAAK;QACzD;IAEJ,OACE,OAAO;IAET,OAAO,IAAI,QAAQ,CAAC;QAClB,IAAI,gBAAgB,UAClB,OAAO,QAAQ,GAAG;QAEpB,OAAO,SAAS,GAAG;QACnB,OAAO,SAAS,GAAG;QACnB,KAAK,YAAY,GAAG;QACpB,eAAe,KAAK,GAAG;YACrB;YACA,IAAI;YACJ,SAAS,YAAY;QACvB;IACF,GAAG,IAAI,CAAC,CAAC;QACP,OAAO,SAAS,GAAG;QACnB,OAAO,MAAM,GAAG,UAAU,MAAM;QAChC,OAAO,KAAK,GAAG,UAAU,MAAM;QAC/B,IAAI,MAAM;YACR,IAAI,KAAK,YAAY,CAAC,sBAAsB,QAC1C,KAAK,KAAK;YAEZ,MAAM,SAAS;gBACb,QAAQ,UAAU,MAAM;gBACxB,OAAO,UAAU,MAAM;YACzB;YACA,KAAK,aAAa,CAChB,IAAI,YAAY,mBAAmB;gBACjC,SAAS;gBACT,YAAY;gBACZ,UAAU;gBACV;YACF;QAEJ;QACA,OAAO;YACL,QAAQ,UAAU,MAAM;YACxB,OAAO,UAAU,MAAM;QACzB;IACF;AACF\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "useQwikMockRouter_useTask_oml2hW1aK6I", + "name": "routeActionQrl_action_submit_YuS5bpdQ360", "entry": null, - "displayName": "index.qwik.mjs_useQwikMockRouter_useTask", - "hash": "oml2hW1aK6I", - "canonicalFilename": "index.qwik.mjs_useQwikMockRouter_useTask_oml2hW1aK6I", + "displayName": "index.qwik.mjs_routeActionQrl_action_submit", + "hash": "YuS5bpdQ360", + "canonicalFilename": "index.qwik.mjs_routeActionQrl_action_submit_YuS5bpdQ360", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, "ctxKind": "function", - "ctxName": "useTask$", + "ctxName": "submit", "captures": true, "loc": [ - 37161, - 37428 - ], - "paramNames": [ - "{track}" + 53055, + 55117 ], "captureNames": [ - "actionState", - "actionsMocks" + "currentAction", + "id", + "loc", + "state" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_serverQrl_RA3PmZ4Oyak.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_serverQrl_w03grD0Ag68.mjs (ENTRY POINT)== +import { k as QDATA_KEY } from "./chunks/route-loaders.qwik.mjs"; +import { j as QFN_KEY } from "./chunks/route-loaders.qwik.mjs"; import { _captures } from "@qwik.dev/core"; -import { _auto_useQwikRouterEnv as useQwikRouterEnv } from "./index.qwik.mjs"; -import { j as QDATA_KEY } from "./chunks/routing.qwik.mjs"; -import { f as QFN_KEY } from "./chunks/routing.qwik.mjs"; -import { _asyncRequestStore } from "@qwik.dev/router/middleware/request-handler"; import { _deserialize } from "@qwik.dev/core/internal"; -import { _getContextEvent } from "@qwik.dev/core/internal"; import { _serialize } from "@qwik.dev/core/internal"; +import { i as getRequestEvent } from "./chunks/route-loaders.qwik.mjs"; import { isServer } from "@qwik.dev/core"; -import { qrl } from "@qwik.dev/core"; // const deserializeStream = async function*(stream, abortSignal) { const reader = stream.getReader(); @@ -3664,21 +3713,11 @@ const deserializeStream = async function*(stream, abortSignal) { reader.releaseLock(); } }; -export const serverQrl_RA3PmZ4Oyak = async function(...args) { - const fetchOptions = _captures[0], headers = _captures[1], method = _captures[2], origin = _captures[3], qrl = _captures[4]; +export const serverQrl_w03grD0Ag68 = async function(...args) { + const fetchOptions2 = _captures[0], headers2 = _captures[1], method2 = _captures[2], origin2 = _captures[3], qrl2 = _captures[4]; const abortSignal = args.length > 0 && args[0] instanceof AbortSignal ? args.shift() : void 0; - if (isServer) { - let requestEvent = _asyncRequestStore?.getStore(); - if (!requestEvent) { - const contexts = [ - useQwikRouterEnv()?.ev, - this, - _getContextEvent() - ]; - requestEvent = contexts.find((v2)=>v2 && Object.prototype.hasOwnProperty.call(v2, 'sharedMap') && Object.prototype.hasOwnProperty.call(v2, 'cookie')); - } - return qrl.apply(requestEvent, args); - } else { + if (isServer) return qrl2.apply(getRequestEvent(this), args); + else { let filteredArgs = args.map((arg)=>{ if (arg instanceof SubmitEvent && arg.target instanceof HTMLFormElement) return new FormData(arg.target); else if (arg instanceof Event) return null; @@ -3686,13 +3725,13 @@ export const serverQrl_RA3PmZ4Oyak = async function(...args) { return arg; }); if (!filteredArgs.length) filteredArgs = void 0; - const qrlHash = qrl.getHash(); + const qrlHash = qrl2.getHash(); let query = ''; const config = { - ...fetchOptions, - method, + ...fetchOptions2, + method: method2, headers: { - ...headers, + ...headers2, 'Content-Type': 'application/qwik-json', Accept: 'application/json, application/qwik-json, text/qwik-json-stream, text/plain', // Required so we don't call accidentally @@ -3700,7 +3739,7 @@ export const serverQrl_RA3PmZ4Oyak = async function(...args) { }, signal: abortSignal }; - const captured = qrl.getCaptured(); + const captured = qrl2.getCaptured(); let toSend = [ filteredArgs ]; @@ -3712,9 +3751,9 @@ export const serverQrl_RA3PmZ4Oyak = async function(...args) { filteredArgs ] : []; const body = await _serialize(toSend); - if (method === 'GET') query += `&${QDATA_KEY}=${encodeURIComponent(body)}`; + if (method2 === 'GET') query += `&${QDATA_KEY}=${encodeURIComponent(body)}`; else config.body = body; - const res = await fetch(`${origin}?${QFN_KEY}=${qrlHash}${query}`, config); + const res = await fetch(`${origin2}?${QFN_KEY}=${qrlHash}${query}`, config); const contentType = res.headers.get('Content-Type'); if (res.ok && contentType === 'text/qwik-json-stream' && res.body) return async function*() { try { @@ -3741,24 +3780,24 @@ export const serverQrl_RA3PmZ4Oyak = async function(...args) { }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"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\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"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\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "serverQrl_RA3PmZ4Oyak", + "name": "serverQrl_w03grD0Ag68", "entry": null, "displayName": "index.qwik.mjs_serverQrl", - "hash": "RA3PmZ4Oyak", - "canonicalFilename": "index.qwik.mjs_serverQrl_RA3PmZ4Oyak", + "hash": "w03grD0Ag68", + "canonicalFilename": "index.qwik.mjs_serverQrl_w03grD0Ag68", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, "ctxKind": "function", - "ctxName": "$", + "ctxName": "server$", "captures": true, "loc": [ - 48020, - 51034 + 57087, + 60068 ], "paramNames": [ "...args" @@ -3772,146 +3811,152 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/ind ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Link_component_handleClientSideNavigation_rMfmL7bFMLU.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_GetForm_component_form_q_e_submit_r3dkP9d2cF8.mjs (ENTRY POINT)== import { _captures } from "@qwik.dev/core"; // -export const Link_component_handleClientSideNavigation_rMfmL7bFMLU = (event, elm)=>{ - const nav = _captures[0], reload = _captures[1], replaceState = _captures[2], scroll = _captures[3]; - if (event.defaultPrevented) { - if (elm.href) { - elm.setAttribute('aria-pressed', 'true'); - nav(elm.href, { - forceReload: reload, - replaceState, - scroll - }).then(()=>{ - elm.removeAttribute('aria-pressed'); - }); - } - } +export const GetForm_component_form_q_e_submit_r3dkP9d2cF8 = async (_evt, form)=>{ + const nav2 = _captures[0]; + const formData = new FormData(form); + const params = new URLSearchParams(); + formData.forEach((value, key)=>{ + if (typeof value === 'string') params.append(key, value); + }); + await nav2('?' + params.toString(), { + type: 'form', + forceReload: true + }); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;qEA6JQ,CAAC,OAAO;;IACR,IAAI,MAAM,gBAAgB,EACxB;QAAA,IAAI,IAAI,IAAI,EAAE;YACZ,IAAI,YAAY,CAAC,gBAAgB;YACjC,IAAI,IAAI,IAAI,EAAE;gBAAE,aAAa;gBAAQ;gBAAc;YAAO,GAAG,IAAI,CAAC;gBAChE,IAAI,eAAe,CAAC;YACtB;QACF;IAAA\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;6DAk0DY,OAAO,MAAM;IACX,MAAM,OAAO,SAAS,CAAC,EAAE;IACzB,MAAM,WAAW,IAAI,SAAS;IAC9B,MAAM,SAAS,IAAI;IACnB,SAAS,OAAO,CAAC,CAAC,OAAO;QACvB,IAAI,OAAO,UAAU,UACnB,OAAO,MAAM,CAAC,KAAK;IAEvB;IACA,MAAM,KAAK,MAAM,OAAO,QAAQ,IAAI;QAClC,MAAM;QACN,aAAa;IACf;AACF\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "Link_component_handleClientSideNavigation_rMfmL7bFMLU", + "name": "GetForm_component_form_q_e_submit_r3dkP9d2cF8", "entry": null, - "displayName": "index.qwik.mjs_Link_component_handleClientSideNavigation", - "hash": "rMfmL7bFMLU", - "canonicalFilename": "index.qwik.mjs_Link_component_handleClientSideNavigation_rMfmL7bFMLU", + "displayName": "index.qwik.mjs_GetForm_component_form_q_e_submit", + "hash": "r3dkP9d2cF8", + "canonicalFilename": "index.qwik.mjs_GetForm_component_form_q_e_submit_r3dkP9d2cF8", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", - "parent": "Link_component_nhj84CU1784", + "parent": "GetForm_component_2U5Z2Z8ryc0", "ctxKind": "function", - "ctxName": "$", + "ctxName": "_jsxSplit", "captures": true, "loc": [ - 4725, - 5043 + 61327, + 61822 ], "paramNames": [ - "event", - "elm" + "_evt", + "form" ], "captureNames": [ - "nav", - "reload", - "replaceState", - "scroll" + "nav" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Link_component_nhj84CU1784.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_GetForm_component_form_q_e_submit_1_cuYklZAOHrA.mjs (ENTRY POINT)== + +export const GetForm_component_form_q_e_submit_1_cuYklZAOHrA = (_evt, form)=>{ + if (form.getAttribute('data-spa-reset') === 'true') form.reset(); + form.dispatchEvent(new CustomEvent('submitcompleted', { + bubbles: false, + cancelable: false, + composed: false, + detail: { + status: 200 + } + })); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"+DAm1DqC,CAAC,MAAM;IAChC,IAAI,KAAK,YAAY,CAAC,sBAAsB,QAC1C,KAAK,KAAK;IAEZ,KAAK,aAAa,CAChB,IAAI,YAAY,mBAAmB;QACjC,SAAS;QACT,YAAY;QACZ,UAAU;QACV,QAAQ;YACN,QAAQ;QACV;IACF;AAEJ\"}") +/* +{ + "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", + "name": "GetForm_component_form_q_e_submit_1_cuYklZAOHrA", + "entry": null, + "displayName": "index.qwik.mjs_GetForm_component_form_q_e_submit_1", + "hash": "cuYklZAOHrA", + "canonicalFilename": "index.qwik.mjs_GetForm_component_form_q_e_submit_1_cuYklZAOHrA", + "path": "../node_modules/@qwik.dev/router", + "extension": "mjs", + "parent": "GetForm_component_2U5Z2Z8ryc0", + "ctxKind": "function", + "ctxName": "_jsxSplit", + "captures": false, + "loc": [ + 61953, + 62381 + ], + "paramNames": [ + "_evt", + "form" + ] +} +*/ +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_GetForm_component_2U5Z2Z8ryc0.mjs (ENTRY POINT)== -import { useLocation } from "./index.qwik.mjs"; -import { useNavigate } from "./index.qwik.mjs"; import { Slot } from "@qwik.dev/core"; +import { _fnSignal } from "@qwik.dev/core"; import { _getConstProps } from "@qwik.dev/core"; import { _getVarProps } from "@qwik.dev/core"; import { _jsxSorted } from "@qwik.dev/core"; import { _jsxSplit } from "@qwik.dev/core"; -import { _qrlSync } from "@qwik.dev/core"; -import { g as getClientNavPath } from "./chunks/routing.qwik.mjs"; +import { _restProps } from "@qwik.dev/core"; import { qrl } from "@qwik.dev/core"; -import { s as shouldPreload } from "./chunks/routing.qwik.mjs"; -import { untrack } from "@qwik.dev/core"; -import { useSignal } from "@qwik.dev/core"; -import { useVisibleTaskQrl } from "@qwik.dev/core"; +import { u as useNavigate } from "./chunks/use-functions.qwik.mjs"; +// +const _hf0 = (p0)=>!p0.reloadDocument; +const _hf0_str = '!p0.reloadDocument'; +const _hf1 = (p0)=>p0.spaReset ? 'true' : void 0; +const _hf1_str = 'p0.spaReset?"true":undefined'; // -const q_Link_component_handleClientSideNavigation_rMfmL7bFMLU = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Link_component_handleClientSideNavigation_rMfmL7bFMLU.mjs"), "Link_component_handleClientSideNavigation_rMfmL7bFMLU"); -const q_Link_component_handlePrefetch_Evus9ZlzXpQ = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Link_component_handlePrefetch_Evus9ZlzXpQ.mjs"), "Link_component_handlePrefetch_Evus9ZlzXpQ"); -const q_Link_component_handlePreload_MhXmSxzp4GE = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Link_component_handlePreload_MhXmSxzp4GE.mjs"), "Link_component_handlePreload_MhXmSxzp4GE"); -const q_Link_component_useVisibleTask_6K6z063D0C4 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Link_component_useVisibleTask_6K6z063D0C4.mjs"), "Link_component_useVisibleTask_6K6z063D0C4"); +const q_GetForm_component_form_q_e_submit_1_cuYklZAOHrA = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_GetForm_component_form_q_e_submit_1_cuYklZAOHrA.mjs"), "GetForm_component_form_q_e_submit_1_cuYklZAOHrA"); +const q_GetForm_component_form_q_e_submit_r3dkP9d2cF8 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_GetForm_component_form_q_e_submit_r3dkP9d2cF8.mjs"), "GetForm_component_form_q_e_submit_r3dkP9d2cF8"); // -export const Link_component_nhj84CU1784 = (props)=>{ +export const GetForm_component_2U5Z2Z8ryc0 = (_rawProps)=>{ + const rest = _restProps(_rawProps, [ + 'action', + 'spaReset', + 'reloadDocument', + 'onSubmit$' + ]); const nav = useNavigate(); - const loc = useLocation(); - const originalHref = props.href; - const anchorRef = useSignal(); - const { onClick$, prefetch: prefetchProp, reload, replaceState, scroll, ...linkProps } = /* @__PURE__ */ (()=>props)(); - const clientNavPath = untrack(getClientNavPath, { - ...linkProps, - reload - }, loc); - linkProps.href = clientNavPath || originalHref; - const prefetchData = !!clientNavPath && prefetchProp !== false && prefetchProp !== 'js' || void 0; - const prefetch = prefetchData || !!clientNavPath && prefetchProp !== false && untrack(shouldPreload, clientNavPath, loc); - const handlePrefetch = prefetch ? q_Link_component_handlePrefetch_Evus9ZlzXpQ : void 0; - const preventDefault = clientNavPath ? _qrlSync((event)=>{ - if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) event.preventDefault(); - }, "event=>{if(!(event.metaKey||event.ctrlKey||event.shiftKey||event.altKey)){event.preventDefault();}}") : void 0; - const handleClientSideNavigation = clientNavPath ? q_Link_component_handleClientSideNavigation_rMfmL7bFMLU.w([ - nav, - reload, - replaceState, - scroll - ]) : void 0; - const handlePreload = q_Link_component_handlePreload_MhXmSxzp4GE; - useVisibleTaskQrl(q_Link_component_useVisibleTask_6K6z063D0C4.w([ - anchorRef, - handlePrefetch, - linkProps, - loc - ])); - return /* @__PURE__ */ _jsxSplit('a', { - ref: anchorRef, - 'q:link': !!clientNavPath, - ..._getVarProps(linkProps), - ..._getConstProps(linkProps), - "q-e:click": [ - preventDefault, - handlePreload, - // needs to be in between preventDefault and onClick$ to ensure it starts asap. - onClick$, - handleClientSideNavigation - ], - 'data-prefetch': prefetchData, - "q-e:mouseover": [ - linkProps.onMouseOver$, - handlePrefetch - ], - "q-e:focus": [ - linkProps.onFocus$, - handlePrefetch + return /* @__PURE__ */ _jsxSplit('form', { + action: 'get', + 'preventdefault:submit': _fnSignal(_hf0, [ + _rawProps + ], _hf0_str), + 'data-spa-reset': _fnSignal(_hf1, [ + _rawProps + ], _hf1_str), + ..._getVarProps(rest), + ..._getConstProps(rest), + 'q-e:submit': [ + ...Array.isArray(_rawProps.onSubmit$) ? _rawProps.onSubmit$ : [ + _rawProps.onSubmit$ + ], + q_GetForm_component_form_q_e_submit_r3dkP9d2cF8.w([ + nav + ]), + q_GetForm_component_form_q_e_submit_1_cuYklZAOHrA ] - }, { - "q-e:qvisible": [] - }, /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, "0K_2"), 0, "0K_3"); + }, null, /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, 'Q4_0'), 0, 'Q4_1'); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;;;;;;;0CAgHwB,CAAC;IACvB,MAAM,MAAM;IACZ,MAAM,MAAM;IACZ,MAAM,eAAe,MAAM,IAAI;IAC/B,MAAM,YAAY;IAClB,MAAM,EACJ,QAAQ,EACR,UAAU,YAAY,EACtB,MAAM,EACN,YAAY,EACZ,MAAM,EACN,GAAG,WACJ,GAAG,aAAa,GAAG,CAAC,IAAM,KAAK;IAChC,MAAM,gBAAgB,QAAQ,kBAAkB;QAAE,GAAG,SAAS;QAAE;IAAO,GAAG;IAC1E,UAAU,IAAI,GAAG,iBAAiB;IAClC,MAAM,eACJ,AAAC,CAAC,CAAC,iBAAiB,iBAAiB,SAAS,iBAAiB,QAAS,KAAK;IAC/E,MAAM,WACJ,gBACC,CAAC,CAAC,iBAAiB,iBAAiB,SAAS,QAAQ,eAAe,eAAe;IACtF,MAAM,iBAAiB,yDAgBnB,KAAK;IACT,MAAM,iBAAiB,yBACb,CAAC;QACL,IAAI,CAAC,CAAC,MAAM,OAAO,IAAI,MAAM,OAAO,IAAI,MAAM,QAAQ,IAAI,MAAM,MAAM,GACpE,MAAM,cAAc;IAExB,4GACA,KAAK;IACT,MAAM,6BAA6B;;;;;SAW/B,KAAK;IACT,MAAM;IAIN;;;;;;IAeA,OAAO,aAAa,GAAG,UAAI;QACzB,KAAK;QACA,UAAU,CAAC,CAAC;wBACd;0BAAA;QACH,aAAU;YACR;YACA;YACA,+EAA+E;YAC/E;YACA;SACD;QACD,iBAAiB;QACjB,iBAAc;YAAC,UAAU,YAAY;YAAE;SAAe;QACtD,aAAU;YAAC,UAAU,QAAQ;YAAE;SAAe;;QAC9C,gBAAa,EAAE;OACL,aAAa,GAAG,WAAI;AAElC\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;;MA+yDM,OAAO,CAAC,KAAO,CAAC,GAAG,cAAc;MACjC,WAAW;MACX,OAAO,CAAC,KAAQ,GAAG,QAAQ,GAAG,SAAS,KAAK;MAC5C,WAAW;;;;;6CAEY,CAAC;IAC1B,MAAM,OAAO,WAAW,WAAW;QAAC;QAAU;QAAY;QAAkB;KAAY;IACxF,MAAM,MAAM;IACZ,OAAO,aAAa,GAAG,UACrB,QACA;QACE,QAAQ;QACR,yBAAyB,UAAU,MAAM;YAAC;SAAU,EAAE;QACtD,kBAAkB,UAAU,MAAM;YAAC;SAAU,EAAE;QAC/C,GAAG,aAAa,KAAK;QACrB,GAAG,eAAe,KAAK;QACvB,cAAc;eACR,MAAM,OAAO,CAAC,UAAU,SAAS,IAAI,UAAU,SAAS,GAAG;gBAAC,UAAU,SAAS;aAAC;;;;;SAkCrF;IACH,GACA,MACA,aAAa,GAAG,WAAW,MAAM,MAAM,MAAM,MAAM,GAAG,SACtD,GACA;AAEJ\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "Link_component_nhj84CU1784", + "name": "GetForm_component_2U5Z2Z8ryc0", "entry": null, - "displayName": "index.qwik.mjs_Link_component", - "hash": "nhj84CU1784", - "canonicalFilename": "index.qwik.mjs_Link_component_nhj84CU1784", + "displayName": "index.qwik.mjs_GetForm_component", + "hash": "2U5Z2Z8ryc0", + "canonicalFilename": "index.qwik.mjs_GetForm_component_2U5Z2Z8ryc0", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", "parent": null, @@ -3919,300 +3964,641 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/ind "ctxName": "component$", "captures": false, "loc": [ - 3323, - 6187 + 60727, + 62568 ], "paramNames": [ - "props" + "_rawProps" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_GetForm_component_form_q_e_submit_D0PAP3eJ0Ng.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_Form_form_q_e_submit_6i0Jq5q8JFg.mjs (ENTRY POINT)== import { _captures } from "@qwik.dev/core"; // -export const GetForm_component_form_q_e_submit_D0PAP3eJ0Ng = async (_evt, form)=>{ - const nav = _captures[0]; - const formData = new FormData(form); - const params = new URLSearchParams(); - formData.forEach((value, key)=>{ - if (typeof value === 'string') params.append(key, value); - }); - await nav('?' + params.toString(), { - type: 'form', - forceReload: true - }); +export const Form_form_q_e_submit_6i0Jq5q8JFg = (evt)=>{ + const action2 = _captures[0]; + if (!action2.submitted) return action2.submit(evt); }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;6DAwmDQ,OAAO,MAAM;;IACb,MAAM,WAAW,IAAI,SAAS;IAC9B,MAAM,SAAS,IAAI;IACnB,SAAS,OAAO,CAAC,CAAC,OAAO;QACvB,IAAI,OAAO,UAAU,UACnB,OAAO,MAAM,CAAC,KAAK;IAEvB;IACA,MAAM,IAAI,MAAM,OAAO,QAAQ,IAAI;QAAE,MAAM;QAAQ,aAAa;IAAK\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;gDA23DkB,CAAC;IACC,MAAM,UAAU,SAAS,CAAC,EAAE;IAC5B,IAAI,CAAC,QAAQ,SAAS,EACpB,OAAO,QAAQ,MAAM,CAAC;AAE1B\"}") /* { "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "GetForm_component_form_q_e_submit_D0PAP3eJ0Ng", + "name": "Form_form_q_e_submit_6i0Jq5q8JFg", "entry": null, - "displayName": "index.qwik.mjs_GetForm_component_form_q_e_submit", - "hash": "D0PAP3eJ0Ng", - "canonicalFilename": "index.qwik.mjs_GetForm_component_form_q_e_submit_D0PAP3eJ0Ng", + "displayName": "index.qwik.mjs_Form_form_q_e_submit", + "hash": "6i0Jq5q8JFg", + "canonicalFilename": "index.qwik.mjs_Form_form_q_e_submit_6i0Jq5q8JFg", "path": "../node_modules/@qwik.dev/router", "extension": "mjs", - "parent": "GetForm_component_OIWHwJ5eKxg", + "parent": null, "ctxKind": "function", - "ctxName": "$", + "ctxName": "_jsxSplit", "captures": true, "loc": [ - 54763, - 55115 + 63221, + 63419 ], "paramNames": [ - "_evt", - "form" + "evt" ], "captureNames": [ - "nav" + "action" ] } */ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_useQwikRouter_goto_OSnb99dm7Ow.mjs (ENTRY POINT)== +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_DocumentHeadTags_component_9CrWYOoCpgY.mjs (ENTRY POINT)== -import { _captures } from "@qwik.dev/core"; -import { QWIK_CITY_SCROLLER } from "./index.qwik.mjs"; -import { QWIK_ROUTER_SCROLLER } from "./index.qwik.mjs"; -import { _auto_getScrollHistory as getScrollHistory } from "./index.qwik.mjs"; -import { _auto_internalState as internalState } from "./index.qwik.mjs"; -import { _auto_preventNav as preventNav } from "./index.qwik.mjs"; -import { _auto_restoreScroll as restoreScroll } from "./index.qwik.mjs"; -import { isBrowser } from "@qwik.dev/core"; -import { isDev } from "@qwik.dev/core"; -import { b as isSameOrigin } from "./chunks/routing.qwik.mjs"; -import { a as isSamePath } from "./chunks/routing.qwik.mjs"; -import { l as loadClientData } from "./chunks/routing.qwik.mjs"; -import { d as loadRoute } from "./chunks/routing.qwik.mjs"; -import * as qwikRouterConfig from "@qwik-router-config"; -import { t as toUrl } from "./chunks/routing.qwik.mjs"; +import { Fragment } from "@qwik.dev/core/jsx-runtime"; +import { _getConstProps } from "@qwik.dev/core"; +import { _getVarProps } from "@qwik.dev/core"; +import { _jsxSorted } from "@qwik.dev/core"; +import { _jsxSplit } from "@qwik.dev/core"; +import { createElement } from "@qwik.dev/core"; +import { b as useDocumentHead } from "./chunks/use-functions.qwik.mjs"; // -export const useQwikRouter_goto_OSnb99dm7Ow = async (path, opt)=>{ - const actionState = _captures[0], navResolver = _captures[1], routeInternal = _captures[2], routeLocation = _captures[3]; - const { type = 'link', forceReload = path === void 0, // Hack for nav() because this API is already set. - replaceState = false, scroll = true } = typeof opt === 'object' ? opt : { - forceReload: opt +export const DocumentHeadTags_component_9CrWYOoCpgY = (props)=>{ + let head = useDocumentHead(); + if (props) head = { + ...head, + ...props }; - internalState.navCount++; - if (isBrowser && type === 'link' && routeInternal.value.type === 'initial') { - const url2 = new URL(window.location.href); - routeInternal.value.dest = url2; - routeLocation.url = url2; + return /* @__PURE__ */ _jsxSorted(Fragment, null, null, [ + head.title && /* @__PURE__ */ _jsxSorted('title', null, null, head.title, 1, 'r5_0'), + head.meta.map((m)=>/* @__PURE__ */ _jsxSplit('meta', { + ..._getVarProps(m) + }, _getConstProps(m), null, 0, 'r5_1')), + head.links.map((l)=>/* @__PURE__ */ _jsxSplit('link', { + ..._getVarProps(l) + }, _getConstProps(l), null, 0, 'r5_2')), + head.styles.map((s)=>{ + const props2 = s.props || s; + return /* @__PURE__ */ createElement('style', { + ...props2, + dangerouslySetInnerHTML: s.style || props2.dangerouslySetInnerHTML, + key: s.key + }); + }), + head.scripts.map((s)=>{ + const props2 = s.props || s; + return /* @__PURE__ */ createElement('script', { + ...props2, + dangerouslySetInnerHTML: s.script || props2.dangerouslySetInnerHTML, + key: s.key + }); + }) + ], 1, 'r5_3'); +}; + + +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;sDAo+D6B,CAAC;IAC1B,IAAI,OAAO;IACX,IAAI,OACF,OAAO;QACL,GAAG,IAAI;QACP,GAAG,KAAK;IACV;IAEF,OAAO,aAAa,GAAG,WACrB,UACA,MACA,MACA;QACE,KAAK,KAAK,IAAI,aAAa,GAAG,WAAW,SAAS,MAAM,MAAM,KAAK,KAAK,EAAE,GAAG;QAC7E,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,IACb,aAAa,GAAG,UACd,QACA;gBACE,GAAG,aAAa,EAAE;YACpB,GACA,eAAe,IACf,MACA,GACA;QAGJ,KAAK,KAAK,CAAC,GAAG,CAAC,CAAC,IACd,aAAa,GAAG,UACd,QACA;gBACE,GAAG,aAAa,EAAE;YACpB,GACA,eAAe,IACf,MACA,GACA;QAGJ,KAAK,MAAM,CAAC,GAAG,CAAC,CAAC;YACf,MAAM,SAAS,EAAE,KAAK,IAAI;YAC1B,OAAO,aAAa,GAAG,cAAc,SAAS;gBAC5C,GAAG,MAAM;gBACT,yBAAyB,EAAE,KAAK,IAAI,OAAO,uBAAuB;gBAClE,KAAK,EAAE,GAAG;YACZ;QACF;QACA,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;YAChB,MAAM,SAAS,EAAE,KAAK,IAAI;YAC1B,OAAO,aAAa,GAAG,cAAc,UAAU;gBAC7C,GAAG,MAAM;gBACT,yBAAyB,EAAE,MAAM,IAAI,OAAO,uBAAuB;gBACnE,KAAK,EAAE,GAAG;YACZ;QACF;KACD,EACD,GACA;AAEJ\"}") +/* +{ + "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", + "name": "DocumentHeadTags_component_9CrWYOoCpgY", + "entry": null, + "displayName": "index.qwik.mjs_DocumentHeadTags_component", + "hash": "9CrWYOoCpgY", + "canonicalFilename": "index.qwik.mjs_DocumentHeadTags_component_9CrWYOoCpgY", + "path": "../node_modules/@qwik.dev/router", + "extension": "mjs", + "parent": null, + "ctxKind": "function", + "ctxName": "component$", + "captures": false, + "loc": [ + 65890, + 67321 + ], + "paramNames": [ + "props" + ] +} +*/ +============================= ../node_modules/@qwik.dev/router/index.qwik.mjs == + +import { qrl } from "@qwik.dev/core"; +import { componentQrl, _jsxSorted, isBrowser, useSignal, isDev, _jsxSplit, _getConstProps, _getVarProps, eventQrl, isServer, useStylesQrl, useServerData, useStore, useContextProvider, useTaskQrl, implicit$FirstArg, withLocale, _wrapProp } from '@qwik.dev/core'; +import * as qwikRouterConfig from '@qwik-router-config'; +import { p } from '@qwik.dev/core/preloader'; +import { l as loadRoute, i as isSamePath, t as toPath, c as createDocumentHead } from './chunks/head.qwik.mjs'; +import { u as useNavigate, a as useLocation, b as useDocumentHead, c as useQwikRouterEnv, d as useAction } from './chunks/use-functions.qwik.mjs'; +import { _getContextHostElement } from '@qwik.dev/core/internal'; +import { Q as QACTION_KEY, e as ensureRouteLoaderSignals, s as setLoaderSignalValue, C as ContentContext, a as ContentInternalContext, D as DocumentHeadContext, H as HttpStatusContext, R as RouteLocationContext, b as RouteNavigateContext, c as RouteStateContext, d as RouteLoaderCtxContext, f as RouteActionContext, g as RoutePreventNavigateContext } from './chunks/route-loaders.qwik.mjs'; +import * as v from 'valibot'; +import * as z from 'zod'; +import swRegister from '@qwik-router-sw-register'; +import { renderToStream } from '@qwik.dev/core/server'; +// +const q_DocumentHeadTags_component_9CrWYOoCpgY = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_DocumentHeadTags_component_9CrWYOoCpgY.mjs"), "DocumentHeadTags_component_9CrWYOoCpgY"); +const q_ErrorBoundary_component_pOa6vjtC7ik = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_ErrorBoundary_component_pOa6vjtC7ik.mjs"), "ErrorBoundary_component_pOa6vjtC7ik"); +const q_Form_form_q_e_submit_6i0Jq5q8JFg = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Form_form_q_e_submit_6i0Jq5q8JFg.mjs"), "Form_form_q_e_submit_6i0Jq5q8JFg"); +const q_GetForm_component_2U5Z2Z8ryc0 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_GetForm_component_2U5Z2Z8ryc0.mjs"), "GetForm_component_2U5Z2Z8ryc0"); +const q_Link_component_VPmar9tb3t4 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_Link_component_VPmar9tb3t4.mjs"), "Link_component_VPmar9tb3t4"); +const q_QwikRouterMockProvider_component_IN4dVpT0x74 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_QwikRouterMockProvider_component_IN4dVpT0x74.mjs"), "QwikRouterMockProvider_component_IN4dVpT0x74"); +const q_QwikRouterProvider_component_6Kjfa79mqlY = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_QwikRouterProvider_component_6Kjfa79mqlY.mjs"), "QwikRouterProvider_component_6Kjfa79mqlY"); +const q_RouterOutlet_component_QwONcWD5gIg = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_RouterOutlet_component_QwONcWD5gIg.mjs"), "RouterOutlet_component_QwONcWD5gIg"); +const q_qwik_view_transition_css_inline_vNfd9raIMI0 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_qwik_view_transition_css_inline_vNfd9raIMI0.mjs"), "qwik_view_transition_css_inline_vNfd9raIMI0"); +const q_routeActionQrl_action_submit_YuS5bpdQ360 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_routeActionQrl_action_submit_YuS5bpdQ360.mjs"), "routeActionQrl_action_submit_YuS5bpdQ360"); +const q_serverQrl_w03grD0Ag68 = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_serverQrl_w03grD0Ag68.mjs"), "serverQrl_w03grD0Ag68"); +const q_spa_init_event_igI1pUsax0E = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_spa_init_event_igI1pUsax0E.mjs"), "spa_init_event_igI1pUsax0E"); +// +qrl(()=>import("./index.qwik.mjs_useQwikMockRouter_createAsync_clbxpuXqpEU.mjs"), "useQwikMockRouter_createAsync_clbxpuXqpEU"); +qrl(()=>import("./index.qwik.mjs_useQwikMockRouter_goto_aViHFxQ1a3s.mjs"), "useQwikMockRouter_goto_aViHFxQ1a3s"); +qrl(()=>import("./index.qwik.mjs_useQwikMockRouter_useTask_tXTLR4tzCy0.mjs"), "useQwikMockRouter_useTask_tXTLR4tzCy0"); +// +const q_useQwikRouter_getScroller_0UhDFwlxeFQ = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikRouter_getScroller_0UhDFwlxeFQ.mjs"), "useQwikRouter_getScroller_0UhDFwlxeFQ"); +const q_useQwikRouter_goto_8j8Vrz2yUIM = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikRouter_goto_8j8Vrz2yUIM.mjs"), "useQwikRouter_goto_8j8Vrz2yUIM"); +const q_useQwikRouter_registerPreventNav_69B0DK0eZJc = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikRouter_registerPreventNav_69B0DK0eZJc.mjs"), "useQwikRouter_registerPreventNav_69B0DK0eZJc"); +const q_useQwikRouter_useTask_XpalYii770E = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_useQwikRouter_useTask_XpalYii770E.mjs"), "useQwikRouter_useTask_XpalYii770E"); +// +export { e as useContent, f as useHttpStatus, g as usePreventNavigate$, h as usePreventNavigateQrl } from './chunks/use-functions.qwik.mjs'; +export { r as routeLoader$, l as routeLoaderQrl } from './chunks/route-loaders.qwik.mjs'; +export { z } from 'zod'; +const ErrorBoundary = /* @__PURE__ */ componentQrl(q_ErrorBoundary_component_pOa6vjtC7ik); +async function prefetchRoute(url, prefetchData, probability = 0.8, manifestHash) { + if (!isBrowser) return; + try { + const loadedRoute = await loadRoute(qwikRouterConfig.routes, qwikRouterConfig.cacheModules, url.pathname); + if (!loadedRoute) return; + let routeName = loadedRoute.$routeName$; + routeName = routeName.endsWith('/') ? routeName : routeName + '/'; + if (routeName.length > 1 && routeName.startsWith('/')) routeName = routeName.slice(1); + p(routeName, probability); + if (!prefetchData || !manifestHash) return; + if (loadedRoute.$loaders$?.length && loadedRoute.$loaderPaths$) { + const basePath = qwikRouterConfig.basePathname ?? '/'; + for (const hash of loadedRoute.$loaders$){ + let loaderPath = loadedRoute.$loaderPaths$?.[hash]; + if (!loaderPath) continue; + if (basePath !== '/' && !loaderPath.startsWith(basePath)) loaderPath = basePath + loaderPath.slice(1); + const pathBase = loaderPath.endsWith('/') ? loaderPath : loaderPath + '/'; + const fetchUrl = `${pathBase}q-loader-${hash}.${manifestHash}.json`; + fetch(fetchUrl).then((r)=>r.blob()).catch(()=>{}); + } + } + } catch {} +} +const Link = /* @__PURE__ */ componentQrl(q_Link_component_VPmar9tb3t4); +const newScrollState = ()=>{ + return { + x: 0, + y: 0, + w: 0, + h: 0 + }; +}; +const clientNavigate = (win, navType, fromURL, toURL, replaceState = false)=>{ + if (navType !== 'popstate') { + const samePath = isSamePath(fromURL, toURL); + const sameHash = fromURL.hash === toURL.hash; + if (!samePath || !sameHash) { + const newState = { + _qRouterScroll: newScrollState() + }; + if (replaceState) win.history.replaceState(newState, '', toPath(toURL)); + else win.history.pushState(newState, '', toPath(toURL)); + } } - const lastDest = routeInternal.value.dest; - const dest = path === void 0 ? lastDest : typeof path === 'number' ? path : toUrl(path, routeLocation.url); - if (preventNav.$cbs$ && (forceReload || typeof dest === 'number' || !isSamePath(dest, lastDest) || !isSameOrigin(dest, lastDest))) { - const ourNavId = internalState.navCount; - const prevents = await Promise.all([ - ...preventNav.$cbs$.values() - ].map((cb)=>cb(dest))); - if (ourNavId !== internalState.navCount || prevents.some(Boolean)) { - if (ourNavId === internalState.navCount && type === 'popstate') history.pushState(null, '', lastDest); - return; +}; +const hashScroll = (toUrl, fromUrl)=>{ + const elmId = toUrl.hash.slice(1); + const elm = elmId && document.getElementById(elmId); + if (elm) { + elm.scrollIntoView(); + return true; + } else if (!elm && toUrl.hash && isSamePath(toUrl, fromUrl)) return true; + return false; +}; +const restoreScroll = (type, toUrl, fromUrl, scroller, scrollState)=>{ + if (type === 'popstate' && scrollState) scroller.scrollTo(scrollState.x, scrollState.y); + else if (type === 'link' || type === 'form') { + if (!hashScroll(toUrl, fromUrl)) scroller.scrollTo(0, 0); + } +}; +const currentScrollState = (elm)=>{ + return { + x: elm.scrollLeft, + y: elm.scrollTop, + w: Math.max(elm.scrollWidth, elm.clientWidth), + h: Math.max(elm.scrollHeight, elm.clientHeight) + }; +}; +const getScrollHistory = ()=>{ + const state = history.state; + return state?._qRouterScroll; +}; +const saveScrollHistory = (scrollState)=>{ + const state = history.state || {}; + state._qRouterScroll = scrollState; + history.replaceState(state, ''); +}; +const spaInit = eventQrl(q_spa_init_event_igI1pUsax0E); +const QWIK_CITY_SCROLLER = '_qCityScroller'; +const QWIK_ROUTER_SCROLLER = '_qRouterScroller'; +const preventNav = {}; +const internalState = { + navCount: 0, + redirectCount: 0 +}; +const useQwikRouter = (props)=>{ + if (!isServer) throw new Error('useQwikRouter can only run during SSR on the server. If you are seeing this, it means you are re-rendering the root of your application. Fix that or use the component around the root of your application.'); + useStylesQrl(q_qwik_view_transition_css_inline_vNfd9raIMI0); + const env = useQwikRouterEnv(); + if (!env?.params) throw new Error(`Missing Qwik Router Env Data for help visit https://github.com/QwikDev/qwik/issues/6237`); + const urlEnv = useServerData('url'); + if (!urlEnv) throw new Error(`Missing Qwik URL Env Data`); + const serverHead = useServerData('documentHead'); + const manifestHash = useServerData('containerAttributes')?.['q:manifest-hash']; + if (env.ev.originalUrl.pathname !== env.ev.url.pathname && !__EXPERIMENTAL__.enableRequestRewrite) throw new Error(`enableRequestRewrite is an experimental feature and is not enabled. Please enable the feature flag by adding \`experimental: ["enableRequestRewrite"]\` to your qwikVite plugin options.`); + const url = new URL(urlEnv); + const routeLocationTarget = { + url, + params: env.params, + isNavigating: false, + prevUrl: void 0 + }; + const routeLocation = useStore(routeLocationTarget, { + deep: false + }); + const navResolver = {}; + env.routeLoaderCtx.manifestHash = manifestHash || ''; + env.routeLoaderCtx.pageUrl = url; + const routeLoaderCtx = useStore(env.routeLoaderCtx); + const loaderState = useStore({}, { + deep: false + }); + const contentModulesForInit = env.loadedRoute.$mods$; + const loaders = ensureRouteLoaderSignals(contentModulesForInit, loaderState, routeLoaderCtx); + for (const loader of loaders){ + const value = env.loaderValues[loader.__id]; + if (value !== void 0) setLoaderSignalValue(loaderState[loader.__id], value); + } + const routeInternal = useSignal({ + type: 'initial', + dest: url, + scroll: true + }); + const documentHead = useStore(()=>createDocumentHead(serverHead, manifestHash)); + const content = useStore({ + headings: void 0, + menu: void 0 + }); + const contentInternal = useSignal(); + const httpStatus = useSignal({ + status: env.response.status, + message: env.loadedRoute.$notFound$ ? 'Not Found' : env.response.statusMessage ?? '' + }); + const currentActionId = env.response.action; + const currentAction = currentActionId ? env.response.actionResult : void 0; + const actionState = useSignal(currentAction ? { + id: currentActionId, + data: env.response.formData, + output: { + result: currentAction, + status: env.response.status + } + } : void 0); + const registerPreventNav = q_useQwikRouter_registerPreventNav_69B0DK0eZJc; + const getScroller = q_useQwikRouter_getScroller_0UhDFwlxeFQ; + const goto = q_useQwikRouter_goto_8j8Vrz2yUIM.w([ + actionState, + getScroller, + manifestHash, + navResolver, + routeInternal, + routeLocation + ]); + useContextProvider(ContentContext, content); + useContextProvider(ContentInternalContext, contentInternal); + useContextProvider(DocumentHeadContext, documentHead); + useContextProvider(HttpStatusContext, httpStatus); + useContextProvider(RouteLocationContext, routeLocation); + useContextProvider(RouteNavigateContext, goto); + useContextProvider(RouteStateContext, loaderState); + useContextProvider(RouteLoaderCtxContext, routeLoaderCtx); + routeLoaderCtx.goto = goto; + useContextProvider(RouteActionContext, actionState); + useContextProvider(RoutePreventNavigateContext, registerPreventNav); + useTaskQrl(q_useQwikRouter_useTask_XpalYii770E.w([ + actionState, + content, + contentInternal, + documentHead, + env, + getScroller, + goto, + httpStatus, + loaderState, + navResolver, + props, + routeInternal, + routeLoaderCtx, + routeLocation, + routeLocationTarget, + serverHead + ]), // We should only wait for navigation to complete on the server + { + deferUpdates: isServer + }); +}; +const QwikRouterProvider = /* @__PURE__ */ componentQrl(q_QwikRouterProvider_component_6Kjfa79mqlY); +const QwikCityProvider = QwikRouterProvider; +const QwikRouterMockProvider = /* @__PURE__ */ componentQrl(q_QwikRouterMockProvider_component_IN4dVpT0x74); +const QwikCityMockProvider = QwikRouterMockProvider; +const RouterOutlet = /* @__PURE__ */ componentQrl(q_RouterOutlet_component_QwONcWD5gIg); +const validatorQrl = (validator)=>{ + if (isServer) return { + validate: validator + }; + return void 0; +}; +const validator$ = /* @__PURE__ */ implicit$FirstArg(validatorQrl); +const flattenValibotIssues = (issues)=>{ + return issues.reduce((acc, issue)=>{ + if (issue.path) { + const hasArrayType = issue.path.some((path)=>path.type === 'array'); + if (hasArrayType) { + const keySuffix = issue.expected === 'Array' ? '[]' : ''; + const key = issue.path.map((item)=>item.type === 'array' ? '*' : item.key).join('.').replace(/\.\*/g, '[]') + keySuffix; + acc[key] = acc[key] || []; + if (Array.isArray(acc[key])) acc[key].push(issue.message); + return acc; + } else acc[issue.path.map((item)=>item.key).join('.')] = issue.message; + } + return acc; + }, {}); +}; +const valibotQrl = (qrl)=>{ + if (!__EXPERIMENTAL__.valibot) throw new Error('Valibot is an experimental feature and is not enabled. Please enable the feature flag by adding `experimental: ["valibot"]` to your qwikVite plugin options.'); + if (isServer) return { + __brand: 'valibot', + async validate (ev, inputData) { + const schema = await qrl.resolve().then((obj)=>typeof obj === 'function' ? obj(ev) : obj); + const data = inputData ?? await ev.parseBody(); + const result = await v.safeParseAsync(schema, data); + if (result.success) return { + success: true, + data: result.output + }; + else { + if (isDev) console.error('ERROR: Valibot validation failed', result.issues); + return { + success: false, + status: 400, + error: { + formErrors: v.flatten(result.issues).root ?? [], + fieldErrors: flattenValibotIssues(result.issues) + } + }; + } } + }; + return void 0; +}; +const valibot$ = /* @__PURE__ */ implicit$FirstArg(valibotQrl); +const flattenZodIssues = (issues)=>{ + issues = Array.isArray(issues) ? issues : [ + issues + ]; + return issues.reduce((acc, issue)=>{ + const isExpectingArray = 'expected' in issue && issue.expected === 'array'; + const hasArrayType = issue.path.some((path)=>typeof path === 'number') || isExpectingArray; + if (hasArrayType) { + const keySuffix = 'expected' in issue && issue.expected === 'array' ? '[]' : ''; + const key = issue.path.map((path)=>typeof path === 'number' ? '*' : path).join('.').replace(/\.\*/g, '[]') + keySuffix; + acc[key] = acc[key] || []; + if (Array.isArray(acc[key])) acc[key].push(issue.message); + return acc; + } else acc[issue.path.join('.')] = issue.message; + return acc; + }, {}); +}; +const zodQrl = (qrl)=>{ + if (isServer) return { + __brand: 'zod', + async validate (ev, inputData) { + const schema = await qrl.resolve().then((obj)=>{ + if (typeof obj === 'function') obj = obj(z, ev); + if (obj instanceof z.Schema) return obj; + else return z.object(obj); + }); + const data = inputData ?? await ev.parseBody(); + const result = await withLocale(ev.locale(), ()=>schema.safeParseAsync(data)); + if (result.success) return result; + else { + if (isDev) console.error('ERROR: Zod validation failed', result.error.issues); + return { + success: false, + status: 400, + error: { + formErrors: result.error.flatten().formErrors, + fieldErrors: flattenZodIssues(result.error.issues) + } + }; + } + } + }; + return void 0; +}; +const zod$ = /* @__PURE__ */ implicit$FirstArg(zodQrl); +const getValidators = (rest, qrl)=>{ + let id; + let invalidate; + const validators = []; + if (rest.length === 1) { + const options = rest[0]; + if (options && typeof options === 'object') { + if ('validate' in options) validators.push(options); + else { + id = options.id; + if (options.validation) validators.push(...options.validation); + if (options.invalidate) invalidate = options.invalidate.map((loader)=>loader.__id); + } + } + } else if (rest.length > 1) validators.push(...rest.filter((v2)=>!!v2)); + if (typeof id === 'string') { + if (isDev) { + if (!/^[\w/.-]+$/.test(id)) throw new Error(`Invalid id: ${id}, id can only contain [a-zA-Z0-9_.-]`); + } + id = `id_${id}`; + } else id = qrl.getHash(); + return { + validators: validators.reverse(), + id, + invalidate + }; +}; +const routeActionQrl = (actionQrl, ...rest)=>{ + const { id, validators, invalidate } = getValidators(rest, actionQrl); + function action() { + const loc = useLocation(); + const currentAction = useAction(); + const initialState = { + actionPath: `?${QACTION_KEY}=${id}`, + submitted: false, + isRunning: false, + status: void 0, + value: void 0, + formData: void 0 + }; + const state = useStore(()=>{ + const value = currentAction.value; + if (value && value?.id === id) { + const data = value.data; + if (data instanceof FormData) initialState.formData = data; + if (value.output) { + const { status, result } = value.output; + initialState.status = status; + initialState.value = result; + } + } + return initialState; + }); + const submit = q_routeActionQrl_action_submit_YuS5bpdQ360.w([ + currentAction, + id, + loc, + state + ]); + initialState.submit = submit; + return state; } - if (typeof dest === 'number') { - if (isBrowser) history.go(dest); - return; + action.__brand = 'server_action'; + action.__validators = validators; + action.__qrl = actionQrl; + action.__id = id; + action.__invalidate = invalidate; + Object.freeze(action); + return action; +}; +const globalActionQrl = (actionQrl, ...rest)=>{ + const action = routeActionQrl(actionQrl, ...rest); + if (isServer) { + if (typeof globalThis._qwikActionsMap === 'undefined') globalThis._qwikActionsMap = /* @__PURE__ */ new Map(); + globalThis._qwikActionsMap.set(action.__id, action); } - if (!isSameOrigin(dest, lastDest)) { - if (isBrowser) location.href = dest.href; - return; + return action; +}; +const routeAction$ = /* @__PURE__ */ implicit$FirstArg(routeActionQrl); +const globalAction$ = /* @__PURE__ */ implicit$FirstArg(globalActionQrl); +const serverQrl = (qrl, options)=>{ + if (isServer) { + const captured = qrl.getCaptured(); + if (captured && captured.length > 0 && !_getContextHostElement()) throw new Error('For security reasons, we cannot serialize QRLs that capture lexical scope.'); } - if (!forceReload && isSamePath(dest, lastDest)) { - if (isBrowser) { - if (type === 'link' && dest.href !== location.href) history.pushState(null, '', dest); - let scroller = document.getElementById(QWIK_ROUTER_SCROLLER); - if (!scroller) { - scroller = document.getElementById(QWIK_CITY_SCROLLER); - if (scroller && isDev) console.warn(`Please update your scroller ID to "${QWIK_ROUTER_SCROLLER}" as "${QWIK_CITY_SCROLLER}" is deprecated and will be removed in V3`); - } - if (!scroller) scroller = document.documentElement; - restoreScroll(type, dest, new URL(location.href), scroller, getScrollHistory()); - if (type === 'popstate') window._qRouterScrollEnabled = true; + const method = options?.method?.toUpperCase?.() || 'POST'; + const headers = options?.headers || {}; + const origin = options?.origin || ''; + const fetchOptions = options?.fetchOptions || {}; + return q_serverQrl_w03grD0Ag68.w([ + fetchOptions, + headers, + method, + origin, + qrl + ]); +}; +const server$ = /* @__PURE__ */ implicit$FirstArg(serverQrl); +const ServiceWorkerRegister = (props)=>/* @__PURE__ */ _jsxSorted('script', { + nonce: _wrapProp(props, 'nonce') + }, { + type: 'module', + dangerouslySetInnerHTML: swRegister + }, null, 3, '1x_0'); +const GetForm = /* @__PURE__ */ componentQrl(q_GetForm_component_2U5Z2Z8ryc0); +const Form = ({ action, spaReset, reloadDocument, onSubmit$, ...rest }, key)=>{ + if (action) { + const isArrayApi = Array.isArray(onSubmit$); + if (isArrayApi) return _jsxSplit('form', { + ..._getVarProps(rest), + ..._getConstProps(rest), + action: _wrapProp(action, 'actionPath'), + 'preventdefault:submit': !reloadDocument, + 'q-e:submit': [ + ...onSubmit$, + // action.submit "submitcompleted" event for onSubmitCompleted$ events + !reloadDocument ? q_Form_form_q_e_submit_6i0Jq5q8JFg.w([ + action + ]) : void 0 + ], + ['data-spa-reset']: spaReset ? 'true' : void 0 + }, { + method: 'post' + }, null, 0, key); + return _jsxSplit('form', { + ..._getVarProps(rest), + ..._getConstProps(rest), + action: _wrapProp(action, 'actionPath'), + 'preventdefault:submit': !reloadDocument, + 'q-e:submit': [ + // Since v2, this fires before the action is executed so it can be prevented + onSubmit$, + // action.submit "submitcompleted" event for onSubmitCompleted$ events + !reloadDocument ? action.submit : void 0 + ], + ['data-spa-reset']: spaReset ? 'true' : void 0 + }, { + method: 'post' + }, null, 0, key); + } else return /* @__PURE__ */ _jsxSplit(GetForm, { + spaReset, + reloadDocument, + onSubmit$, + ...rest + }, null, null, 0, key); +}; +const untypedAppUrl = function appUrl(route, params, paramsPrefix = '') { + const path = route.split('/'); + for(let i = 0; i < path.length; i++){ + const segment = path[i]; + if (segment.startsWith('[') && segment.endsWith(']')) { + const isSpread = segment.startsWith('[...'); + const key = segment.substring(segment.startsWith('[...') ? 4 : 1, segment.length - 1); + const value = params ? params[paramsPrefix + key] || params[key] : ''; + path[i] = isSpread ? value : encodeURIComponent(value); } - return; + if (segment.startsWith('(') && segment.endsWith(')')) path.splice(i, 1); } - routeInternal.value = { - type, - dest, - forceReload, - replaceState, - scroll - }; - if (isBrowser) { - loadClientData(dest); - loadRoute(qwikRouterConfig.routes, qwikRouterConfig.menus, qwikRouterConfig.cacheModules, dest.pathname); + let url = path.join('/'); + let baseURL = '/'; + if (baseURL) { + if (!baseURL.endsWith('/')) baseURL += '/'; + while(url.startsWith('/'))url = url.substring(1); + url = baseURL + url; } - actionState.value = void 0; - routeLocation.isNavigating = true; - return new Promise((resolve)=>{ - navResolver.r = resolve; - }); -}; - - -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;;;8CAqmBiB,OAAO,MAAM;;IAC1B,MAAM,EACJ,OAAO,MAAM,EACb,cAAc,SAAS,KAAK,CAAC,EAC7B,kDAAkD;IAClD,eAAe,KAAK,EACpB,SAAS,IAAI,EACd,GAAG,OAAO,QAAQ,WAAW,MAAM;QAAE,aAAa;IAAI;IACvD,cAAc,QAAQ;IACtB,IAAI,aAAa,SAAS,UAAU,cAAc,KAAK,CAAC,IAAI,KAAK,WAAW;QAC1E,MAAM,OAAO,IAAI,IAAI,OAAO,QAAQ,CAAC,IAAI;QACzC,cAAc,KAAK,CAAC,IAAI,GAAG;QAC3B,cAAc,GAAG,GAAG;IACtB;IACA,MAAM,WAAW,cAAc,KAAK,CAAC,IAAI;IACzC,MAAM,OACJ,SAAS,KAAK,IAAI,WAAW,OAAO,SAAS,WAAW,OAAO,MAAM,MAAM,cAAc,GAAG;IAC9F,IACE,WAAW,KAAK,IAChB,CAAC,eACC,OAAO,SAAS,YAChB,CAAC,WAAW,MAAM,aAClB,CAAC,aAAa,MAAM,SAAS,GAC/B;QACA,MAAM,WAAW,cAAc,QAAQ;QACvC,MAAM,WAAW,MAAM,QAAQ,GAAG,CAAC;eAAI,WAAW,KAAK,CAAC,MAAM;SAAG,CAAC,GAAG,CAAC,CAAC,KAAO,GAAG;QACjF,IAAI,aAAa,cAAc,QAAQ,IAAI,SAAS,IAAI,CAAC,UAAU;YACjE,IAAI,aAAa,cAAc,QAAQ,IAAI,SAAS,YAClD,QAAQ,SAAS,CAAC,MAAM,IAAI;YAE9B;QACF;IACF;IACA,IAAI,OAAO,SAAS,UAAU;QAC5B,IAAI,WACF,QAAQ,EAAE,CAAC;QAEb;IACF;IACA,IAAI,CAAC,aAAa,MAAM,WAAW;QACjC,IAAI,WACF,SAAS,IAAI,GAAG,KAAK,IAAI;QAE3B;IACF;IACA,IAAI,CAAC,eAAe,WAAW,MAAM,WAAW;QAC9C,IAAI,WAAW;YACb,IAAI,SAAS,UAAU,KAAK,IAAI,KAAK,SAAS,IAAI,EAChD,QAAQ,SAAS,CAAC,MAAM,IAAI;YAE9B,IAAI,WAAW,SAAS,cAAc,CAAC;YACvC,IAAI,CAAC,UAAU;gBACb,WAAW,SAAS,cAAc,CAAC;gBACnC,IAAI,YAAY,OACd,QAAQ,IAAI,CACV,CAAC,mCAAmC,EAAE,qBAAqB,MAAM,EAAE,mBAAmB,yCAAyC,CAAC;YAGtI;YACA,IAAI,CAAC,UACH,WAAW,SAAS,eAAe;YAErC,cAAc,MAAM,MAAM,IAAI,IAAI,SAAS,IAAI,GAAG,UAAU;YAC5D,IAAI,SAAS,YACX,OAAO,qBAAqB,GAAG;QAEnC;QACA;IACF;IACA,cAAc,KAAK,GAAG;QACpB;QACA;QACA;QACA;QACA;IACF;IACA,IAAI,WAAW;QACb,eAAe;QACf,UACE,iBAAiB,MAAM,EACvB,iBAAiB,KAAK,EACtB,iBAAiB,YAAY,EAC7B,KAAK,QAAQ;IAEjB;IACA,YAAY,KAAK,GAAG,KAAK;IACzB,cAAc,YAAY,GAAG;IAC7B,OAAO,IAAI,QAAQ,CAAC;QAClB,YAAY,CAAC,GAAG;IAClB\"}") -/* -{ - "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "useQwikRouter_goto_OSnb99dm7Ow", - "entry": null, - "displayName": "index.qwik.mjs_useQwikRouter_goto", - "hash": "OSnb99dm7Ow", - "canonicalFilename": "index.qwik.mjs_useQwikRouter_goto_OSnb99dm7Ow", - "path": "../node_modules/@qwik.dev/router", - "extension": "mjs", - "parent": null, - "ctxKind": "function", - "ctxName": "$", - "captures": true, - "loc": [ - 19896, - 22718 - ], - "paramNames": [ - "path", - "opt" - ], - "captureNames": [ - "actionState", - "navResolver", - "routeInternal", - "routeLocation" - ] -} -*/ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_ErrorBoundary_component_yTCHi5s1o00.mjs (ENTRY POINT)== - -import { Fragment } from "@qwik.dev/core/jsx-runtime"; -import { Slot } from "@qwik.dev/core"; -import { _jsxSorted } from "@qwik.dev/core"; -import { qrl } from "@qwik.dev/core"; -import { useErrorBoundary } from "@qwik.dev/core"; -import { useOnWindow } from "@qwik.dev/core"; -// -const q_ErrorBoundary_component_useOnWindow_GYhPAutMLGk = /*#__PURE__*/ qrl(()=>import("./index.qwik.mjs_ErrorBoundary_component_useOnWindow_GYhPAutMLGk.mjs"), "ErrorBoundary_component_useOnWindow_GYhPAutMLGk"); -// -export const ErrorBoundary_component_yTCHi5s1o00 = (props)=>{ - const store = useErrorBoundary(); - useOnWindow('qerror', q_ErrorBoundary_component_useOnWindow_GYhPAutMLGk.w([ - store - ])); - if (store.error && props.fallback$) return /* @__PURE__ */ _jsxSorted(Fragment, null, null, props.fallback$(store.error), 1, "0K_0"); - return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, "0K_1"); + return url; }; - - -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;;;;;;mDAwEiC,CAAC;IAChC,MAAM,QAAQ;IACd,YACE;;;IAKF,IAAI,MAAM,KAAK,IAAI,MAAM,SAAS,EAChC,OAAO,aAAa,GAAG,WAAI,sBAAsB,MAAM,SAAS,CAAC,MAAM,KAAK;IAE9E,OAAO,aAAa,GAAG,WAAI;AAC7B\"}") -/* -{ - "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "ErrorBoundary_component_yTCHi5s1o00", - "entry": null, - "displayName": "index.qwik.mjs_ErrorBoundary_component", - "hash": "yTCHi5s1o00", - "canonicalFilename": "index.qwik.mjs_ErrorBoundary_component_yTCHi5s1o00", - "path": "../node_modules/@qwik.dev/router", - "extension": "mjs", - "parent": null, - "ctxKind": "function", - "ctxName": "component$", - "captures": false, - "loc": [ - 1620, - 1932 - ], - "paramNames": [ - "props" - ] +function omitProps(obj, keys) { + const omittedObj = {}; + for(const key in obj)if (!key.startsWith('param:') && !keys.includes(key)) omittedObj[key] = obj[key]; + return omittedObj; } -*/ -============================= ../node_modules/@qwik.dev/router/index.qwik.mjs_routeActionQrl_action_submit_JY3C42B1B08.mjs (ENTRY POINT)== - -import { _captures } from "@qwik.dev/core"; -import { isServer } from "@qwik.dev/core"; -import { noSerialize } from "@qwik.dev/core"; -// -export const routeActionQrl_action_submit_JY3C42B1B08 = (input = {})=>{ - const currentAction = _captures[0], id = _captures[1], loc = _captures[2], state = _captures[3]; - if (isServer) throw new Error(`Actions can not be invoked within the server during SSR. -Action.run() can only be called on the browser, for example when a user clicks a button, or submits a form.`); - let data; - let form; - if (input instanceof SubmitEvent) { - form = input.target; - data = new FormData(form); - if ((input.submitter instanceof HTMLInputElement || input.submitter instanceof HTMLButtonElement) && input.submitter.name) { - if (input.submitter.name) data.append(input.submitter.name, input.submitter.value); - } - } else data = input; - return new Promise((resolve)=>{ - if (data instanceof FormData) state.formData = data; - state.submitted = true; - state.isRunning = true; - loc.isNavigating = true; - currentAction.value = { - data, - id, - resolve: noSerialize(resolve) - }; - }).then((_rawProps)=>{ - state.isRunning = false; - state.status = _rawProps.status; - state.value = _rawProps.result; - if (form) { - if (form.getAttribute('data-spa-reset') === 'true') form.reset(); - const detail = { - status: _rawProps.status, - value: _rawProps.result - }; - form.dispatchEvent(new CustomEvent('submitcompleted', { - bubbles: false, - cancelable: false, - composed: false, - detail - })); - } - return { - status: _rawProps.status, - value: _rawProps.result - }; - }); +const createRenderer = (getOptions)=>{ + return (opts)=>{ + const { jsx, options } = getOptions(opts); + return renderToStream(jsx, options); + }; }; +const DocumentHeadTags = /* @__PURE__ */ componentQrl(q_DocumentHeadTags_component_9CrWYOoCpgY); +export { DocumentHeadTags, ErrorBoundary, Form, Link, QWIK_CITY_SCROLLER, QWIK_ROUTER_SCROLLER, QwikCityMockProvider, QwikCityProvider, QwikRouterMockProvider, QwikRouterProvider, RouterOutlet, ServiceWorkerRegister, createRenderer, globalAction$, globalActionQrl, omitProps, routeAction$, routeActionQrl, server$, serverQrl, untypedAppUrl, useDocumentHead, useLocation, useNavigate, useQwikRouter, valibot$, valibotQrl, validator$, validatorQrl, zod$, zodQrl }; +export { QWIK_CITY_SCROLLER as _auto_QWIK_CITY_SCROLLER }; +export { QWIK_ROUTER_SCROLLER as _auto_QWIK_ROUTER_SCROLLER }; +export { clientNavigate as _auto_clientNavigate }; +export { currentScrollState as _auto_currentScrollState }; +export { getScrollHistory as _auto_getScrollHistory }; +export { internalState as _auto_internalState }; +export { prefetchRoute as _auto_prefetchRoute }; +export { preventNav as _auto_preventNav }; +export { restoreScroll as _auto_restoreScroll }; +export { saveScrollHistory as _auto_saveScrollHistory }; +export { spaInit as _auto_spaInit }; +export { useQwikRouter as _auto_useQwikRouter }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\";;;;wDA2oCqB,CAAC,QAAQ,CAAC,CAAC;;IAC1B,IAAI,UACF,MAAM,IAAI,MAAM,CAAC;2GACkF,CAAC;IAEtG,IAAI;IACJ,IAAI;IACJ,IAAI,iBAAiB,aAAa;QAChC,OAAO,MAAM,MAAM;QACnB,OAAO,IAAI,SAAS;QACpB,IACE,CAAC,MAAM,SAAS,YAAY,oBAC1B,MAAM,SAAS,YAAY,iBAAiB,KAC9C,MAAM,SAAS,CAAC,IAAI,EAEpB;YAAA,IAAI,MAAM,SAAS,CAAC,IAAI,EACtB,KAAK,MAAM,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,MAAM,SAAS,CAAC,KAAK;QACzD;IAEJ,OACE,OAAO;IAET,OAAO,IAAI,QAAQ,CAAC;QAClB,IAAI,gBAAgB,UAClB,MAAM,QAAQ,GAAG;QAEnB,MAAM,SAAS,GAAG;QAClB,MAAM,SAAS,GAAG;QAClB,IAAI,YAAY,GAAG;QACnB,cAAc,KAAK,GAAG;YACpB;YACA;YACA,SAAS,YAAY;QACvB;IACF,GAAG,IAAI,CAAC;QACN,MAAM,SAAS,GAAG;QAClB,MAAM,MAAM,aAFK;QAGjB,MAAM,KAAK,aAHF;QAIT,IAAI,MAAM;YACR,IAAI,KAAK,YAAY,CAAC,sBAAsB,QAC1C,KAAK,KAAK;YAEZ,MAAM,SAAS;gBAAE,MAAM,YARR;gBAQU,KAAK,YARvB;YAQgC;YACvC,KAAK,aAAa,CAChB,IAAI,YAAY,mBAAmB;gBACjC,SAAS;gBACT,YAAY;gBACZ,UAAU;gBACV;YACF;QAEJ;QACA,OAAO;YACL,MAAM,YAnBS;YAoBf,KAAK,YApBE;QAqBT;IACF\"}") -/* -{ - "origin": "../node_modules/@qwik.dev/router/index.qwik.mjs", - "name": "routeActionQrl_action_submit_JY3C42B1B08", - "entry": null, - "displayName": "index.qwik.mjs_routeActionQrl_action_submit", - "hash": "JY3C42B1B08", - "canonicalFilename": "index.qwik.mjs_routeActionQrl_action_submit_JY3C42B1B08", - "path": "../node_modules/@qwik.dev/router", - "extension": "mjs", - "parent": null, - "ctxKind": "function", - "ctxName": "$", - "captures": true, - "loc": [ - 39777, - 41467 - ], - "captureNames": [ - "currentAction", - "id", - "loc", - "state" - ] -} -*/ +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/router/index.qwik.mjs\"],\"names\":[],\"mappings\":\"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\"}") == DIAGNOSTICS == [] diff --git a/packages/qwik-router/ARCHITECTURE.md b/packages/qwik-router/ARCHITECTURE.md new file mode 100644 index 00000000000..8910b112e28 --- /dev/null +++ b/packages/qwik-router/ARCHITECTURE.md @@ -0,0 +1,349 @@ +# Qwik Router Architecture + +This document describes how a request flows through Qwik Router, from initial server +hit through SSR, and then how SPA navigation and RPC work on the client. + +## 1. Request Entry — Middleware Chain + +Every request enters through a platform adapter (Cloudflare, Node, Deno, etc.) which +calls `requestHandler(serverRequestEv, opts)`. + +### Route Resolution + +1. The build-time generated `@qwik-router-config` provides the route trie, server + plugins, caching options, and base pathname. +2. `loadRoute(routes, cacheModules, pathname)` walks the trie to find the matching + route. Trie nodes encode layouts (`_L`), pages (`_I`), params (`_W`/`_A`), + loader hashes (`_R`), menus (`_N`), and error/404 modules (`_E`/`_4`). +3. `resolveRequestHandlers(...)` builds the ordered handler chain. + +### The Handler Chain (in order) + +| Handler | Description | +| ----------------------- | --------------------------------------------------------- | +| jsonRequestWrapper | wraps redirects/errors as JSON for loader/action fetches | +| serverErrorMiddleware | catches ServerError, swaps to error module, re-renders | +| csrfCheck | POST/PUT/PATCH/DELETE origin check | +| serverPlugins.onRequest | global plugin middleware | +| routeModules.onRequest | per-route middleware (layouts + page) | +| loaderHandler | serves individual loader JSON (q-loader-\*.json requests) | +| actionHandler | processes ?qaction= JSON requests | +| runServerFunction | handles ?qfunc= RPC calls (server$) | +| fixTrailingSlash | enforces trailing slash policy | +| actionsMiddleware | runs action for form POST (progressive enhancement) | +| loadersMiddleware | runs ALL loaders in parallel via Promise.all | +| eTagMiddleware | checks/sets ETag, returns 304 if matched | +| renderQwikMiddleware | the SSR renderer | + +### Request Recognition + +The request handler recognizes special URL patterns: + +- `q-loader-{id}.{hash}.json` → individual loader data fetch (strips suffix from URL + so middleware sees the clean route path) +- `?qaction={id}` → action invocation +- `?qfunc=` → server$ RPC call + +### RequestEvent + +Each request gets a `RequestEventInternal` that provides `url`, `params`, `headers`, +`cookie`, `sharedMap`, and methods like `next()`, `redirect()`, `error()`, `json()`, +`send()`. The `sharedMap` serves as a per-request blackboard for passing data between +middleware, loaders, actions, and the renderer. + +Key sharedMap entries: + +- `@routeLoaderValues` — pre-computed loader return values +- `@routeLoaderState` — reactive AsyncSignal instances +- `@loaderPathsStore` — RouteLoaderCtx store +- `@actionResult` — action return value (for progressive enhancement) + +On Node-like runtimes, `AsyncLocalStorage` wraps request execution so +`getRequestEvent()` works from any async context without threading. + +## 2. Loaders — `routeLoader$` + +### Server Execution + +During SSR, `loadersMiddleware` runs **all** loaders for the matched route in parallel +via `Promise.all`. Each loader QRL is called with the `RequestEvent`. Results are +stored in `sharedMap['@routeLoaderValues']`. + +### Client Fetch + +On the client, each loader becomes an **AsyncSignal** — a reactive primitive that +lazily computes its value. The compute function for a loader signal: + +1. Reactively tracks `routeLoaderCtx.loaderPaths[id]` and `routeLoaderCtx.pageUrl`. +2. If a pre-loaded value was injected (via `setLoaderSignalValue`), returns it. +3. If `routePath` is undefined (loader not on current route), returns the previous + value (stale-by-default contract). +4. Otherwise, fetches `{basePath}{routePath}/q-loader-{id}.{manifestHash}.json`. + +### Route Loader Context + +`RouteLoaderCtx` is a reactive store shared across the app: + +```ts +{ + loaderPaths: Record, // loader ID → route path + pageUrl: URL, + manifestHash: string, + basePath: string, + goto?: RouteNavigate, // for loader-initiated redirects +} +``` + +On SPA navigation, `updateRouteLoaderPaths()` **adds/updates** entries for loaders on +the new route but **does not clear** old entries. Stale loader signals keep their +previous value until their route is visited again. + +### `ensureRouteLoaderSignals` + +Scans route modules for loader exports, creates AsyncSignals for any not yet in +`loaderState`, returns the list. Called both at SSR init and on each SPA navigation. + +### `setLoaderSignalValue` + +Injects a pre-computed value into an AsyncSignal without triggering re-computation. +Used during SSR hydration and when actions return updated loader data. + +## 3. SSR — Server-Side Rendering + +### `useQwikRouter` — The Root Hook + +Called once at the root component during SSR. Sets up all reactive state: + +1. Reads `env = useQwikRouterEnv()` — the `QwikRouterEnvData` from server data, + containing `loadedRoute`, `loaderValues`, `response`, `params`, `routeLoaderCtx`. +2. Creates stores: `routeLocation`, `loaderState`, `documentHead`, `content`, + `contentInternal`, `actionState`, `actionDataSignal`, `httpStatus`, `navContext`. +3. Calls `ensureRouteLoaderSignals` to create AsyncSignals, then + `setLoaderSignalValue` for each pre-computed loader value. +4. Provides all contexts (`RouteStateContext`, `RouteLocationContext`, + `DocumentHeadContext`, etc.). + +### Task Structure + +`useQwikRouter` registers two `useTask$` hooks: + +#### Nav Task — Route Loading & State Setup + +```ts +useTask$(async ({ track }) => { ... }, { deferUpdates: isServer }) +``` + +Tracks `routeInternal` (destination signal) and `actionState`. + +**Server path:** + +- Uses `env.loadedRoute` and `env.response` directly (no fetch needed). +- Populates `routeLocation`, `contentInternal`, `httpStatus`. +- Resolves head inline via `resolveHead(...)` and writes to `documentHead`. + +**Client path:** + +- Calls `loadRoute(...)` to load route modules for the new URL. +- If an action is pending, calls `submitAction(...)` and processes the result. +- Calls `updateRouteLoaderPaths` + `ensureRouteLoaderSignals`. +- **Triggers** loader signals without awaiting: `loaderState[id].untrackedLoading`. + This starts the fetch but doesn't block navigation. +- Updates `routeLocation`, `contentInternal.untrackedValue`, `actionDataSignal`. +- Sets `navContext.value` — a `noSerialize`'d object with navigation metadata: + `{ routeName, navigation, navType, replaceState, shouldForce* }`. + +> **Why `contentInternal.untrackedValue`?** Subscribers (RouterOutlet, head+commit +> task) must be fired later by `contentInternal.trigger()` inside the view +> transition's update callback. Using `.value` would fire subscribers before +> `startViewTransition` captures the old DOM, breaking view transitions. + +#### Head + Commit Task — Head Resolution & Navigation Commit + +```ts +useTask$(({ track }) => { ... }, { deferUpdates: false }) +``` + +Tracks `contentInternal`, `navContext`, and `actionDataSignal`. Client-only +(returns early on server since head is resolved inline in the nav task). + +1. **Head resolution** via `track(() => resolveHead(...))`. The `track()` wrapper + intercepts thrown promises from async loader signals (whose values haven't + arrived yet) and automatically retries when they resolve. +2. Writes resolved head to `documentHead` store. +3. **Guard:** if `navContext` hasn't changed since the last commit (same object + reference), this is a head-only update triggered by a loader resolving — skip + the navigation commit below. +4. **Scroll setup:** finds scroller element, sets up `__q_scroll_restore__` callback. +5. **SPA init:** calls `initializeSPA(goto, scrollEl)` (one-time setup). +6. **View transition:** calls `startViewTransition({ update: navigate, types })`. + Inside the update callback: `clientNavigate(...)` pushes/replaces history, + `contentInternal.trigger()` fires subscribers (rendering new content), + `_waitUntilRendered(container)` waits for the render cycle. +7. **Post-transition:** sets `q:route` attribute, saves scroll state, enables + scroll tracking, forces any deferred store effects, resolves `navResolver`. + +### Head Resolution — `resolveHead` + +Iterates content modules (layouts → page), collecting `routeConfig` / `head` exports. +Object configs are merged immediately; function configs are collected and called in +inner-before-outer order. Each function receives a `ResolveSyncValue` that reads +loader signals and action data. On the client, reading an unresolved AsyncSignal +throws a promise, which `track()` in the commit task handles automatically. + +### Server Data Assembly + +`getQwikRouterServerData(requestEv)` assembles the payload passed to Qwik's +`render()`: + +```ts +{ + url, requestHeaders, locale, nonce, + containerAttributes: { 'q:route': routeName }, + qwikrouter: { + routeName, ev, params, loadedRoute, + routeLoaderCtx, loaderValues, + response: { status, statusMessage, action, actionResult, formData } + } +} +``` + +## 4. Actions — `routeAction$` + +### Defining an Action + +`routeActionQrl(actionQrl, ...validators)` creates an `ActionInternal` with a stable +`__id` hash. When used in a component, it returns an `ActionStore` with a `.submit()` +method. + +### Client-Side Action Flow + +1. `action.submit(input)` sets `currentAction.value = { data, id, resolve }`. +2. This triggers the nav task (which tracks `actionState`). +3. Nav task calls `submitAction(action, pathname)`: + - POST to `{pathname}/?qaction={id}` with `Accept: application/json`. + - Body is FormData or JSON. +4. Response body: `{ result, loaderHashes? }`. +5. If hashes are present: `signal.invalidate(true)` for those loaders. +6. If hashes are absent: invalidate all current route loaders. +7. Resolves the `action.submit()` promise with `result`, which updates `actionDataSignal` and triggers any + subscribers. + +### Server-Side Action Execution + +**JSON path** (`actionHandler`): For `Accept: application/json` requests: + +1. Find action by ID, parse body, run validators, call QRL. +2. If `action.__invalidate` is set: return specific hashes in `h` for client refetch. +3. Otherwise, return no loader values. The client invalidates all current route loaders unless + `__STRICT_LOADERS__` is enabled, in which case the response includes an empty hash list. + +**Progressive enhancement** (`actionsMiddleware`): For form POST without JS: + +1. Run the action, store result in `sharedMap['@actionResult']`. +2. Proceed to loaders + SSR render. The page renders with the action result available. + +### `globalAction$` + +Registers in `globalThis._qwikActionsMap` so the action can be resolved without being +exported from a route module. Useful for shared actions across routes. + +## 5. SPA Navigation + +### Pre-Framework Boot — `spa-init.ts` + +A QRL event handler that runs before the framework hydrates. Sets up: + +- `popstate` listener → resolves `RouteNavigateContext` from the DOM container and + calls `nav(location.href, { type: 'popstate' })`. +- History patching → `pushState`/`replaceState` always embed `_qRouterScroll` state. +- Click handler → intercepts same-page anchor links. +- Scroll debounce → saves scroll position to history state every 200ms. +- Visibility change → commits scroll state on tab hide (for BFCache). + +Once `window._qRouterSPA` is set (by `initializeSPA` in the commit task), these +early handlers are removed and replaced by the full router handlers. + +### `goto` — The Navigate Function + +`goto(path, opt)` is provided as `RouteNavigateContext`: + +1. **Prevent check:** If `usePreventNavigate$` callbacks are registered, await them. + If any returns true, abort. +2. **Number:** `history.go(n)`. +3. **Cross-origin:** `location.href = dest.href`. +4. **Same path (no force):** Update URL in history, restore scroll, update + `routeLocation.url`. +5. **Different path:** Save scroll, push history, set `routeInternal.value` (triggers + nav task), prefetch route bundles. Returns a Promise resolved after commit. + +### View Transitions + +`startViewTransition({ types, update })` wraps the View Transition API: + +- Tries typed API first (Chrome 125+), falls back to untyped (Chrome 111+). +- Dispatches `qviewtransition` custom event for external listeners. +- Returns `transition.ready` promise. +- If no View Transition API: calls `update()` directly. + +### Navigation Timeline + +``` +goto(url) + │ + ├─ Save scroll state + ├─ Push history (clientNavigate) + ├─ Set routeInternal.value ──────► Nav Task + └─ Return promise │ + ├─ loadRoute(url) + ├─ submitAction (if action pending) + ├─ updateRouteLoaderPaths + ├─ ensureRouteLoaderSignals + ├─ Trigger loader signals (no await!) + ├─ Update routeLocation, content, httpStatus + └─ Set navContext.value ──────► Head+Commit Task + │ + ┌────────────────────────────────┘ + │ + ├─ track(() => resolveHead(...)) + │ └─ If loader throws promise → retry when resolved + ├─ Update documentHead + ├─ (if navContext unchanged → stop, head-only update) + ├─ Setup scroll restore + ├─ initializeSPA (one-time) + ├─ startViewTransition + │ └─ update callback: + │ ├─ clientNavigate (history) + │ ├─ contentInternal.trigger() + │ └─ _waitUntilRendered() + └─ finally: + ├─ Set q:route attribute + ├─ Save scroll, enable scroll tracking + ├─ Force deferred store effects + ├─ routeLocation.isNavigating = false + └─ Resolve goto() promise +``` + +### Stale-by-Default Contract + +Loader signals are **not awaited** before navigation commits. Components see previous +loader values until new data arrives. If a developer wants navigation to wait for a +loader, they set `allowStale: false` on the loader, which causes the AsyncSignal to +throw a promise (suspense-style) when read before data is available, blocking the +subtree render until the data arrives. + +## 6. Context IDs + +All provided by `useQwikRouter` at the root: + +| ID | Name | Type | +| ------- | ----------------------------- | -------------------------------------- | +| `qr-s` | `RouteStateContext` | `Record>` | +| `qr-lc` | `RouteLoaderCtxContext` | `RouteLoaderCtx` store | +| `qr-l` | `RouteLocationContext` | `RouteLocation` store | +| `qr-n` | `RouteNavigateContext` | `goto` function | +| `qr-a` | `RouteActionContext` | `Signal` | +| `qr-h` | `DocumentHeadContext` | `ResolvedDocumentHead` store | +| `qr-c` | `ContentContext` | `ContentState` (headings, menu) | +| `qr-ic` | `ContentInternalContext` | `Signal` | +| `qr-hs` | `HttpStatusContext` | `Signal` | +| `qr-p` | `RoutePreventNavigateContext` | `registerPreventNav` function | diff --git a/packages/qwik-router/global.d.ts b/packages/qwik-router/global.d.ts index d294ce1109b..73b68baea7c 100644 --- a/packages/qwik-router/global.d.ts +++ b/packages/qwik-router/global.d.ts @@ -25,5 +25,15 @@ declare var __NO_TRAILING_SLASH__: boolean; /** Maximum number of SSR-rendered pages to keep in the in-memory cache. */ declare var __SSR_CACHE_SIZE__: number; +/** + * When true, route loaders and actions use strict mode by default: + * + * - Loaders without explicit `search` act as if `search: []` — they only re-fetch on route path + * changes and ignore all URL search params. + * - Actions without explicit `invalidate` act as if `invalidate: []` — they don't re-run any loaders + * after completion. + */ +declare var __STRICT_LOADERS__: boolean; + declare var __QWIK_BUILD_DIR__: string; declare var __QWIK_ASSETS_DIR__: string; diff --git a/packages/qwik-router/src/adapters/cloudflare-pages/vite/index.ts b/packages/qwik-router/src/adapters/cloudflare-pages/vite/index.ts index ed0fed99c06..3e800b9dec9 100644 --- a/packages/qwik-router/src/adapters/cloudflare-pages/vite/index.ts +++ b/packages/qwik-router/src/adapters/cloudflare-pages/vite/index.ts @@ -2,6 +2,7 @@ import type { SsgRenderOptions } from 'packages/qwik-router/src/ssg'; import fs from 'node:fs'; import { join, relative } from 'node:path'; import { normalizePathSlash } from '../../../utils/fs'; +import { ensureSlash } from '../../../utils/pathname'; import { type ServerAdapterOptions, viteAdapter } from '../../shared/vite'; /** @public */ @@ -41,10 +42,7 @@ export function cloudflarePagesAdapter(opts: CloudflarePagesAdapterOptions = {}) const routesJsonPath = join(clientOutDir, '_routes.json'); const hasRoutesJson = fs.existsSync(routesJsonPath); if (!hasRoutesJson && opts.functionRoutes !== false) { - let pathName = assetsDir ? join(basePathname, assetsDir) : basePathname; - if (!pathName.endsWith('/')) { - pathName += '/'; - } + const pathName = ensureSlash(assetsDir ? join(basePathname, assetsDir) : basePathname); const routesJson = { version: 1, include: [basePathname + '*'], diff --git a/packages/qwik-router/src/adapters/shared/vite/post-build.ts b/packages/qwik-router/src/adapters/shared/vite/post-build.ts index e8c8e7d18f9..96c2d93fd9b 100644 --- a/packages/qwik-router/src/adapters/shared/vite/post-build.ts +++ b/packages/qwik-router/src/adapters/shared/vite/post-build.ts @@ -1,6 +1,8 @@ import fs from 'node:fs'; import { join } from 'node:path'; import { getErrorHtml } from '../../../middleware/request-handler/error-handler'; +import { LOADER_REGEX } from '../../../middleware/request-handler/request-path'; +import { ensureSlash } from '../../../utils/pathname'; /** Cleans the client output SSG results if needed and injects the SSG metadata into the build output */ export async function postBuild( @@ -11,25 +13,26 @@ export async function postBuild( cleanStatic: boolean ) { if (pathName && !pathName.endsWith('/')) { - pathName += '/'; + pathName = ensureSlash(pathName); } + const pathNameBase = pathName || '/'; const ignorePathnames = new Set([ - pathName + '/' + (globalThis.__QWIK_BUILD_DIR__ || 'build') + '/', - pathName + '/' + (globalThis.__QWIK_ASSETS_DIR__ || 'assets') + '/', + pathNameBase + (globalThis.__QWIK_BUILD_DIR__ || 'build') + '/', + pathNameBase + (globalThis.__QWIK_ASSETS_DIR__ || 'assets') + '/', ]); - const staticPaths = new Set(userStaticPaths.map(normalizeTrailingSlash)); + const staticPaths = new Set(userStaticPaths.map(ensureSlash)); const notFounds: string[][] = []; const loadItem = async (fsDir: string, fsName: string, pathname: string) => { - pathname = normalizeTrailingSlash(pathname); + pathname = ensureSlash(pathname); if (ignorePathnames.has(pathname)) { return; } const fsPath = join(fsDir, fsName); - if (fsName === 'index.html' || fsName === 'q-data.json') { + if (fsName === 'index.html' || LOADER_REGEX.test('/' + fsName)) { // static index.html file if (!staticPaths.has(pathname) && cleanStatic) { await fs.promises.unlink(fsPath); @@ -46,7 +49,7 @@ export async function postBuild( const stat = await fs.promises.stat(fsPath); if (stat.isDirectory()) { - await loadDir(fsPath, pathname + fsName + '/'); + await loadDir(fsPath, ensureSlash(pathname + fsName)); } else if (stat.isFile()) { staticPaths.add(pathname + fsName); } @@ -67,13 +70,6 @@ export async function postBuild( await injectStatics(staticPathsCode, notFoundPathsCode, serverOutDir); } -function normalizeTrailingSlash(pathname: string) { - if (!pathname.endsWith('/')) { - return pathname + '/'; - } - return pathname; -} - function createNotFoundPathsCode(basePathname: string, notFounds: string[][]) { /** Sort in order of longest path, so that the most specific paths match first */ notFounds.sort((a, b) => { diff --git a/packages/qwik-router/src/buildtime/build.ts b/packages/qwik-router/src/buildtime/build.ts index 72df18ad050..b6e2a6fe057 100644 --- a/packages/qwik-router/src/buildtime/build.ts +++ b/packages/qwik-router/src/buildtime/build.ts @@ -1,5 +1,6 @@ import { addError, addWarning } from '../utils/format'; import { createFileId, getPathnameFromDirPath } from '../utils/fs'; +import { ensureSlash } from '../utils/pathname'; import { resolveMenu } from './markdown/menu'; import { resolveLayout, resolveRoute } from './routing/resolve-source-file'; import { routeSortCompare } from './routing/sort-routes'; @@ -219,7 +220,7 @@ function translateRoute( const replacePath = (part: string) => (config.paths || {})[part] ?? part; const pathnamePrefix = config.prefix ? '/' + config.prefix : ''; - const routeNamePrefix = config.prefix ? config.prefix + '/' : ''; + const routeNamePrefix = config.prefix ? ensureSlash(config.prefix) : ''; const idSuffix = config.prefix?.toUpperCase().replace(/-/g, ''); const patternInfix = config.prefix ? [config.prefix] : []; diff --git a/packages/qwik-router/src/buildtime/context.ts b/packages/qwik-router/src/buildtime/context.ts index bbc476b14eb..9f8b92c6e7a 100644 --- a/packages/qwik-router/src/buildtime/context.ts +++ b/packages/qwik-router/src/buildtime/context.ts @@ -1,5 +1,6 @@ import { isAbsolute, resolve } from 'node:path'; import { normalizePath } from '../utils/fs'; +import { ensureSlash } from '../utils/pathname'; import type { RoutingContext, NormalizedPluginOptions, PluginOptions } from './types'; export function createBuildContext( @@ -52,7 +53,7 @@ function normalizeOptions( `warning: vite's config.base must begin and end with /. This will be an error in v2. If you have a valid use case, please open an issue.` ); if (!viteBasePath.endsWith('/')) { - viteBasePath += '/'; + viteBasePath = ensureSlash(viteBasePath); } } const opts: NormalizedPluginOptions = { ...userOpts } as any; @@ -86,7 +87,7 @@ function normalizeOptions( console.error( `Warning: qwik-router plugin basePathname must end with /. This will be an error in v2` ); - opts.basePathname += '/'; + opts.basePathname = ensureSlash(opts.basePathname); } // cleanup basePathname @@ -95,6 +96,9 @@ function normalizeOptions( opts.mdx = opts.mdx || {}; opts.platform = opts.platform || {}; + if (opts.strictLoaders === undefined) { + opts.strictLoaders = true; + } return opts; } diff --git a/packages/qwik-router/src/buildtime/markdown/markdown-url.ts b/packages/qwik-router/src/buildtime/markdown/markdown-url.ts index 0b56ae83204..10a1dbadff6 100644 --- a/packages/qwik-router/src/buildtime/markdown/markdown-url.ts +++ b/packages/qwik-router/src/buildtime/markdown/markdown-url.ts @@ -3,7 +3,7 @@ import { getSourceFile } from '../routing/source-file'; import type { NormalizedPluginOptions } from '../types'; import { getExtension, getPathnameFromDirPath, isMarkdownExt, normalizePath } from '../../utils/fs'; import { existsSync } from 'node:fs'; -import { isSameOriginUrl } from '../../utils/pathname'; +import { ensureSlash, isSameOriginUrl } from '../../utils/pathname'; export function getMarkdownRelativeUrl( opts: NormalizedPluginOptions, @@ -53,7 +53,7 @@ export function getMarkdownRelativeUrl( url = url.slice(0, -1); } } else if (!globalThis.__NO_TRAILING_SLASH__) { - url += '/'; + url = ensureSlash(url); } } diff --git a/packages/qwik-router/src/buildtime/markdown/markdown-url.unit.ts b/packages/qwik-router/src/buildtime/markdown/markdown-url.unit.ts index a2a951831f6..1e210437f62 100644 --- a/packages/qwik-router/src/buildtime/markdown/markdown-url.unit.ts +++ b/packages/qwik-router/src/buildtime/markdown/markdown-url.unit.ts @@ -80,6 +80,7 @@ const menuFilePath = join(routesDir, 'docs', 'menu.md'); platform: {}, rewriteRoutes: [], defaultLoadersSerializationStrategy: 'never', + strictLoaders: true, }; globalThis.__NO_TRAILING_SLASH__ = !t.trailingSlash; assert.equal(getMarkdownRelativeUrl(opts, menuFilePath, t.href), t.expect); diff --git a/packages/qwik-router/src/buildtime/routing/resolve-source-file.unit.ts b/packages/qwik-router/src/buildtime/routing/resolve-source-file.unit.ts index 13d1c3fbb33..55fb44c2893 100644 --- a/packages/qwik-router/src/buildtime/routing/resolve-source-file.unit.ts +++ b/packages/qwik-router/src/buildtime/routing/resolve-source-file.unit.ts @@ -49,6 +49,7 @@ test('resolveLayout', () => { platform: {}, rewriteRoutes: [], defaultLoadersSerializationStrategy: 'never', + strictLoaders: true, }; const sourceFile: RouteSourceFile = { ...getSourceFile(c.fileName)!, diff --git a/packages/qwik-router/src/buildtime/runtime-generation/generate-qwik-router-config.ts b/packages/qwik-router/src/buildtime/runtime-generation/generate-qwik-router-config.ts index b4b37de688b..26f71ebc826 100644 --- a/packages/qwik-router/src/buildtime/runtime-generation/generate-qwik-router-config.ts +++ b/packages/qwik-router/src/buildtime/runtime-generation/generate-qwik-router-config.ts @@ -8,7 +8,8 @@ import { createServerPlugins } from './generate-server-plugins'; export function generateQwikRouterConfig( ctx: RoutingContext, qwikPlugin: QwikVitePlugin, - isSSR: boolean + isSSR: boolean, + loadersByFile?: Map ) { const esmImports: string[] = []; const c: string[] = []; @@ -24,7 +25,7 @@ export function generateQwikRouterConfig( createServerPlugins(ctx, qwikPlugin, c, esmImports, isSSR); - createRoutes(ctx, qwikPlugin, c, esmImports, isSSR); + createRoutes(ctx, qwikPlugin, c, esmImports, isSSR, loadersByFile); createEntries(ctx, c); diff --git a/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.ts b/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.ts index 9e9e9fb6139..10b8e2d76d0 100644 --- a/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.ts +++ b/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.ts @@ -36,7 +36,8 @@ export function createRoutes( qwikPlugin: QwikVitePlugin, c: string[], esmImports: string[], - isSSR: boolean + isSSR: boolean, + loadersByFile?: Map ) { const includeEndpoints = isSSR; const dynamicImports = ctx.dynamicImports; @@ -134,7 +135,8 @@ export function createRoutes( notFoundFiles, [], isSSR, - '' + '', + loadersByFile ); // Note: both error.tsx and 404.tsx in the same directory is fine. @@ -181,7 +183,8 @@ function serializeBuildTrie( notFoundFiles: Map, ancestorLayouts: LayoutInfo[], isSSR: boolean, - indent: string + indent: string, + loadersByFile?: Map ): string { const lines: string[] = []; const nextIndent = indent + ' '; @@ -314,6 +317,44 @@ function serializeBuildTrie( } } + // Emit _R: routeLoader$ hashes for this node. + // In dev mode (loadersByFile populated after invalidation), emit directly. + // In build mode, emit placeholder string for renderChunk replacement. + { + const routeFiles = node._files + .filter((f) => f.type === 'route' || f.type === 'layout') + .map((f) => f.filePath); + // Include server plugin files at the root trie node (they apply to all routes) + if (node === ctx.routeTrie) { + for (const plugin of ctx.serverPlugins) { + routeFiles.push(plugin.filePath); + } + } + if (routeFiles.length > 0) { + if (loadersByFile) { + // Dev mode: the loader hashes are already known, emit them directly. When no + // routeLoader$ was found in any of the referenced files, skip emitting _R + // entirely so the runtime routing code doesn't see a stale placeholder. + const nodeLoaderHashes: string[] = []; + for (const filePath of routeFiles) { + const hashes = loadersByFile.get(filePath); + if (hashes) { + nodeLoaderHashes.push(...hashes); + } + } + if (nodeLoaderHashes.length > 0) { + lines.push(`${nextIndent}_R: ${JSON.stringify(nodeLoaderHashes)},`); + } + } else { + // Build mode: emit placeholder "__LOADERS:path1|path2__" — replaceLoaderPlaceholders + // in the qwikRouter vite plugin rewrites it to the real array (or strips the whole + // `_R: ...,` entry if no loaders were found). + const placeholder = `__LOADERS:${routeFiles.join('|')}__`; + lines.push(`${nextIndent}_R: ${JSON.stringify(placeholder)},`); + } + } + } + // Emit _E, _4, _N if (errorExpr) { lines.push(`${nextIndent}_E: ${errorExpr},`); @@ -356,7 +397,8 @@ function serializeBuildTrie( notFoundFiles, childAncestors, isSSR, - nextIndent + nextIndent, + loadersByFile ); if (childStr !== '{}') { groupStrs.push(childStr); @@ -380,7 +422,8 @@ function serializeBuildTrie( notFoundFiles, childAncestors, isSSR, - nextIndent + nextIndent, + loadersByFile ); if (childStr !== '{}') { const keyStr = JSON.stringify(key); diff --git a/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.unit.ts b/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.unit.ts index 5b8f8fc3d68..d19b22121ce 100644 --- a/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.unit.ts +++ b/packages/qwik-router/src/buildtime/runtime-generation/generate-routes.unit.ts @@ -48,17 +48,41 @@ const mockQwikPlugin = { } as any; /** Extract the `routes = ...` expression from generated code */ -function getRoutesExpr(trie: BuildTrieNode, routes: BuiltRoute[] = []): string { +function getRoutesExpr( + trie: BuildTrieNode, + routes: BuiltRoute[] = [], + loadersByFile?: Map +): string { const c: string[] = []; const esmImports: string[] = []; const ctx = { - opts: { basePathname: '/', routesDir: '/routes' }, + opts: { + basePathname: '/', + routesDir: '/routes', + platform: null!, + mdx: null!, + serverPluginsDir: '/routes', + mdxPlugins: null!, + rewriteRoutes: null!, + defaultLoadersSerializationStrategy: 'never', + strictLoaders: true, + }, routeTrie: trie, routes, layouts: [], + serverPlugins: [], dynamicImports: true, - } as any; - createRoutes(ctx, mockQwikPlugin, c, esmImports, false); + rootDir: '/', + entries: [], + serviceWorkers: [], + menus: [], + frontmatter: new Map(), + diagnostics: [], + target: 'ssr', + isDirty: false, + activeBuild: null, + } satisfies Parameters[0]; + createRoutes(ctx, mockQwikPlugin, c, esmImports, false, loadersByFile); const routesLine = c.find((line) => line.startsWith('export const routes')); assert.ok(routesLine, 'should have a routes export'); return routesLine; @@ -111,3 +135,53 @@ describe('generate-routes: empty node pruning', () => { assert.include(expr, '_M', 'non-empty group should produce _M'); }); }); + +describe('generate-routes: loadersByFile propagation', () => { + test('loadersByFile emits _R hashes for regular child nodes in dev mode', () => { + const root = makeNode(); + const child = makeNode({ _dirPath: '/test/dashboard' }); + const routeFile = makeRouteFile('/test/dashboard'); + child._files = [routeFile]; + root.children.set('dashboard', child); + + const routes = [makeBuiltRoute(routeFile.filePath)]; + const loadersByFile = new Map([[routeFile.filePath, ['loader-hash-abc']]]); + const expr = getRoutesExpr(root, routes, loadersByFile); + + assert.include(expr, '_R', 'regular child should emit _R when loadersByFile is provided'); + assert.include(expr, 'loader-hash-abc', 'loader hash should appear in the output'); + assert.notInclude(expr, '__LOADERS:', 'should not emit placeholder in dev mode'); + }); + + test('loadersByFile emits _R hashes for group child nodes in dev mode', () => { + const root = makeNode(); + const group = makeNode({ _dirPath: '/test/(common)' }); + const routeFile = makeRouteFile('/test/(common)'); + group._files = [routeFile]; + root.children.set('(common)', group); + + const routes = [makeBuiltRoute(routeFile.filePath)]; + const loadersByFile = new Map([[routeFile.filePath, ['group-loader-hash']]]); + const expr = getRoutesExpr(root, routes, loadersByFile); + + assert.include(expr, 'group-loader-hash', 'group child should emit its loader hash'); + }); + + test('without loadersByFile regular children emit a build placeholder', () => { + const root = makeNode(); + const child = makeNode({ _dirPath: '/test/dashboard' }); + const routeFile = makeRouteFile('/test/dashboard'); + child._files = [routeFile]; + root.children.set('dashboard', child); + + const routes = [makeBuiltRoute(routeFile.filePath)]; + const expr = getRoutesExpr(root, routes); + + assert.include( + expr, + '__LOADERS:', + 'regular child should emit build placeholder without loadersByFile' + ); + assert.notInclude(expr, 'loader-hash', 'no concrete hash expected in build mode'); + }); +}); diff --git a/packages/qwik-router/src/buildtime/types.ts b/packages/qwik-router/src/buildtime/types.ts index b23f0278d11..22c443f59bd 100644 --- a/packages/qwik-router/src/buildtime/types.ts +++ b/packages/qwik-router/src/buildtime/types.ts @@ -169,6 +169,21 @@ export interface PluginOptions { rewriteRoutes?: RewriteRouteOption[]; /** The serialization strategy for route loaders. Defaults to `never`. */ defaultLoadersSerializationStrategy?: SerializationStrategy; + /** + * Enable strict mode for route loaders and actions by default. + * + * When true: + * + * - Loaders without an explicit `search` option act as if `search: []` — they only re-fetch when + * the route path changes and ignore all URL search params. + * - Actions without an explicit `invalidate` option act as if `invalidate: []` — they don't re-run + * any loaders after completion. + * + * Individual loaders/actions can override this by specifying `search` or `invalidate` explicitly. + * + * Defaults to `true`. + */ + strictLoaders?: boolean; } export interface MdxPlugins { diff --git a/packages/qwik-router/src/buildtime/vite/plugin.ts b/packages/qwik-router/src/buildtime/vite/plugin.ts index fd314b40bf0..6a6e386b5d4 100644 --- a/packages/qwik-router/src/buildtime/vite/plugin.ts +++ b/packages/qwik-router/src/buildtime/vite/plugin.ts @@ -5,6 +5,7 @@ import { basename, extname, join, resolve } from 'node:path'; import type { ConfigEnv, EnvironmentOptions, + HmrContext, Plugin, PluginOption, Rollup, @@ -12,7 +13,17 @@ import type { ViteDevServer, } from 'vite'; import { loadEnv } from 'vite'; -import { isMenuFileName, normalizePath, removeExtension } from '../../utils/fs'; +import { + isEntryName, + isErrorName, + isIndexModule, + isLayoutModule, + isMenuFileName, + isPluginModule, + isServiceWorkerName, + normalizePath, + removeExtension, +} from '../../utils/fs'; import { parseRoutesDir } from '../build'; import { createBuildContext, resetBuildContext } from '../context'; import { createMdxTransformer, type MdxTransform } from '../markdown/mdx'; @@ -61,6 +72,107 @@ export function qwikRouter(userOpts?: QwikRouterVitePluginOptions): PluginOption ]; } +/** Replace or strip `_R: "__LOADERS:path1|path2__"` placeholders in a bundle chunk. */ +export function replaceLoaderPlaceholders( + code: string, + loadersByFile: Map +): string { + // Replace `_R: "__LOADERS:path1|path2__"` with the actual hash array, or strip the whole + // `_R: ...` entry when no routeLoader$ was found — that way the client-side routing code + // never sees a stale placeholder string and spreads it character-by-character. + return code.replace(/_R\s*:\s*"__LOADERS:([^"]+)__"\s*,?/g, (_match, paths: string) => { + const filePaths = paths.split('|'); + const hashes: string[] = []; + for (const filePath of filePaths) { + const fileHashes = loadersByFile.get(filePath); + if (fileHashes) { + hashes.push(...fileHashes); + } + } + if (hashes.length > 0) { + return `_R: ${JSON.stringify(hashes)},`; + } + // Trailing commas inside object literals are legal, so removing a mid-object entry + // (and the trailing comma it emitted with) leaves the surrounding trie literal valid. + return ''; + }); +} + +export function addRouteLoaderHash( + loadersByFile: Map, + filePath: string, + hash: string +) { + const normalizedPath = normalizePath(filePath); + const existing = loadersByFile.get(normalizedPath); + if (!existing) { + loadersByFile.set(normalizedPath, [hash]); + return true; + } + if (!existing.includes(hash)) { + existing.push(hash); + return true; + } + return false; +} + +export function clearRouteLoaderHashes(loadersByFile: Map, filePath: string) { + return loadersByFile.delete(normalizePath(filePath)); +} + +export function isRouterSourceFilePath(filePath: string) { + const fileName = basename(filePath); + const extlessName = removeExtension(fileName); + return ( + isMenuFileName(fileName) || + isIndexModule(extlessName) || + isErrorName(extlessName) || + isLayoutModule(extlessName) || + isEntryName(extlessName) || + isServiceWorkerName(extlessName) || + isPluginModule(extlessName) + ); +} + +function isPathInDir(filePath: string, dirPath: string) { + return filePath === dirPath || filePath.startsWith(`${dirPath}/`); +} + +function isRouterSourceFileForContext(filePath: string, ctx: RoutingContext) { + const normalizedPath = normalizePath(filePath); + return ( + isRouterSourceFilePath(normalizedPath) && + (isPathInDir(normalizedPath, ctx.opts.routesDir) || + isPathInDir(normalizedPath, ctx.opts.serverPluginsDir)) + ); +} + +export function invalidateRouterConfigModules(server: ViteDevServer) { + const modules: NonNullable>[] = []; + const moduleGraphs = new Set(); + moduleGraphs.add(server.moduleGraph); + + const environments = (server as any).environments; + if (environments) { + for (const environment of Object.values(environments)) { + const graph = (environment as any)?.moduleGraph; + if (graph) { + moduleGraphs.add(graph); + } + } + } + + for (const graph of moduleGraphs) { + const mod = graph.getModuleById(QWIK_ROUTER_CONFIG_ID); + if (mod) { + graph.invalidateModule(mod); + modules.push(mod); + } + } + + return modules; +} + function qwikRouterPlugin( userOpts: QwikRouterVitePluginOptions | undefined, buildContextRef: BuildContextRef @@ -76,6 +188,8 @@ function qwikRouterPlugin( let devSsrServer = userOpts?.devSsrServer; const routesDir = userOpts?.routesDir ?? 'src/routes'; const serverPluginsDir = userOpts?.serverPluginsDir ?? routesDir; + /** Map from source file path to array of routeLoader$ hashes found in that file */ + const loadersByFile = new Map(); const api: QwikRouterPluginApi = { getBasePathname: () => ctx?.opts.basePathname ?? '/', @@ -105,6 +219,7 @@ function qwikRouterPlugin( 'globalThis.__SSR_CACHE_SIZE__': JSON.stringify( viteEnv.command === 'serve' ? 0 : (userOpts?.ssrCacheSize ?? 50) ), + 'globalThis.__STRICT_LOADERS__': JSON.stringify(userOpts?.strictLoaders !== false), }, appType: 'custom', resolve: { @@ -200,6 +315,18 @@ function qwikRouterPlugin( if (!qwikPlugin) { throw new Error('Missing vite-plugin-qwik'); } + + // Register callback to discover routeLoader$ hashes from optimizer segments + qwikPlugin.api.onSegment((parentId, segment) => { + if (segment.ctxName === 'routeLoader$') { + const changed = addRouteLoaderHash(loadersByFile, parentId, segment.hash); + + // In dev: invalidate @qwik-router-config so it re-emits _R with loader info. + if (changed && devServer) { + invalidateRouterConfigModules(devServer); + } + } + }); if (typeof devSsrServer !== 'boolean') { // read the old option from qwik plugin devSsrServer = qwikPlugin.api._oldDevSsrServer(); @@ -213,26 +340,22 @@ function qwikRouterPlugin( async configureServer(server) { devServer = server; // recursively watch all route files in the src/routes directory - const toWatch = resolve( - rootDir!, - 'src/routes/**/{index,layout,entry,service-worker}{.,@,-}*' - ); + const toWatch = [ + join( + ctx!.opts.routesDir, + '**/{index,index!,index@*,layout,layout!,layout-*,error,404,entry,service-worker,menu}.{ts,tsx,js,jsx,md,mdx}' + ), + join(ctx!.opts.serverPluginsDir, 'plugin{,@*}.{ts,tsx,js,jsx}'), + ]; server.watcher.add(toWatch); await new Promise((resolve) => setTimeout(resolve, 1000)); server.watcher.on('change', (path) => { - // If the path is not an index or layout file, skip - if (!/\/(index[.@]|layout[.-]|entry\.|service-worker\.)[^/]*$/.test(path)) { + if (!isRouterSourceFileForContext(path, ctx!)) { return; } - // Invalidate the router config + clearRouteLoaderHashes(loadersByFile, path); ctx!.isDirty = true; - const graph = server.environments?.ssr?.moduleGraph; - if (graph) { - const mod = graph.getModuleById('@qwik-router-config'); - if (mod) { - graph.invalidateModule(mod); - } - } + invalidateRouterConfigModules(server); }); if (userOpts?.devSsrServer !== false) { @@ -243,6 +366,16 @@ function qwikRouterPlugin( } }, + handleHotUpdate({ file, modules, server }: HmrContext) { + if (!ctx || !isRouterSourceFileForContext(file, ctx)) { + return; + } + clearRouteLoaderHashes(loadersByFile, file); + ctx.isDirty = true; + const configModules = invalidateRouterConfigModules(server); + return [...modules, ...configModules]; + }, + transformIndexHtml() { if (viteCommand !== 'serve') { return; @@ -290,10 +423,15 @@ function qwikRouterPlugin( if (isRouterConfig) { // @qwik-router-config + // In dev server mode, loadersByFile is kept current via onSegment + module + // invalidation, so pass it directly. In build mode the config is loaded before + // route files are optimized, so loadersByFile is empty here; pass undefined to + // emit __LOADERS:...__ placeholders that generateBundle replaces after optimization. return generateQwikRouterConfig( ctx, qwikPlugin!, - this.environment.config.consumer === 'server' + this.environment.config.consumer === 'server', + devServer ? loadersByFile : undefined ); } @@ -351,6 +489,14 @@ function qwikRouterPlugin( }, generateBundle(_, bundles) { + // Replace __LOADERS:...__ placeholder strings with actual loader hash arrays. + // Runs even when no routeLoader$ was found so placeholders collapse to `void 0` + // (otherwise they remain as raw strings and the client iterates them per-character). + for (const chunk of Object.values(bundles)) { + if (chunk.type === 'chunk' && chunk.code.includes('__LOADERS:')) { + chunk.code = replaceLoaderPlaceholders(chunk.code, loadersByFile); + } + } // Turn entry and service worker chunks into entry points if (this.environment.config.consumer === 'client') { const entries = [...ctx!.entries, ...ctx!.serviceWorkers].map((entry) => { diff --git a/packages/qwik-router/src/buildtime/vite/plugin.unit.ts b/packages/qwik-router/src/buildtime/vite/plugin.unit.ts index 68a1a9daa35..4ba3adf4111 100644 --- a/packages/qwik-router/src/buildtime/vite/plugin.unit.ts +++ b/packages/qwik-router/src/buildtime/vite/plugin.unit.ts @@ -1,5 +1,11 @@ import { describe, expect, it } from 'vitest'; -import { qwikRouter } from './plugin'; +import { + addRouteLoaderHash, + clearRouteLoaderHashes, + invalidateRouterConfigModules, + isRouterSourceFilePath, + qwikRouter, +} from './plugin'; describe('qwikRouter plugin', () => { describe('defaultLoadersSerializationStrategy', () => { @@ -59,4 +65,65 @@ describe('qwikRouter plugin', () => { expect(result).toEqual({}); }); }); + + describe('routeLoader$ dev cache', () => { + it('dedupes hashes and can replace stale file entries', () => { + const loadersByFile = new Map(); + + expect(addRouteLoaderHash(loadersByFile, '/app/src/routes/index.tsx', 'aaa')).toBe(true); + expect(addRouteLoaderHash(loadersByFile, '/app/src/routes/index.tsx', 'aaa')).toBe(false); + expect(addRouteLoaderHash(loadersByFile, '/app/src/routes/index.tsx', 'bbb')).toBe(true); + expect(loadersByFile.get('/app/src/routes/index.tsx')).toEqual(['aaa', 'bbb']); + + expect(clearRouteLoaderHashes(loadersByFile, '/app/src/routes/index.tsx')).toBe(true); + expect(addRouteLoaderHash(loadersByFile, '/app/src/routes/index.tsx', 'ccc')).toBe(true); + expect(loadersByFile.get('/app/src/routes/index.tsx')).toEqual(['ccc']); + }); + + it('recognizes all router source files that can affect the route config', () => { + expect(isRouterSourceFilePath('/app/src/routes/index.tsx')).toBe(true); + expect(isRouterSourceFilePath('/app/src/routes/index!.tsx')).toBe(true); + expect(isRouterSourceFilePath('/app/src/routes/index@admin.tsx')).toBe(true); + expect(isRouterSourceFilePath('/app/src/routes/404.tsx')).toBe(true); + expect(isRouterSourceFilePath('/app/src/routes/error.tsx')).toBe(true); + expect(isRouterSourceFilePath('/app/src/routes/layout-main.tsx')).toBe(true); + expect(isRouterSourceFilePath('/app/src/routes/layout!.tsx')).toBe(true); + expect(isRouterSourceFilePath('/app/src/routes/menu.md')).toBe(true); + expect(isRouterSourceFilePath('/app/src/routes/plugin@auth.ts')).toBe(true); + + expect(isRouterSourceFilePath('/app/src/routes/plugin.unit.ts')).toBe(false); + expect(isRouterSourceFilePath('/app/src/routes/component.tsx')).toBe(false); + }); + + it('invalidates router config modules in all dev module graphs', () => { + const createGraph = () => { + const mod = { id: '@qwik-router-config' }; + return { + mod, + invalidated: [] as any[], + getModuleById: (id: string) => (id === '@qwik-router-config' ? mod : undefined), + invalidateModule(module: any) { + this.invalidated.push(module); + }, + }; + }; + const mainGraph = createGraph(); + const clientGraph = createGraph(); + const ssrGraph = createGraph(); + const server = { + moduleGraph: mainGraph, + environments: { + client: { moduleGraph: clientGraph }, + ssr: { moduleGraph: ssrGraph }, + }, + } as any; + + const modules = invalidateRouterConfigModules(server); + + expect(modules).toEqual([mainGraph.mod, clientGraph.mod, ssrGraph.mod]); + expect(mainGraph.invalidated).toEqual([mainGraph.mod]); + expect(clientGraph.invalidated).toEqual([clientGraph.mod]); + expect(ssrGraph.invalidated).toEqual([ssrGraph.mod]); + }); + }); }); diff --git a/packages/qwik-router/src/middleware/aws-lambda/index.ts b/packages/qwik-router/src/middleware/aws-lambda/index.ts index 8dcec1fe29c..1e818c7ba83 100644 --- a/packages/qwik-router/src/middleware/aws-lambda/index.ts +++ b/packages/qwik-router/src/middleware/aws-lambda/index.ts @@ -5,6 +5,7 @@ import type { NodeRequestNextFunction } from '@qwik.dev/router/middleware/node'; import type { ServerRenderOptions } from '@qwik.dev/router/middleware/request-handler'; import type { Http2ServerRequest } from 'node:http2'; import type { IncomingMessage, ServerResponse } from 'node:http'; +import { ensureSlash } from '../../utils/pathname'; interface AwsOpt { render: Render; @@ -57,7 +58,7 @@ export function createQwikRouter(opts: AwsOpt): QwikRouterAwsLambdaMiddleware { return pathT; } if (!url.pathname.endsWith('/')) { - return url.pathname + '/' + url.search; + return ensureSlash(url.pathname) + url.search; } } return pathT; diff --git a/packages/qwik-router/src/middleware/request-handler/handlers/action-handler.ts b/packages/qwik-router/src/middleware/request-handler/handlers/action-handler.ts new file mode 100644 index 00000000000..e2ab182127c --- /dev/null +++ b/packages/qwik-router/src/middleware/request-handler/handlers/action-handler.ts @@ -0,0 +1,173 @@ +import { _serialize, _verifySerializable, isDev } from '@qwik.dev/core/internal'; +import type { + ActionInternal, + DataValidator, + JSONObject, + RequestEvent, + RequestHandler, + ValidatorReturn, +} from '../../../runtime/src/types'; +import { RequestEvSharedActionId, type RequestEventInternal } from '../request-event-core'; +import { IsQAction, QActionId } from '../request-path'; +import type { QRL } from '@qwik.dev/core'; +import type { RequestEventBase } from '../types'; + +/** + * Handler for action requests (`?qaction={actionId}`). + * + * When the request has `Accept: application/json`, returns the action result as JSON. Otherwise, + * falls through to let the page render (progressive enhancement for forms). + * + * If the action has no `invalidate` list, the client invalidates all current route loaders unless + * strict loaders mode treats it as `invalidate: []`. If `invalidate` is specified, only those + * loader hashes are sent for the client to re-fetch. + */ +export function actionHandler(routeActions: ActionInternal[]): RequestHandler { + return async (requestEvent: RequestEvent) => { + const requestEv = requestEvent as RequestEventInternal; + + if (!requestEv.sharedMap.has(IsQAction)) { + return; + } + + // Only intercept when the client accepts JSON (fetch-based submissions). + // Otherwise, fall through to let actionsMiddleware handle it for page renders + // (progressive enhancement for forms). + if (!requestEv.request.headers.get('accept')?.includes('application/json')) { + return; + } + + if (requestEv.headersSent || requestEv.exited) { + return; + } + + const actionId = requestEv.sharedMap.get(QActionId) as string; + const method = requestEv.method; + const devMode = isDev; + + if (devMode && method === 'GET') { + console.warn( + 'Seems like you are submitting a Qwik Action via GET request. Qwik Actions should be submitted via POST request.\nMake sure your
has method="POST" attribute, like this: ' + ); + } + + if (method !== 'POST') { + return; + } + + // Find the action + let action: ActionInternal | undefined; + for (const routeAction of routeActions) { + if (routeAction.__id === actionId) { + action = routeAction; + } + } + + // Try global actions map if not found in route + if (!action) { + const serverActionsMap = globalThis._qwikActionsMap as + | Map + | undefined; + action = serverActionsMap?.get(actionId); + } + + if (!action) { + requestEv.json(404, { error: 'Action not found' }); + return; + } + + // Execute the action + const data = await requestEv.parseBody(); + if (!data || typeof data !== 'object') { + throw new Error(`Expected request data for the action id ${actionId} to be an object`); + } + + let actionResult: unknown; + const result = await runValidators(requestEv, action.__validators, data, devMode); + if (!result.success) { + actionResult = requestEv.fail(result.status ?? 500, result.error); + } else { + const actionResolved = devMode + ? await measure(requestEv, action.__qrl.getHash(), () => + action!.__qrl.call(requestEv, result.data as JSONObject, requestEv) + ) + : await action.__qrl.call(requestEv, result.data as JSONObject, requestEv); + if (devMode) { + verifySerializable(actionResolved, action.__qrl); + } + actionResult = actionResolved; + } + const responseData: Record = { + result: actionResult, + }; + requestEv.sharedMap.set(RequestEvSharedActionId, actionId); + requestEv.sharedMap.set('@actionResult', actionResult); + + if (action.__invalidate) { + // Action specifies which loaders to invalidate — send only hashes, client re-fetches + responseData.loaderHashes = action.__invalidate; + } else if (globalThis.__STRICT_LOADERS__) { + // Strict mode treats omitted invalidate as invalidate: []. + responseData.loaderHashes = []; + } + + const serialized = await _serialize(responseData); + requestEv.headers.set('Content-Type', 'application/json; charset=utf-8'); + requestEv.send(requestEv.status(), serialized); + }; +} + +async function runValidators( + requestEv: RequestEvent, + validators: DataValidator[] | undefined, + data: unknown, + devMode: boolean +) { + let lastResult: ValidatorReturn = { success: true, data }; + if (validators) { + for (const validator of validators) { + if (devMode) { + lastResult = await measure(requestEv, `validator$`, () => + validator.validate(requestEv, data) + ); + } else { + lastResult = await validator.validate(requestEv, data); + } + if (!lastResult.success) { + return lastResult; + } else { + data = lastResult.data; + } + } + } + return lastResult; +} + +function verifySerializable(data: any, qrl: QRL) { + try { + _verifySerializable(data, undefined); + } catch (e: any) { + if (e instanceof Error && qrl.dev) { + (e as any).loc = qrl.dev; + } + throw e; + } +} + +async function measure( + requestEv: RequestEventBase, + name: string, + fn: () => T +): Promise> { + const start = performance.now(); + try { + return await fn(); + } finally { + const duration = performance.now() - start; + let measurements = requestEv.sharedMap.get('@serverTiming'); + if (!measurements) { + requestEv.sharedMap.set('@serverTiming', (measurements = [])); + } + measurements.push([name, duration]); + } +} diff --git a/packages/qwik-router/src/middleware/request-handler/handlers/action-handler.unit.ts b/packages/qwik-router/src/middleware/request-handler/handlers/action-handler.unit.ts new file mode 100644 index 00000000000..bbf1ddd440c --- /dev/null +++ b/packages/qwik-router/src/middleware/request-handler/handlers/action-handler.unit.ts @@ -0,0 +1,161 @@ +import { _deserialize } from '@qwik.dev/core/internal'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { RequestEvSharedActionId } from '../request-event-core'; +import { IsQAction, QActionId } from '../request-path'; +import { actionHandler } from './action-handler'; + +const previousStrictLoaders = globalThis.__STRICT_LOADERS__; + +afterEach(() => { + globalThis.__STRICT_LOADERS__ = previousStrictLoaders; + vi.restoreAllMocks(); +}); + +const createAction = (invalidate?: string[]) => + ({ + __brand: 'server_action', + __id: 'action-a', + __validators: undefined, + __invalidate: invalidate, + __qrl: { + getHash: () => 'action-a', + call: vi.fn(async (_event: unknown, data: unknown) => ({ ok: data })), + }, + }) as any; + +const createActionRequest = () => { + const sent = { + status: undefined as number | undefined, + body: undefined as string | undefined, + }; + let currentStatus = 200; + const request = new Request('http://localhost/test/?qaction=action-a', { + method: 'POST', + headers: { + Accept: 'application/json', + }, + }); + const requestEv = { + sharedMap: new Map([ + [IsQAction, true], + [QActionId, 'action-a'], + ]), + request, + method: 'POST', + headers: new Headers(), + headersSent: false, + exited: false, + parseBody: vi.fn(async () => ({ name: 'Ada' })), + fail: vi.fn((statusCode: number, data: Record) => { + currentStatus = statusCode; + return { failed: true, status: statusCode, ...data }; + }), + status: vi.fn((statusCode?: number) => { + if (typeof statusCode === 'number') { + currentStatus = statusCode; + return statusCode; + } + return currentStatus; + }), + json: vi.fn((statusCode: number, data: unknown) => { + sent.status = statusCode; + sent.body = JSON.stringify(data); + }), + send: vi.fn((statusCode: number, body: string) => { + sent.status = statusCode; + sent.body = body; + }), + }; + return { requestEv, sent }; +}; + +describe('actionHandler', () => { + it('returns only the action result when loaders should all invalidate on the client', async () => { + globalThis.__STRICT_LOADERS__ = false; + const { requestEv, sent } = createActionRequest(); + + await actionHandler([createAction()])(requestEv as any); + + const response = _deserialize>(sent.body!); + expect(sent.status).toBe(200); + expect(response).toEqual({ result: { ok: { name: 'Ada' } } }); + expect(response).not.toHaveProperty('loaders'); + expect(response).not.toHaveProperty('loaderHashes'); + expect(requestEv.sharedMap.get(RequestEvSharedActionId)).toBe('action-a'); + }); + + it('returns explicit loader hashes without loader values', async () => { + globalThis.__STRICT_LOADERS__ = false; + const { requestEv, sent } = createActionRequest(); + + await actionHandler([createAction(['loader-a', 'loader-b'])])(requestEv as any); + + const response = _deserialize>(sent.body!); + expect(response).toEqual({ + result: { ok: { name: 'Ada' } }, + loaderHashes: ['loader-a', 'loader-b'], + }); + expect(response).not.toHaveProperty('loaders'); + }); + + it('returns empty loader hashes for strict mode actions without explicit invalidate', async () => { + globalThis.__STRICT_LOADERS__ = true; + const { requestEv, sent } = createActionRequest(); + + await actionHandler([createAction()])(requestEv as any); + + const response = _deserialize>(sent.body!); + expect(response).toEqual({ + result: { ok: { name: 'Ada' } }, + loaderHashes: [], + }); + expect(response).not.toHaveProperty('loaders'); + }); + + it('reflects validator failure status in HTTP response', async () => { + globalThis.__STRICT_LOADERS__ = false; + const { requestEv, sent } = createActionRequest(); + + const action = { + ...createAction(), + __validators: [ + { + validate: vi.fn(async () => ({ + success: false, + status: 422, + error: { field: 'name', message: 'too short' }, + })), + }, + ], + } as any; + + await actionHandler([action])(requestEv as any); + + expect(sent.status).toBe(422); + const response = _deserialize>(sent.body!); + expect(response.result).toMatchObject({ failed: true, status: 422 }); + }); + + it('reflects fail() status called from action QRL in HTTP response', async () => { + globalThis.__STRICT_LOADERS__ = false; + const { requestEv, sent } = createActionRequest(); + + const action = { + __brand: 'server_action', + __id: 'action-a', + __validators: undefined, + __invalidate: undefined, + __qrl: { + getHash: () => 'action-a', + // First arg is requestEv, second is parsed body data + call: vi.fn(async (ev: any) => ev.fail(500, { msg: 'something went wrong' })), + }, + } as any; + + await actionHandler([action])(requestEv as any); + + expect(sent.status).toBe(500); + const response = _deserialize>(sent.body!); + expect(response.result).toMatchObject({ failed: true, msg: 'something went wrong' }); + }); +}); diff --git a/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.ts b/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.ts new file mode 100644 index 00000000000..924ef5c820c --- /dev/null +++ b/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.ts @@ -0,0 +1,106 @@ +import { isDev } from '@qwik.dev/core'; +import { FULLPATH_HEADER } from '../../../runtime/src/route-loaders'; +import { ensureSlash } from '../../../utils/pathname'; +import { RedirectMessage } from '../redirect-handler'; +import type { RequestEventInternal } from '../request-event-core'; +import { IsQLoader, IsQAction } from '../request-path'; +import { ServerError } from '../server-error'; +import type { RequestHandler, RequestEvent } from '../types'; +import { addVaryHeader, sendJsonResponse, sendActionResponse } from './loader-handler'; + +/** + * Early handler that wraps `next()` for JSON API requests (q-loader and q-action). + * + * For `IsQLoader` requests, it also rewrites the URL using the `X-Qwik-fullpath` header so that + * downstream middleware sees the real page URL. + * + * By calling `await next()` inside a try/catch, middleware redirects and errors are captured and + * returned as JSON envelopes instead of HTTP redirects/error pages. This keeps SPA navigation + * intact on the client. + */ + +export function jsonRequestWrapper(): RequestHandler { + return async (requestEvent: RequestEvent) => { + const requestEv = requestEvent as RequestEventInternal; + + const isLoader = requestEv.sharedMap.has(IsQLoader); + const isActionJson = + requestEv.sharedMap.has(IsQAction) && + requestEv.request.headers.get('accept')?.includes('application/json'); + + if (!isLoader && !isActionJson) { + return; + } + + // For loaders: rewrite URL using X-Qwik-fullpath header so middleware sees the real route + if (isLoader) { + addVaryHeader(requestEv, FULLPATH_HEADER); + const pagePath = requestEv.request.headers.get(FULLPATH_HEADER); + const pagePathname = resolveValidFullPath(requestEv, pagePath); + if (pagePathname) { + requestEv.url.pathname = pagePathname; + } + } + + // Wrap all downstream handlers in try/catch so middleware redirects/errors + // become JSON responses instead of HTTP redirects/error pages + try { + await requestEv.next(); + } catch (err) { + if (requestEv.headersSent) { + return; + } + if (err instanceof RedirectMessage) { + if (isLoader) { + const location = requestEv.headers.get('Location') || '/'; + requestEv.headers.delete('Location'); + await sendJsonResponse(requestEv, { r: location }); + } else { + // Action redirects: let HTTP redirect propagate — client handles via response.redirected + throw err; + } + } else if (err instanceof ServerError) { + if (isLoader) { + await sendJsonResponse(requestEv, { e: err }); + } else { + await sendActionResponse(requestEv, { e: err, s: err.status }); + } + } else if (err instanceof Error) { + console.error('JSON request error:', err); + const message = isDev + ? `${err.message}\n(this is only visible in dev mode)` + : 'Internal Server Error'; + const se = new ServerError(500, message); + if (isLoader) { + await sendJsonResponse(requestEv, { e: se }); + } else { + await sendActionResponse(requestEv, { e: se, s: 500 }); + } + } else { + throw err; // AbortMessage etc. + } + } + }; +} + +function resolveValidFullPath(requestEv: RequestEventInternal, pagePath: string | null) { + if (!pagePath || !pagePath.startsWith('/') || pagePath.startsWith('//')) { + return undefined; + } + try { + // Protect against malicious X-Qwik-fullpath values that could cause SSRF or cache poisoning. Only allow if it's a relative path below the loader pathname. + const pageUrl = new URL(pagePath, requestEv.url.origin); + if (pageUrl.origin !== requestEv.url.origin) { + return undefined; + } + const pagePathname = pageUrl.pathname; + const loaderPathname = requestEv.url.pathname; + if (pagePathname === loaderPathname) { + return undefined; + } + const loaderPrefix = ensureSlash(loaderPathname); + return pagePathname.startsWith(loaderPrefix) ? pagePathname : undefined; + } catch { + return undefined; + } +} diff --git a/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.unit.ts b/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.unit.ts new file mode 100644 index 00000000000..eead92fff8b --- /dev/null +++ b/packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.unit.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from 'vitest'; +import { FULLPATH_HEADER } from '../../../runtime/src/route-loaders'; +import { IsQLoader } from '../request-path'; +import { jsonRequestWrapper } from './json-request-wrapper'; + +describe('jsonRequestWrapper', () => { + it('rewrites loader requests only when X-Qwik-fullpath is below the loader path', async () => { + const requestEv = createLoaderRequestEvent('/products/123/', '/products/123/view/'); + + await jsonRequestWrapper()(requestEv as any); + + expect(requestEv.url.pathname).toBe('/products/123/view/'); + expect(requestEv.next).toHaveBeenCalledOnce(); + }); + + it('ignores X-Qwik-fullpath when it does not have the loader path as a prefix', async () => { + const requestEv = createLoaderRequestEvent('/products/123/', '/admin/'); + + await jsonRequestWrapper()(requestEv as any); + + expect(requestEv.url.pathname).toBe('/products/123/'); + expect(requestEv.next).toHaveBeenCalledOnce(); + }); + + it('adds Vary for X-Qwik-fullpath on loader requests', async () => { + const requestEv = createLoaderRequestEvent('/products/123/', '/products/123/view/'); + requestEv.headers.set('Vary', 'Accept-Encoding'); + + await jsonRequestWrapper()(requestEv as any); + + expect(requestEv.headers.get('Vary')).toBe(`Accept-Encoding, ${FULLPATH_HEADER}`); + }); +}); + +function createLoaderRequestEvent(loaderPathname: string, fullPathname: string) { + return { + sharedMap: new Map([[IsQLoader, true]]), + request: new Request(`http://localhost${loaderPathname}`, { + headers: { + [FULLPATH_HEADER]: fullPathname, + }, + }), + url: new URL(`http://localhost${loaderPathname}`), + headers: new Headers(), + headersSent: false, + next: vi.fn(async () => {}), + }; +} diff --git a/packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.ts b/packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.ts new file mode 100644 index 00000000000..85b857873ba --- /dev/null +++ b/packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.ts @@ -0,0 +1,139 @@ +import { _serialize } from '@qwik.dev/core/internal'; +import { + FULLPATH_HEADER, + getRouteLoaderResponse, + resolveRouteLoaderByHash, +} from '../../../runtime/src/route-loaders'; +import type { LoaderInternal, RequestEvent, RequestHandler } from '../../../runtime/src/types'; +import { type RequestEventInternal } from '../request-event-core'; +import { IsQLoader, QLoaderId } from '../request-path'; + +/** + * Handler that executes the requested loader and returns the result as JSON. Runs AFTER + * plugin/route middleware, so middleware redirects/errors are handled by `jsonRequestWrapper`. The + * loader function's own redirects/errors are caught by getRouteLoaderResponse and serialized in the + * LoaderResponse envelope ({ d, r, e }). + */ +export function loaderHandler(routeLoaders: LoaderInternal[]): RequestHandler { + return async (requestEvent: RequestEvent) => { + const requestEv = requestEvent as RequestEventInternal; + + if (!requestEv.sharedMap.has(IsQLoader)) { + return; + } + + if (requestEv.headersSent || requestEv.exited) { + return; + } + + const loaderId = requestEv.sharedMap.get(QLoaderId) as string; + const loader = resolveRouteLoaderByHash(routeLoaders, loaderId); + + if (!loader) { + requestEv.json(404, { error: 'Loader not found' }); + return; + } + + // ETag support: for string/function eTags, check If-None-Match BEFORE running the loader + if (loader.__eTag && loader.__eTag !== true) { + const eTag = resolvePreETag(loader.__eTag, requestEv); + if (eTag && checkETagMatch(requestEv, eTag)) { + return; + } + } + + const responseData = await getRouteLoaderResponse(loader.__qrl, loader.__validators, requestEv); + const data = await _serialize(responseData); + + // For eTag: true, compute eTag from serialized data AFTER running the loader + if (loader.__eTag === true && responseData.d !== undefined) { + const eTag = `"${fnv1aHash(data)}"`; + if (checkETagMatch(requestEv, eTag)) { + return; + } + } + + await sendLoaderResponse(requestEv, data, loader); + }; +} + +/** Resolve eTag from a static string or function (before running the loader). */ +function resolvePreETag( + eTagOption: string | ((ev: RequestEvent) => string | null), + requestEv: RequestEvent +): string | null { + if (typeof eTagOption === 'string') { + return `"${eTagOption}"`; + } + const result = eTagOption(requestEv); + return result ? `"${result}"` : null; +} + +/** Set the ETag header and check If-None-Match. Returns true if 304 was sent. */ +function checkETagMatch(requestEv: RequestEventInternal, eTag: string): boolean { + requestEv.headers.set('ETag', eTag); + const ifNoneMatch = requestEv.request.headers.get('If-None-Match'); + if ( + ifNoneMatch && + (ifNoneMatch === eTag || ifNoneMatch === `W/${eTag}` || `W/${ifNoneMatch}` === eTag) + ) { + requestEv.send(304 as any, '' as any); + return true; + } + return false; +} + +/** FNV-1a hash for generating eTags from serialized data. */ +function fnv1aHash(str: string): string { + let hash = 0x811c9dc5; // FNV offset basis + for (let i = 0; i < str.length; i++) { + hash ^= str.charCodeAt(i); + hash = (hash * 0x01000193) | 0; // FNV prime, keep 32-bit + } + return (hash >>> 0).toString(36); +} + +async function sendLoaderResponse( + requestEv: RequestEventInternal, + data: string, + loader?: LoaderInternal +) { + requestEv.headers.set('Content-Type', 'application/json; charset=utf-8'); + addVaryHeader(requestEv, FULLPATH_HEADER); + if (loader?.__expires && loader.__expires > 0) { + requestEv.cacheControl({ maxAge: Math.ceil(loader.__expires / 1000) }); + } + requestEv.send(200, data); +} + +export function addVaryHeader(requestEv: RequestEventInternal, value: string) { + const vary = requestEv.headers.get('Vary'); + if (!vary) { + requestEv.headers.set('Vary', value); + return; + } + const existing = vary.split(',').map((item) => item.trim().toLowerCase()); + if (!existing.includes(value.toLowerCase())) { + requestEv.headers.set('Vary', `${vary}, ${value}`); + } +} + +/** Serialize and send a JSON response (used by error/redirect paths in jsonRequestWrapper). */ +export async function sendJsonResponse( + requestEv: RequestEventInternal, + responseData: Record, + status: number = 200 +) { + const data = await _serialize(responseData); + requestEv.headers.set('Content-Type', 'application/json; charset=utf-8'); + requestEv.send(status, data); +} + +export async function sendActionResponse( + requestEv: RequestEventInternal, + responseData: Record +) { + const data = await _serialize(responseData); + requestEv.headers.set('Content-Type', 'application/json; charset=utf-8'); + requestEv.send((responseData.s as number) || 200, data); +} diff --git a/packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.unit.ts b/packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.unit.ts new file mode 100644 index 00000000000..c1bdcfb2275 --- /dev/null +++ b/packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.unit.ts @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from 'vitest'; +import { FULLPATH_HEADER } from '../../../runtime/src/route-loaders'; +import { getLoaderName, IsQLoader, QLoaderId } from '../request-path'; +import { loaderHandler } from './loader-handler'; + +describe('loaderHandler', () => { + it('uses millisecond expires values to derive Cache-Control seconds and varies on full path', async () => { + const requestEv = { + sharedMap: new Map([ + [IsQLoader, true], + [QLoaderId, 'loader-id'], + ]), + headers: new Headers(), + request: new Request(`http://localhost/products/${getLoaderName('loader-id', 'manifest')}`), + headersSent: false, + exited: false, + cacheControl: vi.fn(), + send: vi.fn(), + status: vi.fn(() => 200), + }; + const loader = { + __id: 'loader-id', + __qrl: { + call: vi.fn(async () => 'loader-value'), + }, + __validators: undefined, + __expires: 1500, + __eTag: undefined, + }; + + await loaderHandler([loader as any])(requestEv as any); + + expect(requestEv.cacheControl).toHaveBeenCalledWith({ maxAge: 2 }); + expect(requestEv.headers.get('Vary')).toBe(FULLPATH_HEADER); + expect(requestEv.send).toHaveBeenCalledWith(200, expect.any(String)); + }); +}); diff --git a/packages/qwik-router/src/middleware/request-handler/index.ts b/packages/qwik-router/src/middleware/request-handler/index.ts index 04b52362489..4ff6fc45160 100644 --- a/packages/qwik-router/src/middleware/request-handler/index.ts +++ b/packages/qwik-router/src/middleware/request-handler/index.ts @@ -1,4 +1,5 @@ -export { requestHandler, _asyncRequestStore } from './request-handler'; +export { _asyncRequestStore } from './async-request-store'; +export { requestHandler } from './request-handler'; export { getErrorHtml } from './error-handler'; export { getNotFound } from './not-found-paths'; @@ -10,7 +11,6 @@ export { ServerError } from './server-error'; export { AbortMessage, RedirectMessage } from './redirect-handler'; export { RewriteMessage } from './rewrite-handler'; -export { RequestEvShareQData } from './request-event-core'; export { clearSsrCache } from './etag'; export { _TextEncoderStream_polyfill } from './polyfill'; diff --git a/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md b/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md index 3dc42b8792c..76ad3b4632e 100644 --- a/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md +++ b/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md @@ -16,7 +16,6 @@ import type { RenderOptions } from '@qwik.dev/core/server'; import { RequestEvent as RequestEvent_2 } from '@qwik.dev/router/middleware/request-handler'; import type { RequestHandler as RequestHandler_2 } from '@qwik.dev/router/middleware/request-handler'; import type { ResolveSyncValue as ResolveSyncValue_2 } from '@qwik.dev/router/middleware/request-handler'; -import { SerializationStrategy } from '@qwik.dev/core/internal'; import type { ValueOrPromise } from '@qwik.dev/core'; // @public (undocumented) @@ -168,11 +167,6 @@ export interface RequestEventLoader extends Reque resolveValue: ResolveValue; } -// Warning: (ae-internal-missing-underscore) The name "RequestEvShareQData" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export const RequestEvShareQData = "qData"; - // @public (undocumented) export type RequestHandler = (ev: RequestEvent) => Promise | void; diff --git a/packages/qwik-router/src/middleware/request-handler/request-event-core.ts b/packages/qwik-router/src/middleware/request-handler/request-event-core.ts index 053e7a2d983..e20063b4e93 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-event-core.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-event-core.ts @@ -1,10 +1,4 @@ -import type { ValueOrPromise } from '@qwik.dev/core'; -import { - _deserialize, - _UNINITIALIZED, - isDev, - type SerializationStrategy, -} from '@qwik.dev/core/internal'; +import { _deserialize, isDev } from '@qwik.dev/core/internal'; import type { ActionInternal, FailReturn, @@ -12,6 +6,7 @@ import type { LoadedRoute, LoaderInternal, } from '../../runtime/src/types'; +import { getRouteLoaderValues, loadRouteLoader } from '../../runtime/src/route-loaders'; import type { AbortMessage, RedirectMessage } from './redirect-handler'; import type { RewriteMessage } from './rewrite-handler'; import type { ServerError } from './server-error'; @@ -37,19 +32,19 @@ interface RequestEventDeps { RedirectMessage: new () => RedirectMessage; RewriteMessage: new (pathname: string) => RewriteMessage; ServerError: new (status: number, data: T) => ServerError; - getRouteLoaderPromise: typeof import('./request-loader').getRouteLoaderPromise; - getRouteMatchPathname: typeof import('./request-path').getRouteMatchPathname; - IsQData: string; + recognizeRequest: typeof import('./request-path').recognizeRequest; + trimRecognizedInternalPathname: typeof import('./request-path').trimRecognizedInternalPathname; + IsQLoader: string; + IsQAction: string; + QLoaderId: string; + QActionId: string; + QACTION_KEY: string; encoder: TextEncoder; getContentType: typeof import('./request-utils').getContentType; } -const RequestEvLoaders = Symbol('RequestEvLoaders'); const RequestEvMode = Symbol('RequestEvMode'); const RequestEvRoute = Symbol('RequestEvRoute'); -export const RequestEvLoaderSerializationStrategyMap = Symbol( - 'RequestEvLoaderSerializationStrategyMap' -); export const RequestRouteName = '@routeName'; export const RequestEvSharedActionId = '@actionId'; export const RequestEvSharedActionFormData = '@actionFormData'; @@ -58,8 +53,6 @@ export const RequestEvIsRewrite = '@rewrite'; export const RequestEvShareServerTiming = '@serverTiming'; export const RequestEvETagCacheKey = '@eTagCacheKey'; export const RequestEvHttpStatusMessage = '@httpStatusMessage'; -/** @internal */ -export const RequestEvShareQData = 'qData'; export function createRequestEventWithDeps( deps: RequestEventDeps, @@ -75,12 +68,21 @@ export function createRequestEventWithDeps( const cookie = new deps.Cookie(request.headers.get('cookie')); const headers = new Headers(); const url = new URL(request.url); - const { pathname, isInternal } = deps.getRouteMatchPathname(url.pathname); - if (isInternal) { - // For the middleware callbacks we pretend it's a regular request - url.pathname = pathname; - // But we set this flag so that they can act differently - sharedMap.set(deps.IsQData, true); + // Recognize internal request types (q-loader-*.json) + const recognized = deps.recognizeRequest(url.pathname); + if (recognized) { + url.pathname = deps.trimRecognizedInternalPathname(url.pathname, recognized); + sharedMap.set(recognized.type, true); + if (recognized.data?.loaderId) { + sharedMap.set(deps.QLoaderId, recognized.data.loaderId); + } + } + + // Detect action requests via ?qaction= query parameter + const actionId = url.searchParams.get(deps.QACTION_KEY); + if (actionId) { + sharedMap.set(deps.IsQAction, true); + sharedMap.set(deps.QActionId, actionId); } let routeModuleIndex = -1; @@ -163,10 +165,7 @@ export function createRequestEventWithDeps( return message; }; - const loaders: Record | undefined> = {}; const requestEv: RequestEventInternal = { - [RequestEvLoaders]: loaders, - [RequestEvLoaderSerializationStrategyMap]: new Map(), [RequestEvMode]: serverRequestEv.mode, get [RequestEvRoute]() { return loadedRoute; @@ -213,25 +212,23 @@ export function createRequestEventWithDeps( }, resolveValue: (async (loaderOrAction: LoaderInternal | ActionInternal) => { - // create user request event, which is a narrowed down request context - const id = loaderOrAction.__id; if (loaderOrAction.__brand === 'server_loader') { - if (!(id in loaders)) { - throw new Error( - 'You can not get the returned data of a loader that has not been executed for this request.' - ); - } - if (loaders[id] === _UNINITIALIZED) { - await deps.getRouteLoaderPromise( - loaderOrAction, - loaders, - requestEv[RequestEvLoaderSerializationStrategyMap], - requestEv - ); + // Check if the loader was already run by the middleware + const loaderValues = getRouteLoaderValues(requestEv); + if (loaderOrAction.__id in loaderValues) { + return loaderValues[loaderOrAction.__id]; } + return loadRouteLoader(loaderOrAction, requestEv); } - return loaders[id]; + // Actions are transient (one-shot per request). After action submission, + // the client invalidates loaders and refetches them as standalone GETs + // with no action context, so any loader that read action state would + // produce different data on the inline-render path vs the JSON refetch + // path. To keep loader output a pure function of the URL, we always + // return undefined for actions here. Read action state from the action + // signal at render time (head, components) instead of inside a loader. + return undefined; }) as ResolveValue, status: (statusCode?: number) => { @@ -361,8 +358,6 @@ export function createRequestEventWithDeps( } export interface RequestEventInternal extends Readonly, Readonly { - readonly [RequestEvLoaders]: Record | undefined>; - readonly [RequestEvLoaderSerializationStrategyMap]: Map; readonly [RequestEvMode]: ServerRequestMode; readonly [RequestEvRoute]: LoadedRoute; @@ -387,14 +382,6 @@ export interface RequestEventInternal extends Readonly, Readonly( const { render, checkOrigin } = opts; const config = await getConfig(); - const { pathname, isInternal } = getRouteMatchPathname(serverRequestEv.url.pathname); + const pathname = trimInternalPathname(serverRequestEv.url.pathname); // Ignore requests for .well-known so static servers or other middleware can handle them if (pathname === '/.well-known' || pathname.startsWith('/.well-known/')) { return null; @@ -40,8 +39,7 @@ export async function requestHandler( pathname, serverRequestEv.request.method, checkOrigin ?? true, - render, - isInternal + render ); // When fallthrough is enabled and no route matched, let the adapter handle it @@ -50,15 +48,13 @@ export async function requestHandler( } const rebuildRouteInfo: RebuildRouteInfoInternal = async (url: URL) => { - // once internal, always internal, don't override - const { pathname } = getRouteMatchPathname(url.pathname); + const cleanPathname = trimInternalPathname(url.pathname); return loadRequestHandlers( config, - pathname, + cleanPathname, serverRequestEv.request.method, checkOrigin ?? true, - render, - isInternal + render ); }; @@ -76,18 +72,16 @@ async function loadRequestHandlers( pathname: string, method: string, checkOrigin: boolean | 'lax-proto', - renderFn: Render, - isInternal: boolean + renderFn: Render ) { const { routes, serverPlugins, cacheModules } = qwikRouterConfig; - const loadedRoute = await loadRoute(routes, cacheModules, pathname, isInternal); + const loadedRoute = await loadRoute(routes, cacheModules, pathname); const requestHandlers = resolveRequestHandlers( serverPlugins, loadedRoute, method, checkOrigin, - renderQwikMiddleware(renderFn), - isInternal + renderQwikMiddleware(renderFn) ); return { loadedRoute, requestHandlers }; } diff --git a/packages/qwik-router/src/middleware/request-handler/request-loader.ts b/packages/qwik-router/src/middleware/request-handler/request-loader.ts index d1b711f530b..311d5592b85 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-loader.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-loader.ts @@ -1,7 +1,7 @@ -import type { QRL } from '@qwik.dev/core'; -import { _verifySerializable, isDev, type SerializationStrategy } from '@qwik.dev/core/internal'; -import type { DataValidator, LoaderInternal, ValidatorReturn } from '../../runtime/src/types'; -import type { RequestEvent, RequestEventBase, RequestEventLoader } from './types'; +import type { SerializationStrategy } from '@qwik.dev/core/internal'; +import { getRouteLoaderData } from '../../runtime/src/route-loaders'; +import type { LoaderInternal } from '../../runtime/src/types'; +import type { RequestEvent, RequestEventLoader } from './types'; export async function getRouteLoaderPromise< TRequestEvent extends RequestEvent & RequestEventLoader, @@ -12,96 +12,12 @@ export async function getRouteLoaderPromise< requestEv: TRequestEvent ) { const loaderId = loader.__id; - loaders[loaderId] = runValidators( - requestEv, - loader.__validators, - undefined // data - ) - .then((res) => { - if (res.success) { - if (isDev) { - return measure>(requestEv, loader.__qrl.getHash(), () => - loader.__qrl.call(requestEv, requestEv) - ); - } else { - return loader.__qrl.call(requestEv, requestEv); - } - } else { - return requestEv.fail(res.status ?? 500, res.error); - } - }) - .then((resolvedLoader) => { - if (typeof resolvedLoader === 'function') { - loaders[loaderId] = resolvedLoader(); - } else { - if (isDev) { - verifySerializable(resolvedLoader, loader.__qrl); - } - loaders[loaderId] = resolvedLoader; - } + loaders[loaderId] = getRouteLoaderData(loader.__qrl, loader.__validators, requestEv).then( + (resolvedLoader) => { + loaders[loaderId] = resolvedLoader; return resolvedLoader; - }); + } + ); loadersSerializationStrategy.set(loaderId, loader.__serializationStrategy); return loaders[loaderId]; } - -async function runValidators( - requestEv: RequestEvent, - validators: DataValidator[] | undefined, - data: unknown -) { - let lastResult: ValidatorReturn = { - success: true, - data, - }; - if (validators) { - for (const validator of validators) { - if (isDev) { - lastResult = await measure(requestEv, `validator$`, () => - validator.validate(requestEv, data) - ); - } else { - lastResult = await validator.validate(requestEv, data); - } - if (!lastResult.success) { - return lastResult; - } else { - data = lastResult.data; - } - } - } - return lastResult; -} - -function verifySerializable(data: any, qrl: QRL) { - try { - _verifySerializable(data, undefined); - } catch (e: any) { - if (e instanceof Error && qrl.dev) { - (e as any).loc = qrl.dev; - } - throw e; - } -} - -function now() { - return typeof performance !== 'undefined' ? performance.now() : 0; -} - -async function measure( - requestEv: RequestEventBase, - name: string, - fn: () => T -): Promise> { - const start = now(); - try { - return await fn(); - } finally { - const duration = now() - start; - let measurements = requestEv.sharedMap.get('@serverTiming'); - if (!measurements) { - requestEv.sharedMap.set('@serverTiming', (measurements = [])); - } - measurements.push([name, duration]); - } -} diff --git a/packages/qwik-router/src/middleware/request-handler/request-path.ts b/packages/qwik-router/src/middleware/request-handler/request-path.ts index b29342e2195..0d8ce47722f 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-path.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-path.ts @@ -1,19 +1,65 @@ -export const IsQData = '@isQData'; -export const QDATA_JSON = '/q-data.json'; +import { ensureSlash } from '../../utils/pathname'; + +// New per-loader/per-action URL patterns +export const IsQLoader = '@isQLoader'; +export const IsQAction = '@isQAction'; +export const QLoaderId = '@loaderId'; +export const QActionId = '@actionId'; + +/** Matches `/q-loader-{loaderId}.{manifestHash}.json` */ +export const LOADER_REGEX = /\/(q-loader-([^.]+)\.([^.]+)\.json)$/; + +export const getLoaderName = (loaderId: string, manifestHash: string) => + `q-loader-${loaderId}.${manifestHash}.json`; + +export type RecognizedRequest = { + type: typeof IsQLoader; + trimLength: number; + data: { loaderId?: string; manifestHash?: string } | null; +}; /** - * The pathname used to match in the route regex array. A pathname ending with /q-data.json should - * be treated as a pathname without it. + * Recognize internal request types from the URL pathname. + * + * Returns the request type, how many characters to trim from the pathname, and any extracted data + * (e.g. loaderId for loader requests). */ -export function getRouteMatchPathname(pathname: string) { - const isInternal = pathname.endsWith(QDATA_JSON); - if (isInternal) { - const trimEnd = - pathname.length - QDATA_JSON.length + (globalThis.__NO_TRAILING_SLASH__ ? 0 : 1); - pathname = pathname.slice(0, trimEnd); - if (pathname === '') { - pathname = '/'; - } +export function recognizeRequest(pathname: string): RecognizedRequest | null { + // Quick length check for common cases + if (pathname.length < 10) { + return null; + } + + // Check for per-loader pattern: /q-loader-{loaderId}.{manifestHash}.json + const loaderMatch = pathname.match(LOADER_REGEX); + if (loaderMatch) { + return { + type: IsQLoader, + trimLength: loaderMatch[1].length + 1, // +1 for the leading / + data: { loaderId: loaderMatch[2], manifestHash: loaderMatch[3] }, + }; + } + + return null; +} + +/** Trim a recognized internal URL suffix from a pathname, returning the clean route pathname. */ +export function trimRecognizedInternalPathname( + pathname: string, + recognized: RecognizedRequest +): string { + let trimmed = pathname.slice(0, pathname.length - recognized.trimLength) || '/'; + if (!globalThis.__NO_TRAILING_SLASH__ && !trimmed.endsWith('/')) { + trimmed = ensureSlash(trimmed); + } + return trimmed; +} + +/** Trim any recognized internal URL suffix from a pathname, returning the clean route pathname. */ +export function trimInternalPathname(pathname: string): string { + const recognized = recognizeRequest(pathname); + if (recognized) { + return trimRecognizedInternalPathname(pathname, recognized); } - return { pathname, isInternal }; + return pathname; } diff --git a/packages/qwik-router/src/middleware/request-handler/request-path.unit.ts b/packages/qwik-router/src/middleware/request-handler/request-path.unit.ts new file mode 100644 index 00000000000..5fc0735e41c --- /dev/null +++ b/packages/qwik-router/src/middleware/request-handler/request-path.unit.ts @@ -0,0 +1,37 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { + getLoaderName, + recognizeRequest, + trimInternalPathname, + trimRecognizedInternalPathname, +} from './request-path'; + +describe('request path helpers', () => { + afterEach(() => { + globalThis.__NO_TRAILING_SLASH__ = false; + }); + + it('builds loader file names', () => { + expect(getLoaderName('loader-id', 'manifest')).toBe('q-loader-loader-id.manifest.json'); + }); + + it('trims internal loader pathnames and preserves trailing slash mode', () => { + globalThis.__NO_TRAILING_SLASH__ = false; + const loaderPathname = `/products/${getLoaderName('loader-id', 'manifest')}`; + + expect(trimInternalPathname(loaderPathname)).toBe('/products/'); + + globalThis.__NO_TRAILING_SLASH__ = true; + + expect(trimInternalPathname(loaderPathname)).toBe('/products'); + }); + + it('can trim an already recognized internal request', () => { + globalThis.__NO_TRAILING_SLASH__ = false; + const loaderPathname = `/${getLoaderName('loader-id', 'manifest')}`; + const recognized = recognizeRequest(loaderPathname); + + expect(recognized).not.toBeNull(); + expect(trimRecognizedInternalPathname(loaderPathname, recognized!)).toBe('/'); + }); +}); diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers-core.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers-core.ts index b26bc408726..e3f1783a56c 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers-core.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers-core.ts @@ -1,9 +1,8 @@ -import { inlinedQrl, type QRL } from '@qwik.dev/core'; -import { _serialize, _UNINITIALIZED, _verifySerializable, isDev } from '@qwik.dev/core/internal'; +import { inlinedQrl, isDev, type QRL } from '@qwik.dev/core'; +import { _serialize, _verifySerializable } from '@qwik.dev/core/internal'; import type { Render, RenderToStringResult } from '@qwik.dev/core/server'; import type { ActionInternal, - ClientPageData, ContentModule, DataValidator, DocumentHeadProps, @@ -15,19 +14,24 @@ import type { RouteModule, ValidatorReturn, } from '../../runtime/src/types'; +import { + getRouteLoaderCtx, + getRouteLoaderValues, + loadRouteLoader, + setRouteLoaders, +} from '../../runtime/src/route-loaders'; +import { ensureSlash } from '../../utils/pathname'; import type { RequestEventInternal } from './request-event-core'; +import { loaderHandler } from './handlers/loader-handler'; +import { jsonRequestWrapper } from './handlers/json-request-wrapper'; +import { actionHandler } from './handlers/action-handler'; import type { ErrorCodes, RequestEvent, RequestEventBase, RequestHandler } from './types'; interface ResolveRequestHandlersDeps { QACTION_KEY: string; QFN_KEY: string; - QLOADER_KEY: string; - QDATA_JSON: string; - IsQData: string; RequestEvETagCacheKey: string; RequestEvHttpStatusMessage: string; - RequestEvIsRewrite: string; - RequestEvShareQData: string; RequestEvShareServerTiming: string; RequestEvSharedActionId: string; RequestRouteName: string; @@ -41,10 +45,7 @@ interface ResolveRequestHandlersDeps { isContentType: typeof import('./request-utils').isContentType; getCachedHtml: typeof import('./etag').getCachedHtml; getQwikRouterServerData: typeof import('./response-page').getQwikRouterServerData; - getRequestLoaderSerializationStrategyMap: typeof import('./request-event-core').getRequestLoaderSerializationStrategyMap; - getRequestLoaders: typeof import('./request-event-core').getRequestLoaders; getRequestMode: typeof import('./request-event-core').getRequestMode; - getRouteLoaderPromise: typeof import('./request-loader').getRouteLoaderPromise; loadHttpError: () => Promise; MAX_CACHE_SIZE: number; resolveCacheKey: typeof import('./etag').resolveCacheKey; @@ -59,8 +60,7 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { route: LoadedRoute, method: string, checkOrigin: boolean | 'lax-proto', - renderHandler: RequestHandler, - isInternal: boolean + renderHandler: RequestHandler ) => { const routeLoaders: LoaderInternal[] = []; const routeActions: ActionInternal[] = []; @@ -69,11 +69,12 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { const isPageRoute = !!isLastModulePageRoute(route.$mods$); - if (isInternal) { - requestHandlers.push(handleQDataRedirect); - } - if (isPageRoute) { + /** + * JSON request wrapper must be before all middleware so it can rewrite the URL and catch + * redirects/errors from plugin/route middleware via try/catch on next() + */ + requestHandlers.push(jsonRequestWrapper()); requestHandlers.push(serverErrorMiddleware(route, renderHandler)); } @@ -108,7 +109,12 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { requestHandlers.unshift(csrfCheckMiddleware); } } + if (isPageRoute) { + // Per-loader handler: returns JSON with metadata and exits if IsQLoader is set + requestHandlers.push(loaderHandler(routeLoaders)); + // Per-action handler: returns JSON and exits if IsQAction + Accept: json + requestHandlers.push(actionHandler(routeActions)); if (method === 'POST' || method === 'GET') { requestHandlers.push(runServerFunction); } @@ -116,10 +122,6 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { if (!route.$notFound$) { requestHandlers.push(fixTrailingSlash); } - - if (isInternal) { - requestHandlers.push(renderQData); - } } if (isPageRoute) { @@ -127,7 +129,7 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { ev.sharedMap.set(deps.RequestRouteName, routeName); }); requestHandlers.push(actionsMiddleware(routeActions)); - requestHandlers.push(loadersMiddleware(routeLoaders)); + requestHandlers.push(loadersMiddleware(routeLoaders, route)); requestHandlers.push(eTagMiddleware(route)); requestHandlers.push(renderHandler); } @@ -135,14 +137,14 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { return requestHandlers; }; - const _resolveRequestHandlers = ( + function _resolveRequestHandlers( routeLoaders: LoaderInternal[], routeActions: ActionInternal[], requestHandlers: RequestHandler[], routeModules: RouteModule[], collectActions: boolean, method: string - ) => { + ) { for (const routeModule of routeModules) { if (typeof routeModule.onRequest === 'function') { requestHandlers.push(routeModule.onRequest); @@ -200,7 +202,7 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { } } } - }; + } const checkBrand = (obj: any, brand: string) => { return obj && typeof obj === 'function' && obj.__brand === brand; @@ -214,7 +216,6 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { return; } const { method } = requestEv; - const loaders = deps.getRequestLoaders(requestEv); if (isDev && method === 'GET') { if (requestEv.query.has(deps.QACTION_KEY)) { console.warn( @@ -240,8 +241,9 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { ); } const result = await runValidators(requestEv, action.__validators, data); + let actionResult: unknown; if (!result.success) { - loaders[selectedActionId] = requestEv.fail(result.status ?? 500, result.error); + actionResult = requestEv.fail(result.status ?? 500, result.error); } else { const actionResolved = isDev ? await measure(requestEv, action.__qrl.getHash(), () => @@ -251,28 +253,40 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { if (isDev) { verifySerializable(actionResolved, action.__qrl); } - loaders[selectedActionId] = actionResolved; + actionResult = actionResolved; } + requestEv.sharedMap.set('@actionResult', actionResult); } } } }; } - function loadersMiddleware(routeLoaders: LoaderInternal[]): RequestHandler { + function loadersMiddleware(routeLoaders: LoaderInternal[], route: LoadedRoute): RequestHandler { return async (requestEvent: RequestEvent) => { const requestEv = requestEvent as RequestEventInternal; if (requestEv.headersSent) { requestEv.exit(); return; } - const loaders = deps.getRequestLoaders(requestEv); - const loadersSerializationStrategy = deps.getRequestLoaderSerializationStrategyMap(requestEv); if (routeLoaders.length > 0) { - const resolvedLoadersPromises = routeLoaders.map((loader) => - deps.getRouteLoaderPromise(loader, loaders, loadersSerializationStrategy, requestEv) + // Set up the RouteLoaderCtx with loader paths from the route + const routeLoaderCtx = getRouteLoaderCtx(requestEv); + if (route.$loaderPaths$) { + Object.assign(routeLoaderCtx.loaderPaths, route.$loaderPaths$); + } + + // Store loader internals so SSG can check __expires + setRouteLoaders(requestEv, routeLoaders); + + // Run loaders directly and store raw values. + // Errors/redirects propagate so middleware can catch them (e.g. plugin@errors). + const loaderValues = getRouteLoaderValues(requestEv); + await Promise.all( + routeLoaders.map(async (loader) => { + loaderValues[loader.__id] = await loadRouteLoader(loader, requestEv); + }) ); - await Promise.all(resolvedLoadersPromises); } }; } @@ -282,7 +296,7 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { if (requestEv.headersSent) { return; } - if (requestEv.method !== 'GET' || requestEv.sharedMap.has(deps.IsQData)) { + if (requestEv.method !== 'GET') { return; } @@ -300,17 +314,18 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { return; } - const loaders = deps.getRequestLoaders(requestEv); + const loaderValues = getRouteLoaderValues(requestEv); const getData = ((loaderOrAction: any) => { const id = loaderOrAction.__id; - if (loaderOrAction.__brand === 'server_loader' && !(id in loaders)) { - throw new Error('Loader not executed for this request.'); - } - const data = loaders[id]; - if (data instanceof Promise) { - throw new Error('Loaders returning a promise cannot be resolved for the eTag function.'); + if (loaderOrAction.__brand === 'server_loader') { + if (!(id in loaderValues)) { + throw new Error('Loader not executed for this request.'); + } + return loaderValues[id]; } - return data; + return requestEv.sharedMap.get(deps.RequestEvSharedActionId) === id + ? requestEv.sharedMap.get('@actionResult') + : undefined; }) as ResolveSyncValue; const routeLocation = { @@ -380,10 +395,6 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { throw e; } - if (requestEv.sharedMap.has(deps.IsQData)) { - throw e; - } - const accept = requestEv.request.headers.get('Accept'); if (accept && !accept.includes('text/html')) { throw e; @@ -496,18 +507,17 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { } function fixTrailingSlash(ev: RequestEvent) { - const { basePathname, originalUrl, sharedMap } = ev; + const { basePathname, originalUrl } = ev; const { pathname, search } = originalUrl; - const isQData = sharedMap.has(deps.IsQData); if (!pathname.startsWith('/') || pathname.startsWith('//')) { return; } - if (!isQData && pathname !== basePathname && !pathname.endsWith('.html')) { + if (pathname !== basePathname && !pathname.endsWith('.html')) { if (!globalThis.__NO_TRAILING_SLASH__) { if (!pathname.endsWith('/')) { - throw ev.redirect(deps.HttpStatus.MovedPermanently, pathname + '/' + search); + throw ev.redirect(deps.HttpStatus.MovedPermanently, ensureSlash(pathname) + search); } } else { if (pathname.endsWith('/')) { @@ -542,12 +552,9 @@ export function createResolveRequestHandlers(deps: ResolveRequestHandlersDeps) { function getPathname(url: URL) { url = new URL(url); - if (url.pathname.endsWith(deps.QDATA_JSON)) { - url.pathname = url.pathname.slice(0, -deps.QDATA_JSON.length); - } if (!globalThis.__NO_TRAILING_SLASH__) { if (!url.pathname.endsWith('/')) { - url.pathname += '/'; + url.pathname = ensureSlash(url.pathname); } } else { if (url.pathname.endsWith('/')) { @@ -606,9 +613,6 @@ The request origin "${inputOrigin}" does not match the server origin "${origin}" if (requestEv.headersSent) { return; } - if (requestEv.sharedMap.has(deps.IsQData)) { - return; - } const responseHeaders = requestEv.headers; if (!responseHeaders.has('Content-Type')) { @@ -640,7 +644,6 @@ The request origin "${inputOrigin}" does not match the server origin "${origin}" pipeError = error; }); const stream = writable.getWriter(); - const status = requestEv.status(); try { const isStatic = deps.getRequestMode(requestEv) === 'static'; const serverData = deps.getQwikRouterServerData(requestEv); @@ -653,16 +656,9 @@ The request origin "${inputOrigin}" does not match the server origin "${origin}" ...serverData.containerAttributes, }, }); - const qData: ClientPageData = { - loaders: deps.getRequestLoaders(requestEv), - action: requestEv.sharedMap.get(deps.RequestEvSharedActionId), - status: status !== 200 ? status : 200, - href: getPathname(requestEv.url), - }; if (typeof (result as any as RenderToStringResult).html === 'string') { await stream.write((result as any as RenderToStringResult).html); } - requestEv.sharedMap.set(deps.RequestEvShareQData, qData); } finally { await stream.ready; await stream.close(); @@ -687,97 +683,6 @@ The request origin "${inputOrigin}" does not match the server origin "${origin}" }; } - async function handleQDataRedirect(requestEv: RequestEvent) { - try { - await requestEv.next(); - } catch (err) { - if (!(err instanceof deps.RedirectMessage)) { - throw err; - } - } - if (requestEv.headersSent) { - return; - } - - const status = requestEv.status(); - const location = requestEv.headers.get('Location'); - const isRedirect = status >= 301 && status <= 308 && location; - - if (isRedirect) { - const adaptedLocation = makeQDataPath(location); - if (adaptedLocation) { - requestEv.headers.set('Location', adaptedLocation); - requestEv.getWritableStream().close(); - return; - } else { - requestEv.status(200); - requestEv.headers.delete('Location'); - } - } - } - - async function renderQData(requestEv: RequestEvent) { - await requestEv.next(); - - if (requestEv.headersSent || requestEv.exited) { - return; - } - - const status = requestEv.status(); - const redirectLocation = requestEv.headers.get('Location'); - - requestEv.headers.set('Content-Type', 'application/json; charset=utf-8'); - - let loaders = deps.getRequestLoaders(requestEv); - const selectedLoaderIds = requestEv.query.getAll(deps.QLOADER_KEY); - - const hasCustomLoaders = selectedLoaderIds.length > 0; - - if (hasCustomLoaders) { - const selectedLoaders: Record = {}; - for (const loaderId of selectedLoaderIds) { - const loader = loaders[loaderId]; - selectedLoaders[loaderId] = loader; - } - loaders = selectedLoaders; - } - - const qData: ClientPageData = hasCustomLoaders - ? { - loaders, - status: status !== 200 ? status : 200, - href: getPathname(requestEv.url), - } - : { - loaders, - action: requestEv.sharedMap.get(deps.RequestEvSharedActionId), - status: status !== 200 ? status : 200, - href: getPathname(requestEv.url), - redirect: redirectLocation ?? undefined, - isRewrite: requestEv.sharedMap.get(deps.RequestEvIsRewrite), - }; - const writer = requestEv.getWritableStream().getWriter(); - const data = await _serialize(qData); - writer.write(deps.encoder.encode(data)); - requestEv.sharedMap.set(deps.RequestEvShareQData, qData); - - writer.close(); - } - - function makeQDataPath(href: string) { - if (href.startsWith('/')) { - if (!href.includes(deps.QDATA_JSON)) { - const url = new URL(href, 'http://localhost'); - - const pathname = url.pathname.endsWith('/') ? url.pathname.slice(0, -1) : url.pathname; - return pathname + deps.QDATA_JSON + url.search; - } - return href; - } else { - return undefined; - } - } - function now() { return typeof performance !== 'undefined' ? performance.now() : 0; } diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts index 9bf081a5497..d1897e39493 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts @@ -1,21 +1,15 @@ -import { QACTION_KEY, QFN_KEY, QLOADER_KEY } from '../../runtime/src/constants'; +import { QACTION_KEY, QFN_KEY } from '../../runtime/src/constants'; import { resolveRouteConfig } from '../../runtime/src/head'; import { resolveETag, resolveCacheKey, getCachedHtml, MAX_CACHE_SIZE, setCachedHtml } from './etag'; import { HttpStatus } from './http-status-codes'; import { RequestEvETagCacheKey, RequestEvHttpStatusMessage, - RequestEvIsRewrite, - RequestEvShareQData, RequestEvShareServerTiming, RequestEvSharedActionId, RequestRouteName, - getRequestLoaderSerializationStrategyMap, - getRequestLoaders, getRequestMode, } from './request-event-core'; -import { getRouteLoaderPromise } from './request-loader'; -import { IsQData, QDATA_JSON } from './request-path'; import { getQwikRouterServerData } from './response-page'; import { createResolveRequestHandlers } from './resolve-request-handlers-core'; import { RedirectMessage } from './redirect-handler'; @@ -25,13 +19,8 @@ import { ServerError } from './server-error'; const requestHandlers = createResolveRequestHandlers({ QACTION_KEY, QFN_KEY, - QLOADER_KEY, - QDATA_JSON, - IsQData, RequestEvETagCacheKey, RequestEvHttpStatusMessage, - RequestEvIsRewrite, - RequestEvShareQData, RequestEvShareServerTiming, RequestEvSharedActionId, RequestRouteName, @@ -42,10 +31,7 @@ const requestHandlers = createResolveRequestHandlers({ isContentType, getCachedHtml, getQwikRouterServerData, - getRequestLoaderSerializationStrategyMap, - getRequestLoaders, getRequestMode, - getRouteLoaderPromise, loadHttpError: () => import('../../runtime/src/http-error'), MAX_CACHE_SIZE, resolveCacheKey, diff --git a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts index 3f861199b1a..e5c2319ca60 100644 --- a/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts +++ b/packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; import { getPathname, fixTrailingSlash, resolveRequestHandlers } from './resolve-request-handlers'; -import { RequestEvHttpStatusMessage } from './request-event-core'; +import { RequestEvHttpStatusMessage, RequestEvSharedActionId } from './request-event-core'; import { createRequestEvent } from './request-event'; import { RedirectMessage } from './redirect-handler'; import { isContentType } from './request-utils'; @@ -46,18 +46,7 @@ function createMockRequestEvent(url = 'http://localhost:3000/test', trailingSlas describe('resolve-request-handler', () => { describe('getPathname', () => { - it('should remove q-data.json', () => { - globalThis.__NO_TRAILING_SLASH__ = false; - expect(getPathname(new URL('http://server/path/q-data.json?foo=bar#hash'))).toBe( - '/path/?foo=bar#hash' - ); - globalThis.__NO_TRAILING_SLASH__ = true; - expect(getPathname(new URL('http://server/path/q-data.json?foo=bar#hash'))).toBe( - '/path?foo=bar#hash' - ); - }); - - it('should pass non q-data.json through', () => { + it('should handle pathname with trailing slash', () => { globalThis.__NO_TRAILING_SLASH__ = false; expect(getPathname(new URL('http://server/path?foo=bar#hash'))).toBe('/path/?foo=bar#hash'); globalThis.__NO_TRAILING_SLASH__ = true; @@ -304,7 +293,7 @@ describe('resolve-request-handler', () => { const renderHandler = vi.fn(async (requestEv: { exit: () => void }) => { requestEv.exit(); }); - const handlers = resolveRequestHandlers(undefined, route, 'GET', true, renderHandler, false); + const handlers = resolveRequestHandlers(undefined, route, 'GET', true, renderHandler); const requestEv = createRequestEvent( createMockServerRequestEvent(), route, @@ -320,4 +309,21 @@ describe('resolve-request-handler', () => { expect(requestEv.sharedMap.get(RequestEvHttpStatusMessage)).toBe('teapot'); }); }); + + describe('action result resolution', () => { + it('always returns undefined for actions, even when one was submitted', async () => { + // Loaders must be a pure function of the URL — see route-loader docs and the + // action-state changeset. resolveValue intentionally hides action state from + // loaders so MPA inline-render and SPA JSON refetch produce the same result. + const requestEv = createMockRequestEvent('http://localhost:3000/about/', true); + const actionA = { __brand: 'server_action', __id: 'action-a' }; + const actionB = { __brand: 'server_action', __id: 'action-b' }; + + requestEv.sharedMap.set(RequestEvSharedActionId, 'action-a'); + requestEv.sharedMap.set('@actionResult', { ok: true }); + + await expect(requestEv.resolveValue(actionA as any)).resolves.toBeUndefined(); + await expect(requestEv.resolveValue(actionB as any)).resolves.toBeUndefined(); + }); + }); }); diff --git a/packages/qwik-router/src/middleware/request-handler/response-page-core.ts b/packages/qwik-router/src/middleware/request-handler/response-page-core.ts index 66551fd5dfe..f223c6d4b09 100644 --- a/packages/qwik-router/src/middleware/request-handler/response-page-core.ts +++ b/packages/qwik-router/src/middleware/request-handler/response-page-core.ts @@ -1,4 +1,5 @@ import type { QwikRouterEnvData } from '../../runtime/src/types'; +import { getRouteLoaderCtx, getRouteLoaderValues } from '../../runtime/src/route-loaders'; import type { RequestEvent } from './types'; interface ResponsePageDeps { @@ -8,8 +9,6 @@ interface ResponsePageDeps { RequestEvSharedActionId: string; RequestEvSharedNonce: string; RequestRouteName: string; - getRequestLoaders: typeof import('./request-event-core').getRequestLoaders; - getRequestLoaderSerializationStrategyMap: typeof import('./request-event-core').getRequestLoaderSerializationStrategyMap; getRequestRoute: typeof import('./request-event-core').getRequestRoute; } @@ -19,6 +18,7 @@ export function getQwikRouterServerDataWithDeps(deps: ResponsePageDeps, requestE request.headers.forEach((value, key) => (requestHeaders[key] = value)); const action = requestEv.sharedMap.get(deps.RequestEvSharedActionId) as string; + const actionResult = requestEv.sharedMap.get('@actionResult'); const formData = requestEv.sharedMap.get(deps.RequestEvSharedActionFormData); const routeName = requestEv.sharedMap.get(deps.RequestRouteName) as string; const nonce = requestEv.sharedMap.get(deps.RequestEvSharedNonce); @@ -34,8 +34,8 @@ export function getQwikRouterServerDataWithDeps(deps: ResponsePageDeps, requestE reconstructedUrl.protocol = protocol; } - const loaders = deps.getRequestLoaders(requestEv); - const loadersSerializationStrategy = deps.getRequestLoaderSerializationStrategyMap(requestEv); + const routeLoaderCtx = getRouteLoaderCtx(requestEv); + const loaderValues = getRouteLoaderValues(requestEv); return { url: reconstructedUrl.href, @@ -50,14 +50,15 @@ export function getQwikRouterServerDataWithDeps(deps: ResponsePageDeps, requestE ev: requestEv, params: { ...params }, loadedRoute: deps.getRequestRoute(requestEv), + routeLoaderCtx, + loaderValues, response: { status: status(), statusMessage: requestEv.sharedMap.get(deps.RequestEvHttpStatusMessage) as | string | undefined, - loaders, - loadersSerializationStrategy, action, + actionResult, formData, }, } satisfies QwikRouterEnvData, diff --git a/packages/qwik-router/src/middleware/request-handler/response-page.ts b/packages/qwik-router/src/middleware/request-handler/response-page.ts index f1afe53b2e5..d946397fe1c 100644 --- a/packages/qwik-router/src/middleware/request-handler/response-page.ts +++ b/packages/qwik-router/src/middleware/request-handler/response-page.ts @@ -1,7 +1,5 @@ import { Q_ROUTE } from '../../runtime/src/constants'; import { - getRequestLoaders, - getRequestLoaderSerializationStrategyMap, getRequestRoute, RequestEvHttpStatusMessage, RequestEvSharedActionFormData, @@ -21,8 +19,6 @@ const responsePageDeps = { RequestEvSharedActionId, RequestEvSharedNonce, RequestRouteName, - getRequestLoaders, - getRequestLoaderSerializationStrategyMap, getRequestRoute, }; diff --git a/packages/qwik-router/src/middleware/request-handler/static-paths.ts b/packages/qwik-router/src/middleware/request-handler/static-paths.ts index f5ad56bc2cd..88691970e7e 100644 --- a/packages/qwik-router/src/middleware/request-handler/static-paths.ts +++ b/packages/qwik-router/src/middleware/request-handler/static-paths.ts @@ -1,3 +1,5 @@ +import { LOADER_REGEX } from './request-path'; + // Will be replaced in post-build with the paths generated by SSG // TODO calculate this at build time from the SSG configuration const staticPaths = new Set(['__QWIK_ROUTER_STATIC_PATHS_ARRAY__']); @@ -21,12 +23,12 @@ export function isStaticPath(method: string, url: URL) { if (staticPaths.has(p)) { return true; } - if (p.endsWith('/q-data.json')) { - const pWithoutQdata = p.replace(/\/q-data.json$/, ''); - if (staticPaths.has(pWithoutQdata + '/')) { - return true; - } - if (staticPaths.has(pWithoutQdata)) { + // Per-loader files: q-loader-{id}.{hash}.json + const loaderMatch = p.match(LOADER_REGEX); + if (loaderMatch) { + const pWithoutLoader = p.slice(0, p.length - loaderMatch[0].length); + // don't use ensureSlash since we know it does not have a slash and we check both cases + if (staticPaths.has(pWithoutLoader + '/') || staticPaths.has(pWithoutLoader)) { return true; } } diff --git a/packages/qwik-router/src/runtime/src/client-navigate.ts b/packages/qwik-router/src/runtime/src/client-navigate.ts index 649a1840f47..aa25f77e89c 100644 --- a/packages/qwik-router/src/runtime/src/client-navigate.ts +++ b/packages/qwik-router/src/runtime/src/client-navigate.ts @@ -1,6 +1,7 @@ import { isBrowser } from '@qwik.dev/core'; // @ts-expect-error we don't have types for the preloader yet import { p as preload } from '@qwik.dev/core/preloader'; +import { ensureSlash } from '../../utils/pathname'; import type { NavigationType, ScrollState } from './types'; import { isSamePath, toPath } from './utils'; @@ -43,7 +44,7 @@ export const newScrollState = (): ScrollState => { export const preloadRouteBundles = (path: string, probability: number = 0.8) => { if (isBrowser) { - path = path.endsWith('/') ? path : path + '/'; + path = ensureSlash(path); path = path.length > 1 && path.startsWith('/') ? path.slice(1) : path; preload(path, probability); } diff --git a/packages/qwik-router/src/runtime/src/constants.ts b/packages/qwik-router/src/runtime/src/constants.ts index 3032f6e5149..53622b7d85e 100644 --- a/packages/qwik-router/src/runtime/src/constants.ts +++ b/packages/qwik-router/src/runtime/src/constants.ts @@ -1,10 +1,7 @@ -import type { ClientPageData } from './types'; import type { SerializationStrategy } from '@qwik.dev/core/internal'; export const MODULE_CACHE = /*#__PURE__*/ new WeakMap(); -export const CLIENT_DATA_CACHE = new Map>(); - export const QACTION_KEY = 'qaction'; export const QLOADER_KEY = 'qloaders'; diff --git a/packages/qwik-router/src/runtime/src/contexts.ts b/packages/qwik-router/src/runtime/src/contexts.ts index 25894bed004..9c0182d5e13 100644 --- a/packages/qwik-router/src/runtime/src/contexts.ts +++ b/packages/qwik-router/src/runtime/src/contexts.ts @@ -1,5 +1,6 @@ import { createContextId, type Signal } from '@qwik.dev/core'; import type { AsyncSignal } from '@qwik.dev/core/internal'; +import type { RouteLoaderCtx } from './route-loaders'; import type { ContentState, ContentStateInternal, @@ -14,6 +15,8 @@ import type { export const RouteStateContext = /*#__PURE__*/ createContextId>>('qr-s'); +export const RouteLoaderCtxContext = /*#__PURE__*/ createContextId('qr-lc'); + export const ContentContext = /*#__PURE__*/ createContextId('qr-c'); export const ContentInternalContext = /*#__PURE__*/ createContextId>('qr-ic'); diff --git a/packages/qwik-router/src/runtime/src/head.ts b/packages/qwik-router/src/runtime/src/head.ts index 25fe0fc0e7a..baf029b3248 100644 --- a/packages/qwik-router/src/runtime/src/head.ts +++ b/packages/qwik-router/src/runtime/src/head.ts @@ -1,14 +1,13 @@ import { withLocale } from '@qwik.dev/core'; +import type { AsyncSignal } from '@qwik.dev/core/internal'; import type { CacheKeyFn, ContentModule, ContentModuleETag, RouteLocation, - EndpointResponse, ResolvedDocumentHead, DocumentHeadProps, DocumentHeadValue, - ClientPageData, LoaderInternal, Editable, ResolveSyncValue, @@ -16,7 +15,6 @@ import type { RouteConfig, RouteConfigValue, } from './types'; -import { isPromise } from './utils'; export interface ResolvedRouteConfig { head: ResolvedDocumentHead; @@ -135,9 +133,13 @@ export const resolveRouteConfig = ( /** * Resolve only the document head from all content modules. This is the browser-side entry point * that ignores eTag/cacheKey (server-only concerns). + * + * Signal values may throw a promise on the client when still loading — callers should wrap this in + * `retryOnPromise` to handle that. */ export const resolveHead = ( - endpoint: EndpointResponse | ClientPageData, + actionData: { action?: string; actionResult?: unknown; status: number } | undefined, + loaderState: Record> | undefined, routeLocation: RouteLocation, contentModules: ContentModule[], locale: string, @@ -145,18 +147,19 @@ export const resolveHead = ( ): ResolvedDocumentHead => { const getData = ((loaderOrAction: LoaderInternal | ActionInternal) => { const id = loaderOrAction.__id; - if (loaderOrAction.__brand === 'server_loader') { - if (!(id in endpoint.loaders)) { - throw new Error( - 'You can not get the returned data of a loader that has not been executed for this request.' - ); - } + // Reading `.value` throws a pending promise (suspense-style) when the loader + // is still loading, on both server and client. Callers wrap resolveHead in + // retryOnPromise so head resolution re-runs once the loader settles. + const signal = loaderState?.[id]; + if (signal) { + return signal.value; } - const data = endpoint.loaders[id]; - if (isPromise(data)) { - throw new Error('Loaders returning a promise can not be resolved for the head function.'); + // Check action result + if (actionData?.action === id) { + return actionData.actionResult; } - return data; + // Loader not in current route — return undefined + return undefined; }) as any as ResolveSyncValue; return resolveRouteConfig( @@ -164,7 +167,7 @@ export const resolveHead = ( routeLocation, contentModules, locale, - endpoint.status, + actionData?.status ?? 200, defaults ).head; }; diff --git a/packages/qwik-router/src/runtime/src/head.unit.ts b/packages/qwik-router/src/runtime/src/head.unit.ts index 5938bba381b..02e37da00df 100644 --- a/packages/qwik-router/src/runtime/src/head.unit.ts +++ b/packages/qwik-router/src/runtime/src/head.unit.ts @@ -12,7 +12,14 @@ const defaults = { link: [{ key: 'css', rel: 'stylesheet', href: 'default.css' }], }; const mergeHeads = (...modules: any[]) => - resolveHead(endpoint, routeLocation, modules.map((m) => ({ head: m })) as any, locale, defaults); + resolveHead( + endpoint, + undefined, + routeLocation, + modules.map((m) => ({ head: m })) as any, + locale, + defaults + ); describe('resolveHead', () => { it('should merge contentModule properties correctly', () => { diff --git a/packages/qwik-router/src/runtime/src/index.ts b/packages/qwik-router/src/runtime/src/index.ts index c2e2c4a6732..f5dc431ff70 100644 --- a/packages/qwik-router/src/runtime/src/index.ts +++ b/packages/qwik-router/src/runtime/src/index.ts @@ -76,8 +76,6 @@ export { globalActionQrl, routeAction$, routeActionQrl, - routeLoader$, - routeLoaderQrl, server$, serverQrl, valibot$, @@ -87,6 +85,7 @@ export { zod$, zodQrl, } from './server-functions'; +export { routeLoader$, routeLoaderQrl } from './route-loaders'; export { ServiceWorkerRegister } from './sw-component'; export { useContent, diff --git a/packages/qwik-router/src/runtime/src/link-component.tsx b/packages/qwik-router/src/runtime/src/link-component.tsx index 2488000f14a..023943f009e 100644 --- a/packages/qwik-router/src/runtime/src/link-component.tsx +++ b/packages/qwik-router/src/runtime/src/link-component.tsx @@ -11,15 +11,15 @@ import { type QwikIntrinsicElements, type QwikVisibleEvent, } from '@qwik.dev/core'; -import { preloadRouteBundles } from './client-navigate'; -import { loadClientData } from './use-endpoint'; -import { useLocation, useNavigate } from './use-functions'; +import { prefetchRoute } from './prefetch-route'; +import { useDocumentHead, useLocation, useNavigate } from './use-functions'; import { getClientNavPath, shouldPreload } from './utils'; /** @public */ export const Link = component$((props) => { const nav = useNavigate(); const loc = useLocation(); + const head = useDocumentHead(); const originalHref = props.href; const anchorRef = useSignal(); const { @@ -48,19 +48,6 @@ export const Link = component$((props) => { const shouldPrefetchData = !!clientNavPath && prefetchDataProp !== 'off' && shouldPrefetch && !isDepratedPrefetchDisabled; - const handleBundlePrefetch = shouldPrefetchBundle - ? $((_: any, elm: HTMLAnchorElement) => { - if ((navigator as any).connection?.saveData) { - return; - } - - if (elm && elm.href) { - const url = new URL(elm.href); - preloadRouteBundles(url.pathname); - } - }) - : null; - const handleDataPrefetch = shouldPrefetchData ? $((_: any, elm: HTMLAnchorElement) => { if ((navigator as any).connection?.saveData) { @@ -69,10 +56,7 @@ export const Link = component$((props) => { if (elm && elm.href) { const url = new URL(elm.href); - loadClientData(url, { - preloadRouteBundles: false, - isPrefetch: true, - }); + prefetchRoute(url, true, 0.8, head.manifestHash, false); } }) : null; @@ -111,7 +95,7 @@ export const Link = component$((props) => { const handlePreload = $((_: any, elm: HTMLAnchorElement) => { const url = new URL(elm.href); - preloadRouteBundles(url.pathname, 1); + prefetchRoute(url, false, 1); }); useVisibleTask$(({ track }) => { @@ -134,21 +118,24 @@ export const Link = component$((props) => { const isProdOrTest = !isDev || import.meta.env?.TEST; - if (isProdOrTest && anchorRef.value) { + if (isProdOrTest && anchorRef.value?.href && !(navigator as any).connection?.saveData) { if ( - prefetchBundleProp === 'visible' || - // deprecated prop below, remove in favor of prefetchBundle - prefetchProp === 'js' || - prefetchProp === true + handleDataPrefetch && + (prefetchDataProp === 'visible' || + // deprecated prop below, remove in favor of prefetchData + prefetchProp === true) ) { - handleBundlePrefetch?.(null, anchorRef.value); - } - if ( - prefetchDataProp === 'visible' || - // deprecated prop below, remove in favor of prefetchData - prefetchProp === true + const url = new URL(anchorRef.value.href); + prefetchRoute(url, true, 0.8, head.manifestHash, shouldPrefetchBundle); + } else if ( + shouldPrefetchBundle && + (prefetchBundleProp === 'visible' || + // deprecated prop below, remove in favor of prefetchBundle + prefetchProp === 'js' || + prefetchProp === true) ) { - handleDataPrefetch?.(null, anchorRef.value); + const url = new URL(anchorRef.value.href); + prefetchRoute(url, false, 0.8); } } }); diff --git a/packages/qwik-router/src/runtime/src/link-component.unit.tsx b/packages/qwik-router/src/runtime/src/link-component.unit.tsx index 9684136b405..273958d02b2 100644 --- a/packages/qwik-router/src/runtime/src/link-component.unit.tsx +++ b/packages/qwik-router/src/runtime/src/link-component.unit.tsx @@ -4,18 +4,13 @@ import { QwikRouterMockProvider } from '@qwik.dev/router'; import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing'; import { Link, type PrefetchStrategy } from './link-component'; -const { loadClientDataMock, preloadRouteBundlesMock, getClientNavPathMock } = vi.hoisted(() => ({ - loadClientDataMock: vi.fn(), - preloadRouteBundlesMock: vi.fn(), +const { prefetchRouteMock, getClientNavPathMock } = vi.hoisted(() => ({ + prefetchRouteMock: vi.fn(), getClientNavPathMock: vi.fn(), })); -vi.mock('./use-endpoint', () => ({ - loadClientData: loadClientDataMock, -})); - -vi.mock('./client-navigate', () => ({ - preloadRouteBundles: preloadRouteBundlesMock, +vi.mock('./prefetch-route', () => ({ + prefetchRoute: prefetchRouteMock, })); vi.mock('./utils.ts', async (importOriginal) => { @@ -65,6 +60,17 @@ const renderLink = async ( return { document, anchor: anchor! }; }; +const expectPrefetchRouteCall = ( + callIndex: number, + pathname: string, + ...expectedArgs: unknown[] +) => { + const [url, ...args] = prefetchRouteMock.mock.calls[callIndex]; + expect(url).toBeInstanceOf(URL); + expect((url as URL).pathname).toBe(pathname); + expect(args).toEqual(expectedArgs); +}; + describe.each([ { render: ssrRenderToDom }, // { render: domRender }, // @@ -72,122 +78,100 @@ describe.each([ beforeEach(() => { getClientNavPathMock.mockClear(); getClientNavPathMock.mockReturnValue('http://localhost/test'); - loadClientDataMock.mockClear(); - preloadRouteBundlesMock.mockClear(); + prefetchRouteMock.mockClear(); }); it('prefetches bundles by default and prefetches route data on intent by default', async () => { const { document, anchor } = await renderLink(render); expect(anchor?.getAttribute('href')).toBe('http://localhost/test'); - expect(preloadRouteBundlesMock).toHaveBeenCalledTimes(1); - expect(preloadRouteBundlesMock).toHaveBeenCalledWith('/test'); - expect(loadClientDataMock).not.toHaveBeenCalled(); + expect(prefetchRouteMock).toHaveBeenCalledTimes(1); + expectPrefetchRouteCall(0, '/test', false, 0.8); await trigger(document.body, anchor, 'pointerenter'); - expect(loadClientDataMock).toHaveBeenCalledTimes(1); - expect(loadClientDataMock).toHaveBeenCalledWith(expect.any(URL), { - preloadRouteBundles: false, - isPrefetch: true, - }); - expect(loadClientDataMock.mock.calls[0][0].pathname).toBe('/test'); + expect(prefetchRouteMock).toHaveBeenCalledTimes(2); + expectPrefetchRouteCall(1, '/test', true, 0.8, 'dev', false); }); it('prefetches route data on intent', async () => { const { document, anchor } = await renderLink(render, { prefetchData: 'intent' }); + prefetchRouteMock.mockClear(); await trigger(document.body, anchor, 'pointerdown'); await trigger(document.body, anchor, 'keydown', { key: 'Enter' }); - expect(loadClientDataMock).not.toHaveBeenCalled(); + expect(prefetchRouteMock).not.toHaveBeenCalled(); await trigger(document.body, anchor, 'pointerenter'); await trigger(document.body, anchor, 'focus'); - expect(loadClientDataMock).toHaveBeenCalledTimes(2); - expect(loadClientDataMock).toHaveBeenCalledWith(expect.any(URL), { - preloadRouteBundles: false, - isPrefetch: true, - }); - expect(loadClientDataMock.mock.calls[0][0].pathname).toBe('/test'); - expect(loadClientDataMock.mock.calls[1][0].pathname).toBe('/test'); + expect(prefetchRouteMock).toHaveBeenCalledTimes(2); + expectPrefetchRouteCall(0, '/test', true, 0.8, 'dev', false); + expectPrefetchRouteCall(1, '/test', true, 0.8, 'dev', false); }); it('prefetches route data on commit', async () => { const { document, anchor } = await renderLink(render, { prefetchData: 'commit' }); + prefetchRouteMock.mockClear(); await trigger(document.body, anchor, 'pointerenter'); await trigger(document.body, anchor, 'focus'); await trigger(document.body, anchor, 'keydown', { key: 'Space' }); - expect(loadClientDataMock).not.toHaveBeenCalled(); + expect(prefetchRouteMock).not.toHaveBeenCalled(); await trigger(document.body, anchor, 'pointerdown'); await trigger(document.body, anchor, 'keydown', { key: 'Enter' }); - expect(loadClientDataMock).toHaveBeenCalledTimes(2); - expect(loadClientDataMock.mock.calls[0][0].pathname).toBe('/test'); - expect(loadClientDataMock.mock.calls[1][0].pathname).toBe('/test'); + expect(prefetchRouteMock).toHaveBeenCalledTimes(2); + expectPrefetchRouteCall(0, '/test', true, 0.8, 'dev', false); + expectPrefetchRouteCall(1, '/test', true, 0.8, 'dev', false); }); it('prefetches route data when visible strategy is enabled', async () => { await renderLink(render, { prefetchBundle: 'off', prefetchData: 'visible' }); - expect(loadClientDataMock).toHaveBeenCalledTimes(1); - expect(loadClientDataMock).toHaveBeenCalledWith(expect.any(URL), { - preloadRouteBundles: false, - isPrefetch: true, - }); - expect(loadClientDataMock.mock.calls[0][0].pathname).toBe('/test'); - expect(preloadRouteBundlesMock).not.toHaveBeenCalled(); + expect(prefetchRouteMock).toHaveBeenCalledTimes(1); + expectPrefetchRouteCall(0, '/test', true, 0.8, 'dev', false); }); it('prefetches bundles when visible strategy is enabled', async () => { await renderLink(render, { prefetchBundle: 'visible', prefetchData: 'off' }); - expect(preloadRouteBundlesMock).toHaveBeenCalledTimes(1); - expect(preloadRouteBundlesMock).toHaveBeenCalledWith('/test'); - expect(loadClientDataMock).not.toHaveBeenCalled(); + expect(prefetchRouteMock).toHaveBeenCalledTimes(1); + expectPrefetchRouteCall(0, '/test', false, 0.8); }); it('prefetches bundles and route data when visible strategy is enabled for both', async () => { await renderLink(render, { prefetchBundle: 'visible', prefetchData: 'visible' }); - expect(preloadRouteBundlesMock).toHaveBeenCalledTimes(1); - expect(preloadRouteBundlesMock).toHaveBeenCalledWith('/test'); - expect(loadClientDataMock).toHaveBeenCalledTimes(1); - expect(loadClientDataMock.mock.calls[0][0].pathname).toBe('/test'); + expect(prefetchRouteMock).toHaveBeenCalledTimes(1); + expectPrefetchRouteCall(0, '/test', true, 0.8, 'dev', true); }); it('prefetches bundles when deprecated prefetch is js', async () => { await renderLink(render, { prefetch: 'js' }); - expect(preloadRouteBundlesMock).toHaveBeenCalledTimes(1); - expect(preloadRouteBundlesMock).toHaveBeenCalledWith('/test'); - expect(loadClientDataMock).not.toHaveBeenCalled(); + expect(prefetchRouteMock).toHaveBeenCalledTimes(1); + expectPrefetchRouteCall(0, '/test', false, 0.8); }); it('prefetches bundles and route data when deprecated prefetch is true', async () => { await renderLink(render, { prefetch: true }); - expect(preloadRouteBundlesMock).toHaveBeenCalledTimes(1); - expect(preloadRouteBundlesMock).toHaveBeenCalledWith('/test'); - expect(loadClientDataMock).toHaveBeenCalledTimes(1); - expect(loadClientDataMock).toHaveBeenCalledWith(expect.any(URL), { - preloadRouteBundles: false, - isPrefetch: true, - }); - expect(loadClientDataMock.mock.calls[0][0].pathname).toBe('/test'); + expect(prefetchRouteMock).toHaveBeenCalledTimes(1); + expectPrefetchRouteCall(0, '/test', true, 0.8, 'dev', true); }); it('does not prefetch route data when data prefetching is off', async () => { const { document, anchor } = await renderLink(render, { prefetchData: 'off' }); + prefetchRouteMock.mockClear(); await trigger(document.body, anchor, 'pointerenter'); await trigger(document.body, anchor, 'focus'); await trigger(document.body, anchor, 'pointerdown'); await trigger(document.body, anchor, 'keydown', { key: 'Enter' }); - expect(loadClientDataMock).not.toHaveBeenCalled(); + expect(prefetchRouteMock).not.toHaveBeenCalled(); }); it('does not prefetch route data or bundles when deprecated prefetch is false', async () => { @@ -207,8 +191,7 @@ describe.each([ await trigger(document.body, 'a', 'qvisible'); } - expect(loadClientDataMock).not.toHaveBeenCalled(); - expect(preloadRouteBundlesMock).not.toHaveBeenCalled(); + expect(prefetchRouteMock).not.toHaveBeenCalled(); }); it('preloads route bundles on click before navigation', async () => { @@ -226,11 +209,11 @@ describe.each([ await trigger(document.body, 'a', 'qvisible'); } const anchor = document.querySelector('a'); - preloadRouteBundlesMock.mockClear(); + prefetchRouteMock.mockClear(); await trigger(document.body, anchor, 'click'); - expect(preloadRouteBundlesMock).toHaveBeenCalledTimes(1); - expect(preloadRouteBundlesMock).toHaveBeenCalledWith('/test', 1); + expect(prefetchRouteMock).toHaveBeenCalledTimes(1); + expectPrefetchRouteCall(0, '/test', false, 1); }); }); diff --git a/packages/qwik-router/src/runtime/src/prefetch-route.ts b/packages/qwik-router/src/runtime/src/prefetch-route.ts new file mode 100644 index 00000000000..db3a69e3956 --- /dev/null +++ b/packages/qwik-router/src/runtime/src/prefetch-route.ts @@ -0,0 +1,77 @@ +import * as qwikRouterConfig from '@qwik-router-config'; +import { isBrowser } from '@qwik.dev/core'; +// @ts-expect-error no types for preloader yet +import { p as preload } from '@qwik.dev/core/preloader'; +import { ensureSlash } from '../../utils/pathname'; +import { fetchRouteLoaderData } from './route-loaders'; +import { loadRoute } from './routing'; +/** + * Prefetch a route's JS bundles and optionally its loader data. + * + * Resolves the route from the trie to get the routeName (for the bundle graph preloader) and + * `$loaders$` (for data prefetching). The bundle graph is keyed by route name (e.g. + * `products/[id]/`), not by actual pathname (e.g. `products/123/`). + * + * @param url - The URL pathname to prefetch + * @param prefetchData - Whether to prefetch loader data + * @param probability - Bundle preload probability (0-1, default 0.8) + * @param manifestHash - Build manifest hash for loader URLs (from `useDocumentHead().manifestHash`) + * @param prefetchBundle - Whether to prefetch route JS bundles + */ +export async function prefetchRoute( + url: URL, + prefetchData?: boolean, + probability = 0.8, + manifestHash?: string, + prefetchBundle = true +) { + if (!isBrowser) { + return; + } + + try { + const loadedRoute = await loadRoute( + (qwikRouterConfig as any).routes, + (qwikRouterConfig as any).cacheModules, + url.pathname + ); + if (!loadedRoute) { + return; + } + + if (prefetchBundle) { + // Preload JS bundles using the route NAME (not pathname) — the bundle graph + // is keyed by route name (e.g. "products/[id]/") not actual path + let routeName = loadedRoute.$routeName$; + routeName = ensureSlash(routeName); + if (routeName.length > 1 && routeName.startsWith('/')) { + routeName = routeName.slice(1); + } + preload(routeName, probability); + } + + if (!prefetchData || !manifestHash) { + return; + } + + // Prefetch loader data in parallel (fire-and-forget, consume body for caching) + if (loadedRoute.$loaders$?.length && loadedRoute.$loaderPaths$) { + const basePath = (qwikRouterConfig as any).basePathname ?? '/'; + for (const hash of loadedRoute.$loaders$) { + let loaderPath = loadedRoute.$loaderPaths$?.[hash]; + if (!loaderPath) { + continue; + } + if (basePath !== '/' && !loaderPath.startsWith(basePath)) { + loaderPath = basePath + loaderPath.slice(1); + } + fetchRouteLoaderData(hash, loaderPath, manifestHash, { + pageUrl: url, + basePath, + }).catch(() => {}); + } + } + } catch { + // Silently ignore prefetch errors + } +} diff --git a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx index ff0de4be64d..8f76d0c032a 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx +++ b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx @@ -1,4 +1,37 @@ +/** + * Qwik Router Component + * + * This file contains the main Qwik Router component, which initializes the router and provides the + * necessary context for it to work. It also contains the logic for handling navigation, including + * updating the URL, managing scroll restoration, and resolving document head changes. + * + * Note: This component is designed to work both on the server and the client. During server-side + * rendering (SSR), it initializes the router state based on the URL and route data provided by the + * server environment. On the client, it handles navigation events and updates the router state + * accordingly. + * + * SSR is _required_ for the initial load. + * + * The flow of navigation is as follows: + * + * 1. During SSR, the server environment parses the initial URL and collects the route data. + * 2. It runs the middleware hooks (`onRequest`, `onGet`, `onPost`, etc.), and responds to q-loader, + * action$ and server$ requests. + * 3. If SSR is deemed appropriate, the route data is provided via `useServerData` and this component + * uses it to initialize the router contexts, register Tasks, and render the Slot. This component + * will never render again. + * 4. Then, slotted components like `` and `` can consume the route + * data to render the page. + * 5. On the client, when a navigation event occurs, this calls `goto()`, which updates the URL in the + * same tick, loads the route data and adjusts the router context. + * 6. The changed contexts trigger the slotted components to re-render with the new route data. + * + * Since the head data can depend on route loaders and they get their data asynchronously and can + * update without navigation, the head is resolved in a separate Task that tracks the relevant + * signals. + */ import * as qwikRouterConfig from '@qwik-router-config'; +import { ensureSlash } from '../../utils/pathname'; import { $, component$, @@ -19,23 +52,24 @@ import { import { _getContextContainer, _hasStoreEffects, - _UNINITIALIZED, _waitUntilRendered, + createAsync$, forceStoreEffects, - SerializerSymbol, type AsyncSignal, type ClientContainer, - type SerializationStrategy, + type NoSerialize, type ValueOrPromise, } from '@qwik.dev/core/internal'; import { clientNavigate } from './client-navigate'; -import { CLIENT_DATA_CACHE, DEFAULT_LOADERS_SERIALIZATION_STRATEGY, Q_ROUTE } from './constants'; +import { Q_ROUTE } from './constants'; +import { prefetchRoute } from './prefetch-route'; import { ContentContext, ContentInternalContext, DocumentHeadContext, HttpStatusContext, RouteActionContext, + RouteLoaderCtxContext, RouteLocationContext, RouteNavigateContext, RoutePreventNavigateContext, @@ -52,10 +86,14 @@ import { saveScrollHistory, } from './scroll-restoration'; import spaInit from './spa-init'; +import { + ensureRouteLoaderSignals, + setLoaderSignalValue, + updateRouteLoaderPaths, +} from './route-loaders'; import type { Action, ActionInternal, - ClientPageData, ContentModule, ContentState, ContentStateInternal, @@ -66,6 +104,7 @@ import type { Loader, LoaderInternal, MutableRouteLocation, + NavigationType, PageModule, PreventNavigateCallback, ResolvedDocumentHead, @@ -75,10 +114,10 @@ import type { RouteStateInternal, ScrollState, } from './types'; -import { loadClientData } from './use-endpoint'; +import { submitAction } from './use-endpoint'; import { useQwikRouterEnv } from './use-functions'; -import { createLoaderSignal, isSameOrigin, isSamePath, toPath, toUrl } from './utils'; -import { startViewTransition } from './view-transition'; +import { isSameOrigin, isSamePath, toPath, toUrl } from './utils'; +import { startViewTransition, type ViewTransition } from './view-transition'; declare const window: ClientSPAWindow; @@ -117,9 +156,25 @@ const preventNav: { $handler$?: (event: BeforeUnloadEvent) => void; } = {}; -// Track navigations during prevent so we don't overwrite -// We need to use an object so we can write into it from qrls -const internalState = { navCount: 0 }; +// Track navigations during prevent so we don't overwrite. +// We need to use an object so we can write into it from qrls. +const internalState: { + navCount: number; + currentTransition?: ViewTransition; +} = { navCount: 0 }; + +const getScroller = () => { + let scroller = document.getElementById(QWIK_ROUTER_SCROLLER); + if (!scroller) { + scroller = document.getElementById(QWIK_CITY_SCROLLER); + if (scroller && isDev) { + console.warn( + `Please update your scroller ID to "${QWIK_ROUTER_SCROLLER}" as "${QWIK_CITY_SCROLLER}" is deprecated and will be removed in V3` + ); + } + } + return scroller ?? document.documentElement; +}; /** * @public @@ -166,43 +221,21 @@ export const useQwikRouter = (props?: QwikRouterProps) => { prevUrl: undefined, }; const routeLocation = useStore(routeLocationTarget, { deep: false }); - const navResolver: { r?: () => void } = {}; - const container = _getContextContainer(); - const getSerializationStrategy = (loaderId: string): SerializationStrategy => { - return ( - env.response.loadersSerializationStrategy.get(loaderId) || - DEFAULT_LOADERS_SERIALIZATION_STRATEGY - ); - }; - - // On server this object contains the all the loaders data - // On client after resuming this object contains only keys and _UNINITIALIZED as values - // Thanks to this we can use this object as a capture ref and not to serialize unneeded data - // While resolving the loaders we will override the _UNINITIALIZED with the actual data - const loadersObject: Record = {}; - - // This object contains the signals for the loaders - // It is used for the loaders context RouteStateContext - const loaderState: Record> = {}; - - for (const [key, value] of Object.entries(env.response.loaders)) { - loadersObject[key] = value; - loaderState[key] = createLoaderSignal( - loadersObject, - key, - url, - getSerializationStrategy(key), - container - ); - } - // Serialize it as keys and _UNINITIALIZED as values - (loadersObject as any)[SerializerSymbol] = (obj: Record) => { - const loadersSerializationObject: Record = {}; - for (const [k, v] of Object.entries(obj)) { - loadersSerializationObject[k] = getSerializationStrategy(k) === 'always' ? v : _UNINITIALIZED; + const navResolver: { r?: () => void; p?: Promise } = {}; + // deep: true so that changes to loaderPaths and page path/search properties are tracked by + // AsyncSignal QRLs. + const routeLoaderCtx = useStore(env.routeLoaderCtx); + // Create AsyncSignals whose QRL closures capture the store proxy for client-side reactivity. + // Then set .value from middleware-computed loader values (inert, non-reactive data). + const loaderState = useStore>>({}, { deep: false }); + const contentModulesForInit = env.loadedRoute.$mods$ as ContentModule[]; + const loaders = ensureRouteLoaderSignals(contentModulesForInit, loaderState, routeLoaderCtx); + for (const loader of loaders) { + const value = env.loaderValues[loader.__id]; + if (value !== undefined) { + setLoaderSignalValue(loaderState[loader.__id], value); } - return loadersSerializationObject; - }; + } // The initial state of routeInternal uses the URL provided by the server environment. // It may not be accurate to the actual URL the browser is accessing the site from. @@ -211,7 +244,6 @@ export const useQwikRouter = (props?: QwikRouterProps) => { const routeInternal = useSignal({ type: 'initial', dest: url, - scroll: true, }); const documentHead = useStore>(() => createDocumentHead(serverHead, manifestHash) @@ -223,6 +255,22 @@ export const useQwikRouter = (props?: QwikRouterProps) => { const contentInternal = useSignal(); + /** + * Non-serializable navigation context passed from the nav task to the head+commit task. Only the + * data that can't be derived from existing stores/signals. + */ + const navContext = useSignal< + NoSerialize<{ + routeName: string; + navType: NavigationType; + replaceState: boolean | undefined; + shouldForcePrevUrl: boolean; + shouldForceUrl: boolean; + shouldForceParams: boolean; + navCount: number; + }> + >(); + const httpStatus = useSignal({ status: env.response.status, message: env.loadedRoute.$notFound$ @@ -231,7 +279,7 @@ export const useQwikRouter = (props?: QwikRouterProps) => { }); const currentActionId = env.response.action; - const currentAction = currentActionId ? env.response.loaders[currentActionId] : undefined; + const currentAction = currentActionId ? env.response.actionResult : undefined; const actionState = useSignal( currentAction ? { @@ -244,7 +292,17 @@ export const useQwikRouter = (props?: QwikRouterProps) => { } : undefined ); - + const actionDataSignal = useSignal< + { action?: string; actionResult?: unknown; status: number } | undefined + >( + currentActionId + ? { + action: currentActionId, + actionResult: currentAction, + status: env.response.status, + } + : undefined + ); const registerPreventNav = $((fn$: QRL) => { if (!isBrowser) { return; @@ -284,19 +342,14 @@ export const useQwikRouter = (props?: QwikRouterProps) => { }; }); - const getScroller = $(() => { - let scroller = document.getElementById(QWIK_ROUTER_SCROLLER); - if (!scroller) { - scroller = document.getElementById(QWIK_CITY_SCROLLER); - if (scroller && isDev) { - console.warn( - `Please update your scroller ID to "${QWIK_ROUTER_SCROLLER}" as "${QWIK_CITY_SCROLLER}" is deprecated and will be removed in V3` - ); - } - } - return scroller ?? document.documentElement; - }); - + /** + * This is the `nav()` function that `useNavigation()` returns. It is also used internally for SPA + * navigations and is provided in context for use in loaders and actions. + * + * Note: when goto is called, the address bar change must happen in the same tick for Safari to + * treat it as a user-initiated navigation and allow scroll restoration to work. For this reason, + * make sure to change the address bar before awaiting anything. + */ const goto: RouteNavigate = $(async (path, opt) => { const { type = 'link', @@ -304,8 +357,6 @@ export const useQwikRouter = (props?: QwikRouterProps) => { replaceState = false, scroll = true, } = typeof opt === 'object' ? opt : { forceReload: opt }; - internalState.navCount++; - // If this is the first SPA navigation, we rewrite routeInternal's URL // as the browser location URL to prevent an erroneous origin mismatch. // The initial value of routeInternal is derived from the server env, @@ -326,6 +377,21 @@ export const useQwikRouter = (props?: QwikRouterProps) => { ? path : toUrl(path, routeLocation.url); + if ( + navResolver.p && + !forceReload && + typeof dest !== 'number' && + isSamePath(dest, lastDest) && + dest.href === lastDest.href + ) { + // Repeated redirects/loaders can request the same in-flight destination. Treat that as a + // duplicate so we don't bump navCount and cancel the navigation that would commit it. + return navResolver.p; + } + + internalState.navCount++; + internalState.currentTransition?.skipTransition(); + if ( preventNav.$cbs$ && (forceReload || @@ -368,7 +434,7 @@ export const useQwikRouter = (props?: QwikRouterProps) => { } // Always scroll on same-page popstates, #hash clicks, or links. - const scroller = await getScroller(); + const scroller = getScroller(); restoreScroll(type, dest, new URL(location.href), scroller, getScrollHistory()); @@ -391,7 +457,7 @@ export const useQwikRouter = (props?: QwikRouterProps) => { if (isBrowser && type === 'link' && !forceReload) { // WebKit on iOS may treat async pushState() calls as skippable history entries. // Commit the navigation entry while the original tap/click is still active. - const scroller = await getScroller(); + const scroller = getScroller(); window._qRouterScrollEnabled = false; clearTimeout(window._qRouterScrollDebounce); @@ -412,16 +478,21 @@ export const useQwikRouter = (props?: QwikRouterProps) => { }; if (isBrowser) { - loadClientData(dest); - loadRoute(qwikRouterConfig.routes, qwikRouterConfig.cacheModules, dest.pathname); + // Prefetch: start loading route bundles and optionally loader data + prefetchRoute(dest, true, 0.8, manifestHash); } actionState.value = undefined; routeLocation.isNavigating = true; - return new Promise((resolve) => { - navResolver.r = resolve; + navResolver.p = new Promise((resolve) => { + navResolver.r = () => { + navResolver.r = undefined; + navResolver.p = undefined; + resolve(); + }; }); + return navResolver.p; }); useContextProvider(ContentContext, content); @@ -431,27 +502,38 @@ export const useQwikRouter = (props?: QwikRouterProps) => { useContextProvider(RouteLocationContext, routeLocation); useContextProvider(RouteNavigateContext, goto); useContextProvider(RouteStateContext, loaderState); + useContextProvider(RouteLoaderCtxContext, routeLoaderCtx); useContextProvider(RouteActionContext, actionState); useContextProvider(RoutePreventNavigateContext, registerPreventNav); + /** + * This is split in 3 tasks because we need to update the head once we figured out the route, and + * before we trigger the render, and we need to subscribe only head to loader signal updates + */ useTask$( async ({ track }) => { - const container = _getContextContainer(); const navigation = track(routeInternal); const action = track(actionState); - const locale = getLocale(''); const prevUrl = routeLocation.url; const navType = action ? 'form' : navigation.type; const replaceState = navigation.replaceState; + // Capture navCount at task entry. If another goto() fires while we're awaiting + // loadRoute or loaders, navCount will have been bumped and we should bail so + // the task's next invocation takes over with the newer destination. + const navCountBefore = internalState.navCount; let trackUrl: URL; - let clientPageData: EndpointResponse | ClientPageData | undefined; + let endpointResponse: EndpointResponse | undefined; + let actionData: { action?: string; actionResult?: unknown; status: number } | undefined; + let actionLoaderHashes: string[] | undefined; + let shouldInvalidateActionLoaders = false; let loadedRoute: LoadedRoute; if (isServer) { // server trackUrl = new URL(navigation.dest, routeLocation.url); loadedRoute = env!.loadedRoute; - clientPageData = env!.response; + endpointResponse = env!.response; + actionData = endpointResponse; } else { // client trackUrl = new URL(navigation.dest, location as any as URL); @@ -462,57 +544,100 @@ export const useQwikRouter = (props?: QwikRouterProps) => { trackUrl.pathname = trackUrl.pathname.slice(0, -1); } } else if (!globalThis.__NO_TRAILING_SLASH__) { - trackUrl.pathname += '/'; + trackUrl.pathname = ensureSlash(trackUrl.pathname); } - let loadRoutePromise = loadRoute( + const loadRoutePromise = loadRoute( qwikRouterConfig.routes, qwikRouterConfig.cacheModules, trackUrl.pathname ); - const pageData = (clientPageData = await loadClientData(trackUrl, { - action, - clearCache: true, - })); - if (!pageData) { - // Reset the path to the current path - routeInternal.untrackedValue = { type: navType, dest: trackUrl }; + try { + loadedRoute = await loadRoutePromise; + } catch (e) { + console.error(`Could not load route ${trackUrl.pathname}, reloading:`, e); + window.location.href = trackUrl.href; + return; + } + // Bail if a second nav() was fired while we were loading route modules. + if (internalState.navCount !== navCountBefore) { return; } - const newHref = pageData.href; - const newURL = new URL(newHref, trackUrl); - if (!isSamePath(newURL, trackUrl)) { - // Change our path to the canonical path in the response unless rewrite. - if (!pageData.isRewrite) { - trackUrl = newURL; + + // Submit action if one was triggered + if (action) { + const result = await submitAction(action, trackUrl.pathname); + if (!result) { + // HTTP redirect happened — bail + routeInternal.untrackedValue = { type: navType, dest: trackUrl }; + return; } - loadRoutePromise = loadRoute( - qwikRouterConfig.routes, - qwikRouterConfig.cacheModules, - newURL.pathname // Load the actual required path. - ); - } + actionData = { + status: result.status, + action: action.id, + actionResult: result.result, + }; + + // Resolve the action promise and free the closure + if (action.resolve) { + action.resolve({ + status: result.status, + result: result.result, + }); + action.resolve = undefined; + } - try { - loadedRoute = await loadRoutePromise; - } catch (e) { - console.error(e); - window.location.href = newHref; - return; + actionLoaderHashes = result.loaderHashes; + shouldInvalidateActionLoaders = true; } } const { $routeName$, $params$, $mods$, $menu$, $notFound$ } = loadedRoute; const contentModules = $mods$ as ContentModule[]; + if (!isServer) { + routeLoaderCtx.goto = noSerialize(goto); + } + updateRouteLoaderPaths(routeLoaderCtx, loadedRoute.$loaderPaths$, trackUrl); + const routeLoaders = ensureRouteLoaderSignals(contentModules, loaderState, routeLoaderCtx); + if (shouldInvalidateActionLoaders) { + if (actionLoaderHashes !== undefined) { + for (const hash of actionLoaderHashes) { + loaderState[hash]?.invalidate(true); + } + } else { + for (const loader of routeLoaders) { + loaderState[loader.__id].invalidate(true); + } + } + } + if (routeLoaders.length > 0) { + // Trigger loader signals to fetch data for the new route. No await — + // we want to render ASAP. Loaders update the page when they resolve, + // and SSR awaits them on the server side. A loader that redirects + // fires goto() directly; the new nav starts while this one finishes + // committing, producing a brief flash of the current page. + for (let i = 0; i < routeLoaders.length; i++) { + const loader = routeLoaders[i]; + // trigger load + loaderState[loader.__id].untrackedLoading; + } + } + if (internalState.navCount !== navCountBefore) { + return; + } // Update httpStatus for 404/error pages if ($notFound$) { httpStatus.value = { status: 404, message: 'Not Found' }; - } else { + } else if (endpointResponse) { httpStatus.value = { - status: clientPageData?.status ?? 200, - message: (clientPageData as EndpointResponse)?.statusMessage ?? '', + status: endpointResponse.status, + message: endpointResponse.statusMessage ?? 'OK', }; + } else if (actionData) { + httpStatus.value = { status: actionData.status, message: 'OK' }; + } else { + httpStatus.value = { status: 200, message: 'OK' }; } const pageModule = contentModules[contentModules.length - 1] as PageModule; @@ -545,311 +670,219 @@ export const useQwikRouter = (props?: QwikRouterProps) => { routeLocationTarget.params = $params$; } - routeInternal.untrackedValue = { type: navType, dest: trackUrl }; - - // Needs to be done after routeLocation is updated - const resolvedHead = resolveHead( - clientPageData!, - routeLocation, - contentModules, - locale, - serverHead - ); - - // Update content + const nextRouteInternal: RouteStateInternal = { + type: navType, + dest: trackUrl, + }; + if (navigation.forceReload !== undefined) { + nextRouteInternal.forceReload = navigation.forceReload; + } + if (navigation.replaceState !== undefined) { + nextRouteInternal.replaceState = navigation.replaceState; + } + if (navigation.scroll !== undefined) { + nextRouteInternal.scroll = navigation.scroll; + } + if (navigation.historyUpdated !== undefined) { + nextRouteInternal.historyUpdated = navigation.historyUpdated; + } + routeInternal.untrackedValue = nextRouteInternal; + + // Update content. + // IMPORTANT: contentInternal must use .untrackedValue, NOT .value. Subscribers + // (RouterOutlet, head task) are fired later by contentInternal.trigger() + // inside navigate(), which runs inside the view-transition's update callback. + // Using .value here would fire subscribers before startViewTransition captures + // the old DOM, breaking view transitions (the update callback never gets invoked). content.headings = pageModule.headings; content.menu = $menu$; contentInternal.untrackedValue = noSerialize(contentModules); + actionDataSignal.value = actionData; + + // Preserve historyUpdated/scroll/etc. for the commit task. Without this, the + // commit task reads `navigation.historyUpdated` as undefined and calls + // clientNavigate a second time, pushing an extra history entry. + + // hand off to next tasks + navContext.value = noSerialize({ + routeName: $routeName$, + navType, + replaceState, + shouldForcePrevUrl, + shouldForceUrl, + shouldForceParams, + navCount: navCountBefore, + }); + }, + // We should only wait for head calculation to complete on the server + { deferUpdates: isServer } + ); - // Update document head - documentHead.links = resolvedHead.links; - documentHead.meta = resolvedHead.meta; - documentHead.styles = resolvedHead.styles; - documentHead.scripts = resolvedHead.scripts; - documentHead.title = resolvedHead.title; - documentHead.frontmatter = resolvedHead.frontmatter; - - if (isBrowser) { - let scrollState: ScrollState | undefined; - if (navType === 'popstate') { - scrollState = getScrollHistory(); - } - const scroller = await getScroller(); - - if ( - (navigation.scroll && - (!navigation.forceReload || !isSamePath(trackUrl, prevUrl)) && - (navType === 'link' || navType === 'popstate')) || - // Action might have responded with a redirect. - (navType === 'form' && !isSamePath(trackUrl, prevUrl)) - ) { - // Mark next DOM render to scroll. - (document as any).__q_scroll_restore__ = () => - restoreScroll(navType, trackUrl, prevUrl, scroller, scrollState); - } - - const loaders = clientPageData?.loaders; - if (loaders) { - for (const [key, value] of Object.entries(loaders)) { - const signal = loaderState[key]; - const awaitedValue = await value; - loadersObject[key] = awaitedValue; - if (!signal) { - loaderState[key] = createLoaderSignal( - loadersObject, - key, - trackUrl, - DEFAULT_LOADERS_SERIALIZATION_STRATEGY, - container - ); - } else { - signal.invalidate(); - } - } - } - CLIENT_DATA_CACHE.clear(); - - // See also spa-init.ts - if (!window._qRouterSPA) { - // only add event listener once - window._qRouterSPA = true; - history.scrollRestoration = 'manual'; + /** + * Calc head. This is in a separate task so that loader updates can trigger head recalculation + * without re-running the navigation logic. + * + * Note that on the server these tasks run sequentially. + */ + useTask$( + ({ track }) => { + const contentModules = track(contentInternal); + if (!contentModules) { + return; + } + const actionData = track(actionDataSignal); + + // Resolve head — this might throw a promise so keep it near the top of the function + const head = track(() => + resolveHead( + actionData, + loaderState, + routeLocation, + contentModules, + getLocale(''), + serverHead + ) + ); + documentHead.links = head.links; + documentHead.meta = head.meta; + documentHead.styles = head.styles; + documentHead.scripts = head.scripts; + documentHead.title = head.title; + documentHead.frontmatter = head.frontmatter; + }, + { deferUpdates: isServer } + ); - window.addEventListener('popstate', () => { - // Disable scroll handler eagerly to prevent overwriting history.state. - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); + /** Actual navigation */ + useTask$( + ({ track }) => { + const nav = track(navContext); + if (isServer || !nav) { + return; + } - goto(location.href, { - type: 'popstate', - }); - }); - - window.removeEventListener('popstate', window._qRouterInitPopstate!); - window._qRouterInitPopstate = undefined; - - // Browsers natively will remember scroll on ALL history entries, incl. custom pushState. - // Devs could push their own states that we can't control. - // If a user doesn't initiate scroll after, it will not have any scrollState. - // We patch these to always include scrollState. - // TODO Block this after Navigation API PR, browsers that support it have a Navigation API solution. - if (!window._qRouterHistoryPatch) { - window._qRouterHistoryPatch = true; - const pushState = history.pushState; - const replaceState = history.replaceState; - - const prepareState = (state: any) => { - if (state === null || typeof state === 'undefined') { - state = {}; - } else if (state?.constructor !== Object) { - state = { _data: state }; - - if (isDev) { - console.warn( - 'In a Qwik SPA context, `history.state` is used to store scroll state. ' + - 'Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. ' + - 'We need to be able to automatically attach the scroll state to your state object. ' + - 'A new state object has been created, your data has been moved to: `history.state._data`' - ); - } - } - - state._qRouterScroll = state._qRouterScroll || currentScrollState(scroller); - return state; - }; - - history.pushState = (state, title, url) => { - state = prepareState(state); - return pushState.call(history, state, title, url); - }; - - history.replaceState = (state, title, url) => { - state = prepareState(state); - return replaceState.call(history, state, title, url); - }; - } + if (nav.navCount !== internalState.navCount) { + return; + } - // Chromium and WebKit fire popstate+hashchange for all #anchor clicks, - // ... even if the URL is already on the #hash. - // Firefox only does it once and no more, but will still scroll. It also sets state to null. - // Any tags w/ #hash href will break SPA state in Firefox. - // We patch these events and direct them to Link pipeline during SPA. - document.addEventListener('click', (event) => { - if (event.defaultPrevented) { - return; - } + const container = _getContextContainer(); + const navigation = routeInternal.untrackedValue; + + const { navType, replaceState, routeName } = nav; + const trackUrl = routeLocation.url; + // prevUrl is only assigned when the path changes (see nav task). On the first SPA nav + // after SSR, or on same-path/hash-only navs, prevUrl is undefined — fall back to + // trackUrl so isSamePath() returns true and scroll/history logic no-ops correctly. + const prevUrl = routeLocation.prevUrl ?? trackUrl; + + const scroller = getScroller(); + // Scroll restore setup — must happen before navigation commits + let scrollState: ScrollState | undefined; + if (navType === 'popstate') { + scrollState = getScrollHistory(); + } + if ( + (navigation.scroll && + (!navigation.forceReload || !isSamePath(trackUrl, prevUrl)) && + (navType === 'link' || navType === 'popstate')) || + // Action might have responded with a redirect. + (navType === 'form' && !isSamePath(trackUrl, prevUrl)) + ) { + // Mark next DOM render to scroll. + (document as any).__q_scroll_restore__ = () => + restoreScroll(navType, trackUrl, prevUrl, scroller, scrollState); + } - const target = (event.target as HTMLElement).closest('a[href]'); - - if (target && !target.hasAttribute('preventdefault:click')) { - const href = target.getAttribute('href')!; - const prev = new URL(location.href); - const dest = new URL(href, prev); - // Patch only same-page anchors. - if (isSameOrigin(dest, prev) && isSamePath(dest, prev)) { - event.preventDefault(); - - // Simulate same-page (no hash) anchor reload. - // history.scrollRestoration = 'manual' makes these not scroll. - if (!dest.hash && !dest.href.endsWith('#')) { - if (dest.href !== prev.href) { - history.pushState(null, '', dest); - } - - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); - saveScrollHistory({ - ...currentScrollState(scroller), - x: 0, - y: 0, - }); - location.reload(); - return; - } - - goto(target.getAttribute('href')!); - } - } - }); - - document.removeEventListener('click', window._qRouterInitAnchors!); - window._qRouterInitAnchors = undefined; - - // TODO Remove block after Navigation API PR. - // Calling `history.replaceState` during `visibilitychange` in Chromium will nuke BFCache. - // Only Chromium 96 - 101 have BFCache without Navigation API. (<1% of users) - if (!(window as any).navigation) { - // Commit scrollState on refresh, cross-origin navigation, mobile view changes, etc. - document.addEventListener( - 'visibilitychange', - () => { - if ( - (window._qRouterScrollEnabled || window._qCityScrollEnabled) && - document.visibilityState === 'hidden' - ) { - if (window._qCityScrollEnabled) { - console.warn( - '"_qCityScrollEnabled" is deprecated. Use "_qRouterScrollEnabled" instead.' - ); - } - // Last & most reliable point to commit state. - // Do not clear timeout here in case debounce gets to run later. - const scrollState = currentScrollState(scroller); - saveScrollHistory(scrollState); - } - }, - { passive: true } - ); + initializeSPA(goto, scroller); - document.removeEventListener('visibilitychange', window._qRouterInitVisibility!); - window._qRouterInitVisibility = undefined; - } + if (navType !== 'popstate') { + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); - window.addEventListener( - 'scroll', - () => { - // TODO: remove "_qCityScrollEnabled" condition in v3 - if (!window._qRouterScrollEnabled && !window._qCityScrollEnabled) { - return; - } - - clearTimeout(window._qRouterScrollDebounce); - window._qRouterScrollDebounce = setTimeout(() => { - const scrollState = currentScrollState(scroller); - saveScrollHistory(scrollState); - // Needed for e2e debounceDetector. - window._qRouterScrollDebounce = undefined; - }, 200); - }, - { passive: true } - ); - - removeEventListener('scroll', window._qRouterInitScroll!); - window._qRouterInitScroll = undefined; - - // Cache SPA recovery script. - spaInit.resolve(); + if (!navigation.historyUpdated) { + // Save the final scroll state before pushing new state. + // Upgrades/replaces state with scroll pos on nav as needed. + const scrollState = currentScrollState(scroller); + saveScrollHistory(scrollState); } + } - if (navType !== 'popstate') { - window._qRouterScrollEnabled = false; - clearTimeout(window._qRouterScrollDebounce); - - if (!navigation.historyUpdated) { - // Save the final scroll state before pushing new state. - // Upgrades/replaces state with scroll pos on nav as needed. - const scrollState = currentScrollState(scroller); - saveScrollHistory(scrollState); + let didNavigate = false; + let navigatePromise: Promise | undefined; + let currentTransition: ViewTransition | undefined; + const navigate = () => { + if (navigatePromise) { + return navigatePromise; + } + if (nav.navCount !== internalState.navCount) { + return Promise.resolve(); + } + didNavigate = true; + if (navigation.historyUpdated) { + const currentPath = location.pathname + location.search + location.hash; + const nextPath = toPath(trackUrl); + if (currentPath !== nextPath) { + // The history entry was already created under the original user gesture. + // We only normalize the current entry here once async navigation resolves. + history.replaceState(history.state, '', nextPath); } + } else { + clientNavigate(window, navType, prevUrl, trackUrl, replaceState); } + contentInternal.trigger(); + return (navigatePromise = _waitUntilRendered(container!)); + }; - const navigate = () => { - if (navigation.historyUpdated) { - const currentPath = location.pathname + location.search + location.hash; - const nextPath = toPath(trackUrl); - if (currentPath !== nextPath) { - // The history entry was already created under the original user gesture. - // We only normalize the current entry here once async navigation resolves. - history.replaceState(history.state, '', nextPath); + const _waitNextPage = () => { + if (props?.viewTransition === false || !('startViewTransition' in document)) { + return navigate().then(() => undefined as ViewTransition | undefined); + } + const { ready, transition } = startViewTransition({ + update: navigate, + types: ['qwik-navigation'], + }); + currentTransition = transition; + internalState.currentTransition = transition; + return ready.then( + () => transition, + (reason) => { + if (!didNavigate && nav.navCount === internalState.navCount) { + return navigate().then(() => transition); } - } else { - clientNavigate(window, navType, prevUrl, trackUrl, replaceState); - } - contentInternal.trigger(); - return _waitUntilRendered(container!); - }; - - const _waitNextPage = () => { - if (isServer || props?.viewTransition === false) { - return navigate(); - } else { - const viewTransition = startViewTransition({ - update: navigate, - types: ['qwik-navigation'], - }); - if (!viewTransition) { - return Promise.resolve(); + if ((reason as Error)?.name === 'AbortError') { + return transition; } - return viewTransition.ready; + throw reason; } - }; - _waitNextPage() - .catch((err) => { - // If the view transition fails, navigate to the page anyway - navigate(); - if (err instanceof DOMException && err.name === 'TimeoutError') { - throw new Error( - 'View transition timed out. This can happen if you have "disableCache" in your browser devtools enabled.' - ); - } - // Re-throw the error on console - throw err; - }) - .finally(() => { - (container as ClientContainer).element.setAttribute?.(Q_ROUTE, $routeName$); - const scrollState = currentScrollState(scroller); - saveScrollHistory(scrollState); - window._qRouterScrollEnabled = true; - if (isBrowser) { - callRestoreScrollOnDocument(); - } - - if (shouldForcePrevUrl) { - forceStoreEffects(routeLocation, 'prevUrl'); - } - if (shouldForceUrl) { - forceStoreEffects(routeLocation, 'url'); - } - if (shouldForceParams) { - forceStoreEffects(routeLocation, 'params'); - } - routeLocation.isNavigating = false; - navResolver.r?.(); - }); - } + ); + }; + _waitNextPage().finally(() => { + if (currentTransition && internalState.currentTransition === currentTransition) { + internalState.currentTransition = undefined; + } + if (nav.navCount !== internalState.navCount) { + return; + } + (container as ClientContainer).element.setAttribute?.(Q_ROUTE, routeName); + const scrollState = currentScrollState(scroller); + saveScrollHistory(scrollState); + window._qRouterScrollEnabled = true; + callRestoreScrollOnDocument(); + + if (nav.shouldForcePrevUrl) { + forceStoreEffects(routeLocation, 'prevUrl'); + } + if (nav.shouldForceUrl) { + forceStoreEffects(routeLocation, 'url'); + } + if (nav.shouldForceParams) { + forceStoreEffects(routeLocation, 'params'); + } + routeLocation.isNavigating = false; + navResolver.r?.(); + }); }, - // We should only wait for navigation to complete on the server - { deferUpdates: isServer } + { deferUpdates: false } ); }; @@ -950,25 +983,16 @@ const useQwikMockRouter = (props: QwikRouterMockProps) => { { deep: false } ); - // Mirror useQwikRouter: each loader id must be backed by an AsyncSignal so - // that user code reading `useFooLoader().value` (and `.loading`/`.error`/ - // `.promise`) works. Storing raw data here would surface as `undefined` - // once the framework reads `state[id].value`. - const container = _getContextContainer(); - const loadersObject: Record = {}; - const loadersState: Record> = {}; - if (props.loaders) { - for (const { loader, data } of props.loaders) { - const id = (loader as LoaderInternal).__id; - loadersObject[id] = data; - loadersState[id] = createLoaderSignal( - loadersObject, - id, - url, - DEFAULT_LOADERS_SERIALIZATION_STRATEGY, - container - ); - } + const loadersData = props.loaders?.reduce( + (acc, { loader, data }) => { + acc[(loader as LoaderInternal).__id] = data; + return acc; + }, + {} as Record + ); + const loadersState = useStore>>({}, { deep: false }); + for (const [loaderId, data] of Object.entries(loadersData ?? {})) { + loadersState[loaderId] ||= createAsync$(async () => data, { initial: data }); } const goto: RouteNavigate = @@ -1037,29 +1061,177 @@ export const QwikRouterMockProvider = component$((props) => export const QwikCityMockProvider = QwikRouterMockProvider; export interface ClientSPAWindow extends Window { - /** @deprecated Use "_qRouterHistoryPatch" instead. Will be removed in V3 */ - _qCityHistoryPatch?: boolean; - /** @deprecated Use "_qRouterSPA" instead. Will be removed in V3 */ - _qCitySPA?: boolean; - /** @deprecated Use "_qRouterScrollEnabled" instead. Will be removed in V3 */ - _qCityScrollEnabled?: boolean; - /** @deprecated Use "_qRouterScrollDebounce" instead. Will be removed in V3 */ - _qCityScrollDebounce?: ReturnType; - /** @deprecated Use "_qRouterInitPopstate" instead. Will be removed in V3 */ - _qCityInitPopstate?: () => void; - /** @deprecated Use "_qRouterInitAnchors" instead. Will be removed in V3 */ - _qCityInitAnchors?: (event: MouseEvent) => void; - /** @deprecated Use "_qRouterInitVisibility" instead. Will be removed in V3 */ - _qCityInitVisibility?: () => void; - /** @deprecated Use "_qRouterInitScroll" instead. Will be removed in V3 */ - _qCityInitScroll?: () => void; + /** @internal */ _qRouterHistoryPatch?: boolean; + /** @internal */ _qRouterSPA?: boolean; + /** @internal */ _qRouterScrollEnabled?: boolean; + /** @internal */ _qRouterScrollDebounce?: ReturnType; + /** @internal */ _qRouterInitPopstate?: () => void; + /** @internal */ _qRouterInitAnchors?: (event: MouseEvent) => void; + /** @internal */ _qRouterInitVisibility?: () => void; + /** @internal */ _qRouterInitScroll?: () => void; + /** @internal */ _qcs?: boolean; } + +// See also spa-init.ts +function initializeSPA(goto: RouteNavigate, scroller: HTMLElement) { + if (!window._qRouterSPA) { + // only add event listener once + window._qRouterSPA = true; + history.scrollRestoration = 'manual'; + + window.addEventListener('popstate', () => { + // Disable scroll handler eagerly to prevent overwriting history.state. + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + + goto(location.href, { + type: 'popstate', + }); + }); + + window.removeEventListener('popstate', window._qRouterInitPopstate!); + window._qRouterInitPopstate = undefined; + + // Browsers natively will remember scroll on ALL history entries, incl. custom pushState. + // Devs could push their own states that we can't control. + // If a user doesn't initiate scroll after, it will not have any scrollState. + // We patch these to always include scrollState. + // TODO Block this after Navigation API PR, browsers that support it have a Navigation API solution. + if (!window._qRouterHistoryPatch) { + window._qRouterHistoryPatch = true; + const pushState = history.pushState; + const replaceState = history.replaceState; + + const prepareState = (state: any) => { + if (state === null || typeof state === 'undefined') { + state = {}; + } else if (state?.constructor !== Object) { + state = { _data: state }; + + if (isDev) { + console.warn( + 'In a Qwik SPA context, `history.state` is used to store scroll state. ' + + 'Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. ' + + 'We need to be able to automatically attach the scroll state to your state object. ' + + 'A new state object has been created, your data has been moved to: `history.state._data`' + ); + } + } + + state._qRouterScroll = state._qRouterScroll || currentScrollState(scroller); + return state; + }; + + history.pushState = (state, title, url) => { + state = prepareState(state); + return pushState.call(history, state, title, url); + }; + + history.replaceState = (state, title, url) => { + state = prepareState(state); + return replaceState.call(history, state, title, url); + }; + } + + // Chromium and WebKit fire popstate+hashchange for all #anchor clicks, + // ... even if the URL is already on the #hash. + // Firefox only does it once and no more, but will still scroll. It also sets state to null. + // Any tags w/ #hash href will break SPA state in Firefox. + // We patch these events and direct them to Link pipeline during SPA. + document.addEventListener('click', (event) => { + if (event.defaultPrevented) { + return; + } + + const target = (event.target as HTMLElement).closest('a[href]'); + + if (target && !target.hasAttribute('preventdefault:click')) { + const href = target.getAttribute('href')!; + const prev = new URL(location.href); + const dest = new URL(href, prev); + // Patch only same-page anchors. + if (isSameOrigin(dest, prev) && isSamePath(dest, prev)) { + event.preventDefault(); + + // Simulate same-page (no hash) anchor reload. + // history.scrollRestoration = 'manual' makes these not scroll. + if (!dest.hash && !dest.href.endsWith('#')) { + if (dest.href !== prev.href) { + history.pushState(null, '', dest); + } + + window._qRouterScrollEnabled = false; + clearTimeout(window._qRouterScrollDebounce); + saveScrollHistory({ + ...currentScrollState(scroller), + x: 0, + y: 0, + }); + location.reload(); + return; + } + + goto(target.getAttribute('href')!); + } + } + }); + + document.removeEventListener('click', window._qRouterInitAnchors!); + window._qRouterInitAnchors = undefined; + + // TODO Remove block after Navigation API PR. + // Calling `history.replaceState` during `visibilitychange` in Chromium will nuke BFCache. + // Only Chromium 96 - 101 have BFCache without Navigation API. (<1% of users) + if (!(window as any).navigation) { + // Commit scrollState on refresh, cross-origin navigation, mobile view changes, etc. + document.addEventListener( + 'visibilitychange', + () => { + if (window._qRouterScrollEnabled && document.visibilityState === 'hidden') { + // Last & most reliable point to commit state. + // Do not clear timeout here in case debounce gets to run later. + const scrollState = currentScrollState(scroller); + saveScrollHistory(scrollState); + } + }, + { passive: true } + ); + + document.removeEventListener('visibilitychange', window._qRouterInitVisibility!); + window._qRouterInitVisibility = undefined; + } + + window.addEventListener( + 'scroll', + () => { + if (!window._qRouterScrollEnabled) { + return; + } + + clearTimeout(window._qRouterScrollDebounce); + window._qRouterScrollDebounce = setTimeout(() => { + const scrollState = currentScrollState(scroller); + saveScrollHistory(scrollState); + // Needed for e2e debounceDetector. + window._qRouterScrollDebounce = undefined; + }, 200); + }, + { passive: true } + ); + + removeEventListener('scroll', window._qRouterInitScroll!); + window._qRouterInitScroll = undefined; + + // Cache SPA recovery script. + spaInit.resolve(); + } +} diff --git a/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md b/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md index 5446530037d..f4681f5aabb 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md +++ b/packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md @@ -12,6 +12,7 @@ import { CookieValue } from '@qwik.dev/router/middleware/request-handler'; import { DeferReturn } from '@qwik.dev/router/middleware/request-handler'; import type { EnvGetter } from '@qwik.dev/router/middleware/request-handler'; import { JSXOutput } from '@qwik.dev/core'; +import { NoSerialize } from '@qwik.dev/core'; import { QRL } from '@qwik.dev/core'; import { QRLEventHandlerMulti } from '@qwik.dev/core'; import { QwikIntrinsicElements } from '@qwik.dev/core'; @@ -375,11 +376,17 @@ export interface QwikRouterEnvData { // (undocumented) loadedRoute: LoadedRoute; // (undocumented) + loaderValues: Record; + // (undocumented) params: PathParams; // Warning: (ae-forgotten-export) The symbol "EndpointResponse" needs to be exported by the entry point index.d.ts // // (undocumented) response: EndpointResponse; + // Warning: (ae-forgotten-export) The symbol "RouteLoaderCtx" needs to be exported by the entry point index.d.ts + // + // (undocumented) + routeLoaderCtx: RouteLoaderCtx; // (undocumented) routeName: string; } @@ -446,7 +453,7 @@ export type ResolvedDocumentHead = Recor readonly manifestHash: string; }; -// @public (undocumented) +// @public export const routeAction$: ActionConstructor; // Warning: (ae-internal-missing-underscore) The name "routeActionQrl" should be prefixed with an underscore because the declaration is marked as @internal @@ -484,11 +491,12 @@ export interface RouteData { // Warning: (ae-forgotten-export) The symbol "MenuModuleLoader" needs to be exported by the entry point index.d.ts _N?: MenuModuleLoader; _P?: string; + _R?: string[]; } // Warning: (ae-forgotten-export) The symbol "LoaderConstructor" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export const routeLoader$: LoaderConstructor; // Warning: (ae-forgotten-export) The symbol "LoaderConstructorQRL" needs to be exported by the entry point index.d.ts diff --git a/packages/qwik-router/src/runtime/src/route-loaders.spec.tsx b/packages/qwik-router/src/runtime/src/route-loaders.spec.tsx new file mode 100644 index 00000000000..fb9264a8f12 --- /dev/null +++ b/packages/qwik-router/src/runtime/src/route-loaders.spec.tsx @@ -0,0 +1,258 @@ +/** + * Spec tests for route loader signal reactivity. + * + * These test the core mechanism: useStore + createAsync$ + track() + invalidate(__v). They run in + * the qwik core test infrastructure since they need ssrRenderToDom/domRender. + */ +// This file should be moved to packages/qwik/src/core/tests/ if it needs the rendering infra. +// For now, test the mechanism at the unit level using the signal test infrastructure. + +import { createAsync$, implicit$FirstArg, type QRL } from '@qwik.dev/core'; +import { + _AsyncSignalImpl as AsyncSignalImpl, + _Container as Container, + _createStore as createStore, + _delay as delay, + _getDomContainer as getDomContainer, + _getSubscriber as getSubscriber, + _HostElement as HostElement, + _invoke as invoke, + _isStore as isStore, + _newInvokeContext as newInvokeContext, + _QRLInternal as QRLInternal, + _retryOnPromise as retryOnPromise, + _Task as Task, + _vnode_newVirtual as vnode_newVirtual, + _vnode_setProp as vnode_setProp, +} from '@qwik.dev/core/internal'; +import { createDocument } from '@qwik.dev/core/testing'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { setLoaderSignalValue } from './route-loaders'; + +describe('route loader store + async signal tracking', () => { + let container: Container = null!; + let task: Task | null = null; + + beforeEach(() => { + const document = createDocument({ html: '' }); + container = getDomContainer(document.body); + task = null; + }); + + afterEach(async () => { + await container.$renderPromise$; + container = null!; + }); + + it('should track store property and re-compute on change', async () => { + await withContainer(async () => { + const ctx = createStore(container, { pageUrl: '/a' }, 1 /* StoreFlags.RECURSIVE */); + + const computeLog: string[] = []; + const signal = createAsync$(async ({ track }) => { + const url = track(ctx, 'pageUrl') as string; + computeLog.push(url); + return `loaded:${url}`; + }) as AsyncSignalImpl; + + // Subscribe so effects fire + await retryOnPromise(() => { + effect$(() => signal.value); + }); + + expect(signal.value).toBe('loaded:/a'); + expect(computeLog).toEqual(['/a']); + + // Change store property → should trigger re-computation + ctx.pageUrl = '/b'; + await signal.promise(); + + expect(signal.value).toBe('loaded:/b'); + expect(computeLog).toEqual(['/a', '/b']); + }); + }); + + it('should track nested store property', async () => { + await withContainer(async () => { + const ctx = createStore( + container, + { loaderPaths: { x: '/a/' } as Record }, + 1 /* StoreFlags.RECURSIVE */ + ); + + const computeLog: string[] = []; + const signal = createAsync$(async ({ track }) => { + const path = track(ctx.loaderPaths, 'x') as string | undefined; + computeLog.push(path || 'none'); + return `path:${path}`; + }) as AsyncSignalImpl; + + await retryOnPromise(() => { + effect$(() => signal.value); + }); + + expect(signal.value).toBe('path:/a/'); + + // Change nested property + ctx.loaderPaths.x = '/b/'; + await signal.promise(); + + expect(signal.value).toBe('path:/b/'); + expect(computeLog).toEqual(['/a/', '/b/']); + }); + }); + + it('should re-compute after __v info is consumed', async () => { + await withContainer(async () => { + const ctx = createStore(container, { pageUrl: '/initial' }, 1 /* StoreFlags.RECURSIVE */); + + const computeLog: string[] = []; + const signal = createAsync$(async ({ track, info }) => { + const url = track(ctx, 'pageUrl') as string; + if (info && typeof info === 'object' && '__v' in (info as object)) { + computeLog.push(`__v`); + return (info as { __v: string }).__v; + } + computeLog.push(`fetch:${url}`); + return `fetched:${url}`; + }) as AsyncSignalImpl; + + await retryOnPromise(() => { + effect$(() => signal.value); + }); + + expect(signal.value).toBe('fetched:/initial'); + expect(computeLog).toEqual(['fetch:/initial']); + + // Simulate action: invalidate with __v, then force compute + setLoaderSignalValue(signal, 'action-value'); + await delay(0); + + expect(signal.value).toBe('action-value'); + expect(computeLog).toEqual(['fetch:/initial', '__v']); + + // Now change the store → should trigger re-computation with fetch, NOT stale __v + ctx.pageUrl = '/after-nav'; + await signal.promise(); + + expect(signal.value).toBe('fetched:/after-nav'); + expect(computeLog).toEqual(['fetch:/initial', '__v', 'fetch:/after-nav']); + }); + }); + + it('should re-compute for TWO sequential store changes after __v', async () => { + await withContainer(async () => { + const ctx = createStore(container, { pageUrl: '/initial' }, 1 /* StoreFlags.RECURSIVE */); + + const computeLog: string[] = []; + const signal = createAsync$(async ({ track, info }) => { + const url = track(ctx, 'pageUrl') as string; + if (info && typeof info === 'object' && '__v' in (info as object)) { + computeLog.push(`__v`); + return (info as { __v: string }).__v; + } + computeLog.push(`fetch:${url}`); + return `fetched:${url}`; + }) as AsyncSignalImpl; + + await retryOnPromise(() => { + effect$(() => signal.value); + }); + + expect(signal.value).toBe('fetched:/initial'); + + // Inject action value + setLoaderSignalValue(signal, 'action-value'); + await delay(0); + expect(signal.value).toBe('action-value'); + + // First store change + ctx.pageUrl = '/stuff'; + await signal.promise(); + expect(signal.value).toBe('fetched:/stuff'); + + // Second store change — this is what fails in e2e + ctx.pageUrl = '/welcome'; + await signal.promise(); + expect(signal.value).toBe('fetched:/welcome'); + + expect(computeLog).toEqual(['fetch:/initial', '__v', 'fetch:/stuff', 'fetch:/welcome']); + }); + }); + + it('should handle multiple sequential store changes', async () => { + await withContainer(async () => { + const ctx = createStore(container, { pageUrl: '/page0' }, 1 /* StoreFlags.RECURSIVE */); + + const signal = createAsync$(async ({ track }) => { + const url = track(ctx, 'pageUrl') as string; + return `loaded:${url}`; + }) as AsyncSignalImpl; + + await retryOnPromise(() => { + effect$(() => signal.value); + }); + + expect(signal.value).toBe('loaded:/page0'); + + ctx.pageUrl = '/page1'; + await signal.promise(); + expect(signal.value).toBe('loaded:/page1'); + + ctx.pageUrl = '/page2'; + await signal.promise(); + expect(signal.value).toBe('loaded:/page2'); + + ctx.pageUrl = '/page3'; + await signal.promise(); + expect(signal.value).toBe('loaded:/page3'); + }); + }); + + it('should verify store is reactive', async () => { + await withContainer(async () => { + const ctx = createStore(container, { pageUrl: '/test' }, 1 /* StoreFlags.RECURSIVE */); + expect(isStore(ctx)).toBe(true); + + const signal = createAsync$(async ({ track }) => { + return track(ctx, 'pageUrl') as string; + }) as AsyncSignalImpl; + + await retryOnPromise(() => { + effect$(() => signal.value); + }); + + expect(signal.value).toBe('/test'); + // After track(), the store should have effects on 'pageUrl' + // (verified by the fact that changing it triggers re-computation) + ctx.pageUrl = '/changed'; + await signal.promise(); + expect(signal.value).toBe('/changed'); + }); + }); + + //////////////////////////////////////// + + function withContainer(fn: () => T): T { + const ctx = newInvokeContext(); + ctx.$container$ = container; + return invoke(ctx, fn); + } + + function effectQrl(fnQrl: QRL<() => void>) { + const qrl = fnQrl as QRLInternal<() => void>; + const element: HostElement = vnode_newVirtual(); + task = task || new Task(0, 0, element, fnQrl as QRLInternal, undefined, null); + vnode_setProp(element, 'q:seq', [task]); + if (!qrl.resolved) { + throw qrl.resolve(); + } else { + const ctx = newInvokeContext(); + ctx.$container$ = container; + ctx.$effectSubscriber$ = getSubscriber(task, ':' /* EffectProperty.COMPONENT */); + return invoke(ctx, qrl.getFn(ctx)); + } + } + + const effect$ = /*#__PURE__*/ implicit$FirstArg(effectQrl); +}); diff --git a/packages/qwik-router/src/runtime/src/route-loaders.ts b/packages/qwik-router/src/runtime/src/route-loaders.ts new file mode 100644 index 00000000000..39192bf18d2 --- /dev/null +++ b/packages/qwik-router/src/runtime/src/route-loaders.ts @@ -0,0 +1,768 @@ +import * as qwikRouterConfig from '@qwik-router-config'; +import { implicit$FirstArg, isDev, isServer, type NoSerialize, type QRL } from '@qwik.dev/core'; +import { + _deserialize, + _getContextEvent, + _getContextContainer, + _injectAsyncSignalValue, + _resolveContextWithoutSequentialScope, + _verifySerializable, + createAsync$, + SerializerSymbol, + type AsyncSignal, + type SerializationStrategy, +} from '@qwik.dev/core/internal'; +import type { + RequestEvent as RequestEventBase, + RequestEventLoader as ServerRequestEventLoader, +} from '@qwik.dev/router/middleware/request-handler'; +// Import directly from leaf modules to avoid circular dependency: +// route-loaders.ts → middleware barrel → runtime/src/ (same module group) +import { _asyncRequestStore } from '../../middleware/request-handler/async-request-store'; +import { getLoaderName } from '../../middleware/request-handler/request-path'; +import { RedirectMessage } from '../../middleware/request-handler/redirect-handler'; +import { ServerError } from '../../middleware/request-handler/server-error'; +import { ensureSlash } from '../../utils/pathname'; +import { DEFAULT_LOADERS_SERIALIZATION_STRATEGY } from './constants'; +import { RouteLoaderCtxContext, RouteStateContext } from './contexts'; +import type { + DataValidator, + LoaderConstructor, + LoaderConstructorQRL, + LoaderInternal, + LoaderOptions, + RequestEvent, + RequestEventLoader, + RouteNavigate, + RouteModule, + ValidatorReturn, +} from './types'; + +/** + * Route loaders read data before the route rendering starts, based on the route being navigated to. + * They automatically update when the route changes on the client, and can also be made to poll for + * changes. + * + * They are represented by an AsyncSignal. + */ + +const REQUEST_ROUTE_LOADER_STATE = '@routeLoaderState'; +const REQUEST_LOADER_PATHS_STORE = '@loaderPathsStore'; +const REQUEST_ROUTE_LOADERS = '@routeLoaders'; +const REQUEST_ROUTE_LOADER_PROMISES = '@routeLoaderPromises'; + +/** Header name sent by client to tell the server the actual page URL for loader requests. */ +export const FULLPATH_HEADER = 'X-Qwik-fullpath'; + +/** + * Response envelope for loader.json requests. Exactly one of `d`, `r`, or `e` is set. + * + * - `d` — data: the loader's successful return value + * - `r` — redirect: URL to navigate to (from `throw redirect()`) + * - `e` — error: a ServerError (from `fail()` or `throw serverError()`) + */ +export type LoaderResponse = { + d?: unknown; + r?: string; + e?: InstanceType; +}; + +type LoaderFetchCacheEntry = { + promise?: Promise; + value?: LoaderResponse; + expires: number; +}; + +const LOADER_FETCH_CACHE_TTL = 5_000; +const LOADER_FETCH_CACHE_MAX = 128; +/** Preload fetch cache, prevents duplicate fetches */ +const fetchCache = new Map(); + +const perfNow = () => globalThis.performance?.now() ?? Date.now(); + +const setCache = (key: string, entry: LoaderFetchCacheEntry) => { + fetchCache.set(key, entry); + if (fetchCache.size > LOADER_FETCH_CACHE_MAX) { + const oldestKey = fetchCache.keys().next().value; + if (oldestKey !== undefined) { + fetchCache.delete(oldestKey); + } + } +}; + +/** We don't have aborts when preloading so we just pretend we do */ +const wrapWithAbort = (promise: Promise, signal: AbortSignal): Promise => { + if (signal.aborted) { + return Promise.reject(signal.reason); + } + return new Promise((resolve, reject) => { + const abort = () => reject(signal.reason); + signal.addEventListener('abort', abort, { once: true }); + promise.finally(() => signal.removeEventListener('abort', abort)).then(resolve, reject); + }); +}; + +/** + * Reactive context for route loaders. On the server this is stored in sharedMap, on the client it's + * a store that gets updated on navigation. + * + * - `loaderPaths`: loader ID → fetch path (the longest route path for that loader) + * - `pagePathname` / `pageSearch`: client-only navigation state used for loader invalidation and + * q-loader fetches. They are intentionally omitted from SSR state and fall back to `location` + * until the first SPA navigation. + */ +export type RouteLoaderCtx = { + loaderPaths: Record; + pagePathname?: string; + pageSearch?: string; + /** SPA navigation function. Client-only and intentionally omitted from SSR state. */ + goto?: NoSerialize; +}; + +class ServerRouteLoaderCapture { + constructor( + readonly hash: string, + readonly qrl: QRL<(event: RequestEventLoader) => unknown>, + readonly validators: DataValidator[] | undefined + ) {} + + load() { + const requestEv = getRequestEvent(); + // Use pre-computed value from loadersMiddleware if available, + // to avoid re-running the loader after the response stream is open. + const values = getRouteLoaderValues(requestEv); + if (this.hash in values) { + return values[this.hash]; + } + return loadRouteLoaderByQrl(this.hash, this.qrl, this.validators, requestEv); + } + + [SerializerSymbol]() { + return this.hash; + } +} + +const isRequestEvent = (value: unknown): value is RequestEvent => + !!value && + typeof value === 'object' && + Object.prototype.hasOwnProperty.call(value, 'sharedMap') && + Object.prototype.hasOwnProperty.call(value, 'cookie'); + +const isLoaderInternal = (value: unknown): value is LoaderInternal => + typeof value === 'function' && (value as LoaderInternal).__brand === 'server_loader'; + +const getClientManifestHash = (ctx: unknown) => { + // We cheat and grab the internal signal of the AsyncJob + // Maybe we should expose .signal? Would be good for implementing channels + const container = (ctx as { $signal$?: { $container$?: unknown } }).$signal$?.$container$; + return ( + (container as { qManifestHash?: string } | undefined)?.qManifestHash || + (_getContextContainer() as { qManifestHash?: string } | undefined)?.qManifestHash || + 'dev' + ); +}; + +/** + * Fetch a single loader's data from the server. + * + * URL pattern: `{basePath}{routePath}/q-loader-{loaderId}.{manifestHash}.json` + */ +/** Fetch a loader's JSON response from the server. Returns the LoaderResponse envelope. */ +export const fetchRouteLoaderData = async ( + loaderId: string, + routePath: string | undefined, + manifestHash: string, + opts?: { + pageUrl?: URL; + basePath?: string; + ignoreCache?: boolean; + signal?: AbortSignal; + } +): Promise => { + if (!routePath) { + return undefined; + } + // Ensure the route path includes the base path (root trie loaders get '/' but + // need the full base path for fetching) + let resolvedPath = routePath; + const basePath = opts?.basePath ?? '/'; + if (basePath !== '/' && !resolvedPath.startsWith(basePath)) { + resolvedPath = basePath + resolvedPath.slice(1); + } + const pathBase = ensureSlash(resolvedPath); + const pageUrl = opts?.pageUrl; + const search = pageUrl?.search ?? ''; + // TODO allowList search params + compat flag that allows search params + const url = `${pathBase}${getLoaderName(loaderId, manifestHash)}${search}`; + + const headers: Record = {}; + if (pageUrl && pageUrl.pathname !== pathBase) { + // TODO disable when nocompat + headers[FULLPATH_HEADER] = pageUrl.pathname; + } + + const cacheKey = `${url}\n${headers[FULLPATH_HEADER] ?? ''}`; + if (!opts?.ignoreCache) { + const entry = fetchCache.get(cacheKey); + if (entry) { + if (entry.promise) { + return opts?.signal ? wrapWithAbort(entry.promise, opts.signal) : entry.promise; + } + if (entry.expires > perfNow()) { + return entry.value; + } + fetchCache.delete(cacheKey); + } + } + + const request = async () => { + const response = await fetch(url, { + signal: opts?.signal, + cache: opts?.ignoreCache ? 'reload' : 'default', + headers, + }); + // Middleware redirects produce HTTP 3xx — convert to LoaderResponse + if (response.redirected) { + return { r: response.url }; + } + if (!response.ok) { + return undefined; + } + const text = await response.text(); + return _deserialize(text) ?? undefined; + }; + + if (opts?.ignoreCache || opts?.signal) { + // Never cache these requests, since they're either one-off or have abort signals that can't be shared + // The browser cache will still apply + return request(); + } + + const promise = request().then( + (value) => { + if (value === undefined) { + fetchCache.delete(cacheKey); + } else { + setCache(cacheKey, { + value, + expires: perfNow() + LOADER_FETCH_CACHE_TTL, + }); + } + return value; + }, + (err) => { + fetchCache.delete(cacheKey); + throw err; + } + ); + setCache(cacheKey, { + promise, + expires: perfNow() + LOADER_FETCH_CACHE_TTL, + }); + return promise; +}; + +const createRouteLoaderSignal = (loader: LoaderInternal, routeLoaderCtx: RouteLoaderCtx) => { + const capture = isServer + ? new ServerRouteLoaderCapture(loader.__id, loader.__qrl, loader.__validators) + : loader.__id; + const searchFilter = loader.__search; + let lastFilteredSearch: string | undefined; + let lastRoutePath: string | undefined; + return createAsync$( + async (ctx) => { + const { track, info, previous, abortSignal } = ctx; + // Pre-loaded value injection (from middleware via setLoaderSignalValue, or from + // an action response). Skipping the track() calls below is safe: route loader + // functions run on the server and have no access to client-side reactive state, + // so their compute never registers subscriptions of its own. The track() calls + // in the client branch only exist to react to route/page-URL changes, which fire + // a fresh invalidate (without info.__v) when they happen. + if (info && typeof info === 'object' && '__v' in (info as object)) { + return (info as { __v: unknown }).__v; + } + if (isServer) { + return (capture as ServerRouteLoaderCapture).load(); + } + const id = capture as string; + // Track reactive dependencies so the signal re-fetches when the route path changes + const routePath = track(routeLoaderCtx.loaderPaths, id) as string | undefined; + // Track the client page path/search. These fields are only assigned on SPA navigation; before + // that, `location` is the source of truth and avoids serializing a duplicate URL in SSR state. + const pagePathname = + (track(routeLoaderCtx, 'pagePathname') as string | undefined) || location.pathname; + const pageSearch = + (track(routeLoaderCtx, 'pageSearch') as string | undefined) || location.search; + const pageUrl = new URL(pagePathname + pageSearch, location.href); + const mHash = getClientManifestHash(ctx); + const basePath = (qwikRouterConfig as any).basePathname ?? '/'; + // A loader that's never been on any route we've visited has no fetch path yet — + // return whatever value it has (undefined on the very first run). In practice + // this branch only fires on the initial client-side read for a loader that + // wasn't prefilled by SSR; normal navs leave stale entries in loaderPaths so + // this compute only runs when there's a fresh path to fetch against. + if (!routePath) { + return previous; + } + + // Filter search params: only include allowed params and skip fetch if unchanged + let fetchUrl = pageUrl; + if (searchFilter) { + const filteredSearch = filterSearchParams(pageUrl.searchParams, searchFilter); + if ( + previous !== undefined && + filteredSearch === lastFilteredSearch && + routePath === lastRoutePath + ) { + // Relevant search params didn't change and path didn't change — return previous value + return previous; + } + lastFilteredSearch = filteredSearch; + lastRoutePath = routePath; + // Build a URL with only the allowed search params for the fetch + fetchUrl = new URL(pageUrl.href); + fetchUrl.search = filteredSearch; + } + + // Fetch from server + const response = await fetchRouteLoaderData(id, routePath, mHash, { + pageUrl: fetchUrl, + basePath, + ignoreCache: info === true, + signal: abortSignal, + }); + if (!response) { + throw new Error(`Loader ${id} returned empty response`); + } + if (response.r) { + // Redirect — fire SPA goto if available, else full page nav. We don't + // await or coordinate with the current nav: the new nav starts while + // this one finishes committing, producing a brief flash of stale data + // before the new route's loaders resolve. That trade-off is intentional + // — awaiting all loader promises just to catch redirects is too costly + // for the common case. + // + const goto = routeLoaderCtx.goto; + if (goto) { + goto(response.r, { replaceState: true }); + } else { + location.href = response.r; + } + // Return `previous` (stale data) rather than throwing: an AsyncSignal + // in error state can drop Resource subscriptions, which would prevent + // the redirect-target fetch from updating the UI once it arrives. + return previous; + } + if (response.e) { + // Error — throw so AsyncSignal enters error state + throw response.e; + } + return response.d; + }, + + { + serializationStrategy: loader.__serializationStrategy, + expires: loader.__expires, + poll: loader.__poll, + allowStale: loader.__allowStale, + } + ); +}; + +/** Build a sorted, stable search string from only the allowed param names. */ +const filterSearchParams = (params: URLSearchParams, allowed: string[]): string => { + const filtered = new URLSearchParams(); + for (let i = 0; i < allowed.length; i++) { + const name = allowed[i]; + const values = params.getAll(name); + for (let j = 0; j < values.length; j++) { + filtered.append(name, values[j]); + } + } + filtered.sort(); + return filtered.toString() ? `?${filtered.toString()}` : ''; +}; + +const getLoaderOptions = (rest: (LoaderOptions | DataValidator)[]) => { + let serializationStrategy: SerializationStrategy = DEFAULT_LOADERS_SERIALIZATION_STRATEGY; + let expires: number | undefined; + let poll: boolean | undefined; + let eTag: LoaderOptions['eTag'] | undefined; + let search: string[] | undefined; + let allowStale = true; + const validators: DataValidator[] = []; + + if (rest.length === 1) { + const options = rest[0]; + if (options && typeof options === 'object') { + if ('validate' in options) { + validators.push(options); + } else { + if (options.serializationStrategy) { + serializationStrategy = options.serializationStrategy; + } + if (options.validation) { + validators.push(...options.validation); + } + if ('expires' in options) { + expires = options.expires; + } + if ('poll' in options) { + poll = options.poll; + } + if ('eTag' in options) { + eTag = options.eTag; + } + if (options.search) { + search = options.search; + } else if (globalThis.__STRICT_LOADERS__) { + search = []; + } + if (options.allowStale === false) { + allowStale = false; + } + } + } + } else if (rest.length > 1) { + validators.push(...(rest.filter(Boolean) as DataValidator[])); + } + + return { + validators: validators.reverse(), + serializationStrategy, + expires, + poll, + eTag, + search, + allowStale, + }; +}; + +export const getRequestEvent = (thisArg?: unknown): RequestEvent => { + if (!isServer) { + throw new Error('getRequestEvent() can only be used on the server.'); + } + const requestEvent = + _asyncRequestStore?.getStore() || [thisArg, _getContextEvent()].find(isRequestEvent); + if (!requestEvent) { + throw new Error('Unable to determine the current RequestEvent.'); + } + return requestEvent; +}; + +const REQUEST_ROUTE_LOADER_VALUES = '@routeLoaderValues'; + +export function getRouteLoaderState( + requestEv: RequestEventBase +): Record> { + let state = requestEv.sharedMap.get(REQUEST_ROUTE_LOADER_STATE) as + | Record> + | undefined; + if (!state) { + state = {}; + requestEv.sharedMap.set(REQUEST_ROUTE_LOADER_STATE, state); + } + return state; +} + +/** Get/create the record of pre-loaded loader values (used by middleware before component). */ +export function getRouteLoaderValues(requestEv: RequestEventBase): Record { + let values = requestEv.sharedMap.get(REQUEST_ROUTE_LOADER_VALUES) as + | Record + | undefined; + if (!values) { + values = {}; + requestEv.sharedMap.set(REQUEST_ROUTE_LOADER_VALUES, values); + } + return values; +} + +function getRouteLoaderPromises(requestEv: RequestEventBase): Record> { + let promises = requestEv.sharedMap.get(REQUEST_ROUTE_LOADER_PROMISES) as + | Record> + | undefined; + if (!promises) { + promises = {}; + requestEv.sharedMap.set(REQUEST_ROUTE_LOADER_PROMISES, promises); + } + return promises; +} + +/** Store the route loader internals on the request for SSG to read. */ +export function setRouteLoaders(requestEv: RequestEventBase, loaders: LoaderInternal[]) { + requestEv.sharedMap.set(REQUEST_ROUTE_LOADERS, loaders); +} + +/** Get the route loader internals stored on the request. */ +export function getRouteLoaders(requestEv: RequestEventBase): LoaderInternal[] { + return requestEv.sharedMap.get(REQUEST_ROUTE_LOADERS) ?? []; +} + +export function getRouteLoaderCtx(requestEv: RequestEventBase): RouteLoaderCtx { + let ctx = requestEv.sharedMap.get(REQUEST_LOADER_PATHS_STORE) as RouteLoaderCtx | undefined; + if (!ctx) { + ctx = { + loaderPaths: {}, + }; + requestEv.sharedMap.set(REQUEST_LOADER_PATHS_STORE, ctx); + } + return ctx; +} + +/** + * Update the loader paths store on client-side navigation. + * + * Only adds/updates entries for loaders present on the new route. Entries for loaders that are NOT + * on the new route are left untouched — their AsyncSignals keep their prior values, and no track() + * fires to invalidate them. If the user navigates back to a route where the loader IS present, the + * path updates and the signal re-fetches. This is the "stale is fine by default" contract: readers + * see old data until new data arrives. + */ +export const updateRouteLoaderPaths = ( + ctx: RouteLoaderCtx, + loaderPaths: Record | undefined, + pageUrl: URL +) => { + if (!isServer) { + ctx.pagePathname = pageUrl.pathname; + ctx.pageSearch = pageUrl.search; + } + if (loaderPaths) { + for (const key in loaderPaths) { + ctx.loaderPaths[key] = loaderPaths[key]; + } + } +}; + +export const getModuleRouteLoaders = (mods: readonly (RouteModule | undefined)[]) => { + const routeLoaders: LoaderInternal[] = []; + const seen = new Set(); + for (let i = 0; i < mods.length; i++) { + const mod = mods[i]; + if (!mod) { + continue; + } + for (const key in mod) { + const value = mod[key as keyof typeof mod]; + if (isLoaderInternal(value) && !seen.has(value.__id)) { + seen.add(value.__id); + routeLoaders.push(value); + } + } + } + return routeLoaders; +}; + +export const ensureRouteLoaderSignal = ( + loader: LoaderInternal, + state: Record>, + routeLoaderCtx: RouteLoaderCtx +) => { + return (state[loader.__id] ||= createRouteLoaderSignal(loader, routeLoaderCtx)); +}; + +export const ensureRouteLoaderSignals = ( + mods: readonly (RouteModule | undefined)[], + state: Record>, + routeLoaderCtx: RouteLoaderCtx +) => { + const loaders = getModuleRouteLoaders(mods); + for (let i = 0; i < loaders.length; i++) { + const loader = loaders[i]; + ensureRouteLoaderSignal(loader, state, routeLoaderCtx); + } + return loaders; +}; + +/** + * Inject a pre-loaded value into an AsyncSignal while preserving track() subscriptions. Delegates + * to the core helper which calls invalidate({ __v }) + $computeIfNeeded$() so the compute function + * runs synchronously, registers subscriptions via track(), and returns the pre-loaded value without + * fetching. Must go through the core helper because $computeIfNeeded$ is mangled in core builds and + * not directly callable from this package. + */ +export const setLoaderSignalValue = (signal: AsyncSignal, value: unknown) => { + _injectAsyncSignalValue(signal, value); +}; + +export const resolveRouteLoaderByHash = ( + routeLoaders: readonly LoaderInternal[], + loaderId: string +) => { + return routeLoaders.find((loader) => loader.__id === loaderId); +}; + +/** Run a loader and return its raw value. Errors/redirects propagate as exceptions. */ +export const getRouteLoaderData = async ( + loaderQrl: QRL<(event: RequestEventLoader) => unknown>, + validators: DataValidator[] | undefined, + requestEv: RequestEvent +) => { + const loaderRequestEv = requestEv as unknown as RequestEventLoader; + + const result = await runValidators(requestEv, validators, undefined); + if (!result.success) { + return loaderRequestEv.fail(result.status ?? 500, result.error); + } + const resolved = await loaderQrl.call( + loaderRequestEv as unknown as ServerRequestEventLoader, + loaderRequestEv + ); + const value = typeof resolved === 'function' ? resolved() : resolved; + if (isDev) { + verifySerializable(value, loaderQrl); + } + return value; +}; + +export const loadRouteLoaderByQrl = ( + loaderId: string, + loaderQrl: QRL<(event: RequestEventLoader) => unknown>, + validators: DataValidator[] | undefined, + requestEv: RequestEvent +) => { + const values = getRouteLoaderValues(requestEv); + if (loaderId in values) { + return Promise.resolve(values[loaderId]); + } + + const promises = getRouteLoaderPromises(requestEv); + let promise = promises[loaderId]; + if (!promise) { + promise = getRouteLoaderData(loaderQrl, validators, requestEv).then( + (value) => { + values[loaderId] = value; + return value; + }, + (err) => { + delete promises[loaderId]; + throw err; + } + ); + promises[loaderId] = promise; + } + return promise; +}; + +export const loadRouteLoader = (loader: LoaderInternal, requestEv: RequestEvent) => + loadRouteLoaderByQrl(loader.__id, loader.__qrl, loader.__validators, requestEv); + +/** Run a loader and wrap the result in a LoaderResponse envelope. Catches redirects/errors. */ +export const getRouteLoaderResponse = async ( + loaderQrl: QRL<(event: RequestEventLoader) => unknown>, + validators: DataValidator[] | undefined, + requestEv: RequestEvent +): Promise => { + try { + const value = await getRouteLoaderData(loaderQrl, validators, requestEv); + if (value && typeof value === 'object' && (value as any).failed) { + return { e: new ServerError(requestEv.status(), value) }; + } + return { d: value }; + } catch (err) { + if (err instanceof RedirectMessage) { + const location = requestEv.headers.get('Location') || '/'; + requestEv.headers.delete('Location'); + return { r: location }; + } + if (err instanceof ServerError) { + return { e: err }; + } + throw err; + } +}; + +/** @internal */ +export const routeLoaderQrl = (( + loaderQrl: QRL<(event: RequestEventLoader) => unknown>, + ...rest: (LoaderOptions | DataValidator)[] +): LoaderInternal => { + const { validators, serializationStrategy, expires, poll, eTag, search, allowStale } = + getLoaderOptions(rest); + + function loader() { + const state = _resolveContextWithoutSequentialScope(RouteStateContext)!; + let signal = state[loader.__id]; + if (!signal) { + const routeLoaderCtx = _resolveContextWithoutSequentialScope(RouteLoaderCtxContext)!; + signal = ensureRouteLoaderSignal(loader, state, routeLoaderCtx); + } + void signal.promise(); + return signal; + } + + loader.__brand = 'server_loader' as const; + loader.__qrl = loaderQrl; + loader.__validators = validators; + loader.__id = loaderQrl.getHash(); + loader.__serializationStrategy = serializationStrategy; + loader.__expires = expires ?? 120_000; // 2 minutes + loader.__poll = poll ?? false; + loader.__eTag = eTag; + loader.__search = search; + loader.__allowStale = allowStale; + Object.freeze(loader); + return loader; +}) as LoaderConstructorQRL; + +/** + * Define a route loader that fetches data before the route renders. + * + * Route loaders run on the server during SSR and return data as an `AsyncSignal`. On the client, + * loaders automatically re-fetch when the route changes (SPA navigation). Each loader gets its own + * JSON endpoint (`q-loader-{id}.{hash}.json`), so only the loaders present on the target route are + * fetched. + * + * **Important:** Route loader data uses Qwik's custom serialization format, not standard JSON. This + * means the data supports features like circular references, Dates, and other non-JSON types, but + * it cannot be consumed by external clients expecting plain JSON. + * + * ## Options + * + * - `search: string[]` — Allowlist of URL search params the loader depends on. Only listed params are + * sent in the request and changes to other params are ignored. `search: []` means no search + * params are sent and only route path changes trigger a re-fetch. + * - `allowStale: false` — Clears the previous value when re-fetching, so components see a loading + * state instead of stale data during navigation. Useful when old data would be confusing. + * - `eTag` — Enable ETag-based caching. Can be `true` (auto-hash), a string, or a function. + * - `expires` / `poll` — Control client-side caching and polling behavior. + * + * The `strictLoaders` Vite plugin option applies `search: []` globally for all loaders that don't + * specify an explicit `search` option. + * + * @public + */ +export const routeLoader$: LoaderConstructor = /*#__PURE__*/ implicit$FirstArg(routeLoaderQrl); + +async function runValidators( + requestEv: RequestEvent, + validators: DataValidator[] | undefined, + data: unknown +) { + let lastResult: ValidatorReturn = { + success: true, + data, + }; + if (validators) { + for (let i = 0; i < validators.length; i++) { + const validator = validators[i]; + lastResult = await validator.validate(requestEv, data); + if (!lastResult.success) { + return lastResult; + } + data = lastResult.data; + } + } + return lastResult; +} + +function verifySerializable(data: any, qrl: QRL) { + try { + _verifySerializable(data, undefined); + } catch (error: any) { + if (error instanceof Error && qrl.dev) { + (error as any).loc = qrl.dev; + } + throw error; + } +} diff --git a/packages/qwik-router/src/runtime/src/route-loaders.unit.ts b/packages/qwik-router/src/runtime/src/route-loaders.unit.ts new file mode 100644 index 00000000000..0289d12b71d --- /dev/null +++ b/packages/qwik-router/src/runtime/src/route-loaders.unit.ts @@ -0,0 +1,86 @@ +import { describe, expect, it, vi } from 'vitest'; +import { loadRouteLoader, routeLoaderQrl } from './route-loaders'; +import type { LoaderInternal } from './types'; + +describe('search filter early-return logic', () => { + // Mirrors the closure variables and condition in createRouteLoaderSignal. + // Before the fix, only filteredSearch was checked — path changes were silently suppressed. + it('does not skip fetch when routePath changes even if filtered search is unchanged', () => { + let lastFilteredSearch: string | undefined; + let lastRoutePath: string | undefined; + + const shouldSkipFetch = (routePath: string, filteredSearch: string) => { + const hasPrevious = lastFilteredSearch !== undefined; + if (hasPrevious && filteredSearch === lastFilteredSearch && routePath === lastRoutePath) { + return true; + } + lastFilteredSearch = filteredSearch; + lastRoutePath = routePath; + return false; + }; + + // First call — no previous value, always fetches + expect(shouldSkipFetch('/a', 'page=1')).toBe(false); + // Same path + same search: skip is correct + expect(shouldSkipFetch('/a', 'page=1')).toBe(true); + // Different path, same filtered search: must NOT skip (was the bug) + expect(shouldSkipFetch('/b', 'page=1')).toBe(false); + // Same path + same search again: skip is correct + expect(shouldSkipFetch('/b', 'page=1')).toBe(true); + // Same path, different search: fetch + expect(shouldSkipFetch('/b', 'page=2')).toBe(false); + }); +}); + +describe('route loader execution', () => { + it('memoizes in-flight loader executions on the request', async () => { + const requestEv: any = { + sharedMap: new Map(), + }; + + const parentLoader = createLoader('parent', async () => 'parent-value'); + const childLoader = createLoader('child', async (_thisArg, ev) => { + const value = await ev.resolveValue(parentLoader); + return `child:${value}`; + }); + + requestEv.resolveValue = (loader: LoaderInternal) => loadRouteLoader(loader, requestEv); + + await expect( + Promise.all([ + loadRouteLoader(parentLoader, requestEv), + loadRouteLoader(childLoader, requestEv), + ]) + ).resolves.toEqual(['parent-value', 'child:parent-value']); + expect(parentLoader.__qrl.call).toHaveBeenCalledOnce(); + }); + + it('stores loader expires values in milliseconds', () => { + const loader = routeLoaderQrl(createQrl('timed-loader'), { expires: 60_000 }) as LoaderInternal; + + expect(loader.__expires).toBe(60_000); + }); +}); + +function createLoader(id: string, fn: (thisArg: unknown, ev: any) => unknown): LoaderInternal { + return { + __brand: 'server_loader', + __id: id, + __qrl: createQrl(id, fn), + __validators: undefined, + __serializationStrategy: 'never', + __expires: 0, + __poll: false, + __eTag: undefined, + __search: undefined, + __allowStale: true, + } as any; +} + +function createQrl(id: string, fn: (thisArg: unknown, ev: any) => unknown = async () => undefined) { + return { + call: vi.fn(fn), + getHash: () => id, + getSymbol: () => id, + } as any; +} diff --git a/packages/qwik-router/src/runtime/src/routing.ts b/packages/qwik-router/src/runtime/src/routing.ts index 32661fe46a4..bbefc59b7fd 100644 --- a/packages/qwik-router/src/runtime/src/routing.ts +++ b/packages/qwik-router/src/runtime/src/routing.ts @@ -1,4 +1,5 @@ import type { ValueOrPromise } from '@qwik.dev/core'; +import { ensureSlash } from '../../utils/pathname'; import { deepFreeze } from './deepFreeze'; import { type ContentMenu, @@ -18,8 +19,7 @@ const MODULE_CACHE = /*#__PURE__*/ new WeakMap(); export const loadRoute = async ( routes: RouteData | undefined, cacheModules: boolean | undefined, - pathname: string, - isInternal?: boolean + pathname: string ): Promise => { const result = matchRouteTree(routes, pathname); @@ -29,6 +29,8 @@ export const loadRoute = async ( routeParts, notFound, routeBundleNames, + loaderHashes, + loaderPathsByHash, menuLoader, errorLoader, notFoundLoader, @@ -39,25 +41,23 @@ export const loadRoute = async ( const modules: RouteModule[] = new Array(loaders.length); const pendingLoads: Promise[] = []; - loaders.forEach((moduleLoader, i) => { + for (let i = 0; i < loaders.length; i++) { + const moduleLoader = loaders[i]; loadModule( moduleLoader, pendingLoads, (routeModule) => (modules[i] = routeModule), cacheModules ); - }); + } let menu: ContentMenu | undefined = undefined; - // No need to load menu for internal QData requests - if (!isInternal) { - loadModule( - menuLoader, - pendingLoads, - (menuModule) => (menu = menuModule?.default), - cacheModules - ); - } + loadModule( + menuLoader, + pendingLoads, + (menuModule) => (menu = menuModule?.default), + cacheModules + ); // For not-found routes, create a wrapper module that renders 404.tsx for 404 status // and the default error handler for other statuses, with cacheKey based on status. @@ -90,6 +90,8 @@ export const loadRoute = async ( $routeBundleNames$: routeBundleNames, $notFound$: notFound, $errorLoader$: errorLoader, + $loaders$: loaderHashes, + $loaderPaths$: loaderPathsByHash, }; } @@ -105,6 +107,8 @@ export const loadRoute = async ( $routeBundleNames$: routeBundleNames, $notFound$: notFound, $errorLoader$: errorLoader, + $loaders$: loaderHashes, + $loaderPaths$: loaderPathsByHash, }; }; @@ -124,12 +128,14 @@ function walkTrieKeys( if (node._L) { layouts.push(node._L); } - for (const key of keys) { + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; let next = node[key] as RouteData | undefined; // If not a direct child, search inside _M group nodes if (!next && node._M) { - for (const group of node._M) { + for (let j = 0; j < node._M.length; j++) { + const group = node._M[j]; next = group[key] as RouteData | undefined; if (next) { // Collect the group's layout @@ -179,7 +185,8 @@ function resolveLoaders( if (!targetNode._I && targetNode._G == null) { const indexResult = findIndexNode(targetNode); if (indexResult) { - for (const g of indexResult.groups) { + for (let j = 0; j < indexResult.groups.length; j++) { + const g = indexResult.groups[j]; if (g._L) { targetLayouts.push(g._L); } @@ -224,12 +231,25 @@ function collectNodeMeta( layouts: ModuleLoader[], errorLoaderRef: { v: ContentModuleLoader | undefined }, notFoundLoaderRef: { v: ContentModuleLoader | undefined }, - menuLoaderRef: { v: MenuModuleLoader | undefined } + menuLoaderRef: { v: MenuModuleLoader | undefined }, + loaderHashes?: string[], + loaderPathsByHash?: Record, + matchedPathname = '/' ) { - for (const g of groups) { + for (let j = 0; j < groups.length; j++) { + const g = groups[j]; if (g._L) { layouts.push(g._L); } + if (g._R && loaderHashes) { + loaderHashes.push(...g._R); + if (loaderPathsByHash) { + for (let i = 0; i < g._R.length; i++) { + const hash = g._R[i]; + loaderPathsByHash[hash] = matchedPathname; + } + } + } if (g._E) { errorLoaderRef.v = g._E; } @@ -243,6 +263,15 @@ function collectNodeMeta( if (node._L) { layouts.push(node._L); } + if (node._R && loaderHashes) { + loaderHashes.push(...node._R); + if (loaderPathsByHash) { + for (let i = 0; i < node._R.length; i++) { + const hash = node._R[i]; + loaderPathsByHash[hash] = matchedPathname; + } + } + } if (node._E) { errorLoaderRef.v = node._E; } @@ -254,10 +283,26 @@ function collectNodeMeta( } } +enum ChildMatchKind { + Exact, + Wildcard, + Rest, +} + +interface ChildMatch { + next: RouteData; + groups: RouteData[]; + routePart: string; + done: boolean; + kind: ChildMatchKind; + paramName?: string; + paramValue?: string; +} + /** * Try to find a child matching `partLower` in `node`, including inside group children (_M). Returns - * the matched child node, the groups entered to reach it, and what to push to routeParts. Returns - * undefined if no match. + * the matched child node, the groups entered to reach it, what to push to routeParts, and which + * kind of edge matched. Returns undefined if no match. * * Priority: exact match → _M groups (recursively) → _W wildcard → _A rest wildcard. This ensures * exact matches inside nested groups take precedence over wildcards at parent level. @@ -267,21 +312,27 @@ function findChild( part: string, partLower: string, parts: string[], - partIndex: number, - params: PathParams -): { next: RouteData; groups: RouteData[]; routePart: string; done: boolean } | undefined { + partIndex: number +): ChildMatch | undefined { // 1. Try exact match on this node's direct children const exact = node[partLower] as RouteData | undefined; if (exact) { - return { next: exact, groups: [], routePart: part, done: false }; + return { + next: exact, + groups: [], + routePart: part, + done: false, + kind: ChildMatchKind.Exact, + }; } // 2. Try matching inside group children (_M array). // Groups can have their own exact matches and wildcards, so check them // before this node's wildcards. if (node._M) { - for (const group of node._M) { - const groupResult = findChild(group, part, partLower, parts, partIndex, params); + for (let j = 0; j < node._M.length; j++) { + const group = node._M[j]; + const groupResult = findChild(group, part, partLower, parts, partIndex); if (groupResult) { // Prepend this group to the groups chain groupResult.groups.unshift(group); @@ -291,7 +342,7 @@ function findChild( } // 3. Try wildcard [param] on this node - const wildcard = tryWildcardMatch(node, part, partLower, parts, partIndex, params); + const wildcard = tryWildcardMatch(node, part, partLower, parts, partIndex); if (wildcard) { return { ...wildcard, groups: [] }; } @@ -305,9 +356,8 @@ function tryWildcardMatch( part: string, partLower: string, parts: string[], - partIndex: number, - params: PathParams -): { next: RouteData; routePart: string; done: boolean } | undefined { + partIndex: number +): Omit | undefined { // Wildcard [param] let next = node._W as RouteData | undefined; if (next) { @@ -323,13 +373,25 @@ function tryWildcardMatch( ) { const paramName = next._P!; const value = part.slice(pre.length, suf ? part.length - suf.length : undefined); - params[paramName] = value; - return { next, routePart: `${pre}[${paramName}]${suf}`, done: false }; + return { + next, + routePart: `${pre}[${paramName}]${suf}`, + done: false, + kind: ChildMatchKind.Wildcard, + paramName, + paramValue: value, + }; } } else { const paramName = next._P!; - params[paramName] = part; - return { next, routePart: `[${paramName}]`, done: false }; + return { + next, + routePart: `[${paramName}]`, + done: false, + kind: ChildMatchKind.Wildcard, + paramName, + paramValue: part, + }; } } @@ -338,8 +400,14 @@ function tryWildcardMatch( if (next) { const paramName = next._P!; const restValue = parts.slice(partIndex).join('/'); - params[paramName] = restValue; - return { next, routePart: `[...${paramName}]`, done: true }; + return { + next, + routePart: `[...${paramName}]`, + done: true, + kind: ChildMatchKind.Rest, + paramName, + paramValue: restValue, + }; } return undefined; @@ -354,7 +422,8 @@ function findIndexNode(node: RouteData): { target: RouteData; groups: RouteData[ return { target: node, groups: [] }; } if (node._M) { - for (const group of node._M) { + for (let j = 0; j < node._M.length; j++) { + const group = node._M[j]; const result = findIndexNode(group); if (result) { // Only add the group to the chain if it's not already the target @@ -378,7 +447,8 @@ function findRestNode(node: RouteData): { next: RouteData; groups: RouteData[] } return { next: node._A as RouteData, groups: [] }; } if (node._M) { - for (const group of node._M) { + for (let j = 0; j < node._M.length; j++) { + const group = node._M[j]; const result = findRestNode(group); if (result) { result.groups.unshift(group); @@ -408,6 +478,8 @@ function matchRouteTree( routeParts: string[]; notFound: boolean; routeBundleNames: string[] | undefined; + loaderHashes: string[] | undefined; + loaderPathsByHash: Record | undefined; menuLoader: MenuModuleLoader | undefined; /** The nearest _E (error.tsx) loader in the ancestor chain */ errorLoader: ContentModuleLoader | undefined; @@ -418,6 +490,8 @@ function matchRouteTree( const params: PathParams = {}; const routeParts: string[] = []; const layouts: ModuleLoader[] = []; + const loaderHashes: string[] = []; + const loaderPathsByHash: Record = {}; const errorLoaderRef: { v: ContentModuleLoader | undefined } = { v: undefined }; const notFoundLoaderRef: { v: ContentModuleLoader | undefined } = { v: undefined }; const menuLoaderRef: { v: MenuModuleLoader | undefined } = { v: undefined }; @@ -426,6 +500,13 @@ function matchRouteTree( if (root._L) { layouts.push(root._L); } + if (root._R) { + loaderHashes.push(...root._R); + for (let i = 0; i < root._R.length; i++) { + const hash = root._R[i]; + loaderPathsByHash[hash] = '/'; + } + } if (root._E) { errorLoaderRef.v = root._E; } @@ -454,6 +535,7 @@ function matchRouteTree( routeParts: string[]; params: PathParams; layouts: ModuleLoader[]; + loaderPathsByHash: Record; errorLoader: ContentModuleLoader | undefined; notFoundLoader: ContentModuleLoader | undefined; menuLoader: MenuModuleLoader | undefined; @@ -466,9 +548,15 @@ function matchRouteTree( const part = parts[i]; const partLower = part.toLowerCase(); - // Before matching this segment, check if the current node has _A as a fallback. - // If the primary match (_W or exact) eventually leads to no route, we can fall back here. - const restInfo = findRestNode(node); + const found = findChild(node, part, partLower, parts, i); + if (!found) { + matched = false; + break; + } + + // Wildcard route subtrees can still fall back to a sibling rest route if they dead-end later. + // Exact route subtrees own their unmatched descendants. + const restInfo = found.kind === ChildMatchKind.Wildcard ? findRestNode(node) : undefined; if (restInfo) { restFallback = { aNode: restInfo.next, @@ -478,22 +566,32 @@ function matchRouteTree( routeParts: [...routeParts], params: { ...params }, layouts: [...layouts], + loaderPathsByHash: { ...loaderPathsByHash }, errorLoader: errorLoaderRef.v, notFoundLoader: notFoundLoaderRef.v, menuLoader: menuLoaderRef.v, }; } - const found = findChild(node, part, partLower, parts, i, params); - if (!found) { - matched = false; - break; + if (found.paramName) { + params[found.paramName] = found.paramValue!; } routeParts.push(found.routePart); done = found.done; node = found.next; - collectNodeMeta(node, found.groups, layouts, errorLoaderRef, notFoundLoaderRef, menuLoaderRef); + const matchedPathname = `/${parts.slice(0, found.done ? len : i + 1).join('/')}/`; + collectNodeMeta( + node, + found.groups, + layouts, + errorLoaderRef, + notFoundLoaderRef, + menuLoaderRef, + loaderHashes, + loaderPathsByHash, + matchedPathname + ); } // If we consumed all parts but the current node has no _I, @@ -511,7 +609,10 @@ function matchRouteTree( layouts, errorLoaderRef, notFoundLoaderRef, - menuLoaderRef + menuLoaderRef, + loaderHashes, + loaderPathsByHash, + pathname ); node = indexResult.target; } @@ -546,7 +647,10 @@ function matchRouteTree( layouts, errorLoaderRef, notFoundLoaderRef, - menuLoaderRef + menuLoaderRef, + loaderHashes, + loaderPathsByHash, + pathname ); node = next; } @@ -564,11 +668,23 @@ function matchRouteTree( const fbParams = { ...fb.params, [fb.paramName]: fb.restValue }; const fbRouteParts = [...fb.routeParts, `[...${fb.paramName}]`]; const fbLayouts = [...fb.layouts]; + const fbLoaderPathsByHash = { ...fb.loaderPathsByHash }; const fbErrorRef: { v: ContentModuleLoader | undefined } = { v: fb.errorLoader }; const fbNotFoundRef: { v: ContentModuleLoader | undefined } = { v: fb.notFoundLoader }; const fbMenuRef: { v: MenuModuleLoader | undefined } = { v: fb.menuLoader }; - collectNodeMeta(fb.aNode, fb.groups, fbLayouts, fbErrorRef, fbNotFoundRef, fbMenuRef); + const fbLoaderHashes: string[] = []; + collectNodeMeta( + fb.aNode, + fb.groups, + fbLayouts, + fbErrorRef, + fbNotFoundRef, + fbMenuRef, + fbLoaderHashes, + fbLoaderPathsByHash, + pathname + ); const fbLoaders = resolveLoaders(root, fb.aNode, fbLayouts); if (fbLoaders) { @@ -578,6 +694,9 @@ function matchRouteTree( routeParts: fbRouteParts, notFound: false, routeBundleNames: fb.aNode._B as string[] | undefined, + loaderHashes: fbLoaderHashes.length > 0 ? fbLoaderHashes : undefined, + loaderPathsByHash: + Object.keys(fbLoaderPathsByHash).length > 0 ? fbLoaderPathsByHash : undefined, menuLoader: fbMenuRef.v, errorLoader: fbErrorRef.v, notFoundLoader: fbNotFoundRef.v, @@ -598,18 +717,32 @@ function matchRouteTree( routeParts, notFound: true, routeBundleNames: undefined, + loaderHashes: undefined, + loaderPathsByHash: undefined, menuLoader: menuLoaderRef.v, errorLoader: errorLoaderRef.v, notFoundLoader: notFoundLoaderRef.v, }; } + // Also collect _R from the final matched node (page-level loaders) + if (node._R) { + loaderHashes.push(...node._R); + const matchedPathname = ensureSlash(pathname); + for (let i = 0; i < node._R.length; i++) { + const hash = node._R[i]; + loaderPathsByHash[hash] = matchedPathname; + } + } + return { loaders, params, routeParts, notFound: false, routeBundleNames: node._B as string[] | undefined, + loaderHashes: loaderHashes.length > 0 ? loaderHashes : undefined, + loaderPathsByHash: Object.keys(loaderPathsByHash).length > 0 ? loaderPathsByHash : undefined, menuLoader: menuLoaderRef.v, errorLoader: errorLoaderRef.v, notFoundLoader: notFoundLoaderRef.v, diff --git a/packages/qwik-router/src/runtime/src/routing.unit.ts b/packages/qwik-router/src/runtime/src/routing.unit.ts index 39de9244944..e75f101d4b2 100644 --- a/packages/qwik-router/src/runtime/src/routing.unit.ts +++ b/packages/qwik-router/src/runtime/src/routing.unit.ts @@ -225,6 +225,28 @@ test('loadRoute — root route matches /', async () => { assert.deepEqual(result.$params$, {}); }); +test('loadRoute — loader paths are replaced by deeper matches', async () => { + const routes: RouteData = { + _R: ['shared-loader'], + products: { + _W: { + _P: 'id', + _R: ['shared-loader'], + view: { + _I: makeLoader(), + }, + }, + }, + }; + + const result = await loadRoute(routes, false, '/products/123/view'); + + assert.isFalse(result.$notFound$); + assert.deepEqual(result.$loaderPaths$, { + 'shared-loader': '/products/123/', + }); +}); + test('loadRoute — 404 fallback used when no route matches', async () => { const marker = () => 'not-found-sentinel'; const sentinel = { default: marker }; @@ -554,14 +576,35 @@ test('loadRoute — _A fallback with multi-segment rest value', async () => { const catchallLoader = makeLoader(); const routes: RouteData = { _A: { _P: 'rest', _I: catchallLoader }, - blog: { _W: { _P: 'slug' } }, // no _I on slug — dead end + _W: { _P: 'section', _W: { _P: 'slug' } }, // no _I on slug — dead end }; - // /blog/post/extra — _W matches "post" but no _I, fallback to _A + // /blog/post/extra — _W matches but no _I, fallback to _A const result = await loadRoute(routes, false, '/blog/post/extra'); assert.isFalse(result.$notFound$); assert.deepEqual(result.$params$, { rest: 'blog/post/extra' }); }); +test('loadRoute — exact child dead end does not fall back to sibling _M catchall', async () => { + const catchallLoader = makeLoader(); + const routes: RouteData = { + _4: makeLoader(), + _M: [ + { + _A: { _P: 'catchall', _I: catchallLoader }, + }, + ], + 'loader-redirect': { + source: { + _I: makeLoader(), + }, + }, + }; + + const result = await loadRoute(routes, false, '/loader-redirect/notexist'); + assert.isTrue(result.$notFound$); + assert.notDeepEqual(result.$params$, { catchall: 'loader-redirect/notexist' }); +}); + // ─── Menu (_N) trie tests ─────────────────────────────────────────────────────── test('loadRoute — _N menu from ancestor is used for child route', async () => { @@ -642,20 +685,6 @@ test('loadRoute — _N in _M group is picked up', async () => { assert.deepEqual(result.$menu$, { text: 'Group Menu' }); }); -test('loadRoute — _N not loaded for internal requests', async () => { - const menuLoader: MenuModuleLoader = async () => ({ default: { text: 'Docs' } }); - const pageLoader = makeLoader(); - const routes: RouteData = { - _N: menuLoader, - blog: { - _I: pageLoader, - }, - }; - const result = await loadRoute(routes, false, '/blog', true /* isInternal */); - assert.isFalse(result.$notFound$); - assert.isUndefined(result.$menu$); -}); - // ─── Empty node tests ───────────────────────────────────────────────────────── test('loadRoute — empty object leaf node is a 404 (no false match)', async () => { diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index b10357a36c9..283e9b418c0 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -4,32 +4,17 @@ import { isDev, isServer, noSerialize, - untrack, useStore, withLocale, type QRL, type ValueOrPromise, } from '@qwik.dev/core'; -import { - _deserialize, - _getContextHostElement, - _getContextEvent, - _resolveContextWithoutSequentialScope, - _serialize, - type SerializationStrategy, -} from '@qwik.dev/core/internal'; -import { _asyncRequestStore } from '@qwik.dev/router/middleware/request-handler'; +import { _deserialize, _getContextHostElement, _serialize } from '@qwik.dev/core/internal'; import * as v from 'valibot'; import * as z from 'zod'; -import type { RequestEventLoader } from '../../middleware/request-handler/types'; -import { - DEFAULT_LOADERS_SERIALIZATION_STRATEGY, - QACTION_KEY, - QDATA_KEY, - QFN_KEY, -} from './constants'; -import { RouteStateContext } from './contexts'; +import { QACTION_KEY, QDATA_KEY, QFN_KEY } from './constants'; import type { FormSubmitCompletedDetail } from './form-component'; +import { getRequestEvent } from './route-loaders'; import type { ActionConstructor, ActionConstructorQRL, @@ -39,10 +24,6 @@ import type { DataValidator, Editable, JSONObject, - LoaderConstructor, - LoaderConstructorQRL, - LoaderInternal, - LoaderOptions, RequestEvent, RequestEventAction, RequestEventBase, @@ -61,14 +42,14 @@ import type { ZodConstructorQRL, ZodDataValidator, } from './types'; -import { useAction, useLocation, useQwikRouterEnv } from './use-functions'; +import { useAction, useLocation } from './use-functions'; /** @internal */ export const routeActionQrl = (( actionQrl: QRL<(form: JSONObject, event: RequestEventAction) => unknown>, ...rest: (ActionOptions | DataValidator)[] ) => { - const { id, validators } = getValidators(rest, actionQrl); + const { id, validators, invalidate } = getValidators(rest, actionQrl); function action() { const loc = useLocation() as Editable; const currentAction = useAction(); @@ -162,6 +143,7 @@ Action.run() can only be called on the browser, for example when a user clicks a action.__validators = validators; action.__qrl = actionQrl; action.__id = id; + action.__invalidate = invalidate; Object.freeze(action); return action satisfies ActionInternal; @@ -182,7 +164,26 @@ export const globalActionQrl = (( return action; }) as ActionConstructorQRL; -/** @public */ +/** + * Define a route action that handles form submissions or programmatic invocations. + * + * Actions run on the server when submitted from the client. The result is returned as an + * `ActionStore` with `.value` for success data and `.error` for errors (including validation errors + * from `zod$`/`valibot$` where `.fieldErrors` etc. are accessible directly on `.error`). + * + * By default, after an action completes, ALL current route loaders are invalidated on the client + * and re-fetched as needed (so that the browser cache is correct). This can be controlled with: + * + * - `invalidate: [loader1, loader2]` — Only invalidate specific loaders. The client re-fetches them + * individually. Other loaders keep their current data. + * - `invalidate: []` — No loaders are invalidated. The action response only contains the action + * result. Use this when the action doesn't affect any loader data. + * + * The `strictLoaders` Vite plugin option applies `invalidate: []` globally for all actions that + * don't specify an explicit `invalidate` option. + * + * @public + */ export const routeAction$: ActionConstructor = /*#__PURE__*/ implicit$FirstArg( routeActionQrl ) as any; @@ -192,49 +193,6 @@ export const globalAction$: ActionConstructor = /*#__PURE__*/ implicit$FirstArg( globalActionQrl ) as any; -const getValue = (obj: T) => obj.value; -/** @internal */ -export const routeLoaderQrl = (( - loaderQrl: QRL<(event: RequestEventLoader) => unknown>, - ...rest: (LoaderOptions | DataValidator)[] -): LoaderInternal => { - const { id, validators, serializationStrategy } = getValidators(rest, loaderQrl); - function loader() { - const state = _resolveContextWithoutSequentialScope(RouteStateContext)!; - - if (!(id in state)) { - throw new Error(`routeLoader$ "${loaderQrl.getSymbol()}" was invoked in a route where it was not declared. - This is because the routeLoader$ was not exported in a 'layout.tsx' or 'index.tsx' file of the existing route. - For more information check: https://qwik.dev/docs/route-loader/ - - If your are managing reusable logic or a library it is essential that this function is re-exported from within 'layout.tsx' or 'index.tsx file of the existing route otherwise it will not run or throw exception. - For more information check: https://qwik.dev/docs/re-exporting-loaders/`); - } - const loaderData = state[id]; - // Force the signal to be initialized. - // It is an async computed signal. - // We have two options: - // - we already have data from signal or from previous fetch - // - we don't have data yet, so we need to fetch it from the server - // Calling it will trigger fetch the data from the server. - // Under the hood, it will throw a promise and await for it, so the client will load the data synchronously. - untrack(getValue, loaderData); - return loaderData; - } - loader.__brand = 'server_loader' as const; - loader.__qrl = loaderQrl; - loader.__validators = validators; - loader.__id = id; - loader.__serializationStrategy = serializationStrategy; - loader.__expires = -1; // -1 means no expiration - Object.freeze(loader); - - return loader; -}) as LoaderConstructorQRL; - -/** @public */ -export const routeLoader$: LoaderConstructor = /*#__PURE__*/ implicit$FirstArg(routeLoaderQrl); - /** @internal */ export const validatorQrl = (( validator: QRL<(ev: RequestEvent, data: unknown) => ValueOrPromise> @@ -417,19 +375,7 @@ export const serverQrl = ( if (isServer) { // Running during SSR, we can call the function directly - let requestEvent = _asyncRequestStore?.getStore() as RequestEvent | undefined; - - if (!requestEvent) { - const contexts = [useQwikRouterEnv()?.ev, this, _getContextEvent()] as RequestEvent[]; - requestEvent = contexts.find( - (v) => - v && - Object.prototype.hasOwnProperty.call(v, 'sharedMap') && - Object.prototype.hasOwnProperty.call(v, 'cookie') - ); - } - - return qrl.apply(requestEvent, args); + return qrl.apply(getRequestEvent(this), args); } else { // Running on the client, we need to call the function via HTTP let filteredArgs: unknown[] | undefined = args.map((arg: unknown) => { @@ -518,9 +464,9 @@ export const serverQrl = ( /** @public */ export const server$ = /*#__PURE__*/ implicit$FirstArg(serverQrl); -const getValidators = (rest: (LoaderOptions | DataValidator)[], qrl: QRL) => { +const getValidators = (rest: (ActionOptions | DataValidator)[], qrl: QRL) => { let id: string | undefined; - let serializationStrategy: SerializationStrategy = DEFAULT_LOADERS_SERIALIZATION_STRATEGY; + let invalidate: string[] | undefined; const validators: DataValidator[] = []; if (rest.length === 1) { const options = rest[0]; @@ -529,12 +475,14 @@ const getValidators = (rest: (LoaderOptions | DataValidator)[], qrl: QRL) => { validators.push(options); } else { id = options.id; - if (options.serializationStrategy) { - serializationStrategy = options.serializationStrategy; - } if (options.validation) { validators.push(...options.validation); } + if ((options as ActionOptions).invalidate) { + invalidate = (options as ActionOptions).invalidate!.map( + (loader: any) => loader.__id as string + ); + } } } } else if (rest.length > 1) { @@ -554,7 +502,7 @@ const getValidators = (rest: (LoaderOptions | DataValidator)[], qrl: QRL) => { return { validators: validators.reverse(), id, - serializationStrategy, + invalidate, }; }; diff --git a/packages/qwik-router/src/runtime/src/spa-init.ts b/packages/qwik-router/src/runtime/src/spa-init.ts index 7943a40c784..7a76fd0ef67 100644 --- a/packages/qwik-router/src/runtime/src/spa-init.ts +++ b/packages/qwik-router/src/runtime/src/spa-init.ts @@ -197,16 +197,14 @@ export default event$((_: Event, el: Element) => { window._qRouterScrollEnabled = true; - setTimeout(() => { - window.addEventListener('popstate', window._qRouterInitPopstate!); - window.addEventListener('scroll', window._qRouterInitScroll!, { passive: true }); - document.addEventListener('click', window._qRouterInitAnchors!); - - if (!(window as any).navigation) { - document.addEventListener('visibilitychange', window._qRouterInitVisibility!, { - passive: true, - }); - } - }, 0); + window.addEventListener('popstate', window._qRouterInitPopstate!); + window.addEventListener('scroll', window._qRouterInitScroll!, { passive: true }); + document.addEventListener('click', window._qRouterInitAnchors!); + + if (!(window as any).navigation) { + document.addEventListener('visibilitychange', window._qRouterInitVisibility!, { + passive: true, + }); + } } }); diff --git a/packages/qwik-router/src/runtime/src/typed-routes.ts b/packages/qwik-router/src/runtime/src/typed-routes.ts index 9c58d070356..f6521d6a97a 100644 --- a/packages/qwik-router/src/runtime/src/typed-routes.ts +++ b/packages/qwik-router/src/runtime/src/typed-routes.ts @@ -1,3 +1,5 @@ +import { ensureSlash } from '../../utils/pathname'; + /** * @beta * @experimental @@ -23,9 +25,7 @@ export const untypedAppUrl = function appUrl( let url = path.join('/'); let baseURL = import.meta.env?.BASE_URL; if (baseURL) { - if (!baseURL.endsWith('/')) { - baseURL += '/'; - } + baseURL = ensureSlash(baseURL); while (url.startsWith('/')) { url = url.substring(1); } diff --git a/packages/qwik-router/src/runtime/src/types.ts b/packages/qwik-router/src/runtime/src/types.ts index d2a0ee7ce76..9446d35c78d 100644 --- a/packages/qwik-router/src/runtime/src/types.ts +++ b/packages/qwik-router/src/runtime/src/types.ts @@ -20,6 +20,7 @@ import type { import type * as v from 'valibot'; import type * as z from 'zod'; import type { Q_ROUTE } from './constants'; +import type { RouteLoaderCtx } from './route-loaders'; export type { Cookie, @@ -276,8 +277,6 @@ export type EndpointModuleLoader = () => ValueOrPromise; export type ModuleLoader = ContentModuleLoader | EndpointModuleLoader; //| RouteLoaderLoader; export type MenuModuleLoader = () => Promise; -export type RouteLoaderInfo = [qrl: string, expires: number, live?: true]; - /** * A nested route trie structure. The root represents `/` and each level represents a URL segment. * @@ -320,6 +319,8 @@ export interface RouteData { _M?: RouteData[]; /** Menu loader for this subtree (from menu.md). Runtime uses nearest ancestor during traversal. */ _N?: MenuModuleLoader; + /** Array of routeLoader$ hashes for this node's loaders */ + _R?: string[]; /** Child route segments (any key not starting with `_`) */ [part: string]: | RouteData @@ -413,25 +414,19 @@ export interface LoadedRoute { $notFound$?: boolean; /** The error module loader (nearest _E ancestor), for rendering ServerErrors */ $errorLoader$?: ContentModuleLoader; + /** Merged array of routeLoader$ hashes from all matched nodes (layouts + page) */ + $loaders$?: string[]; + /** Runtime-only mapping of routeLoader$ hashes to the matched pathname used for q-loader fetches */ + $loaderPaths$?: Record; } export interface EndpointResponse { status: number; statusMessage?: string; - loaders: Record; - loadersSerializationStrategy: Map; formData?: FormData; action?: string; -} - -export interface ClientPageData extends Omit { - href: string; - redirect?: string; - isRewrite?: boolean; -} - -export interface LoaderData { - loaders: Record; + actionResult?: unknown; + loaderHashes?: string[]; } /** @public */ @@ -455,6 +450,8 @@ export interface QwikRouterEnvData { params: PathParams; response: EndpointResponse; loadedRoute: LoadedRoute; + routeLoaderCtx: RouteLoaderCtx; + loaderValues: Record; } /** @public The server data that is provided by Qwik Router during SSR rendering. It can be retrieved with `useServerData(key)` in the server, but it is not available in the client. */ @@ -521,6 +518,12 @@ export type GetValidatorType = export type ActionOptions = { readonly id?: string; readonly validation?: DataValidator[]; + /** + * Route loaders to invalidate after this action completes. The loader hooks' hashes are sent to + * the client so it knows which loaders to re-fetch. If omitted, ALL route loaders are invalidated + * (unless `strictLoaders` is enabled globally in the Vite plugin). + */ + readonly invalidate?: Loader[]; }; /** @public */ @@ -762,9 +765,54 @@ export type ActionConstructorQRL = { /** @public */ export type LoaderOptions = { + /** @deprecated Unused */ readonly id?: string; readonly validation?: DataValidator[]; readonly serializationStrategy?: SerializationStrategy; + /** + * Time in milliseconds after which the loader data is considered stale. The server derives + * `Cache-Control: max-age` seconds from this value on loader responses. + * + * On the client, the loader's AsyncSignal `expires` is set to this value. If `poll` is true, the + * signal auto-refetches when expired. If `poll` is false (default), the data is marked stale but + * not auto-refetched. + */ + readonly expires?: number; + /** + * When true AND `expires` is set, the loader data is automatically refetched when it expires + * (polling behavior). When false (default), expired data is marked stale but not auto-refetched. + */ + readonly poll?: boolean; + /** + * Enable ETag-based caching for this loader's JSON responses. + * + * - `true` — auto-compute an ETag by hashing the serialized response data (loader still runs) + * - `string` — static ETag value; if `If-None-Match` matches, the loader is skipped entirely + * - `(ev: RequestEvent) => string | null` — compute the ETag from the request context (params, URL, + * headers, etc.); if `If-None-Match` matches, the loader is skipped entirely. Return null to + * skip eTag for this request. + * + * When set, the server includes an `ETag` header on `q-loader-*.json` responses and returns `304` + * if the client sends a matching `If-None-Match` header. + */ + readonly eTag?: boolean | string | ((ev: RequestEvent) => string | null); + /** + * Allowlist of URL search parameter names that this loader depends on. + * + * When set, the loader only re-fetches when the listed search params change — other param changes + * are ignored. Only the listed params are sent in the loader JSON request URL. + * + * When not set, all search params are sent and any change triggers a re-fetch. + */ + readonly search?: string[]; + /** + * When true (default), the previous value is kept while the loader re-fetches after navigation, + * so components see stale data until the new response arrives. + * + * When false, the value is cleared on re-fetch, causing reads to suspend (show a loading + * boundary). This is useful when showing old data during navigation would be confusing. + */ + readonly allowStale?: boolean; }; /** @public */ @@ -911,7 +959,10 @@ export interface LoaderInternal extends Loader { __validators: DataValidator[] | undefined; __serializationStrategy: SerializationStrategy; __expires: number; - // __live: boolean; + __poll: boolean; + __eTag: boolean | string | ((ev: RequestEvent) => string | null) | undefined; + __search: string[] | undefined; + __allowStale: boolean; (): LoaderSignal; } @@ -930,6 +981,8 @@ export interface ActionInternal extends Action { __id: string; __qrl: QRL<(form: JSONObject, event: RequestEventAction) => ValueOrPromise>; __validators: DataValidator[] | undefined; + /** Loader hashes to invalidate after this action. Undefined = invalidate all. */ + __invalidate: string[] | undefined; (): ActionStore; } diff --git a/packages/qwik-router/src/runtime/src/use-endpoint.ts b/packages/qwik-router/src/runtime/src/use-endpoint.ts index 7142e458c7b..6f4764ed6fa 100644 --- a/packages/qwik-router/src/runtime/src/use-endpoint.ts +++ b/packages/qwik-router/src/runtime/src/use-endpoint.ts @@ -1,136 +1,80 @@ -import { getClientDataPath } from './utils'; -import { CLIENT_DATA_CACHE } from './constants'; -import type { ClientPageData, RouteActionValue } from './types'; +import type { RouteActionValue } from './types'; import { _deserialize } from '@qwik.dev/core/internal'; -import { preloadRouteBundles } from './client-navigate'; +import { ensureSlash } from '../../utils/pathname'; +import { QACTION_KEY } from './constants'; -const MAX_Q_DATA_RETRY_COUNT = 3; - -export const loadClientData = async ( - url: URL, - opts?: { - action?: RouteActionValue; - loaderIds?: string[]; - clearCache?: boolean; - preloadRouteBundles?: boolean; - isPrefetch?: boolean; - }, - retryCount: number = 0 -): Promise => { - const pagePathname = url.pathname; - const pageSearch = url.search; - const clientDataPath = getClientDataPath(pagePathname, pageSearch, { - actionId: opts?.action?.id, - loaderIds: opts?.loaderIds, - }); - let qData: Promise | undefined; - if (!opts?.action) { - qData = CLIENT_DATA_CACHE.get(clientDataPath); - } - - if (opts?.preloadRouteBundles !== false) { - preloadRouteBundles(pagePathname, 0.8); - } - let resolveFn: () => void | undefined; - - if (!qData) { - const fetchOptions = getFetchOptions(opts?.action, opts?.clearCache); - if (opts?.action) { - opts.action.data = undefined; +/** + * Submit an action to the server and get the result. + * + * POSTs to `/routePath/?qaction={actionId}` with `Accept: application/json`. The server runs the + * action and returns the action result, optionally with the loader hashes that should be + * invalidated. + */ +export async function submitAction( + action: NonNullable, + routePath: string +): Promise< + | { + status: number; + result: unknown; + redirect?: string; + loaderHashes?: string[]; } - qData = fetch(clientDataPath, fetchOptions).then((rsp) => { - if (rsp.status === 404 && opts?.loaderIds && retryCount < MAX_Q_DATA_RETRY_COUNT) { - // retry if the q-data.json is not found with all options - // we want to retry with all the loaders - opts.loaderIds = undefined; - return loadClientData(url, opts, retryCount + 1); - } - if (rsp.redirected) { - const redirectedURL = new URL(rsp.url); - const isQData = redirectedURL.pathname.endsWith('/q-data.json'); - if (!isQData || redirectedURL.origin !== location.origin) { - // Captive portal etc. We can't talk to the server, so redirect as asked, except when prefetching - if (!opts?.isPrefetch) { - location.href = redirectedURL.href; - } - return; - } - } - if ((rsp.headers.get('content-type') || '').includes('json')) { - // we are safe we are reading a q-data.json - return rsp.text().then((text) => { - const clientData = _deserialize(text); - if (!clientData) { - // Something went wrong, show to the user - location.href = url.href; - return; - } - if (opts?.clearCache) { - CLIENT_DATA_CACHE.delete(clientDataPath); - } - if (clientData.redirect) { - // server function asked for redirect - location.href = clientData.redirect; - } else if (opts?.action) { - const { action } = opts; - const actionData = clientData.loaders[action.id]; - resolveFn = () => { - action!.resolve!({ status: rsp.status, result: actionData }); - }; - } - return clientData; - }); - } else { - if (opts?.isPrefetch !== true) { - location.href = url.href; - } - return undefined; - } - }); + | undefined +> { + const pathBase = ensureSlash(routePath); + const url = `${pathBase}?${QACTION_KEY}=${encodeURIComponent(action.id)}`; - if (!opts?.action) { - CLIENT_DATA_CACHE.set(clientDataPath, qData); - } - } + const actionData = action.data; + // Clear immediately so a task rerun can't re-submit the same payload + action.data = undefined; + let fetchOptions: RequestInit; - return qData.then((v) => { - if (!v) { - CLIENT_DATA_CACHE.delete(clientDataPath); - } - resolveFn && resolveFn(); - return v; - }); -}; - -const getFetchOptions = ( - action: RouteActionValue | undefined, - noCache: boolean | undefined -): RequestInit | undefined => { - const actionData = action?.data; - if (!actionData) { - if (noCache) { - return { - cache: 'no-cache', - headers: { - 'Cache-Control': 'no-cache', - Pragma: 'no-cache', - }, - }; - } - return undefined; - } if (actionData instanceof FormData) { - return { + fetchOptions = { method: 'POST', body: actionData, + headers: { + Accept: 'application/json', + }, }; } else { - return { + fetchOptions = { method: 'POST', body: JSON.stringify(actionData), headers: { 'Content-Type': 'application/json; charset=UTF-8', + Accept: 'application/json', }, }; } -}; + + const response = await fetch(url, fetchOptions); + + if (response.redirected) { + const redirectedURL = new URL(response.url); + if (redirectedURL.origin !== location.origin) { + location.href = redirectedURL.href; + return undefined; + } + location.href = redirectedURL.href; + return undefined; + } + + if ((response.headers.get('content-type') || '').includes('json')) { + const text = await response.text(); + const data = _deserialize<{ + result: unknown; + redirect?: string; + loaderHashes?: string[]; + }>(text); + return { + status: response.status, + result: data?.result, + redirect: data?.redirect, + loaderHashes: data?.loaderHashes, + }; + } + + return undefined; +} diff --git a/packages/qwik-router/src/runtime/src/use-endpoint.unit.ts b/packages/qwik-router/src/runtime/src/use-endpoint.unit.ts new file mode 100644 index 00000000000..9325e5ea482 --- /dev/null +++ b/packages/qwik-router/src/runtime/src/use-endpoint.unit.ts @@ -0,0 +1,234 @@ +import { _serialize } from '@qwik.dev/core/internal'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { getLoaderName } from '../../middleware/request-handler/request-path'; +import { FULLPATH_HEADER, fetchRouteLoaderData } from './route-loaders'; +import { submitAction } from './use-endpoint'; + +describe('submitAction', () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + const makeJsonResponse = async (payload: object, status = 200) => + new Response(await _serialize(payload), { + status, + headers: { 'Content-Type': 'application/json' }, + }); + + it('clears action.data immediately after capture to prevent re-submission on task rerun', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue(await makeJsonResponse({ result: { ok: true } })) + ); + + const action = { id: 'act-a', data: { name: 'Ada' } }; + await submitAction(action as any, '/test/'); + + expect(action.data).toBeUndefined(); + }); + + it('clears action.data even when fetch throws', async () => { + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network error'))); + + const action = { id: 'act-a', data: { field: 'value' } }; + await expect(submitAction(action as any, '/test/')).rejects.toThrow('network error'); + + expect(action.data).toBeUndefined(); + }); + + it('returns result and status from JSON action response', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + await makeJsonResponse({ result: { ok: true }, loaderHashes: ['hash-1'] }, 200) + ) + ); + + const result = await submitAction({ id: 'act-a', data: {} } as any, '/test/'); + + expect(result).toEqual({ + status: 200, + result: { ok: true }, + loaderHashes: ['hash-1'], + redirect: undefined, + }); + }); + + it('reflects non-200 status from action response (fail/validator)', async () => { + vi.stubGlobal( + 'fetch', + vi + .fn() + .mockResolvedValue( + await makeJsonResponse({ result: { failed: true, message: 'bad' } }, 422) + ) + ); + + const result = await submitAction({ id: 'act-a', data: {} } as any, '/test/'); + + expect(result?.status).toBe(422); + expect(result?.result).toMatchObject({ failed: true }); + }); +}); + +describe('fetchRouteLoaderData', () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it('returns undefined when the loader is not valid for the current route', async () => { + await expect( + fetchRouteLoaderData('loader-hash', undefined, 'manifest-hash') + ).resolves.toBeUndefined(); + }); + + it('bypasses the browser cache when forced', async () => { + const fetchSpy = vi.fn().mockResolvedValue( + new Response('', { + status: 404, + }) + ); + vi.stubGlobal('fetch', fetchSpy); + + await fetchRouteLoaderData('loader-hash', '/products/123/', 'manifest-hash', { + pageUrl: new URL('http://localhost/products/123/?view=full'), + ignoreCache: true, + }); + + expect(fetchSpy).toHaveBeenCalledWith( + `/products/123/${getLoaderName('loader-hash', 'manifest-hash')}?view=full`, + expect.objectContaining({ + cache: 'reload', + }) + ); + }); + + it('sends X-Qwik-fullpath when fetching a loader for a deeper page path', async () => { + const fetchSpy = vi.fn().mockResolvedValue( + new Response('', { + status: 404, + }) + ); + vi.stubGlobal('fetch', fetchSpy); + + await fetchRouteLoaderData('loader-hash', '/products/123/', 'manifest-hash', { + pageUrl: new URL('http://localhost/products/123/view/?tab=details'), + }); + + expect(fetchSpy).toHaveBeenCalledWith( + `/products/123/${getLoaderName('loader-hash', 'manifest-hash')}?tab=details`, + expect.objectContaining({ + headers: { + [FULLPATH_HEADER]: '/products/123/view/', + }, + }) + ); + }); + + it('dedupes concurrent loader fetches for the same request', async () => { + const body = await _serialize({ d: 'prefetched' }); + let resolveFetch: (() => void) | undefined; + const fetchSpy = vi.fn( + () => + new Promise((resolve) => { + resolveFetch = () => resolve(new Response(body)); + }) + ); + vi.stubGlobal('fetch', fetchSpy); + + const url = new URL('http://localhost/products/123/?view=full'); + const first = fetchRouteLoaderData('dedupe-concurrent', '/products/123/', 'manifest-hash', { + pageUrl: url, + }); + const second = fetchRouteLoaderData('dedupe-concurrent', '/products/123/', 'manifest-hash', { + pageUrl: url, + signal: new AbortController().signal, + }); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + resolveFetch!(); + + await expect(Promise.all([first, second])).resolves.toEqual([ + { d: 'prefetched' }, + { d: 'prefetched' }, + ]); + }); + + it('lets an abortable caller stop waiting for a shared prefetch', async () => { + const body = await _serialize({ d: 'prefetched' }); + let resolveFetch: (() => void) | undefined; + const fetchSpy = vi.fn( + () => + new Promise((resolve) => { + resolveFetch = () => resolve(new Response(body)); + }) + ); + vi.stubGlobal('fetch', fetchSpy); + + const url = new URL('http://localhost/products/123/?view=full'); + const prefetch = fetchRouteLoaderData('abort-shared', '/products/123/', 'manifest-hash', { + pageUrl: url, + }); + const controller = new AbortController(); + const navigation = fetchRouteLoaderData('abort-shared', '/products/123/', 'manifest-hash', { + pageUrl: url, + signal: controller.signal, + }); + + controller.abort('stale navigation'); + await expect(navigation).rejects.toBe('stale navigation'); + expect(fetchSpy).toHaveBeenCalledTimes(1); + + resolveFetch!(); + await expect(prefetch).resolves.toEqual({ d: 'prefetched' }); + }); + + it('reuses a recently completed loader fetch', async () => { + const body = await _serialize({ d: 'cached' }); + const fetchSpy = vi.fn().mockResolvedValue(new Response(body)); + vi.stubGlobal('fetch', fetchSpy); + + const url = new URL('http://localhost/products/123/?view=full'); + const first = await fetchRouteLoaderData( + 'dedupe-completed', + '/products/123/', + 'manifest-hash', + { + pageUrl: url, + } + ); + const second = await fetchRouteLoaderData( + 'dedupe-completed', + '/products/123/', + 'manifest-hash', + { + pageUrl: url, + } + ); + + expect(first).toEqual({ d: 'cached' }); + expect(second).toEqual({ d: 'cached' }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + it('does not let abortable requests populate the shared loader fetch cache', async () => { + const body = await _serialize({ d: 'uncached' }); + const fetchSpy = vi.fn().mockImplementation(() => Promise.resolve(new Response(body))); + vi.stubGlobal('fetch', fetchSpy); + + const url = new URL('http://localhost/products/123/?view=full'); + await fetchRouteLoaderData('abort-uncached', '/products/123/', 'manifest-hash', { + pageUrl: url, + signal: new AbortController().signal, + }); + await fetchRouteLoaderData('abort-uncached', '/products/123/', 'manifest-hash', { + pageUrl: url, + }); + + expect(fetchSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/qwik-router/src/runtime/src/utils.ts b/packages/qwik-router/src/runtime/src/utils.ts index ec733b29a2d..f56b42ea5b2 100644 --- a/packages/qwik-router/src/runtime/src/utils.ts +++ b/packages/qwik-router/src/runtime/src/utils.ts @@ -1,14 +1,6 @@ +import { ensureSlash } from '../../utils/pathname'; import type { SimpleURL } from './types'; -import { createAsync$, isBrowser } from '@qwik.dev/core'; -import { - _UNINITIALIZED, - type _Container, - type SerializationStrategy, -} from '@qwik.dev/core/internal'; -import { QACTION_KEY, QLOADER_KEY } from './constants'; -import { loadClientData } from './use-endpoint'; - /** Gets an absolute url path string (url.pathname + url.search + url.hash) */ export const toPath = (url: URL) => url.pathname + url.search + url.hash; @@ -18,11 +10,10 @@ export const toUrl = (url: string | URL, baseUrl: SimpleURL) => new URL(url, bas /** Checks only if the origins are the same. */ export const isSameOrigin = (a: SimpleURL, b: SimpleURL) => a.origin === b.origin; -const withSlash = (path: string) => (path.endsWith('/') ? path : path + '/'); /** Checks only if the pathnames are the same for the URLs (doesn't include search and hash) */ export const isSamePathname = ({ pathname: a }: SimpleURL, { pathname: b }: SimpleURL) => { const lDiff = Math.abs(a.length - b.length); - return lDiff === 0 ? a === b : lDiff === 1 && withSlash(a) === withSlash(b); + return lDiff === 0 ? a === b : lDiff === 1 && ensureSlash(a) === ensureSlash(b); }; /** Checks only if the search query strings are the same for the URLs */ export const isSameSearchQuery = (a: SimpleURL, b: SimpleURL) => a.search === b.search; @@ -35,26 +26,6 @@ export const isSamePath = (a: SimpleURL, b: SimpleURL) => export const isSameOriginDifferentPathname = (a: SimpleURL, b: SimpleURL) => isSameOrigin(a, b) && !isSamePath(a, b); -export const getClientDataPath = ( - pathname: string, - pageSearch?: string, - options?: { - actionId?: string; - loaderIds?: string[]; - } -) => { - let search = pageSearch ?? ''; - if (options?.actionId) { - search += (search ? '&' : '?') + QACTION_KEY + '=' + encodeURIComponent(options.actionId); - } - if (options?.loaderIds) { - for (const loaderId of options.loaderIds) { - search += (search ? '&' : '?') + QLOADER_KEY + '=' + encodeURIComponent(loaderId); - } - } - return pathname + (pathname.endsWith('/') ? '' : '/') + 'q-data.json' + search; -}; - export const getClientNavPath = (props: Record, baseUrl: { url: URL }) => { const href = props.href; if (typeof href === 'string' && typeof props.target !== 'string' && !props.reload) { @@ -87,27 +58,3 @@ export const isPromise = (value: any): value is Promise => { // not using "value instanceof Promise" to have zone.js support return value && typeof value.then === 'function'; }; - -export const createLoaderSignal = ( - loadersObject: Record, - loaderId: string, - url: URL, - serializationStrategy: SerializationStrategy, - container?: _Container -) => { - return createAsync$( - async () => { - if (isBrowser && loadersObject[loaderId] === _UNINITIALIZED) { - const data = await loadClientData(url, { - loaderIds: [loaderId], - }); - loadersObject[loaderId] = data?.loaders[loaderId] ?? _UNINITIALIZED; - } - return loadersObject[loaderId]; - }, - { - container: container as _Container, - serializationStrategy, - } - ); -}; diff --git a/packages/qwik-router/src/runtime/src/utils.unit.ts b/packages/qwik-router/src/runtime/src/utils.unit.ts index a915d25f995..32533d01604 100644 --- a/packages/qwik-router/src/runtime/src/utils.unit.ts +++ b/packages/qwik-router/src/runtime/src/utils.unit.ts @@ -1,6 +1,5 @@ import { assert, test } from 'vitest'; import { - getClientDataPath, getClientNavPath, shouldPreload, isSameOrigin, @@ -63,29 +62,6 @@ import { }); }); -[ - { pathname: '/', expect: '/q-data.json' }, - { pathname: '/about', expect: '/about/q-data.json' }, - { pathname: '/about/', expect: '/about/q-data.json' }, -].forEach((t) => { - test(`getClientEndpointUrl("${t.pathname}")`, () => { - const endpointPath = getClientDataPath(t.pathname); - assert.equal(endpointPath, t.expect); - }); -}); - -[ - { pathname: '/', search: '?foo=bar', expect: '/q-data.json?foo=bar' }, - { pathname: '/about', search: '?foo=bar', expect: '/about/q-data.json?foo=bar' }, - { pathname: '/about/', search: '?foo=bar', expect: '/about/q-data.json?foo=bar' }, - { pathname: '/about/', search: '?foo=bar&baz=qux', expect: '/about/q-data.json?foo=bar&baz=qux' }, -].forEach((t) => { - test(`getClientEndpointUrl("${t.pathname}", "${t.search}")`, () => { - const endpointPath = getClientDataPath(t.pathname, t.search); - assert.equal(endpointPath, t.expect); - }); -}); - [ { url: 'http://qwik.dev/', diff --git a/packages/qwik-router/src/runtime/src/view-transition.tsx b/packages/qwik-router/src/runtime/src/view-transition.tsx index 91be5973068..1bb0df91134 100644 --- a/packages/qwik-router/src/runtime/src/view-transition.tsx +++ b/packages/qwik-router/src/runtime/src/view-transition.tsx @@ -13,7 +13,7 @@ interface DocumentViewTransition extends Omit { } /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ViewTransition) */ -interface ViewTransition { +export interface ViewTransition { /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ViewTransition/finished) */ readonly finished: Promise; /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/ViewTransition/ready) */ @@ -25,10 +25,10 @@ interface ViewTransition { types?: Set; } -export const startViewTransition = (params: StartViewTransitionOptions) => { - if (!params.update) { - return; - } +export const startViewTransition = (params: { + types: string[]; + update: () => Promise; +}): { ready: Promise; transition?: ViewTransition } => { if ('startViewTransition' in document) { let transition: ViewTransition; try { @@ -40,8 +40,8 @@ export const startViewTransition = (params: StartViewTransitionOptions) => { } const event = new CustomEvent('qviewtransition', { detail: transition }); document.dispatchEvent(event); - return transition; + return { ready: transition.ready as Promise, transition }; } else { - params.update?.(); + return { ready: params.update() }; } }; diff --git a/packages/qwik-router/src/ssg/bundle-isolation.unit.ts b/packages/qwik-router/src/ssg/bundle-isolation.unit.ts index 69f81f9fe83..fd07cb9f5f9 100644 --- a/packages/qwik-router/src/ssg/bundle-isolation.unit.ts +++ b/packages/qwik-router/src/ssg/bundle-isolation.unit.ts @@ -67,6 +67,7 @@ describe('SSG worker bundle isolation', () => { ssr: true, rollupOptions: { input: join(appDir, 'run-ssg.ts'), + external: ['@qwik-router-config'], }, }, }); diff --git a/packages/qwik-router/src/ssg/orchestrator.ts b/packages/qwik-router/src/ssg/orchestrator.ts index abd1f741ffa..d4ae18d25c5 100644 --- a/packages/qwik-router/src/ssg/orchestrator.ts +++ b/packages/qwik-router/src/ssg/orchestrator.ts @@ -2,7 +2,7 @@ import type { PageModule, PathParams, RouteData } from '@qwik.dev/router'; import { bold, dim, green, magenta, red } from 'kleur/colors'; import { relative } from 'node:path'; import { msToString } from '../utils/format'; -import { getPathnameForDynamicRoute } from '../utils/pathname'; +import { ensureSlash, getPathnameForDynamicRoute } from '../utils/pathname'; import { createRouteTester } from './routes'; import type { SsgGenerateOptions, SsgResult, SsgRoute, System } from './types'; @@ -157,7 +157,7 @@ export async function mainThread(sys: System) { const lastSegment = segments[segments.length - 1]; if (!lastSegment.includes('.')) { - pathname += '/'; + pathname = ensureSlash(pathname); } } } else { @@ -202,7 +202,7 @@ export async function mainThread(sys: System) { // Reconstruct the original pathname from path parts const joinedParts = pathParts.join('/'); - const originalPathname = basePathname + (joinedParts ? joinedParts + '/' : ''); + const originalPathname = basePathname + (joinedParts ? ensureSlash(joinedParts) : ''); const paramNames = pathParts .filter((p) => p.startsWith('[') && p.endsWith(']')) .map((p) => (p.startsWith('[...') ? p.slice(4, -1) : p.slice(1, -1))); diff --git a/packages/qwik-router/src/ssg/orchestrator.unit.ts b/packages/qwik-router/src/ssg/orchestrator.unit.ts index 782b12904f0..a253cfd3dd6 100644 --- a/packages/qwik-router/src/ssg/orchestrator.unit.ts +++ b/packages/qwik-router/src/ssg/orchestrator.unit.ts @@ -1,5 +1,6 @@ import type { QwikRouterConfig, RouteData } from '@qwik.dev/router'; import { assert, test } from 'vitest'; +import { getLoaderName } from '../middleware/request-handler/request-path'; import { mainThread } from './orchestrator'; import type { Logger, MainContext, System } from './types'; @@ -123,7 +124,8 @@ function createSystem({ createTimer: () => () => 0, getRouteFilePath: (pathname) => `C:/tmp/out${pathname === '/' ? '/index.html' : `${pathname}/index.html`}`, - getDataFilePath: (pathname) => `C:/tmp/out${pathname}/q-data.json`, + getLoaderFilePath: (pathname, loaderId, manifestHash) => + `C:/tmp/out${pathname}${getLoaderName(loaderId, manifestHash)}`, getEnv: () => undefined, platform: {}, }; diff --git a/packages/qwik-router/src/ssg/request-event-ssg.ts b/packages/qwik-router/src/ssg/request-event-ssg.ts index d990d84af71..2530b708b10 100644 --- a/packages/qwik-router/src/ssg/request-event-ssg.ts +++ b/packages/qwik-router/src/ssg/request-event-ssg.ts @@ -1,10 +1,13 @@ import { createCacheControl } from '@qwik-router-ssg-worker/middleware/request-handler/cache-control'; import { Cookie } from '@qwik-router-ssg-worker/middleware/request-handler/cookie'; import { createRequestEventWithDeps } from '@qwik-router-ssg-worker/middleware/request-handler/request-event-core'; -import { getRouteLoaderPromise } from '@qwik-router-ssg-worker/middleware/request-handler/request-loader'; import { - getRouteMatchPathname, - IsQData, + IsQAction, + IsQLoader, + QActionId, + QLoaderId, + recognizeRequest, + trimRecognizedInternalPathname, } from '@qwik-router-ssg-worker/middleware/request-handler/request-path'; import { encoder, @@ -16,13 +19,14 @@ import { } from '@qwik-router-ssg-worker/middleware/request-handler/redirect-handler'; import { RewriteMessage } from '@qwik-router-ssg-worker/middleware/request-handler/rewrite-handler'; import { ServerError } from '@qwik-router-ssg-worker/middleware/request-handler/server-error'; -import { QDATA_KEY, isPromise } from './worker-imports/runtime'; +import { QACTION_KEY, QDATA_KEY, isPromise } from './worker-imports/runtime'; type CreateRequestEventArgs = Parameters extends [any, ...infer Rest] ? Rest : never; const requestEventDeps = { QDATA_KEY, + QACTION_KEY, isPromise, createCacheControl, Cookie, @@ -30,9 +34,12 @@ const requestEventDeps = { RedirectMessage, RewriteMessage, ServerError, - getRouteLoaderPromise, - getRouteMatchPathname, - IsQData, + recognizeRequest, + trimRecognizedInternalPathname, + IsQLoader, + IsQAction, + QLoaderId, + QActionId, encoder, getContentType, }; diff --git a/packages/qwik-router/src/ssg/resolve-request-handlers-ssg.ts b/packages/qwik-router/src/ssg/resolve-request-handlers-ssg.ts index f0b515577b6..367a98bbb8b 100644 --- a/packages/qwik-router/src/ssg/resolve-request-handlers-ssg.ts +++ b/packages/qwik-router/src/ssg/resolve-request-handlers-ssg.ts @@ -1,4 +1,4 @@ -import { QACTION_KEY, QFN_KEY, QLOADER_KEY, resolveRouteConfig } from './worker-imports/runtime'; +import { QACTION_KEY, QFN_KEY, resolveRouteConfig } from './worker-imports/runtime'; import { resolveETag, resolveCacheKey, @@ -8,22 +8,13 @@ import { } from '@qwik-router-ssg-worker/middleware/request-handler/etag'; import { HttpStatus } from '@qwik-router-ssg-worker/middleware/request-handler/http-status-codes'; import { - getRequestLoaderSerializationStrategyMap, - getRequestLoaders, getRequestMode, RequestEvETagCacheKey, RequestEvHttpStatusMessage, - RequestEvIsRewrite, - RequestEvShareQData, RequestEvShareServerTiming, RequestEvSharedActionId, RequestRouteName, } from '@qwik-router-ssg-worker/middleware/request-handler/request-event-core'; -import { getRouteLoaderPromise } from '@qwik-router-ssg-worker/middleware/request-handler/request-loader'; -import { - IsQData, - QDATA_JSON, -} from '@qwik-router-ssg-worker/middleware/request-handler/request-path'; import { encoder, isContentType, @@ -36,13 +27,8 @@ import { getQwikRouterServerData } from './response-page-ssg'; const requestHandlers = createResolveRequestHandlers({ QACTION_KEY, QFN_KEY, - QLOADER_KEY, - QDATA_JSON, - IsQData, RequestEvETagCacheKey, RequestEvHttpStatusMessage, - RequestEvIsRewrite, - RequestEvShareQData, RequestEvShareServerTiming, RequestEvSharedActionId, RequestRouteName, @@ -53,10 +39,7 @@ const requestHandlers = createResolveRequestHandlers({ isContentType, getCachedHtml, getQwikRouterServerData, - getRequestLoaderSerializationStrategyMap, - getRequestLoaders, getRequestMode, - getRouteLoaderPromise, loadHttpError: () => import('../runtime/src/http-error'), MAX_CACHE_SIZE, resolveCacheKey, diff --git a/packages/qwik-router/src/ssg/response-page-ssg.ts b/packages/qwik-router/src/ssg/response-page-ssg.ts index b8a45b6647d..6d30ea29e6b 100644 --- a/packages/qwik-router/src/ssg/response-page-ssg.ts +++ b/packages/qwik-router/src/ssg/response-page-ssg.ts @@ -1,6 +1,4 @@ import { - getRequestLoaders, - getRequestLoaderSerializationStrategyMap, getRequestRoute, RequestEvHttpStatusMessage, RequestEvSharedActionFormData, @@ -10,7 +8,6 @@ import { } from '@qwik-router-ssg-worker/middleware/request-handler/request-event-core'; import { getQwikRouterServerDataWithDeps } from '@qwik-router-ssg-worker/middleware/request-handler/response-page-core'; import { Q_ROUTE } from './worker-imports/runtime'; - type GetQwikRouterServerDataArgs = Parameters extends [any, ...infer Rest] ? Rest : never; @@ -21,8 +18,6 @@ const responsePageDeps = { RequestEvSharedActionId, RequestEvSharedNonce, RequestRouteName, - getRequestLoaders, - getRequestLoaderSerializationStrategyMap, getRequestRoute, }; diff --git a/packages/qwik-router/src/ssg/system.ts b/packages/qwik-router/src/ssg/system.ts index 58b35c42d0d..ad12fee465c 100644 --- a/packages/qwik-router/src/ssg/system.ts +++ b/packages/qwik-router/src/ssg/system.ts @@ -3,7 +3,9 @@ import type { SsgGenerateOptions, System } from './types'; import fs from 'node:fs'; import { dirname, join } from 'node:path'; import { createWorkerPool } from './worker-pool'; +import { getLoaderName } from '../middleware/request-handler/request-path'; import { normalizePath } from '../utils/fs'; +import { ensureSlash } from '../utils/pathname'; /** @public */ export async function createSystem(opts: SsgGenerateOptions, threadId?: number): Promise { @@ -65,13 +67,10 @@ export async function createSystem(opts: SsgGenerateOptions, threadId?: number): return join(outDir, pathname); }; - const getDataFilePath = (pathname: string) => { + const getLoaderFilePath = (pathname: string, loaderId: string, manifestHash: string) => { pathname = decodeURIComponent(pathname.slice(basenameLen)); - if (pathname.endsWith('/')) { - pathname += 'q-data.json'; - } else { - pathname += '/q-data.json'; - } + const suffix = getLoaderName(loaderId, manifestHash); + pathname = ensureSlash(pathname) + suffix; return join(outDir, pathname); }; @@ -84,7 +83,7 @@ export async function createSystem(opts: SsgGenerateOptions, threadId?: number): createTimer, access, getRouteFilePath, - getDataFilePath, + getLoaderFilePath, getEnv: (key) => process.env[key], platform: { static: true, diff --git a/packages/qwik-router/src/ssg/types.ts b/packages/qwik-router/src/ssg/types.ts index bd2e3d00e0c..6a73ba17efa 100644 --- a/packages/qwik-router/src/ssg/types.ts +++ b/packages/qwik-router/src/ssg/types.ts @@ -12,7 +12,7 @@ export interface System { createWriteStream: (filePath: string) => StaticStreamWriter; createTimer: () => () => number; getRouteFilePath: (pathname: string, isHtml: boolean) => string; - getDataFilePath: (pathname: string) => string; + getLoaderFilePath: (pathname: string, loaderId: string, manifestHash: string) => string; getEnv: (key: string) => string | undefined; platform: { [key: string]: any }; } @@ -68,13 +68,13 @@ export interface SsgRenderOptions extends RenderOptions { log?: 'debug' | 'quiet'; /** * Set to `false` if the generated static HTML files should not be written to disk. Setting to - * `false` is useful if the SSG should only write the `q-data.json` files to disk. Defaults to + * `false` is useful if the SSG should only write the per-loader data files to disk. Defaults to * `true`. */ emitHtml?: boolean; /** - * Set to `false` if the generated `q-data.json` data files should not be written to disk. - * Defaults to `true`. + * Set to `false` if the generated per-loader data files should not be written to disk. Defaults + * to `true`. */ emitData?: boolean; /** diff --git a/packages/qwik-router/src/ssg/worker-thread.ts b/packages/qwik-router/src/ssg/worker-thread.ts index 42e2ccf95bd..fb6488d6adc 100644 --- a/packages/qwik-router/src/ssg/worker-thread.ts +++ b/packages/qwik-router/src/ssg/worker-thread.ts @@ -1,8 +1,9 @@ +import { trimInternalPathname } from '@qwik-router-ssg-worker/middleware/request-handler/request-path'; +import { _serialize as serialize } from '@qwik.dev/core/internal'; import { parentPort } from 'node:worker_threads'; -// Use the global WritableStream, not node:stream/web — in worker threads they can be -// different classes, causing instanceof checks in pipeTo/TransformStream to fail. -import type { ClientPageData } from '../runtime/src/types'; import type { ServerRequestEvent } from '../middleware/request-handler/types'; +import { getRouteLoaderValues, getRouteLoaders } from '../runtime/src/route-loaders'; +import { renderQwikMiddleware, resolveRequestHandlers } from './resolve-request-handlers-ssg'; import type { SsgHandlerOptions, SsgRoute, @@ -12,31 +13,26 @@ import type { WorkerInputMessage, WorkerOutputMessage, } from './types'; -import { renderQwikMiddleware, resolveRequestHandlers } from './resolve-request-handlers-ssg'; import { runQwikRouter } from './user-response-ssg'; import { loadRoute } from './worker-imports/runtime'; -import { RequestEvShareQData } from '@qwik-router-ssg-worker/middleware/request-handler/request-event-core'; -import { getRouteMatchPathname } from '@qwik-router-ssg-worker/middleware/request-handler/request-path'; interface StaticWorkerThreadDeps { - RequestEvShareQData: string; loadRoute: typeof loadRoute; renderQwikMiddleware: typeof renderQwikMiddleware; resolveRequestHandlers: typeof resolveRequestHandlers; - getRouteMatchPathname: typeof getRouteMatchPathname; + trimInternalPathname: typeof trimInternalPathname; runQwikRouter: typeof runQwikRouter; } interface WorkerThreadDeps extends StaticWorkerThreadDeps { - serialize: typeof import('@qwik.dev/core/internal')._serialize; + serialize: typeof serialize; } const staticWorkerThreadDeps: StaticWorkerThreadDeps = { - RequestEvShareQData, loadRoute, renderQwikMiddleware, resolveRequestHandlers, - getRouteMatchPathname, + trimInternalPathname, runQwikRouter, }; @@ -48,7 +44,7 @@ export async function workerThread(sys: System) { const pendingPromises = new Set>(); const deps: WorkerThreadDeps = { ...staticWorkerThreadDeps, - serialize: await loadSerialize(), + serialize, }; // Prevent unhandled errors/rejections from crashing the worker thread. @@ -186,13 +182,12 @@ async function workerRender( } const hasRouteWriter = isHtml ? opts.emitHtml !== false : true; - const writeQDataEnabled = isHtml && opts.emitData !== false; + const writeLoaderDataEnabled = isHtml && opts.emitData !== false; const stream = new WritableStream({ async start() { try { - if (hasRouteWriter || writeQDataEnabled) { - // for html pages, endpoints or q-data.json + if (hasRouteWriter || writeLoaderDataEnabled) { // ensure the containing directory is created await sys.ensureDir(routeFilePath); } @@ -237,13 +232,26 @@ async function workerRender( const writePromises: Promise[] = []; try { - if (writeQDataEnabled) { - const qData: ClientPageData = requestEv.sharedMap.get(deps.RequestEvShareQData); - if (qData && !is404ErrorPage) { - // write q-data.json file when enabled and qData is set - const qDataFilePath = sys.getDataFilePath(url.pathname); - const dataWriter = sys.createWriteStream(qDataFilePath); - dataWriter.on('error', (e) => { + if (writeLoaderDataEnabled && !is404ErrorPage) { + const routeLoaders = getRouteLoaders(requestEv); + const loaderValues = getRouteLoaderValues(requestEv); + const manifestHash = (opts.manifest as any)?.manifestHash || 'dev'; + // Write individual per-loader files for static loaders (expires === 0) + for (const loader of routeLoaders) { + if (loader.__expires !== 0) { + continue; + } + const data = loaderValues[loader.__id]; + if (data === undefined) { + continue; + } + const loaderFilePath = sys.getLoaderFilePath( + url.pathname, + loader.__id, + manifestHash + ); + const loaderWriter = sys.createWriteStream(loaderFilePath); + loaderWriter.on('error', (e) => { console.error(e); result.error = { message: e.message, @@ -251,14 +259,15 @@ async function workerRender( }; }); - const serialized = await deps.serialize(qData); - dataWriter.write(serialized); + // Match the LoaderResponse envelope { d, r, e } produced by loaderHandler + // so the client deserializer reads the same shape from static and dynamic responses. + const serialized = await deps.serialize({ d: data }); + loaderWriter.write(serialized); writePromises.push( new Promise((resolve) => { - // set the static file path for the result result.filePath = routeFilePath; - dataWriter.end(resolve); + loaderWriter.end(resolve); }) ); } @@ -357,12 +366,6 @@ async function workerRender( /** Create a fresh no-op WritableStream (must be a real instance for pipeTo checks). */ const createNoopWritableStream = () => new WritableStream(); -async function loadSerialize(): Promise { - // Import qwik after resetting the global singleton so the worker gets a fresh serializer instance. - const { _serialize: serialize } = await import('@qwik.dev/core/internal'); - return serialize; -} - function isRedirectMessage(value: unknown) { return ( typeof value === 'object' && @@ -382,20 +385,19 @@ async function requestHandlerForSsg( throw new Error('qwikRouterConfig is required.'); } - const { pathname, isInternal } = deps.getRouteMatchPathname(serverRequestEv.url.pathname); + const pathname = deps.trimInternalPathname(serverRequestEv.url.pathname); if (pathname === '/.well-known' || pathname.startsWith('/.well-known/')) { return null; } const { routes, serverPlugins, cacheModules } = qwikRouterConfig; - const loadedRoute = await deps.loadRoute(routes, cacheModules, pathname, isInternal); + const loadedRoute = await deps.loadRoute(routes, cacheModules, pathname); const requestHandlers = deps.resolveRequestHandlers( serverPlugins, loadedRoute, serverRequestEv.request.method, checkOrigin ?? true, - deps.renderQwikMiddleware(render), - isInternal + deps.renderQwikMiddleware(render) ); if (qwikRouterConfig.fallthrough && loadedRoute.$notFound$) { @@ -403,15 +405,14 @@ async function requestHandlerForSsg( } const rebuildRouteInfo = async (url: URL) => { - const { pathname } = deps.getRouteMatchPathname(url.pathname); - const loadedRoute = await deps.loadRoute(routes, cacheModules, pathname, isInternal); + const cleanPathname = deps.trimInternalPathname(url.pathname); + const loadedRoute = await deps.loadRoute(routes, cacheModules, cleanPathname); const requestHandlers = deps.resolveRequestHandlers( serverPlugins, loadedRoute, serverRequestEv.request.method, checkOrigin ?? true, - deps.renderQwikMiddleware(render), - isInternal + deps.renderQwikMiddleware(render) ); return { loadedRoute, diff --git a/packages/qwik-router/src/utils/fs.ts b/packages/qwik-router/src/utils/fs.ts index 6f362d6066d..2606573fbf6 100644 --- a/packages/qwik-router/src/utils/fs.ts +++ b/packages/qwik-router/src/utils/fs.ts @@ -1,7 +1,7 @@ import { basename, dirname, normalize, relative } from 'node:path'; import type { NormalizedPluginOptions } from '../buildtime/types'; import { toTitleCase } from './format'; -import { normalizePathname } from './pathname'; +import { ensureSlash, normalizePathname } from './pathname'; export function parseRouteIndexName(extlessName: string) { let layoutName = ''; @@ -42,11 +42,8 @@ export function getPathnameFromDirPath(opts: NormalizedPluginOptions, dirPath: s export function getMenuPathname(opts: NormalizedPluginOptions, filePath: string) { let pathname = normalizePath(relative(opts.routesDir, filePath)); pathname = `/` + normalizePath(dirname(pathname)); - let result = normalizePathname(pathname, opts.basePathname)!; - if (!result.endsWith('/')) { - result += '/'; - } - return result; + const result = normalizePathname(pathname, opts.basePathname)!; + return ensureSlash(result); } export function getExtension(fileName: string) { diff --git a/packages/qwik-router/src/utils/fs.unit.ts b/packages/qwik-router/src/utils/fs.unit.ts index 40918d02699..843984abbde 100644 --- a/packages/qwik-router/src/utils/fs.unit.ts +++ b/packages/qwik-router/src/utils/fs.unit.ts @@ -264,6 +264,7 @@ test('createFileId, Menu', () => { platform: {}, rewriteRoutes: [], defaultLoadersSerializationStrategy: 'never', + strictLoaders: true, }; globalThis.__NO_TRAILING_SLASH__ = !t.trailingSlash; const pathname = getPathnameFromDirPath(opts, t.dirPath); @@ -366,6 +367,7 @@ test('parseRouteIndexName', () => { platform: {}, rewriteRoutes: [], defaultLoadersSerializationStrategy: 'never', + strictLoaders: true, }; globalThis.__NO_TRAILING_SLASH__ = !t.trailingSlash; const pathname = getMenuPathname(opts, t.filePath); diff --git a/packages/qwik-router/src/utils/pathname.ts b/packages/qwik-router/src/utils/pathname.ts index 42925235baf..d140c3c26aa 100644 --- a/packages/qwik-router/src/utils/pathname.ts +++ b/packages/qwik-router/src/utils/pathname.ts @@ -1,4 +1,8 @@ -import type { PathParams } from '../runtime/src'; +import type { PathParams } from '../runtime/src/types'; + +/** Ensures the pathname ends with a slash */ +export const ensureSlash = (pathname: string) => + pathname.endsWith('/') ? pathname : pathname + '/'; export function normalizePathname(pathname: string | undefined | null, basePathname: string) { if (typeof pathname === 'string') { @@ -24,7 +28,7 @@ export function normalizePathname(pathname: string | undefined | null, basePathn const lastSegment = segments[segments.length - 1]; if (!lastSegment.includes('.')) { - pathname += '/'; + pathname = ensureSlash(pathname); } } } else { diff --git a/packages/qwik-router/tsconfig.json b/packages/qwik-router/tsconfig.json index f53b801906d..d1dded51925 100644 --- a/packages/qwik-router/tsconfig.json +++ b/packages/qwik-router/tsconfig.json @@ -2,7 +2,6 @@ "extends": "../../tsconfig.json", "compilerOptions": { "paths": { - "@qwik.dev/optimizer": ["packages/optimizer/src"], "@qwik.dev/router": ["packages/qwik-router/src/runtime/src/index.ts"], "@qwik.dev/router/service-worker": [ "packages/qwik-router/src/runtime/src/service-worker/index.ts" diff --git a/packages/qwik-vite/src/plugins/plugin.ts b/packages/qwik-vite/src/plugins/plugin.ts index ba31e65b7c4..b25c27e4668 100644 --- a/packages/qwik-vite/src/plugins/plugin.ts +++ b/packages/qwik-vite/src/plugins/plugin.ts @@ -97,6 +97,7 @@ export function createQwikPlugin(optimizerOptions: OptimizerOptions = {}) { const serverTransformedOutputs = new Map(); const parentIds = new Map(); + const segmentCallbacks = new Set<(parentId: string, segment: SegmentAnalysis) => void>(); let internalOptimizer: Optimizer | null = null; let linter: QwikLinter | undefined = undefined; @@ -920,6 +921,12 @@ export function createQwikPlugin(optimizerOptions: OptimizerOptions = {}) { preserveSignature: 'allow-extension', }); } + // Notify segment callbacks + if (mod.segment && segmentCallbacks.size > 0) { + for (const cb of segmentCallbacks) { + cb(id, mod.segment); + } + } } } @@ -1253,6 +1260,7 @@ export const isDev = ${JSON.stringify(isDev)}; normalizePath, onDiagnostics, resolveId, + segmentCallbacks, transform, validateSource, setSourceMapSupport, diff --git a/packages/qwik-vite/src/plugins/vite.ts b/packages/qwik-vite/src/plugins/vite.ts index 4f0e77ee143..2ef219e1a99 100644 --- a/packages/qwik-vite/src/plugins/vite.ts +++ b/packages/qwik-vite/src/plugins/vite.ts @@ -13,6 +13,7 @@ import type { Optimizer, OptimizerOptions, QwikManifest, + SegmentAnalysis, TransformModule, } from '../types'; import { type BundleGraphAdder } from './bundle-graph'; @@ -145,6 +146,9 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any { getClientPublicOutDir: () => clientPublicOutDir, getAssetsDir: () => viteAssetsDir, registerBundleGraphAdder: (adder: BundleGraphAdder) => bundleGraphAdders.add(adder), + onSegment: (callback: SegmentCallback) => { + qwikPlugin.segmentCallbacks.add(callback); + }, _oldDevSsrServer: () => qwikViteOpts.devSsrServer, }; @@ -705,7 +709,7 @@ export function qwikVite(qwikViteOpts: QwikVitePluginOptions = {}): any { // Source files live in the SSR module graph. When they change, notify the // client's loaded QRL segments via the client environment's HMR channel. // Some non-source imports (e.g. .css?inline) are type 'js' but their URL - // isn't a JS/TS file. For those, emit their JS importers instead, since + // is not a JS/TS file. For those, emit their JS importers instead, since // the component that needs to re-render is the importer. const files = new Set(); const isSourceUrl = (url: string) => /\.([mc]?[jt]sx?|mdx?)$/.test(url.split('?')[0]); @@ -1143,6 +1147,9 @@ interface QwikVitePluginCSROptions extends QwikVitePluginCommonOptions { export type QwikVitePluginOptions = QwikVitePluginCSROptions | QwikVitePluginSSROptions; export { ExperimentalFeatures } from './plugin'; +/** @public */ +export type SegmentCallback = (parentId: string, segment: SegmentAnalysis) => void; + /** @public */ export interface QwikVitePluginApi { getOptimizer: () => Optimizer | null; @@ -1153,6 +1160,8 @@ export interface QwikVitePluginApi { getClientPublicOutDir: () => string | null; getAssetsDir: () => string | undefined; registerBundleGraphAdder: (adder: BundleGraphAdder) => void; + /** Register a callback that fires for each segment emitted during transform. */ + onSegment: (callback: SegmentCallback) => void; /** @internal */ _oldDevSsrServer: () => boolean | undefined; } diff --git a/packages/qwik-vite/src/qwik.optimizer.api.md b/packages/qwik-vite/src/qwik.optimizer.api.md index afefa243f8f..5268be64d1b 100644 --- a/packages/qwik-vite/src/qwik.optimizer.api.md +++ b/packages/qwik-vite/src/qwik.optimizer.api.md @@ -8,6 +8,7 @@ import { EntryStrategy } from '@qwik.dev/optimizer'; import { Optimizer } from '@qwik.dev/optimizer'; import { OptimizerOptions } from '@qwik.dev/optimizer'; import type { Plugin as Plugin_2 } from 'vite'; +import { SegmentAnalysis } from '@qwik.dev/optimizer'; import { TransformModule } from '@qwik.dev/optimizer'; import { TransformModuleInput } from '@qwik.dev/optimizer'; @@ -176,6 +177,8 @@ export interface QwikVitePluginApi { getRootDir: () => string | null; // @internal (undocumented) _oldDevSsrServer: () => boolean | undefined; + // Warning: (ae-forgotten-export) The symbol "SegmentCallback" needs to be exported by the entry point index.d.ts + onSegment: (callback: SegmentCallback) => void; // (undocumented) registerBundleGraphAdder: (adder: BundleGraphAdder) => void; } diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index 24cea68ae83..f3a6c8b889a 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -32,20 +32,27 @@ export { vnode_isMaterialized as _vnode_isMaterialized, vnode_isTextVNode as _vnode_isTextVNode, vnode_isVirtualVNode as _vnode_isVirtualVNode, + vnode_newVirtual as _vnode_newVirtual, + vnode_setProp as _vnode_setProp, vnode_toString as _vnode_toString, } from './client/vnode-utils'; export { _executeSsrChores } from './shared/cursor/ssr-chore-execution'; export { getAsyncLocalStorage as _getAsyncLocalStorage } from './shared/platform/async-local-storage'; -export type { Container as _Container } from './shared/types'; +export type { Container as _Container, HostElement as _HostElement } from './shared/types'; export type { ElementVNode as _ElementVNode } from './shared/vnode/element-vnode'; export type { TextVNode as _TextVNode } from './shared/vnode/text-vnode'; export type { VirtualVNode as _VirtualVNode } from './shared/vnode/virtual-vnode'; export type { VNode as _VNode } from './shared/vnode/vnode'; export { _EFFECT_BACK_REF } from './reactive-primitives/backref'; -export { _hasStoreEffects, isStore as _isStore } from './reactive-primitives/impl/store'; +export { + _hasStoreEffects, + createStore as _createStore, + isStore as _isStore, +} from './reactive-primitives/impl/store'; export { isSignal } from './reactive-primitives/utils'; export { _wrapProp, _wrapSignal } from './reactive-primitives/internal-api'; +export { getSubscriber as _getSubscriber } from './reactive-primitives/subscriber'; export { SubscriptionData as _SubscriptionData } from './reactive-primitives/subscription-data'; export { isStringifiable as _isStringifiable, @@ -66,6 +73,7 @@ export { verifySerializable as _verifySerializable } from './shared/serdes/verif export { _SharedContainer } from './shared/shared-container'; export { _CONST_PROPS, _IMMUTABLE, _UNINITIALIZED, _VAR_PROPS } from './shared/utils/constants'; export { EMPTY_ARRAY as _EMPTY_ARRAY, EMPTY_OBJ as _EMPTY_OBJ } from './shared/utils/flyweight'; +export { ELEMENT_SEQ as _ELEMENT_SEQ } from './shared/utils/markers'; export { _restProps } from './shared/utils/prop'; export { _walkJSX } from './ssr/ssr-render-jsx'; export { _resolveContextWithoutSequentialScope } from './use/use-context'; @@ -75,12 +83,20 @@ export { _getContextHostElement, _jsxBranch, _waitUntilRendered, + invoke as _invoke, + newInvokeContext as _newInvokeContext, } from './use/use-core'; export { useLexicalScope } from './use/use-lexical-scope.public'; -export { isTask as _isTask, scheduleTask as _task } from './use/use-task'; +export { isTask as _isTask, scheduleTask as _task, Task as _Task } from './use/use-task'; export { _captures } from './shared/qrl/qrl-class'; export { _rsc } from './use/use-resource'; -export type { AsyncSignalOptions } from './reactive-primitives/types'; +export type { AsyncSignalImpl as _AsyncSignalImpl } from './reactive-primitives/impl/async-signal-impl'; +export { _injectAsyncSignalValue } from './reactive-primitives/impl/async-signal-impl'; +export { + EffectProperty as _EffectProperty, + StoreFlags as _StoreFlags, + type AsyncSignalOptions, +} from './reactive-primitives/types'; export { setEvent as _setEvent } from './ssr/ssr-events'; export { _useHmr, _hmr } from './use/use-hmr'; export { @@ -89,6 +105,7 @@ export { _updateProjectionProps, _removeProjection, } from './shared/projection/external-projection'; +export { delay as _delay, retryOnPromise as _retryOnPromise } from './shared/utils/promises'; export { _createDeserializeContainer } from './shared/serdes/serdes.public'; /** TESTING */ @@ -98,8 +115,6 @@ export { vnode_getProp as _vnode_getProp, vnode_getVNodeForChildNode as _vnode_getVNodeForChildNode, vnode_insertBefore as _vnode_insertBefore, - vnode_newVirtual as _vnode_newVirtual, vnode_remove as _vnode_remove, - vnode_setProp as _vnode_setProp, vnode_isElementVNode as _vnode_isElementVNode, } from './client/vnode-utils'; diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md index 38d4684ac65..fadf141118d 100644 --- a/packages/qwik/src/core/qwik.core.api.md +++ b/packages/qwik/src/core/qwik.core.api.md @@ -38,6 +38,85 @@ export interface AsyncSignal extends ComputedSignal { untrackedLoading: boolean; } +// Warning: (ae-forgotten-export) The symbol "ComputedSignalImpl" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "AsyncQRL" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "BackRef" needs to be exported by the entry point index.d.ts +// +// @internal +export class _AsyncSignalImpl extends ComputedSignalImpl> implements BackRef, AsyncSignal { + // (undocumented) + $computationTimeoutId$: ReturnType | undefined; + $computeIfNeeded$(): void; + // (undocumented) + $concurrency$: number | undefined; + // Warning: (ae-forgotten-export) The symbol "AsyncJob" needs to be exported by the entry point index.d.ts + // + // (undocumented) + $current$: AsyncJob | null; + $destroy$(): Promise; + // (undocumented) + $errorEffects$: undefined | Set; + // (undocumented) + $expires$: number | undefined; + // (undocumented) + $info$: unknown | undefined; + // (undocumented) + $infoVersion$: number | undefined; + // (undocumented) + $jobs$: AsyncJob[] | undefined; + // Warning: (ae-forgotten-export) The symbol "EffectSubscription" needs to be exported by the entry point index.d.ts + // + // (undocumented) + $loadingEffects$: undefined | Set; + // (undocumented) + $pollTimeoutId$: ReturnType | undefined; + // (undocumented) + $requestCleanups$(job: AsyncJob, reason?: any): void; + $runCleanups$(job: AsyncJob): Promise | undefined; + // (undocumented) + $runComputation$(running: AsyncJob): Promise; + $scheduleEagerCleanup$(): void; + $setError$(job: AsyncJob, error: Error): void; + // (undocumented) + $setInvalid$(allowRecalc: boolean, mustClear: boolean | number): void; + // (undocumented) + $timeoutMs$: number | undefined; + // (undocumented) + $untrackedError$: Error | undefined; + // (undocumented) + $untrackedLoading$: boolean; + // (undocumented) + [_EFFECT_BACK_REF]: Map<_EffectProperty | string, EffectSubscription> | undefined; + // Warning: (ae-forgotten-export) The symbol "SignalFlags" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "SerializationSignalFlags" needs to be exported by the entry point index.d.ts + constructor(container: _Container | null, fn: AsyncQRL, flags?: SignalFlags | SerializationSignalFlags, options?: AsyncSignalOptions); + abort(reason?: any): void; + get error(): Error | undefined; + // (undocumented) + get expires(): number; + set expires(value: number); + // @deprecated (undocumented) + get interval(): number; + set interval(value: number); + invalidate(info?: unknown): Promise; + get loading(): boolean; + // (undocumented) + get poll(): boolean; + set poll(value: boolean); + promise(): Promise; + set untrackedError(value: Error | undefined); + // (undocumented) + get untrackedError(): Error | undefined; + set untrackedLoading(value: boolean); + // (undocumented) + get untrackedLoading(): boolean; + // (undocumented) + get untrackedValue(): T; + set untrackedValue(value: T); + get value(): T; + set value(value: T); +} + // @public (undocumented) export interface AsyncSignalOptions extends ComputedOptions { allowStale?: boolean; @@ -133,7 +212,7 @@ export const _CONST_PROPS: unique symbol; // @internal (undocumented) export interface _Container { // (undocumented) - $appendStyle$(content: string, styleId: string, host: HostElement, scoped: boolean): void; + $appendStyle$(content: string, styleId: string, host: _HostElement, scoped: boolean): void; // (undocumented) $buildBase$: string | null; // (undocumented) @@ -160,17 +239,15 @@ export interface _Container { readonly $storeProxyMap$: ObjToProxyMap; // (undocumented) readonly $version$: string; - ensureProjectionResolved(host: HostElement): void; + ensureProjectionResolved(host: _HostElement): void; // (undocumented) - getHostProp(host: HostElement, name: string): T | null; + getHostProp(host: _HostElement, name: string): T | null; // (undocumented) - getParentHost(host: HostElement): HostElement | null; - // Warning: (ae-forgotten-export) The symbol "HostElement" needs to be exported by the entry point index.d.ts - // + getParentHost(host: _HostElement): _HostElement | null; // (undocumented) - handleError(err: any, $host$: HostElement | null): void; + handleError(err: any, $host$: _HostElement | null): void; // (undocumented) - resolveContext(host: HostElement, contextId: ContextId): T | undefined; + resolveContext(host: _HostElement, contextId: ContextId): T | undefined; // Warning: (ae-forgotten-export) The symbol "SymbolToChunkResolver" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SerializationContext" needs to be exported by the entry point index.d.ts // @@ -185,9 +262,9 @@ export interface _Container { }; } | null, symbolToChunkResolver: SymbolToChunkResolver, writer?: StreamWriter): SerializationContext; // (undocumented) - setContext(host: HostElement, context: ContextId, value: T): void; + setContext(host: _HostElement, context: ContextId, value: T): void; // (undocumented) - setHostProp(host: HostElement, name: string, value: T): void; + setHostProp(host: _HostElement, name: string, value: T): void; } // @internal (undocumented) @@ -223,16 +300,14 @@ export interface CorrectedToggleEvent extends Event { // @public export const createAsync$: (qrl: (arg: AsyncCtx) => Promise, options?: AsyncSignalOptions) => AsyncSignal; -// Warning: (ae-forgotten-export) The symbol "AsyncSignalImpl" needs to be exported by the entry point index.d.ts // Warning: (ae-internal-missing-underscore) The name "createAsyncQrl" should be prefixed with an underscore because the declaration is marked as @internal // // @internal (undocumented) -export const createAsyncQrl: (qrl: QRL>, options?: AsyncSignalOptions) => AsyncSignalImpl; +export const createAsyncQrl: (qrl: QRL>, options?: AsyncSignalOptions) => _AsyncSignalImpl; // @public export const createComputed$: (qrl: () => T, options?: ComputedOptions) => ComputedReturnType; -// Warning: (ae-forgotten-export) The symbol "ComputedSignalImpl" needs to be exported by the entry point index.d.ts // Warning: (ae-internal-missing-underscore) The name "createComputedQrl" should be prefixed with an underscore because the declaration is marked as @internal // // @internal (undocumented) @@ -271,11 +346,17 @@ export const createSignal: { (value: T): Signal; }; +// @internal (undocumented) +export function _createStore(container: _Container | null | undefined, obj: T, flags: _StoreFlags): T; + // @public (undocumented) export interface CSSProperties extends CSS_2.Properties, CSS_2.PropertiesHyphen { [v: `--${string}`]: string | number | undefined; } +// @internal (undocumented) +export const _delay: (timeout: number) => Promise; + // @internal export function _deserialize(rawStateData: string): T; @@ -326,7 +407,7 @@ class DomContainer extends _SharedContainer implements ClientContainer { // (undocumented) ensureProjectionResolved(vNode: _VirtualVNode): void; // (undocumented) - getHostProp(host: HostElement, name: string): T | null; + getHostProp(host: _HostElement, name: string): T | null; // (undocumented) getParentHost(host: _VNode): _VNode | null; // (undocumented) @@ -346,7 +427,7 @@ class DomContainer extends _SharedContainer implements ClientContainer { // (undocumented) setContext(host: _VNode, context: ContextId, value: T): void; // (undocumented) - setHostProp(host: HostElement, name: string, value: T): void; + setHostProp(host: _HostElement, name: string, value: T): void; // (undocumented) vNodeLocate: (id: string | Element) => _VNode; } @@ -374,6 +455,17 @@ export const _eaT: (input: TaskCtx) => Promise; // @internal (undocumented) export const _EFFECT_BACK_REF: unique symbol; +// @internal (undocumented) +export const enum _EffectProperty { + // (undocumented) + COMPONENT = ":", + // (undocumented) + VNODE = "." +} + +// @internal (undocumented) +export const _ELEMENT_SEQ = "q:seq"; + // @internal (undocumented) export class _ElementVNode extends _VirtualVNode { // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts @@ -454,7 +546,7 @@ export const _getContextContainer: () => _Container | undefined; export const _getContextEvent: () => unknown; // @internal (undocumented) -export const _getContextHostElement: () => HostElement | undefined; +export const _getContextHostElement: () => _HostElement | undefined; // Warning: (ae-incompatible-release-tags) The symbol "getDomContainer" is marked as @public, but its signature references "ClientContainer" which is marked as @internal // @@ -472,6 +564,11 @@ export const getPlatform: () => CorePlatform; // @internal (undocumented) export function _getQContainerElement(element: Element): Element | null; +// Warning: (ae-forgotten-export) The symbol "Consumer" needs to be exported by the entry point index.d.ts +// +// @internal (undocumented) +export function _getSubscriber(effect: Consumer, prop: _EffectProperty | string, data?: _SubscriptionData): EffectSubscription; + // @internal export const _getVarProps: (props: PropsProxy | Record | null | undefined) => Props | null; @@ -489,6 +586,9 @@ export const _hmr: (this: string | undefined, event: CustomEvent<{ t: number; }>, element: Element) => void; +// @internal (undocumented) +export type _HostElement = _VNode | ISsrNode; + // Warning: (ae-forgotten-export) The symbol "HTMLAttributesBase" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "FilterBase" needs to be exported by the entry point index.d.ts // @@ -502,6 +602,9 @@ export const _IMMUTABLE: unique symbol; // @public export const implicit$FirstArg: (fn: (qrl: QRL, ...rest: REST) => RET) => ((qrl: FIRST, ...rest: REST) => RET); +// @internal +export const _injectAsyncSignalValue: (signal: AsyncSignal, value: unknown) => void; + // @public export const inlinedQrl: (symbol: T | null, symbolName: string, lexicalScopeCapture?: Readonly) => QRL; @@ -511,6 +614,11 @@ export const inlinedQrl: (symbol: T | null, symbolName: string, lexicalScopeC // @internal (undocumented) export const inlinedQrlDEV: (symbol: T, symbolName: string, opts: QRLDev, lexicalScopeCapture?: Readonly) => QRL; +// Warning: (ae-forgotten-export) The symbol "InvokeContext" needs to be exported by the entry point index.d.ts +// +// @internal +export function _invoke any>(this: unknown, context: InvokeContext | undefined, fn: FN, ...args: Parameters): ReturnType; + export { isBrowser } export { isDev } @@ -553,10 +661,8 @@ export const _isStore: (value: object) => boolean; // @internal (undocumented) export function _isStringifiable(value: unknown): value is _Stringifiable; -// Warning: (ae-forgotten-export) The symbol "Task" needs to be exported by the entry point index.d.ts -// // @internal (undocumented) -export const _isTask: (value: any) => value is Task; +export const _isTask: (value: any) => value is _Task; // Warning: (ae-forgotten-export) The symbol "JsxDevOpts" needs to be exported by the entry point index.d.ts // @@ -678,6 +784,12 @@ export type NativeUIEvent = UIEvent; // @public @deprecated (undocumented) export type NativeWheelEvent = WheelEvent; +// Warning: (ae-forgotten-export) The symbol "PossibleEvents" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RenderEvent" needs to be exported by the entry point index.d.ts +// +// @internal (undocumented) +export function _newInvokeContext(locale?: string, hostElement?: _HostElement, event?: Exclude): InvokeContext; + // @internal (undocumented) export const _noopQrl: (symbolName: string, lexicalScopeCapture?: Readonly) => QRL; @@ -1031,6 +1143,9 @@ export const _restProps: (props: PropsProxy, omit?: string[], target?: Props) => // @internal (undocumented) export const _reT: (input: TaskCtx) => void; +// @internal +export function _retryOnPromise(fn: () => ValueOrPromise, onError?: (e: any) => ValueOrPromise): ValueOrPromise; + // Warning: (ae-incompatible-release-tags) The symbol "Reveal" is marked as @public, but its signature references "_reC" which is marked as @internal // // @public (undocumented) @@ -1074,7 +1189,7 @@ export function _setProjectionTarget(vnode: _VirtualVNode, targetElement: Elemen // @internal (undocumented) export abstract class _SharedContainer implements _Container { // (undocumented) - abstract $appendStyle$(content: string, styleId: string, host: HostElement, scoped: boolean): void; + abstract $appendStyle$(content: string, styleId: string, host: _HostElement, scoped: boolean): void; // (undocumented) $buildBase$: string | null; // (undocumented) @@ -1101,15 +1216,15 @@ export abstract class _SharedContainer implements _Container { readonly $version$: string; constructor(serverData: Record, locale: string); // (undocumented) - abstract ensureProjectionResolved(host: HostElement): void; + abstract ensureProjectionResolved(host: _HostElement): void; // (undocumented) - abstract getHostProp(host: HostElement, name: string): T | null; + abstract getHostProp(host: _HostElement, name: string): T | null; // (undocumented) - abstract getParentHost(host: HostElement): HostElement | null; + abstract getParentHost(host: _HostElement): _HostElement | null; // (undocumented) - abstract handleError(err: any, $host$: HostElement | null): void; + abstract handleError(err: any, $host$: _HostElement | null): void; // (undocumented) - abstract resolveContext(host: HostElement, contextId: ContextId): T | undefined; + abstract resolveContext(host: _HostElement, contextId: ContextId): T | undefined; // (undocumented) serializationCtxFactory(NodeConstructor: { new (...rest: any[]): { @@ -1121,11 +1236,11 @@ export abstract class _SharedContainer implements _Container { }; } | null, symbolToChunkResolver: SymbolToChunkResolver, writer?: StreamWriter): SerializationContext; // (undocumented) - abstract setContext(host: HostElement, context: ContextId, value: T): void; + abstract setContext(host: _HostElement, context: ContextId, value: T): void; // (undocumented) - abstract setHostProp(host: HostElement, name: string, value: T): void; + abstract setHostProp(host: _HostElement, name: string, value: T): void; // (undocumented) - trackSignalValue(signal: Signal, subscriber: HostElement, property: string, data: _SubscriptionData): T; + trackSignalValue(signal: Signal, subscriber: _HostElement, property: string, data: _SubscriptionData): T; } // @public @@ -1236,6 +1351,16 @@ export interface SSRStreamWriter { write(chunk: JSXOutput): void; } +// @internal (undocumented) +export const enum _StoreFlags { + // (undocumented) + IMMUTABLE = 2, + // (undocumented) + NONE = 0, + // (undocumented) + RECURSIVE = 1 +} + // Warning: (ae-internal-missing-underscore) The name "StreamWriter" should be prefixed with an underscore because the declaration is marked as @internal // // @internal (undocumented) @@ -1821,6 +1946,29 @@ export type SyncQRL = QRL & { dev?: QRLDev | null; } & BivariantQrlFn, QrlReturn>; +// Warning: (ae-forgotten-export) The symbol "DescriptorBase" needs to be exported by the entry point index.d.ts +// +// @internal (undocumented) +export class _Task extends BackRef implements DescriptorBase> { + // (undocumented) + $destroy$: (() => void) | null; + // (undocumented) + $destroyPromise$: Promise | undefined; + // (undocumented) + $el$: _HostElement; + // (undocumented) + $flags$: number; + // (undocumented) + $index$: number; + // (undocumented) + $qrl$: _QRLInternal; + // (undocumented) + $state$: Signal | undefined; + // (undocumented) + $taskPromise$: Promise | null; + constructor($flags$: number, $index$: number, $el$: _HostElement, $qrl$: _QRLInternal, $state$: Signal | undefined, $destroy$: (() => void) | null); +} + // @internal export function _task(this: string, _event: Event, element: Element): void; @@ -2042,8 +2190,6 @@ export class _VirtualVNode extends _VNode { // @public (undocumented) export type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | 'document-idle'; -// Warning: (ae-forgotten-export) The symbol "BackRef" needs to be exported by the entry point index.d.ts -// // @internal (undocumented) export abstract class _VNode implements BackRef { // (undocumented) diff --git a/packages/qwik/src/core/reactive-primitives/impl/async-signal-impl.ts b/packages/qwik/src/core/reactive-primitives/impl/async-signal-impl.ts index 1095d45e3ff..0687fb7315f 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/async-signal-impl.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/async-signal-impl.ts @@ -664,3 +664,17 @@ export class AsyncSignalImpl } } } + +/** + * Inject a pre-loaded value into an AsyncSignal while preserving track() subscriptions. Calls + * `invalidate({ __v })` so the compute function reads the value from `info`, then triggers an + * immediate synchronous compute via the private $computeIfNeeded$() method (which is mangled in + * core builds, so callers from other packages must go through this helper). + * + * @internal + */ +export const _injectAsyncSignalValue = (signal: AsyncSignal, value: unknown) => { + const impl = signal as AsyncSignalImpl; + impl.invalidate({ __v: value }); + impl.$computeIfNeeded$(); +}; diff --git a/packages/qwik/src/core/reactive-primitives/impl/store.ts b/packages/qwik/src/core/reactive-primitives/impl/store.ts index 5900470488f..a0a083d6525 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/store.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/store.ts @@ -74,6 +74,7 @@ export const isStore = (value: object): boolean => { return STORE_TARGET in value; }; +/** @internal */ export function createStore( container: Container | null | undefined, obj: T, diff --git a/packages/qwik/src/core/reactive-primitives/subscriber.ts b/packages/qwik/src/core/reactive-primitives/subscriber.ts index d98372812eb..0ec0a7aec7b 100644 --- a/packages/qwik/src/core/reactive-primitives/subscriber.ts +++ b/packages/qwik/src/core/reactive-primitives/subscriber.ts @@ -5,6 +5,7 @@ import { Consumer, EffectProperty, EffectSubscription } from './types'; import { _EFFECT_BACK_REF, type BackRef } from './backref'; import type { SubscriptionData } from './subscription-data'; +/** @internal */ export function getSubscriber( effect: Consumer, prop: EffectProperty | string, diff --git a/packages/qwik/src/core/reactive-primitives/types.ts b/packages/qwik/src/core/reactive-primitives/types.ts index b225ab89e07..8f311ee3c00 100644 --- a/packages/qwik/src/core/reactive-primitives/types.ts +++ b/packages/qwik/src/core/reactive-primitives/types.ts @@ -212,6 +212,7 @@ export class EffectSubscription { export type EffectBackRef = SignalImpl | StoreTarget | PropsProxy; +/** @internal */ export const enum EffectProperty { COMPONENT = ':', VNODE = '.', @@ -281,6 +282,7 @@ export const STORE_ALL_PROPS = Symbol('store.all'); export type StoreTarget = Record; +/** @internal */ export const enum StoreFlags { NONE = 0, RECURSIVE = 1, diff --git a/packages/qwik/src/core/shared/types.ts b/packages/qwik/src/core/shared/types.ts index 7de25a54b64..8bb0f36fe57 100644 --- a/packages/qwik/src/core/shared/types.ts +++ b/packages/qwik/src/core/shared/types.ts @@ -58,6 +58,7 @@ export interface Container { ): SerializationContext; } +/** @internal */ export type HostElement = VNode | ISsrNode; export interface QElement extends Element { diff --git a/packages/qwik/src/core/shared/utils/markers.ts b/packages/qwik/src/core/shared/utils/markers.ts index 24944980d4c..12d05f9d7bf 100644 --- a/packages/qwik/src/core/shared/utils/markers.ts +++ b/packages/qwik/src/core/shared/utils/markers.ts @@ -80,6 +80,7 @@ export const QDefaultSlot = ''; export const ELEMENT_ID = 'q:id'; export const ELEMENT_KEY = 'q:key'; export const ELEMENT_PROPS = 'q:props'; +/** @internal */ export const ELEMENT_SEQ = 'q:seq'; export const ELEMENT_SEQ_IDX = 'q:seqIdx'; export const ELEMENT_BACKPATCH_DATA = 'qwik/backpatch'; diff --git a/packages/qwik/src/core/shared/utils/promises.ts b/packages/qwik/src/core/shared/utils/promises.ts index 13445e8c155..cef63fbf3e8 100644 --- a/packages/qwik/src/core/shared/utils/promises.ts +++ b/packages/qwik/src/core/shared/utils/promises.ts @@ -85,6 +85,7 @@ export const isNotNullable = (v: T): v is NonNullable => { return v != null; }; +/** @internal */ export const delay = (timeout: number) => { return new Promise((resolve) => { setTimeout(resolve, timeout); @@ -103,6 +104,8 @@ const justThrow = (e: any) => { /** * Retries a function that throws a promise. If you pass `onError`, you're responsible for handling * errors. + * + * @internal */ export function retryOnPromise( fn: () => ValueOrPromise, diff --git a/packages/qwik/src/core/use/use-core.ts b/packages/qwik/src/core/use/use-core.ts index acd2721b943..58006694e3e 100644 --- a/packages/qwik/src/core/use/use-core.ts +++ b/packages/qwik/src/core/use/use-core.ts @@ -87,7 +87,11 @@ export function useBindInvokeContext any>( } as FN; } -/** Call a function with the given InvokeContext and given arguments. */ +/** + * Call a function with the given InvokeContext and given arguments. + * + * @internal + */ export function invoke any>( this: unknown, context: InvokeContext | undefined, @@ -140,6 +144,7 @@ export function newRenderInvokeContext( return ctx; } +/** @internal */ // TODO how about putting url and locale (and event/custom?) in to a "static" object export function newInvokeContext( locale?: string, diff --git a/packages/qwik/src/core/use/use-task.ts b/packages/qwik/src/core/use/use-task.ts index f6d36d6c8c7..e455a665646 100644 --- a/packages/qwik/src/core/use/use-task.ts +++ b/packages/qwik/src/core/use/use-task.ts @@ -235,6 +235,7 @@ export const runTask = ( return result; }; +/** @internal */ export class Task extends BackRef implements DescriptorBase>