Skip to content

Commit efca71a

Browse files
Varixowmertens
authored andcommitted
feat: add loader info to qwik router config
1 parent c249d02 commit efca71a

28 files changed

+611
-113
lines changed

packages/docs/src/routes/api/qwik-router/api.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@
656656
}
657657
],
658658
"kind": "Interface",
659-
"content": "```typescript\nexport interface QwikRouterConfig \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[basePathname?](./router.qwikrouterconfig.basepathname.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[cacheModules?](./router.qwikrouterconfig.cachemodules.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[menus?](./router.qwikrouterconfig.menus.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n[MenuData](#menudata)<!-- -->\\[\\]\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[routes](./router.qwikrouterconfig.routes.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n[RouteData](#routedata)<!-- -->\\[\\]\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[serverPlugins?](./router.qwikrouterconfig.serverplugins.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\nRouteModule\\[\\]\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[trailingSlash?](./router.qwikrouterconfig.trailingslash.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>",
659+
"content": "```typescript\nexport interface QwikRouterConfig \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[basePathname?](./router.qwikrouterconfig.basepathname.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[cacheModules?](./router.qwikrouterconfig.cachemodules.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[loaderIdToRoute?](./router.qwikrouterconfig.loaderidtoroute.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\nRecord&lt;string, string&gt;\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[menus?](./router.qwikrouterconfig.menus.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n[MenuData](#menudata)<!-- -->\\[\\]\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[routes](./router.qwikrouterconfig.routes.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\n[RouteData](#routedata)<!-- -->\\[\\]\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[serverPlugins?](./router.qwikrouterconfig.serverplugins.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\nRouteModule\\[\\]\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[trailingSlash?](./router.qwikrouterconfig.trailingslash.md)\n\n\n</td><td>\n\n`readonly`\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>",
660660
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/runtime/src/types.ts",
661661
"mdFile": "router.qwikrouterconfig.md"
662662
},

packages/docs/src/routes/api/qwik-router/index.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,6 +1510,23 @@ _(Optional)_
15101510
</td></tr>
15111511
<tr><td>
15121512
1513+
[loaderIdToRoute?](./router.qwikrouterconfig.loaderidtoroute.md)
1514+
1515+
</td><td>
1516+
1517+
`readonly`
1518+
1519+
</td><td>
1520+
1521+
Record&lt;string, string&gt;
1522+
1523+
</td><td>
1524+
1525+
_(Optional)_
1526+
1527+
</td></tr>
1528+
<tr><td>
1529+
15131530
[menus?](./router.qwikrouterconfig.menus.md)
15141531
15151532
</td><td>

packages/qwik-router/modules.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ declare module '@qwik-router-config' {
44
export const trailingSlash: boolean;
55
export const basePathname: string;
66
export const cacheModules: boolean;
7+
export const loaderIdToRoute: Record<string, string>;
78
const defaultExport: {
89
routes: any[];
910
menus: any[];
1011
trailingSlash: boolean;
1112
basePathname: string;
1213
cacheModules: boolean;
14+
loaderIdToRoute: Record<string, string>;
1315
};
1416
export default defaultExport;
1517
}

packages/qwik-router/src/buildtime/runtime-generation/generate-qwik-router-config.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { QwikVitePlugin } from '@qwik.dev/core/optimizer';
22
import type { RoutingContext } from '../types';
33
import { createEntries } from './generate-entries';
44
import { createMenus } from './generate-menus';
5-
import { createRoutes } from './generate-routes';
5+
import { createLoaderIdToRoute, createRoutes } from './generate-routes';
66
import { createServerPlugins } from './generate-server-plugins';
77

