Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: split loaders from qdata.json
  • Loading branch information
Varixo authored and wmertens committed Sep 21, 2025
commit c249d02b1cda524937c2196a520f021db40bd54b
2 changes: 1 addition & 1 deletion packages/qwik-router/src/buildtime/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { generateQwikRouterEntries } from '../runtime-generation/generate-entrie
import { generateQwikRouterConfig } from '../runtime-generation/generate-qwik-router-config';
import { generateServiceWorkerRegister } from '../runtime-generation/generate-service-worker';
import type { RoutingContext } from '../types';
import { getRouterIndexTags, makeRouterDevMiddleware } from './dev-middleware';
import { getRouteImports } from './get-route-imports';
import { imagePlugin } from './image-jsx';
import type {
Expand All @@ -21,7 +22,6 @@ import type {
QwikRouterVitePluginOptions,
} from './types';
import { validatePlugin } from './validate-plugin';
import { getRouterIndexTags, makeRouterDevMiddleware } from './dev-middleware';

export const QWIK_ROUTER_CONFIG_ID = '@qwik-router-config';
const QWIK_ROUTER_ENTRIES_ID = '@qwik-router-entries';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { _serialize, _UNINITIALIZED } from '@qwik.dev/core/internal';
import type {
DataValidator,
LoaderInternal,
RequestHandler,
ValidatorReturn,
} from '../../runtime/src/types';
import {
getRequestLoaders,
getRequestLoaderSerializationStrategyMap,
getRequestMode,
} from './request-event';
import { measure, verifySerializable } from './resolve-request-handlers';
import type { RequestEvent } from './types';
import { IsQLoader, IsQLoaderData, QLoaderId } from './user-response';

export function loaderDataHandler(routeLoaders: LoaderInternal[]): RequestHandler {
return async (requestEvent: RequestEvent) => {
const requestEv = requestEvent as RequestEventInternal;

const isQLoaderData = requestEv.sharedMap.has(IsQLoaderData);
if (!isQLoaderData) {
return;
}

if (requestEv.headersSent || requestEv.exited) {
return;
}

// Set cache headers - aggressive for loaders
requestEv.cacheControl({
maxAge: 300, // 5 minutes
staleWhileRevalidate: 3600, // 1 hour
});

// return loader ids
const loaderIds = routeLoaders.map((l) => l.__id);
return requestEv.json(200, { loaderIds });
};
}

export function singleLoaderHandler(routeLoaders: LoaderInternal[]): RequestHandler {
return async (requestEvent: RequestEvent) => {
const requestEv = requestEvent as RequestEventInternal;

const isQLoader = requestEv.sharedMap.has(IsQLoader);
if (!isQLoader) {
return;
}

if (requestEv.headersSent || requestEv.exited) {
return;
}
const loaderId = requestEv.sharedMap.get(QLoaderId);

try {
// Execute just this loader
const loaders = getRequestLoaders(requestEv);
const isDev = getRequestMode(requestEv) === 'dev';

let loader: LoaderInternal | undefined;
for (const routeLoader of routeLoaders) {
if (routeLoader.__id === loaderId) {
loader = routeLoader;
} else if (!loaders[routeLoader.__id]) {
loaders[routeLoader.__id] = _UNINITIALIZED;
}
}

if (!loader) {
return requestEv.json(404, { error: 'Loader not found' });
}

await executeLoader(loader, loaders, requestEv, isDev);

// Set cache headers - aggressive for loaders
requestEv.cacheControl({
maxAge: 300, // 5 minutes
staleWhileRevalidate: 3600, // 1 hour
});

const data = await _serialize([loaders[loaderId]]);

requestEv.headers.set('Content-Type', 'application/json; charset=utf-8');

// Return just this loader's result
return requestEv.send(200, data);
} catch (error) {
console.error(`Loader ${loaderId} failed:`, error);
return requestEv.json(500, { error: 'Loader execution failed' });
}
};
}

export async function executeLoader(
loader: LoaderInternal,
loaders: Record<string, unknown>,
requestEv: RequestEventInternal,
isDev: boolean
) {
const loaderId = loader.__id;
loaders[loaderId] = runValidators(
requestEv,
loader.__validators,
undefined, // data
isDev
)
.then((res) => {
if (res.success) {
if (isDev) {
return measure<Promise<unknown>>(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;
}
return resolvedLoader;
});
const loadersSerializationStrategy = getRequestLoaderSerializationStrategyMap(requestEv);
loadersSerializationStrategy.set(loaderId, loader.__serializationStrategy);
return loaders[loaderId];
}

export async function runValidators(
requestEv: RequestEvent,
validators: DataValidator[] | undefined,
data: unknown,
isDev: boolean
) {
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// requestEv.sharedMap.get(RequestEvSharedActionId)

import type { RequestEvent } from '@qwik.dev/router';
import { _serialize } from 'packages/qwik/core-internal';
import { RequestEvIsRewrite, RequestEvSharedActionId } from './request-event';
import { getPathname } from './resolve-request-handlers';
import { IsQData } from './user-response';

export interface QData {
status: number;
href: string;
action?: string;
redirect?: string;
isRewrite?: boolean;
}

export async function qDataHandler(requestEv: RequestEvent) {
const isPageDataReq = requestEv.sharedMap.has(IsQData);
if (!isPageDataReq) {
return;
}

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');

const qData: QData = {
status,
href: getPathname(requestEv.url),
action: requestEv.sharedMap.get(RequestEvSharedActionId),
redirect: redirectLocation ?? undefined,
isRewrite: requestEv.sharedMap.get(RequestEvIsRewrite),
};

// Set cache headers
requestEv.cacheControl({
maxAge: 300, // 5 minutes
staleWhileRevalidate: 3600, // 1 hour
});

// write just the page json data to the response body
const data = await _serialize([qData]);

requestEv.send(200, data);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
ServerError,
RewriteMessage,
} from '@qwik.dev/router/middleware/request-handler';
import { encoder, getRouteLoaderPromise } from './resolve-request-handlers';
import { encoder } from './resolve-request-handlers';
import type {
CacheControl,
CacheControlTarget,
Expand All @@ -31,7 +31,18 @@ import type {
ServerRequestEvent,
ServerRequestMode,
} from './types';
import { IsQData, QDATA_JSON, QDATA_JSON_LEN } from './user-response';
import {
IsQData,
IsQLoader,
IsQLoaderData,
Q_LOADER_DATA_JSON,
Q_LOADER_DATA_JSON_LEN,
QDATA_JSON,
QDATA_JSON_LEN,
QLoaderId,
SINGLE_LOADER_REGEX,
} from './user-response';
import { executeLoader } from './loader-endpoints';

const RequestEvLoaders = Symbol('RequestEvLoaders');
const RequestEvMode = Symbol('RequestEvMode');
Expand Down Expand Up @@ -61,12 +72,26 @@ export function createRequestEvent(
const cookie = new Cookie(request.headers.get('cookie'));
const headers = new Headers();
const url = new URL(request.url);
if (url.pathname.endsWith(QDATA_JSON)) {
url.pathname = url.pathname.slice(0, -QDATA_JSON_LEN);

const trimEnd = (length: number) => {
url.pathname = url.pathname.slice(0, -length);
if (!globalThis.__NO_TRAILING_SLASH__ && !url.pathname.endsWith('/')) {
url.pathname += '/';
}
};

if (url.pathname.endsWith(QDATA_JSON)) {
trimEnd(QDATA_JSON_LEN);
sharedMap.set(IsQData, true);
} else if (url.pathname.endsWith(Q_LOADER_DATA_JSON)) {
trimEnd(Q_LOADER_DATA_JSON_LEN);
sharedMap.set(IsQLoaderData, true);
}
const loaderMatch = url.pathname.match(SINGLE_LOADER_REGEX);
if (loaderMatch) {
trimEnd(loaderMatch[0].length);
sharedMap.set(IsQLoader, true);
sharedMap.set(QLoaderId, loaderMatch[1]); // Store which loader was requested
}

let routeModuleIndex = -1;
Expand Down Expand Up @@ -211,7 +236,7 @@ export function createRequestEvent(
}
if (loaders[id] === _UNINITIALIZED) {
const isDev = getRequestMode(requestEv) === 'dev';
await getRouteLoaderPromise(loaderOrAction, loaders, requestEv, isDev);
await executeLoader(loaderOrAction, loaders, requestEv, isDev);
}
}

Expand Down
Loading