Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/gentle-laws-see.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/router': minor
---

FEAT: if a server$ function throws an error that is not a `ServerError`, it will now log the error on the server
6 changes: 6 additions & 0 deletions .changeset/pretty-parents-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@qwik.dev/router': patch
'@qwik.dev/core': patch
---

FEAT: withLocale() uses AsyncLocalStorage for server-side requests when available. This allows async operations to retain the correct locale context.
7 changes: 7 additions & 0 deletions packages/docs/src/repl/bundler/rollup-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export const replResolver = (
if (id.startsWith('/qwik/')) {
return id;
}
// Replace node: with modules that throw on import
if (id.startsWith('node:')) {
return id;
}
const match = id.match(/(@builder\.io\/qwik|@qwik\.dev\/core)(.*)/);
if (match) {
const pkgName = match[2];
Expand Down Expand Up @@ -114,6 +118,9 @@ export const replResolver = (
if (input && typeof input.code === 'string') {
return input.code;
}
if (id.startsWith('node:')) {
return `throw new Error('Module "${id}" is not available in the REPL environment.');`;
}
if (id.startsWith('/qwik/')) {
const path = id.slice('/qwik'.length);
if (path === '/build') {
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/src/repl/ui/repl-output-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const updateReplOutput = async (store: ReplStore, result: ReplResult) =>
deepUpdate(store.clientBundles, result.clientBundles);
deepUpdate(store.ssrModules, result.ssrModules);

if (result.diagnostics.length === 0) {
if (!result.diagnostics.some((d) => d.category === 'error' || d.category === 'sourceError')) {
if (result.html && store.html !== result.html) {
store.html = result.html;
store.events = result.events;
Expand Down
8 changes: 5 additions & 3 deletions packages/qwik-router/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/* eslint-disable no-var */
// Globals used by qwik-router, for internal use only

declare module '*?compiled-string' {
const str: string;
export default str;
}

type RequestEventInternal =
import('./middleware/request-handler/request-event').RequestEventInternal;
type AsyncStore = import('node:async_hooks').AsyncLocalStorage<RequestEventInternal>;
type SerializationStrategy = import('@qwik.dev/core/internal').SerializationStrategy;

declare var qcAsyncRequestStore: AsyncStore | undefined;
declare var _qwikActionsMap: Map<string, ActionInternal> | undefined;

type ExperimentalFeatures = import('@qwik.dev/core/optimizer').ExperimentalFeatures;
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@
"directory": "packages/qwik-router"
},
"scripts": {
"build": "cd src/runtime && vite build --mode lib"
"build": "vite build"
},
"sideEffects": false,
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik-router/src/buildtime/vite/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import swRegister from '@qwik-router-sw-register-build';
import swRegister from '../runtime-generation/sw-register-build?compiled-string';
import type { QwikVitePlugin } from '@qwik.dev/core/optimizer';
import fs from 'node:fs';
import { basename, extname, join, resolve } from 'node:path';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { requestHandler } from './request-handler';
export { requestHandler, _asyncRequestStore } from './request-handler';

export { getErrorHtml } from './error-handler';
export { getNotFound } from './not-found-paths';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
```ts

import type { Action } from '@qwik.dev/router';
import type { AsyncLocalStorage } from 'node:async_hooks';
import type { EnvGetter as EnvGetter_2 } from '@qwik.dev/router/middleware/request-handler';
import type { FailReturn } from '@qwik.dev/router';
import type { Loader as Loader_2 } from '@qwik.dev/router';
Expand All @@ -24,6 +25,11 @@ import type { ValueOrPromise } from '@qwik.dev/core';
export class AbortMessage {
}

// Warning: (ae-forgotten-export) The symbol "RequestEventInternal" needs to be exported by the entry point index.d.ts
//
// @internal (undocumented)
export let _asyncRequestStore: AsyncLocalStorage<RequestEventInternal> | undefined;

// Warning: (ae-forgotten-export) The symbol "CacheControlOptions" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
Expand Down Expand Up @@ -240,8 +246,6 @@ export interface ServerRequestEvent<T = unknown> {
// @public (undocumented)
export type ServerRequestMode = 'dev' | 'static' | 'server';

// Warning: (ae-forgotten-export) The symbol "RequestEventInternal" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export type ServerResponseHandler<T = any> = (status: number, headers: Headers, cookies: Cookie, resolve: (response: T) => void, requestEv: RequestEventInternal) => WritableStream<Uint8Array>;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,33 @@
import { isServer } from '@qwik.dev/core/build';
import type { Render } from '@qwik.dev/core/server';
import type { AsyncLocalStorage } from 'node:async_hooks';
import { loadRoute } from '../../runtime/src/routing';
import type { QwikRouterConfig, RebuildRouteInfoInternal } from '../../runtime/src/types';
import type { RequestEventInternal } from './request-event';
import { renderQwikMiddleware, resolveRequestHandlers } from './resolve-request-handlers';
import type { ServerRenderOptions, ServerRequestEvent } from './types';
import { getRouteMatchPathname, runQwikRouter, type QwikRouterRun } from './user-response';

/** @internal */
export let _asyncRequestStore: AsyncLocalStorage<RequestEventInternal> | undefined;
if (isServer) {
// TODO when we drop cjs support, await this
import('node:async_hooks')
.then((module) => {
_asyncRequestStore = new module.AsyncLocalStorage();
})
.catch((err) => {
console.warn(
'\n=====================\n' +
' Qwik Router Warning:\n' +
' AsyncLocalStorage is not available, continuing without it.\n' +
' This impacts concurrent async server calls, where they lose access to the ServerRequestEv object.\n' +
'=====================\n\n',
err
);
});
}

/**
* We need to delay importing the config until the first request, because vite also imports from
* this file and @qwik-router-config doesn't exist from the vite config before the build.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,18 +339,18 @@ function isAsyncIterator(obj: unknown): obj is AsyncIterable<unknown> {
}

async function runServerFunction(ev: RequestEvent) {
const fn = ev.query.get(QFN_KEY);
const serverFnHash = ev.query.get(QFN_KEY);
if (
fn &&
ev.request.headers.get('X-QRL') === fn &&
serverFnHash &&
ev.request.headers.get('X-QRL') === serverFnHash &&
ev.request.headers.get('Content-Type') === 'application/qwik-json'
) {
ev.exit();
const isDev = getRequestMode(ev) === 'dev';
const data = await ev.parseBody();
if (Array.isArray(data)) {
const [qrl, ...args] = data;
if (isQrl(qrl) && qrl.getHash() === fn) {
if (isQrl(qrl) && qrl.getHash() === serverFnHash) {
let result: unknown;
try {
if (isDev) {
Expand All @@ -364,6 +364,7 @@ async function runServerFunction(ev: RequestEvent) {
if (err instanceof ServerError) {
throw ev.error(err.status as ErrorCodes, err.data);
}
console.error(`Server function ${serverFnHash} failed:`, err);
throw ev.error(500, 'Invalid request');
}
if (isAsyncIterator(result)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
RedirectMessage,
RewriteMessage,
ServerError,
_asyncRequestStore,
} from '@qwik.dev/router/middleware/request-handler';

export interface QwikRouterRun<T> {
Expand All @@ -36,20 +37,6 @@ export interface QwikRouterRun<T> {
completion: Promise<RedirectMessage | Error | undefined>;
}

let asyncStore: AsyncStore | undefined;
import('node:async_hooks')
.then((module) => {
const AsyncLocalStorage = module.AsyncLocalStorage;
asyncStore = new AsyncLocalStorage<RequestEventInternal>();
globalThis.qcAsyncRequestStore = asyncStore;
})
.catch((err) => {
console.warn(
'AsyncLocalStorage not available, continuing without it. This might impact concurrent server calls.',
err
);
});

export function runQwikRouter<T>(
serverRequestEv: ServerRequestEvent<T>,
loadedRoute: LoadedRoute | null,
Expand All @@ -70,8 +57,8 @@ export function runQwikRouter<T>(
return {
response: responsePromise,
requestEv,
completion: asyncStore
? asyncStore.run(requestEv, runNext, requestEv, rebuildRouteInfo, resolve!)
completion: _asyncRequestStore
? _asyncRequestStore.run(requestEv, runNext, requestEv, rebuildRouteInfo, resolve!)
: runNext(requestEv, rebuildRouteInfo, resolve!),
};
}
Expand Down
77 changes: 38 additions & 39 deletions packages/qwik-router/src/runtime/src/head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,54 +21,53 @@ export const resolveHead = (
contentModules: ContentModule[],
locale: string,
defaults?: DocumentHeadValue
) => {
const head = createDocumentHead(defaults);
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.'
);
) =>
withLocale(locale, () => {
const head = createDocumentHead(defaults);
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.'
);
}
}
}
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;
}) as any as ResolveSyncValue;
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;
}) as any as ResolveSyncValue;

const fns: Extract<ContentModuleHead, Function>[] = [];
for (const contentModule of contentModules) {
const contentModuleHead = contentModule?.head;
if (contentModuleHead) {
if (typeof contentModuleHead === 'function') {
// Functions are executed inner before outer
fns.unshift(contentModuleHead);
} else if (typeof contentModuleHead === 'object') {
// Objects are merged inner over outer
resolveDocumentHead(head, contentModuleHead);
const fns: Extract<ContentModuleHead, Function>[] = [];
for (const contentModule of contentModules) {
const contentModuleHead = contentModule?.head;
if (contentModuleHead) {
if (typeof contentModuleHead === 'function') {
// Functions are executed inner before outer
fns.unshift(contentModuleHead);
} else if (typeof contentModuleHead === 'object') {
// Objects are merged inner over outer
resolveDocumentHead(head, contentModuleHead);
}
}
}
}
if (fns.length) {
const headProps: DocumentHeadProps = {
head,
withLocale: (fn) => withLocale(locale, fn),
resolveValue: getData,
...routeLocation,
};
if (fns.length) {
const headProps: DocumentHeadProps = {
head,
withLocale: (fn) => fn(),
resolveValue: getData,
...routeLocation,
};

withLocale(locale, () => {
for (const fn of fns) {
resolveDocumentHead(head, fn(headProps));
}
});
}
}

return head;
};
return head;
});

const resolveDocumentHead = (
resolvedHead: Editable<ResolvedDocumentHead>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export interface DocumentHeadProps extends RouteLocation {
readonly head: ResolvedDocumentHead;
// (undocumented)
readonly resolveValue: ResolveSyncValue;
// (undocumented)
// @deprecated (undocumented)
readonly withLocale: <T>(fn: () => T) => T;
}

Expand Down
Loading