88
/** Generates the Qwik Router Config runtime code */
@@ -25,14 +25,16 @@ export function generateQwikRouterConfig(
2525

2626
createEntries(ctx, c);
2727

28+
createLoaderIdToRoute(ctx, qwikPlugin, c);
29+
2830
c.push(`export const trailingSlash = ${JSON.stringify(!globalThis.__NO_TRAILING_SLASH__)};`);
2931

3032
c.push(`export const basePathname = ${JSON.stringify(ctx.opts.basePathname)};`);
3133

3234
c.push(`export const cacheModules = !isDev;`);
3335

3436
c.push(
35-
`export default { routes, serverPlugins, menus, trailingSlash, basePathname, cacheModules };`
37+
`export default { routes, serverPlugins, menus, trailingSlash, basePathname, cacheModules, loaderIdToRoute };`
3638
);
3739
return esmImports.join('\n') + c.join('\n');
3840
}

packages/qwik-router/src/buildtime/runtime-generation/generate-routes.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { QwikManifest, QwikVitePlugin } from '@qwik.dev/core/optimizer';
2-
import { isModuleExt, isPageExt, removeExtension } from '../../utils/fs';
2+
import { getPathnameFromDirPath, isModuleExt, isPageExt, removeExtension } from '../../utils/fs';
33
import type { RoutingContext, BuiltRoute } from '../types';
44
import { getImportPath } from './utils';
55

@@ -121,3 +121,62 @@ function getClientRouteBundleNames(qwikPlugin: QwikVitePlugin, r: BuiltRoute) {
121121

122122
return bundlesNames;
123123
}
124+
125+
export function createLoaderIdToRoute(
126+
ctx: RoutingContext,
127+
qwikPlugin: QwikVitePlugin,
128+
c: string[]
129+
) {
130+
const manifest = qwikPlugin.api.getManifest();
131+
const loaderSymbols: Record<string, string> = {};
132+
const mainDir = ctx.routes[0].routeName;
133+
const routesDir = ctx.opts.routesDir.split('/').pop()!;
134+
135+
if (manifest) {
136+
for (const symbolData of Object.values(manifest.symbols)) {
137+
if (symbolData.ctxName === 'routeLoader$') {
138+
// extract file name from origin
139+
const fileName = symbolData.origin.split('/').pop();
140+
if (!fileName) {
141+
console.warn(`File name not found for loader: ${symbolData.origin}`);
142+
continue;
143+
}
144+
145+
const filePath =
146+
mainDir +
147+
symbolData.origin
148+
.replace(routesDir, '')
149+
.replace(fileName, '')
150+
// remove trailing slash
151+
.substring(1);
152+
153+
const routePath = getPathnameFromDirPath(ctx.opts, filePath);
154+
const route = ctx.routes.find((r) => r.pathname === routePath);
155+
if (route) {
156+
// everything fine, route exists we can use that
157+
loaderSymbols[symbolData.hash] = routePath;
158+
} else {
159+
/**
160+
* Route not found, we need to get first available route this is the case for folders with
161+
* layout files only like:
162+
*
163+
* ```
164+
* layout-only/
165+
* ├─ inner-route/
166+
* │ └─ index.tsx
167+
* └─ layout.tsx
168+
* ```
169+
*/
170+
const firstRoute = ctx.routes.find((r) => r.pathname.startsWith(filePath));
171+
if (firstRoute) {
172+
loaderSymbols[symbolData.hash] = firstRoute.pathname;
173+
} else {
174+
console.warn(`Route not found for loader: ${symbolData.origin}`);
175+
}
176+
}
177+
}
178+
}
179+
}
180+
181+
c.push(`export const loaderIdToRoute = ${JSON.stringify(loaderSymbols)};`);
182+
}

packages/qwik-router/src/middleware/request-handler/loader-endpoints.ts

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import {
99
getRequestLoaders,
1010
getRequestLoaderSerializationStrategyMap,
1111
getRequestMode,
12+
RequestEventInternal,
1213
} from './request-event';
1314
import { measure, verifySerializable } from './resolve-request-handlers';
1415
import type { RequestEvent } from './types';
1516
import { IsQLoader, IsQLoaderData, QLoaderId } from './user-response';
17+
import qwikRouterConfig from '@qwik-router-config';
18+
import { getPathnameForDynamicRoute } from '../../utils/pathname';
1619

1720
export function loaderDataHandler(routeLoaders: LoaderInternal[]): RequestHandler {
1821
return async (requestEvent: RequestEvent) => {
@@ -27,15 +30,30 @@ export function loaderDataHandler(routeLoaders: LoaderInternal[]): RequestHandle
2730
return;
2831
}
2932

30-
// Set cache headers - aggressive for loaders
33+
// Set cache headers - cache it as never expires
3134
requestEv.cacheControl({
32-
maxAge: 300, // 5 minutes
33-
staleWhileRevalidate: 3600, // 1 hour
35+
maxAge: 365 * 24 * 60 * 60, // 1 year
3436
});
3537

36-
// return loader ids
37-
const loaderIds = routeLoaders.map((l) => l.__id);
38-
return requestEv.json(200, { loaderIds });
38+
// return loader data: id and route
39+
const loaderData = routeLoaders.map((l) => {
40+
const loaderId = l.__id;
41+
let loaderRoute = qwikRouterConfig.loaderIdToRoute[loaderId];
42+
const params = requestEv.params;
43+
if (Object.keys(params).length > 0) {
44+
const pathname = getPathnameForDynamicRoute(
45+
requestEv.url.pathname,
46+
Object.keys(params),
47+
params
48+
);
49+
loaderRoute = pathname;
50+
}
51+
return {
52+
id: loaderId,
53+
route: loaderRoute,
54+
};
55+
});
56+
requestEv.json(200, { loaderData });
3957
};
4058
}
4159

@@ -53,42 +71,38 @@ export function singleLoaderHandler(routeLoaders: LoaderInternal[]): RequestHand
5371
}
5472
const loaderId = requestEv.sharedMap.get(QLoaderId);
5573

56-
try {
57-
// Execute just this loader
58-
const loaders = getRequestLoaders(requestEv);
59-
const isDev = getRequestMode(requestEv) === 'dev';
60-
61-
let loader: LoaderInternal | undefined;
62-
for (const routeLoader of routeLoaders) {
63-
if (routeLoader.__id === loaderId) {
64-
loader = routeLoader;
65-
} else if (!loaders[routeLoader.__id]) {
66-
loaders[routeLoader.__id] = _UNINITIALIZED;
67-
}
68-
}
74+
// Execute just this loader
75+
const loaders = getRequestLoaders(requestEv);
76+
const isDev = getRequestMode(requestEv) === 'dev';
6977

70-
if (!loader) {
71-
return requestEv.json(404, { error: 'Loader not found' });
78+
let loader: LoaderInternal | undefined;
79+
for (const routeLoader of routeLoaders) {
80+
if (routeLoader.__id === loaderId) {
81+
loader = routeLoader;
82+
} else if (!loaders[routeLoader.__id]) {
83+
loaders[routeLoader.__id] = _UNINITIALIZED;
7284
}
85+
}
7386

74-
await executeLoader(loader, loaders, requestEv, isDev);
87+
if (!loader) {
88+
requestEv.json(404, { error: 'Loader not found' });
89+
return;
90+
}
7591

76-
// Set cache headers - aggressive for loaders
77-
requestEv.cacheControl({
78-
maxAge: 300, // 5 minutes
79-
staleWhileRevalidate: 3600, // 1 hour
80-
});
92+
await executeLoader(loader, loaders, requestEv, isDev);
8193

82-
const data = await _serialize([loaders[loaderId]]);
94+
// Set cache headers - aggressive for loaders
95+
requestEv.cacheControl({
96+
maxAge: 300, // 5 minutes
97+
staleWhileRevalidate: 3600, // 1 hour
98+
});
8399

84-
requestEv.headers.set('Content-Type', 'application/json; charset=utf-8');
100+
const data = await _serialize([loaders[loaderId]]);
85101

86-
// Return just this loader's result
87-
return requestEv.send(200, data);
88-
} catch (error) {
89-
console.error(`Loader ${loaderId} failed:`, error);
90-
return requestEv.json(500, { error: 'Loader execution failed' });
91-
}
102+
requestEv.headers.set('Content-Type', 'application/json; charset=utf-8');
103+
104+
// Return just this loader's result
105+
requestEv.send(200, data);
92106
};
93107
}
94108

packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { Loader as Loader_2 } from '@qwik.dev/router';
1111
import type { QwikCityPlan } from '@qwik.dev/router';
1212
import type { QwikIntrinsicElements } from '@qwik.dev/core';
1313
import type { QwikRouterConfig } from '@qwik.dev/router';
14+
import { RedirectMessage as RedirectMessage_2 } from '@qwik.dev/router/middleware/request-handler';
1415
import type { Render } from '@qwik.dev/core/server';
1516
import type { RenderOptions } from '@qwik.dev/core/server';
1617
import { RequestEvent as RequestEvent_2 } from '@qwik.dev/router/middleware/request-handler';

packages/qwik-router/src/middleware/request-handler/request-event.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ import {
3535
IsQData,
3636
IsQLoader,
3737
IsQLoaderData,
38-
Q_LOADER_DATA_JSON,
39-
Q_LOADER_DATA_JSON_LEN,
38+
Q_LOADER_DATA_REGEX,
4039
QDATA_JSON,
4140
QDATA_JSON_LEN,
4241
QLoaderId,
@@ -80,18 +79,14 @@ export function createRequestEvent(
8079
}
8180
};
8281

83-
if (url.pathname.endsWith(QDATA_JSON)) {
84-
trimEnd(QDATA_JSON_LEN);
85-
sharedMap.set(IsQData, true);
86-
} else if (url.pathname.endsWith(Q_LOADER_DATA_JSON)) {
87-
trimEnd(Q_LOADER_DATA_JSON_LEN);
88-
sharedMap.set(IsQLoaderData, true);
89-
}
90-
const loaderMatch = url.pathname.match(SINGLE_LOADER_REGEX);
91-
if (loaderMatch) {
92-
trimEnd(loaderMatch[0].length);
93-
sharedMap.set(IsQLoader, true);
94-
sharedMap.set(QLoaderId, loaderMatch[1]); // Store which loader was requested
82+
const requestRecognized = recognizeRequest(url.pathname);
83+
if (requestRecognized) {
84+
sharedMap.set(requestRecognized.type, true);
85+
if (requestRecognized.type === IsQLoader && requestRecognized.data) {
86+
sharedMap.set(QLoaderId, requestRecognized.data.loaderId);
87+
}
88+
89+
trimEnd(requestRecognized.trimLength);
9590
}
9691

9792
let routeModuleIndex = -1;
@@ -463,3 +458,40 @@ const formToObj = (formData: FormData): Record<string, any> => {
463458
// Return values object
464459
return values;
465460
};
461+
462+
export function recognizeRequest(pathname: string) {
463+
// Quick length check for common cases
464+
if (pathname.length < 10) {
465+
return null;
466+
}
467+
468+
// Check exact matches first (fastest)
469+
if (pathname.endsWith(QDATA_JSON)) {
470+
return {
471+
type: IsQData,
472+
trimLength: QDATA_JSON_LEN,
473+
data: null,
474+
};
475+
}
476+
477+
// Check for loader patterns
478+
const loaderDataMatch = pathname.match(Q_LOADER_DATA_REGEX);
479+
if (loaderDataMatch) {
480+
return {
481+
type: IsQLoaderData,
482+
trimLength: loaderDataMatch[0].length,
483+
data: null,
484+
};
485+
}
486+
487+
const loaderMatch = pathname.match(SINGLE_LOADER_REGEX);
488+
if (loaderMatch) {
489+
return {
490+
type: IsQLoader,
491+
trimLength: loaderMatch[0].length,
492+
data: { loaderId: loaderMatch[1] },
493+
};
494+
}
495+
496+
return null;
497+
}

0 commit comments

Comments
 (0)