q-data.json: split per loader#8501
Conversation
🦋 Changeset detectedLatest commit: b8ca8ab The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
07ecc7d to
042e959
Compare
822296f to
627914d
Compare
Varixo
left a comment
There was a problem hiding this comment.
Great work! Added some comments/questions
| [ | ||
| { 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); | ||
| }); | ||
| }); | ||
|
|
4dd423a to
d296b44
Compare
built with Refined Cloudflare Pages Action⚡ Cloudflare Pages Deployment
|
8f666d8 to
9cca76c
Compare
3e44f25 to
7488793
Compare
34881a5 to
68dc6d2
Compare
@qwik.dev/core
@qwik.dev/router
eslint-plugin-qwik
create-qwik
@qwik.dev/optimizer
commit: |
f23a05e to
21d1bb0
Compare
There was a problem hiding this comment.
Pull request overview
This PR refactors Qwik Router’s data fetching model by replacing monolithic q-data.json requests with per-loader cacheable JSON endpoints and aligning route loaders with the new AsyncSignal-based loader runtime. It also introduces supporting middleware/handlers, build-time route metadata emission for loader hashes, SSG output changes, and updates docs/tests accordingly.
Changes:
- Replace
q-data.jsonSPA navigation with per-loaderq-loader-*.jsonfetching + caching/ETag support, and make route loaders AsyncSignals. - Add new request recognition + middleware handlers for loader/action JSON flows, and update SSG to emit per-loader files.
- Enhance the router Vite plugin to track loader segment hashes and inject them into the generated route trie (
_R), including dev HMR invalidation.
Reviewed changes
Copilot reviewed 125 out of 128 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/qwik/src/core/use/use-task.ts | Marks Task as internal. |
| packages/qwik/src/core/use/use-core.ts | Marks invoke helpers as internal. |
| packages/qwik/src/core/shared/utils/promises.ts | Marks delay/retry utilities as internal. |
| packages/qwik/src/core/shared/utils/markers.ts | Marks ELEMENT_SEQ as internal. |
| packages/qwik/src/core/shared/types.ts | Marks HostElement as internal and re-exports internally. |
| packages/qwik/src/core/reactive-primitives/types.ts | Marks enums as internal. |
| packages/qwik/src/core/reactive-primitives/subscriber.ts | Marks getSubscriber() as internal. |
| packages/qwik/src/core/reactive-primitives/impl/store.ts | Marks createStore() as internal. |
| packages/qwik/src/core/reactive-primitives/impl/async-signal-impl.ts | Adds internal helper to inject AsyncSignal values. |
| packages/qwik/src/core/internal.ts | Expands internal exports needed by router loader tests/runtime. |
| packages/qwik-vite/src/qwik.optimizer.api.md | Updates generated optimizer API docs to include onSegment. |
| packages/qwik-vite/src/plugins/vite.ts | Adds onSegment() API + public SegmentCallback type. |
| packages/qwik-vite/src/plugins/plugin.ts | Collects segment callbacks and notifies during transform. |
| packages/qwik-router/tsconfig.json | Removes optimizer path alias. |
| packages/qwik-router/src/utils/pathname.ts | Adds ensureSlash() helper and adjusts imports. |
| packages/qwik-router/src/utils/fs.unit.ts | Updates test fixtures for new strictLoaders option. |
| packages/qwik-router/src/utils/fs.ts | Uses ensureSlash() in pathname generation. |
| packages/qwik-router/src/ssg/worker-thread.ts | Writes per-loader SSG data files instead of q-data.json; updates internal pathname handling. |
| packages/qwik-router/src/ssg/types.ts | Renames getDataFilePath → getLoaderFilePath; updates docs. |
| packages/qwik-router/src/ssg/system.ts | Implements getLoaderFilePath() using q-loader-*.json naming. |
| packages/qwik-router/src/ssg/response-page-ssg.ts | Removes q-data loader serialization deps for SSG response assembly. |
| packages/qwik-router/src/ssg/resolve-request-handlers-ssg.ts | Updates SSG handler resolution to new internal request model. |
| packages/qwik-router/src/ssg/request-event-ssg.ts | Uses new request recognition + action/loader flags in SSG request event creation. |
| packages/qwik-router/src/ssg/orchestrator.unit.ts | Updates system mock to use getLoaderFilePath(). |
| packages/qwik-router/src/ssg/orchestrator.ts | Uses ensureSlash() in SSG URL generation. |
| packages/qwik-router/src/ssg/bundle-isolation.unit.ts | Externalizes @qwik-router-config for worker isolation tests. |
| packages/qwik-router/src/runtime/src/view-transition.tsx | Exposes ViewTransition type and returns { ready, transition } from helper. |
| packages/qwik-router/src/runtime/src/utils.unit.ts | Removes tests for removed getClientDataPath(). |
| packages/qwik-router/src/runtime/src/utils.ts | Removes q-data helpers; switches pathname comparisons to ensureSlash(). |
| packages/qwik-router/src/runtime/src/use-endpoint.unit.ts | Adds unit tests for loader fetch/dedupe/abort behavior. |
| packages/qwik-router/src/runtime/src/use-endpoint.ts | Replaces loadClientData() with submitAction() implementation. |
| packages/qwik-router/src/runtime/src/types.ts | Adds loader hash metadata (_R, $loaders$, $loaderPaths$) and new loader/action options. |
| packages/qwik-router/src/runtime/src/typed-routes.ts | Uses ensureSlash() for base URL handling. |
| packages/qwik-router/src/runtime/src/spa-init.ts | Removes delayed listener registration; registers immediately. |
| packages/qwik-router/src/runtime/src/server-functions.ts | Moves routeLoader implementation out; adds invalidate option for actions; simplifies SSR request event lookup. |
| packages/qwik-router/src/runtime/src/routing.unit.ts | Adds routing tests for loader path tracking and dead-end behavior. |
| packages/qwik-router/src/runtime/src/route-loaders.unit.ts | Adds unit tests for loader memoization + expires units. |
| packages/qwik-router/src/runtime/src/route-loaders.spec.tsx | Adds spec tests validating AsyncSignal/store tracking mechanics. |
| packages/qwik-router/src/runtime/src/qwik-router.runtime.api.md | Updates generated router runtime API docs (_R, routeLoader$, env data fields). |
| packages/qwik-router/src/runtime/src/prefetch-route.ts | Adds route prefetch helper for bundles + per-loader data. |
| packages/qwik-router/src/runtime/src/link-component.unit.tsx | Updates Link prefetch tests to use prefetchRoute(). |
| packages/qwik-router/src/runtime/src/link-component.tsx | Switches Link prefetching to unified prefetchRoute() and uses manifest hash. |
| packages/qwik-router/src/runtime/src/index.ts | Re-exports route loader APIs from new route-loaders module. |
| packages/qwik-router/src/runtime/src/head.unit.ts | Updates resolveHead() call signature in tests. |
| packages/qwik-router/src/runtime/src/head.ts | Resolves head from loader AsyncSignals + action result instead of q-data payload. |
| packages/qwik-router/src/runtime/src/contexts.ts | Adds RouteLoaderCtxContext. |
| packages/qwik-router/src/runtime/src/constants.ts | Removes CLIENT_DATA_CACHE. |
| packages/qwik-router/src/runtime/src/client-navigate.ts | Uses ensureSlash() for bundle preload path formatting. |
| packages/qwik-router/src/middleware/request-handler/static-paths.ts | Treats q-loader-*.json as static-path eligible instead of q-data.json. |
| packages/qwik-router/src/middleware/request-handler/response-page.ts | Removes old loader serialization deps from response assembly. |
| packages/qwik-router/src/middleware/request-handler/response-page-core.ts | Includes routeLoaderCtx + loaderValues in env data; adds actionResult. |
| packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.unit.ts | Updates tests for new handler signature and action resolution semantics. |
| packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts | Removes q-data wiring from handler resolution. |
| packages/qwik-router/src/middleware/request-handler/request-path.unit.ts | Adds unit tests for new loader pathname recognition/trimming helpers. |
| packages/qwik-router/src/middleware/request-handler/request-path.ts | Introduces loader request recognition (q-loader-*.json) and trim helpers. |
| packages/qwik-router/src/middleware/request-handler/request-loader.ts | Delegates loader execution to runtime route-loader logic. |
| packages/qwik-router/src/middleware/request-handler/request-handler.ts | Uses internal pathname trimming; removes internal flag plumbing. |
| packages/qwik-router/src/middleware/request-handler/request-event.ts | Uses new request recognition deps (loader/action). |
| packages/qwik-router/src/middleware/request-handler/request-event-core.ts | Reworks loader/action resolveValue() semantics; removes q-data sharedMap storage. |
| packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md | Updates generated middleware API docs; removes RequestEvShareQData export. |
| packages/qwik-router/src/middleware/request-handler/index.ts | Adjusts exports (async store exported from its module); removes RequestEvShareQData export. |
| packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.unit.ts | Adds unit tests for loader handler cache-control/vary behavior. |
| packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.ts | Adds loader JSON handler with ETag support and LoaderResponse envelope. |
| packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.unit.ts | Adds unit tests for JSON wrapper fullpath rewrite + Vary header. |
| packages/qwik-router/src/middleware/request-handler/handlers/json-request-wrapper.ts | Adds wrapper to convert redirects/errors to JSON envelopes and rewrite loader URLs. |
| packages/qwik-router/src/middleware/request-handler/handlers/action-handler.unit.ts | Adds unit tests for action JSON responses + strict loaders behavior. |
| packages/qwik-router/src/middleware/request-handler/handlers/action-handler.ts | Adds action JSON handler returning action result and optional loader invalidation hashes. |
| packages/qwik-router/src/middleware/aws-lambda/index.ts | Uses ensureSlash() for pathname normalization. |
| packages/qwik-router/src/buildtime/vite/plugin.unit.ts | Adds tests for loader-hash dev cache and router-config invalidation. |
| packages/qwik-router/src/buildtime/vite/plugin.ts | Tracks routeLoader$ segment hashes via optimizer, injects into route trie, improves HMR invalidation, adds strictLoaders define. |
| packages/qwik-router/src/buildtime/types.ts | Adds strictLoaders option documentation. |
| packages/qwik-router/src/buildtime/runtime-generation/generate-routes.unit.ts | Updates unit test harness opts shape and defaults. |
| packages/qwik-router/src/buildtime/runtime-generation/generate-routes.ts | Emits _R loader hash arrays (or placeholders) into route trie serialization. |
| packages/qwik-router/src/buildtime/runtime-generation/generate-qwik-router-config.ts | Plumbs loadersByFile into route generation. |
| packages/qwik-router/src/buildtime/routing/resolve-source-file.unit.ts | Updates test opts for strictLoaders. |
| packages/qwik-router/src/buildtime/markdown/markdown-url.unit.ts | Updates test opts for strictLoaders. |
| packages/qwik-router/src/buildtime/markdown/markdown-url.ts | Uses ensureSlash() when enforcing trailing slash policy. |
| packages/qwik-router/src/buildtime/context.ts | Normalizes base paths via ensureSlash(); defaults strictLoaders to true. |
| packages/qwik-router/src/buildtime/build.ts | Uses ensureSlash() when building route name prefixes. |
| packages/qwik-router/src/adapters/shared/vite/post-build.ts | Treats loader JSON files as static artifacts; uses LOADER_REGEX + ensureSlash. |
| packages/qwik-router/src/adapters/cloudflare-pages/vite/index.ts | Uses ensureSlash() for assets pathname. |
| packages/qwik-router/global.d.ts | Adds __STRICT_LOADERS__ global type. |
| packages/qwik-router/ARCHITECTURE.md | Adds architecture document describing new loader/action/navigation flow. |
| packages/docs/src/routes/api/qwik-router/index.mdx | Updates generated router docs for new env fields and loader/action behavior. |
| packages/docs/src/routes/api/qwik-router/api.json | Updates generated router API JSON output. |
| packages/docs/src/routes/api/qwik-router-ssg/index.mdx | Updates generated SSG docs to refer to per-loader files. |
| packages/docs/src/routes/api/qwik-router-ssg/api.json | Updates generated SSG API JSON output. |
| packages/docs/src/routes/api/qwik-optimizer/index.mdx | Updates generated optimizer docs to include onSegment. |
| packages/docs/src/routes/api/qwik-optimizer/api.json | Updates generated optimizer API JSON output. |
| e2e/qwik-e2e/tests/starter-partytown.e2e.ts | Increases timeout for worker-based Partytown test stability. |
| e2e/qwik-e2e/tests/qwikrouter/nav.e2e.ts | Updates SPA navigation tests for per-loader fetches and new timing behavior; adds new scenarios. |
| e2e/qwik-e2e/tests/qwikrouter/loaders.e2e.ts | Updates loader/action expectations given “no action state in loaders”; adds SPA/MPA distinctions. |
| e2e/qwik-e2e/tests/qwikrouter/auth.e2e.ts | Adds stabilization waits/timeouts for auth flows. |
| e2e/qwik-e2e/playwright.config.ts | Enables retries and configures output directory. |
| e2e/qwik-e2e/dev-server.ts | Sets strictLoaders: false to preserve legacy e2e expectations. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx | Ensures eager loader is read during SSR path for serialization. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/target/index.tsx | Adds redirect target route fixture with loader. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/source/index.tsx | Adds redirect source route fixture with middleware + loader. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/loader-redirect/index.tsx | Adds navigation entry route fixture for loader redirect test. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/issue7732/b/index.tsx | Adds loader to ensure SPA hits loader endpoint and triggers redirect middleware. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/issue4502/broken/index.tsx | Adds loader + component to ensure middleware redirect runs under SPA. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/c/index.tsx | Adds route C fixture with loader data. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/b/index.tsx | Adds route B fixture with loader data. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/double-nav/a/index.tsx | Adds route A fixture that triggers back-to-back nav calls. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/loaders/[id]/index.tsx | Removes action-state dependency from loader; updates redirect/json behavior commentary. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/session-data.ts | Adds cookie-backed session storage fixture for e2e. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/other/index.tsx | Switches from module-global array to session cookie-backed storage. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/index.tsx | Uses session cookie to persist data across redirect. |
| e2e/qwik-e2e/apps/qwikrouter-test/src/routes/(common)/issue2644/data.ts | Removes old shared module-global data array. |
| e2e/qwik-e2e/apps/qwikrouter-ssg-snapshot/expected.ssg.html | Updates snapshot expected output due to routing/runtime changes. |
| .gitignore | Anchors dist-dev/target ignores to repo root. |
| .changeset/warm-deer-take.md | Adds changeset for loader search + strictLoaders. |
| .changeset/nasty-pans-tickle.md | Adds breaking changeset for per-loader JSON and caching. |
| .changeset/loaders-no-action-state.md | Adds breaking changeset for removing action state from loaders. |
| .changeset/green-experts-turn.md | Adds breaking changeset for loaders as AsyncSignals. |
| .changeset/fresh-loaders-update.md | Adds patch changeset for dev route trie loader metadata updates. |
| .changeset/flat-hornets-fetch.md | Adds feature changeset for loader eTag. |
| .changeset/cuddly-bags-push.md | Adds minor changeset for loader redirect envelope behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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; | ||
| 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', |
| @@ -1133,6 +1140,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; | |||
| 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. |
| 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. |
| 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. | ||
|
|
| \#\# 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. |
node 24 compat also ensure that e2e failures result in artifacts by retrying
- Each loader gets its own .json file served where it's exported - `q-data.json` no longer exists - declarative redirects are found in the routing trie on the client - programmatic redirects are passed in the JSON results - route loaders get expiry and polling - SPA navigation no longer waits for route loaders - `showStale: false` prevents showing the user stale data - `search` allows defining which search params matter for the loader
instead of sending data that the browser can't cache
- search: [] was skipping path changes - actions should set HTTP status - don't lose loadersByFile
a16b7ab to
960b3e2
Compare
|
there's still an issue with http://localhost:3300/qwikrouter-test/loader-redirect/, on firefox it does not remove the old index when redirecting |
1d52156 to
b8ca8ab
Compare
Big sanity improvement, some breaking changes:
Route loaders now each get their own cacheable .json path, and SPA navigation no longer needs to contact the server on every click.
Breaking:
loader.value.failedin-band data is no longer available. Instead, read fromloader.error.There are several other ways to use action results, so this isn't limiting.
The net result of this change: