From 3c0836ec856f6239bd418baf3dc46409b5fa9ed3 Mon Sep 17 00:00:00 2001 From: Varixo Date: Fri, 28 Feb 2025 17:23:56 +0100 Subject: [PATCH 1/8] feat: add QLOADER_KEY constant and implement loadersMiddleware --- package.json | 1 + .../resolve-request-handlers.ts | 126 ++++++++++++------ .../resolve-request-handlers.unit.ts | 3 + .../qwik-router/src/runtime/src/constants.ts | 2 + 4 files changed, 90 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index e8816238f1d..5840d962ae0 100644 --- a/package.json +++ b/package.json @@ -188,6 +188,7 @@ "build.cli": "tsx --require ./scripts/runBefore.ts scripts/index.ts --cli --dev", "build.cli.prod": "tsx --require ./scripts/runBefore.ts scripts/index.ts --cli", "build.core": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwik --insights --qwikrouter --api --platform-binding", + "build.router": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --qwikrouter --api", "build.eslint": "tsx --require ./scripts/runBefore.ts scripts/index.ts --eslint", "build.full": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding --wasm", "build.local": "tsx --require ./scripts/runBefore.ts scripts/index.ts --tsc --tsc-docs --qwik --insights --supabaseauthhelpers --api --eslint --qwikrouter --qwikworker --qwikreact --cli --platform-binding-wasm-copy", 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 b3098ce7b5f..89254df8087 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,6 +1,6 @@ import type { QRL } from '@qwik.dev/core'; import type { Render, RenderToStringResult } from '@qwik.dev/core/server'; -import { QACTION_KEY, QFN_KEY } from '../../runtime/src/constants'; +import { QACTION_KEY, QFN_KEY, QLOADER_KEY } from '../../runtime/src/constants'; import type { ActionInternal, ClientPageData, @@ -82,7 +82,8 @@ export const resolveRequestHandlers = ( // Set the current route name ev.sharedMap.set(RequestRouteName, routeName); }); - requestHandlers.push(actionsMiddleware(routeActions, routeLoaders) as any); + requestHandlers.push(actionsMiddleware(routeActions)); + requestHandlers.push(loadersMiddleware(routeLoaders)); requestHandlers.push(renderHandler); } } @@ -160,8 +161,9 @@ export const checkBrand = (obj: any, brand: string) => { return obj && typeof obj === 'function' && obj.__brand === brand; }; -export function actionsMiddleware(routeActions: ActionInternal[], routeLoaders: LoaderInternal[]) { - return async (requestEv: RequestEventInternal) => { +export function actionsMiddleware(routeActions: ActionInternal[]): RequestHandler { + return async (requestEvent: RequestEvent) => { + const requestEv = requestEvent as RequestEventInternal; if (requestEv.headersSent) { requestEv.exit(); return; @@ -211,51 +213,91 @@ export function actionsMiddleware(routeActions: ActionInternal[], routeLoaders: } } } + }; +} +export function loadersMiddleware(routeLoaders: LoaderInternal[]): RequestHandler { + return async (requestEvent: RequestEvent) => { + const requestEv = requestEvent as RequestEventInternal; + if (requestEv.headersSent) { + requestEv.exit(); + return; + } + const loaders = getRequestLoaders(requestEv); + const isDev = getRequestMode(requestEv) === 'dev'; + const qwikSerializer = requestEv[RequestEvQwikSerializer]; if (routeLoaders.length > 0) { - const resolvedLoadersPromises = routeLoaders.map((loader) => { - const loaderId = loader.__id; - loaders[loaderId] = runValidators( - requestEv, - loader.__validators, - undefined, // data - isDev - ) - .then((res) => { - if (res.success) { - if (isDev) { - return measure>( - requestEv, - loader.__qrl.getSymbol().split('_', 1)[0], - () => 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(qwikSerializer, resolvedLoader, loader.__qrl); - } - loaders[loaderId] = resolvedLoader; - } - return resolvedLoader; - }); - - return loaders[loaderId]; - }); + let currentLoaders: LoaderInternal[] = []; + if (requestEv.query.has(QLOADER_KEY)) { + const selectedLoaderIds = requestEv.query.getAll(QLOADER_KEY); + const skippedLoaders: LoaderInternal[] = []; + for (const loader of routeLoaders) { + if (selectedLoaderIds.includes(loader.__id)) { + currentLoaders.push(loader); + } else { + skippedLoaders.push(loader); + } + } + // mark skipped loaders as null + for (const skippedLoader of skippedLoaders) { + loaders[skippedLoader.__id] = null; + } + } else { + currentLoaders = routeLoaders; + } + const resolvedLoadersPromises = currentLoaders.map((loader) => + getRouteLoaderPromise(loader, loaders, requestEv, isDev, qwikSerializer) + ); await Promise.all(resolvedLoadersPromises); } }; } +async function getRouteLoaderPromise( + loader: LoaderInternal, + loaders: Record, + requestEv: RequestEventInternal, + isDev: boolean, + qwikSerializer: QwikSerializer +) { + const loaderId = loader.__id; + loaders[loaderId] = runValidators( + requestEv, + loader.__validators, + undefined, // data + isDev + ) + .then((res) => { + if (res.success) { + if (isDev) { + return measure>( + requestEv, + loader.__qrl.getSymbol().split('_', 1)[0], + () => 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(qwikSerializer, resolvedLoader, loader.__qrl); + } + loaders[loaderId] = resolvedLoader; + } + return resolvedLoader; + }); + + return loaders[loaderId]; +} + async function runValidators( requestEv: RequestEvent, validators: DataValidator[] | undefined, @@ -399,7 +441,7 @@ export function getPathname(url: URL, trailingSlash: boolean | undefined) { } } // strip internal search params - const search = url.search.slice(1).replaceAll(/&?q(action|data|func)=[^&]+/g, ''); + const search = url.search.slice(1).replaceAll(/&?q(action|data|func|loader)=[^&]+/g, ''); return `${url.pathname}${search ? `?${search}` : ''}${url.hash}`; } 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 fd6e49bfaf6..5306ade84ce 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 @@ -28,6 +28,9 @@ describe('resolve-request-handler', () => { expect(getPathname(new URL('http://server/path?foo=1&qfunc=f&bar=2'), false)).toBe( '/path?foo=1&bar=2' ); + expect(getPathname(new URL('http://server/path?foo=1&qloader=f&bar=2'), false)).toBe( + '/path?foo=1&bar=2' + ); }); }); diff --git a/packages/qwik-router/src/runtime/src/constants.ts b/packages/qwik-router/src/runtime/src/constants.ts index c54e0a9a517..8226d20cddf 100644 --- a/packages/qwik-router/src/runtime/src/constants.ts +++ b/packages/qwik-router/src/runtime/src/constants.ts @@ -8,6 +8,8 @@ export const PREFETCHED_NAVIGATE_PATHS = new Set(); export const QACTION_KEY = 'qaction'; +export const QLOADER_KEY = 'qloader'; + export const QFN_KEY = 'qfunc'; export const QDATA_KEY = 'qdata'; From 647631f8e3a87d60a87d9c0b7a11bd5cca1e0fde Mon Sep 17 00:00:00 2001 From: Varixo Date: Fri, 28 Feb 2025 17:51:22 +0100 Subject: [PATCH 2/8] fix: replace string literals with constants for server timing and route data properties --- .../src/buildtime/vite/dev-server.ts | 3 +- .../request-handler/request-event.ts | 11 +++++- .../resolve-request-handlers.ts | 37 ++++++++++--------- .../qwik-router/src/runtime/src/routing.ts | 31 +++++++++------- packages/qwik-router/src/runtime/src/types.ts | 28 +++++++++----- packages/qwik-router/src/static/not-found.ts | 5 ++- .../qwik-router/src/static/worker-thread.ts | 3 +- 7 files changed, 72 insertions(+), 46 deletions(-) diff --git a/packages/qwik-router/src/buildtime/vite/dev-server.ts b/packages/qwik-router/src/buildtime/vite/dev-server.ts index aa5adf2bd28..94fb95b6bf1 100644 --- a/packages/qwik-router/src/buildtime/vite/dev-server.ts +++ b/packages/qwik-router/src/buildtime/vite/dev-server.ts @@ -33,6 +33,7 @@ import { getExtension, normalizePath } from '../../utils/fs'; import { updateBuildContext } from '../build'; import type { BuildContext, BuildRoute } from '../types'; import { formatError } from './format-error'; +import { RequestEvShareServerTiming } from '../../middleware/request-handler/request-event'; export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { const matchRouteRequest = (pathname: string) => { @@ -188,7 +189,7 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { res.setHeader('Set-Cookie', cookieHeaders); } - const serverTiming = requestEv.sharedMap.get('@serverTiming') as + const serverTiming = requestEv.sharedMap.get(RequestEvShareServerTiming) as | [string, number][] | undefined; if (serverTiming) { diff --git a/packages/qwik-router/src/middleware/request-handler/request-event.ts b/packages/qwik-router/src/middleware/request-handler/request-event.ts index c345081ee63..23ac2f4682c 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-event.ts @@ -37,6 +37,8 @@ export const RequestRouteName = '@routeName'; export const RequestEvSharedActionId = '@actionId'; export const RequestEvSharedActionFormData = '@actionFormData'; export const RequestEvSharedNonce = '@nonce'; +export const RequestEvShareServerTiming = '@serverTiming'; +export const RequestEvShareQData = 'qData'; export function createRequestEvent( serverRequestEv: ServerRequestEvent, @@ -271,9 +273,14 @@ export function createRequestEvent( getWritableStream: () => { if (writableStream === null) { if (serverRequestEv.mode === 'dev') { - const serverTiming = sharedMap.get('@serverTiming') as [string, number][] | undefined; + const serverTiming = sharedMap.get(RequestEvShareServerTiming) as + | [string, number][] + | undefined; if (serverTiming) { - headers.set('Server-Timing', serverTiming.map((a) => `${a[0]};dur=${a[1]}`).join(',')); + headers.set( + 'Server-Timing', + serverTiming.map(([name, duration]) => `${name};dur=${duration}`).join(',') + ); } } writableStream = serverRequestEv.getWritableStream( 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 89254df8087..2aed2c925f1 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,16 +1,17 @@ import type { QRL } from '@qwik.dev/core'; import type { Render, RenderToStringResult } from '@qwik.dev/core/server'; import { QACTION_KEY, QFN_KEY, QLOADER_KEY } from '../../runtime/src/constants'; -import type { - ActionInternal, - ClientPageData, - DataValidator, - JSONObject, - LoadedRoute, - LoaderInternal, - PageModule, - RouteModule, - ValidatorReturn, +import { + type ActionInternal, + type ClientPageData, + type DataValidator, + type JSONObject, + type LoadedRoute, + LoadedRouteProp, + type LoaderInternal, + type PageModule, + type RouteModule, + type ValidatorReturn, } from '../../runtime/src/types'; import { HttpStatus } from './http-status-codes'; import { RedirectMessage } from './redirect-handler'; @@ -22,6 +23,8 @@ import { getRequestMode, getRequestTrailingSlash, type RequestEventInternal, + RequestEvShareServerTiming, + RequestEvShareQData, } from './request-event'; import { getQwikRouterServerData } from './response-page'; import type { QwikSerializer, RequestEvent, RequestEventBase, RequestHandler } from './types'; @@ -38,7 +41,7 @@ export const resolveRequestHandlers = ( const routeActions: ActionInternal[] = []; const requestHandlers: RequestHandler[] = []; - const isPageRoute = !!(route && isLastModulePageRoute(route[2])); + const isPageRoute = !!(route && isLastModulePageRoute(route[LoadedRouteProp.Mods])); if (serverPlugins) { _resolveRequestHandlers( routeLoaders, @@ -51,7 +54,7 @@ export const resolveRequestHandlers = ( } if (route) { - const routeName = route[0]; + const routeName = route[LoadedRouteProp.RouteName]; if ( checkOrigin && (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE') @@ -67,7 +70,7 @@ export const resolveRequestHandlers = ( requestHandlers.push(fixTrailingSlash); requestHandlers.push(renderQData); } - const routeModules = route[2]; + const routeModules = route[LoadedRouteProp.Mods]; requestHandlers.push(handleRedirect); _resolveRequestHandlers( routeLoaders, @@ -513,7 +516,7 @@ export function renderQwikMiddleware(render: Render) { // write the already completed html to the stream await stream.write((result as any as RenderToStringResult).html); } - requestEv.sharedMap.set('qData', qData); + requestEv.sharedMap.set(RequestEvShareQData, qData); } finally { await stream.ready; await stream.close(); @@ -587,7 +590,7 @@ export async function renderQData(requestEv: RequestEvent) { // write just the page json data to the response body const data = await qwikSerializer._serialize([qData]); writer.write(encoder.encode(data)); - requestEv.sharedMap.set('qData', qData); + requestEv.sharedMap.set(RequestEvShareQData, qData); writer.close(); } @@ -618,9 +621,9 @@ export async function measure( return await fn(); } finally { const duration = now() - start; - let measurements = requestEv.sharedMap.get('@serverTiming'); + let measurements = requestEv.sharedMap.get(RequestEvShareServerTiming); if (!measurements) { - requestEv.sharedMap.set('@serverTiming', (measurements = [])); + requestEv.sharedMap.set(RequestEvShareServerTiming, (measurements = [])); } measurements.push([name, duration]); } diff --git a/packages/qwik-router/src/runtime/src/routing.ts b/packages/qwik-router/src/runtime/src/routing.ts index 2fbe62ba5e3..0412bb8ed95 100644 --- a/packages/qwik-router/src/runtime/src/routing.ts +++ b/packages/qwik-router/src/runtime/src/routing.ts @@ -1,17 +1,18 @@ import { MODULE_CACHE } from './constants'; import { matchRoute } from './route-matcher'; -import type { - ContentMenu, - LoadedRoute, - MenuData, - MenuModule, - ModuleLoader, - RouteData, - RouteModule, +import { + type ContentMenu, + type LoadedRoute, + type MenuData, + MenuDataProp, + type MenuModule, + type ModuleLoader, + type RouteData, + RouteDataProp, + type RouteModule, } from './types'; import { deepFreeze } from './utils'; -export const CACHE = new Map>(); /** LoadRoute() runs in both client and server. */ export const loadRoute = async ( routes: RouteData[] | undefined, @@ -23,13 +24,13 @@ export const loadRoute = async ( return null; } for (const routeData of routes) { - const routeName = routeData[0]; + const routeName = routeData[RouteDataProp.RouteName]; const params = matchRoute(routeName, pathname); if (!params) { continue; } - const loaders = routeData[1]; - const routeBundleNames = routeData[3]; + const loaders = routeData[RouteDataProp.Loaders]; + const routeBundleNames = routeData[RouteDataProp.RouteBundleNames]; const modules: RouteModule[] = new Array(loaders.length); const pendingLoads: Promise[] = []; @@ -93,10 +94,12 @@ export const getMenuLoader = (menus: MenuData[] | undefined, pathname: string) = if (menus) { pathname = pathname.endsWith('/') ? pathname : pathname + '/'; const menu = menus.find( - (m) => m[0] === pathname || pathname.startsWith(m[0] + (pathname.endsWith('/') ? '' : '/')) + (m) => + m[MenuDataProp.Pathname] === pathname || + pathname.startsWith(m[MenuDataProp.Pathname] + (pathname.endsWith('/') ? '' : '/')) ); if (menu) { - return menu[1]; + return menu[MenuDataProp.MenuLoader]; } } }; diff --git a/packages/qwik-router/src/runtime/src/types.ts b/packages/qwik-router/src/runtime/src/types.ts index 522673f081e..6998b32fb07 100644 --- a/packages/qwik-router/src/runtime/src/types.ts +++ b/packages/qwik-router/src/runtime/src/types.ts @@ -258,9 +258,21 @@ export type RouteData = routeBundleNames: string[], ]; +export const enum RouteDataProp { + RouteName, + Loaders, + OriginalPathname, + RouteBundleNames, +} + /** @public */ export type MenuData = [pathname: string, menuLoader: MenuModuleLoader]; +export const enum MenuDataProp { + Pathname, + MenuLoader, +} + /** * @deprecated Use `QwikRouterConfig` instead. Will be removed in V3. * @public @@ -292,16 +304,14 @@ export type LoadedRoute = [ routeBundleNames: string[] | undefined, ]; -export interface LoadedContent extends LoadedRoute { - pageModule: PageModule; +export const enum LoadedRouteProp { + RouteName, + Params, + Mods, + Menu, + RouteBundleNames, } -export type RequestHandlerBody = BODY | string | number | boolean | undefined | null | void; - -export type RequestHandlerBodyFunction = () => - | RequestHandlerBody - | Promise>; - export interface EndpointResponse { status: number; loaders: Record; @@ -665,8 +675,6 @@ export type LoaderConstructorQRL = { ): Loader>>>; }; -export type LoaderStateHolder = Record>; - /** @public */ export type ActionReturn = { readonly status?: number; diff --git a/packages/qwik-router/src/static/not-found.ts b/packages/qwik-router/src/static/not-found.ts index 4584aeb5e56..d29a7dc2ee1 100644 --- a/packages/qwik-router/src/static/not-found.ts +++ b/packages/qwik-router/src/static/not-found.ts @@ -1,6 +1,7 @@ import type { RouteData } from '@qwik.dev/router'; import { getErrorHtml } from '@qwik.dev/router/middleware/request-handler'; import type { StaticGenerateOptions, System } from './types'; +import { RouteDataProp } from '../runtime/src/types'; export async function generateNotFoundPages( sys: System, @@ -11,7 +12,9 @@ export async function generateNotFoundPages( const basePathname = opts.basePathname || '/'; const rootNotFoundPathname = basePathname + '404.html'; - const hasRootNotFound = routes.some((r) => r[2] === rootNotFoundPathname); + const hasRootNotFound = routes.some( + (r) => r[RouteDataProp.OriginalPathname] === rootNotFoundPathname + ); if (!hasRootNotFound) { const filePath = sys.getRouteFilePath(rootNotFoundPathname, true); diff --git a/packages/qwik-router/src/static/worker-thread.ts b/packages/qwik-router/src/static/worker-thread.ts index 82da1350300..841ce5c2064 100644 --- a/packages/qwik-router/src/static/worker-thread.ts +++ b/packages/qwik-router/src/static/worker-thread.ts @@ -12,6 +12,7 @@ import type { StaticWorkerRenderResult, System, } from './types'; +import { RequestEvShareQData } from '../middleware/request-handler/request-event'; export async function workerThread(sys: System) { const ssgOpts = sys.getOptions(); @@ -178,7 +179,7 @@ async function workerRender( try { if (writeQDataEnabled) { - const qData: ClientPageData = requestEv.sharedMap.get('qData'); + const qData: ClientPageData = requestEv.sharedMap.get(RequestEvShareQData); if (qData && !is404ErrorPage) { // write q-data.json file when enabled and qData is set const qDataFilePath = sys.getDataFilePath(url.pathname); From 781af110ba8f18c20f3facd214a2d1682041fa3b Mon Sep 17 00:00:00 2001 From: Varixo Date: Fri, 28 Feb 2025 19:03:17 +0100 Subject: [PATCH 3/8] feat: add new test place --- .../routes/loaders-serialization/index.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx diff --git a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx new file mode 100644 index 00000000000..6ff615b886b --- /dev/null +++ b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx @@ -0,0 +1,25 @@ +import { component$, useSignal } from "@qwik.dev/core"; +import { routeLoader$ } from "@qwik.dev/router"; + +export const useTestLoader = routeLoader$(async () => { + return { test: "test" }; +}); + +export default component$(() => { + const testSignal = useTestLoader(); + const toggle = useSignal(false); + return ( + <> + {testSignal.value.test} + + {toggle.value && } + + ); +}); + +export const Child = component$(() => { + const testSignal = useTestLoader(); + return
{testSignal.value.test}
; +}); From 18ea0a6e6a8ba2412327a3e96cf0f3e5fb5b92cc Mon Sep 17 00:00:00 2001 From: Varixo Date: Sat, 1 Mar 2025 08:17:48 +0100 Subject: [PATCH 4/8] feat: don't use q:seq for routeLoader$ and allow loading client data if missing --- packages/docs/src/routes/api/qwik/api.json | 2 +- packages/docs/src/routes/api/qwik/index.md | 2 ++ .../src/runtime/src/link-component.tsx | 2 +- .../src/runtime/src/server-functions.ts | 25 +++++++++++++------ packages/qwik/src/core/core.api.md | 7 ++++++ packages/qwik/src/core/internal.ts | 2 +- packages/qwik/src/core/use/use-core.ts | 2 +- .../routes/loaders-serialization/index.tsx | 2 +- 8 files changed, 31 insertions(+), 13 deletions(-) diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index f1099fda25c..ebe59ccf696 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -2144,7 +2144,7 @@ } ], "kind": "Function", - "content": "Assign a value to a Context.\n\nUse `useContextProvider()` to assign a value to a context. The assignment happens in the component's function. Once assigned, use `useContext()` in any child component to retrieve the value.\n\nContext is a way to pass stores to the child components without prop-drilling. Note that scalar values are allowed, but for reactivity you need signals or stores.\n\n\\#\\#\\# Example\n\n```tsx\n// Declare the Context type.\ninterface TodosStore {\n items: string[];\n}\n// Create a Context ID (no data is saved here.)\n// You will use this ID to both create and retrieve the Context.\nexport const TodosContext = createContextId('Todos');\n\n// Example of providing context to child components.\nexport const App = component$(() => {\n useContextProvider(\n TodosContext,\n useStore({\n items: ['Learn Qwik', 'Build Qwik app', 'Profit'],\n })\n );\n\n return ;\n});\n\n// Example of retrieving the context provided by a parent component.\nexport const Items = component$(() => {\n const todos = useContext(TodosContext);\n return (\n
    \n {todos.items.map((item) => (\n
  • {item}
  • \n ))}\n
\n );\n});\n\n```\n\n\n```typescript\nuseContextProvider: (context: ContextId, newValue: STATE) => void\n```\n\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\ncontext\n\n\n\n\n[ContextId](#contextid)<STATE>\n\n\n\n\nThe context to assign a value to.\n\n\n
\n\nnewValue\n\n\n\n\nSTATE\n\n\n\n\n\n
\n**Returns:**\n\nvoid", + "content": "Assign a value to a Context.\n\nUse `useContextProvider()` to assign a value to a context. The assignment happens in the component's function. Once assigned, use `useContext()` in any child component to retrieve the value.\n\nContext is a way to pass stores to the child components without prop-drilling. Note that scalar values are allowed, but for reactivity you need signals or stores.\n\n\\#\\#\\# Example\n\n```tsx\n// Declare the Context type.\ninterface TodosStore {\n items: string[];\n}\n// Create a Context ID (no data is saved here.)\n// You will use this ID to both create and retrieve the Context.\nexport const TodosContext = createContextId('Todos');\n\n// Example of providing context to child components.\nexport const App = component$(() => {\n useContextProvider(\n TodosContext,\n useStore({\n items: ['Learn Qwik', 'Build Qwik app', 'Profit'],\n })\n );\n\n return ;\n});\n\n// Example of retrieving the context provided by a parent component.\nexport const Items = component$(() => {\n const todos = useContext(TodosContext);\n return (\n
    \n {todos.items.map((item) => (\n
  • {item}
  • \n ))}\n
\n );\n});\n\n```\n\n\n```typescript\nuseContextProvider: (context: ContextId, newValue: STATE) => void\n```\n\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\ncontext\n\n\n\n\n[ContextId](#contextid)<STATE>\n\n\n\n\nThe context to assign a value to.\n\n\n
\n\nnewValue\n\n\n\n\nSTATE\n\n\n\n\nThe value to assign to the context.\n\n\n
\n**Returns:**\n\nvoid", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts", "mdFile": "core.usecontextprovider.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.md b/packages/docs/src/routes/api/qwik/index.md index a82d3ab35fa..1c5314cd5ff 100644 --- a/packages/docs/src/routes/api/qwik/index.md +++ b/packages/docs/src/routes/api/qwik/index.md @@ -8631,6 +8631,8 @@ STATE +The value to assign to the context. + **Returns:** diff --git a/packages/qwik-router/src/runtime/src/link-component.tsx b/packages/qwik-router/src/runtime/src/link-component.tsx index 4ba6c565d0d..80eeb6ce726 100644 --- a/packages/qwik-router/src/runtime/src/link-component.tsx +++ b/packages/qwik-router/src/runtime/src/link-component.tsx @@ -63,7 +63,7 @@ export const Link = component$((props) => { }) : undefined; const preventDefault = clientNavPath - ? sync$((event: MouseEvent, target: HTMLAnchorElement) => { + ? sync$((event: MouseEvent) => { if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) { event.preventDefault(); } diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index d065522cd54..a087d76ea83 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -2,10 +2,11 @@ import { $, implicit$FirstArg, noSerialize, - useContext, useStore, type QRL, type ValueOrPromise, + untrack, + isBrowser, } from '@qwik.dev/core'; import { _deserialize, @@ -19,7 +20,7 @@ import * as v from 'valibot'; import { z } from 'zod'; import type { RequestEventLoader } from '../../middleware/request-handler/types'; import { QACTION_KEY, QDATA_KEY, QFN_KEY } from './constants'; -import { RouteStateContext } from './contexts'; +import { RouteLocationContext, RouteStateContext } from './contexts'; import type { ActionConstructor, ActionConstructorQRL, @@ -53,9 +54,11 @@ import type { import { useAction, useLocation, useQwikRouterEnv } from './use-functions'; import { isDev, isServer } from '@qwik.dev/core'; +import { _useInvokeContext } from '@qwik.dev/core/internal'; import type { FormSubmitCompletedDetail } from './form-component'; import { deepFreeze } from './utils'; +import { loadClientData } from './use-endpoint'; /** @internal */ export const routeActionQrl = (( @@ -193,17 +196,23 @@ export const routeLoaderQrl = (( ): LoaderInternal => { const { id, validators } = getValidators(rest, loaderQrl); function loader() { - return useContext(RouteStateContext, (state) => { - if (!(id in state)) { - throw new Error(`routeLoader$ "${loaderQrl.getSymbol()}" was invoked in a route where it was not declared. + const iCtx = _useInvokeContext(); + const state = iCtx.$container$.resolveContext(iCtx.$hostElement$, RouteStateContext)!; + const location = iCtx.$container$.resolveContext(iCtx.$hostElement$, RouteLocationContext)!; + + 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/`); - } - return _wrapStore(state, id); - }); + } + const data = untrack(() => state[id]); + if (!data && isBrowser) { + throw loadClientData(location.url, iCtx.$hostElement$); + } + return _wrapStore(state, id); } loader.__brand = 'server_loader' as const; loader.__qrl = loaderQrl; diff --git a/packages/qwik/src/core/core.api.md b/packages/qwik/src/core/core.api.md index 5f23e47dff1..3759911ea12 100644 --- a/packages/qwik/src/core/core.api.md +++ b/packages/qwik/src/core/core.api.md @@ -874,6 +874,8 @@ export abstract class _SharedContainer implements Container { abstract handleError(err: any, $host$: HostElement): void; // (undocumented) abstract resolveContext(host: HostElement, contextId: ContextId): T | undefined; + // (undocumented) + resolveContextForHost(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 // @@ -1629,6 +1631,11 @@ export const useErrorBoundary: () => ErrorBoundaryStore; // @public (undocumented) export const useId: () => string; +// Warning: (ae-forgotten-export) The symbol "RenderInvokeContext" needs to be exported by the entry point index.d.ts +// +// @internal (undocumented) +export const _useInvokeContext: () => RenderInvokeContext; + // Warning: (ae-internal-missing-underscore) The name "useLexicalScope" should be prefixed with an underscore because the declaration is marked as @internal // // @internal diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index 604b1d767a5..f5c4417751a 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -14,13 +14,13 @@ export { _getContextEvent, _jsxBranch, _waitUntilRendered, + useInvokeContext as _useInvokeContext, } from './use/use-core'; export { _jsxSorted, _jsxSplit, isJSXNode as _isJSXNode } from './shared/jsx/jsx-runtime'; export { _fnSignal } from './shared/qrl/inlined-fn'; export type { ContainerElement as _ContainerElement, VNode as _VNode, - VNodeFlags as _VNodeFlags, VirtualVNode as _VirtualVNode, TextVNode as _TextVNode, QDocument as _QDocument, diff --git a/packages/qwik/src/core/use/use-core.ts b/packages/qwik/src/core/use/use-core.ts index bcb1721ef9e..81fb4cd9a5f 100644 --- a/packages/qwik/src/core/use/use-core.ts +++ b/packages/qwik/src/core/use/use-core.ts @@ -66,7 +66,6 @@ export interface InvokeContext { let _context: InvokeContext | undefined; -/** @public */ export const tryGetInvokeContext = (): InvokeContext | undefined => { if (!_context) { const context = typeof document !== 'undefined' && document && document.__q_context__; @@ -89,6 +88,7 @@ export const getInvokeContext = (): InvokeContext => { return ctx; }; +/** @internal */ export const useInvokeContext = (): RenderInvokeContext => { const ctx = tryGetInvokeContext(); if (!ctx || ctx.$event$ !== RenderEvent) { diff --git a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx index 6ff615b886b..53a37da632c 100644 --- a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx +++ b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx @@ -2,7 +2,7 @@ import { component$, useSignal } from "@qwik.dev/core"; import { routeLoader$ } from "@qwik.dev/router"; export const useTestLoader = routeLoader$(async () => { - return { test: "test" }; + return { test: "should not serialize this" }; }); export default component$(() => { From f5f1e33fd02298ea2b4c31b640e7ce88a3c241b5 Mon Sep 17 00:00:00 2001 From: Varixo Date: Sat, 1 Mar 2025 08:49:34 +0100 Subject: [PATCH 5/8] feat: implement WeakObject serialization --- packages/docs/src/routes/api/qwik/api.json | 2 +- packages/docs/src/routes/api/qwik/index.md | 2 - .../src/runtime/src/qwik-router-component.tsx | 2 +- .../src/runtime/src/server-functions.ts | 5 ++- packages/qwik/src/core/core.api.md | 44 +++---------------- .../src/core/shared/shared-serialization.ts | 30 +++++++++---- .../core/shared/shared-serialization.unit.ts | 8 ++-- .../src/core/shared/utils/serialize-utils.ts | 10 ++++- .../routes/loaders-serialization/index.tsx | 2 +- 9 files changed, 47 insertions(+), 58 deletions(-) diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index ebe59ccf696..f1099fda25c 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -2144,7 +2144,7 @@ } ], "kind": "Function", - "content": "Assign a value to a Context.\n\nUse `useContextProvider()` to assign a value to a context. The assignment happens in the component's function. Once assigned, use `useContext()` in any child component to retrieve the value.\n\nContext is a way to pass stores to the child components without prop-drilling. Note that scalar values are allowed, but for reactivity you need signals or stores.\n\n\\#\\#\\# Example\n\n```tsx\n// Declare the Context type.\ninterface TodosStore {\n items: string[];\n}\n// Create a Context ID (no data is saved here.)\n// You will use this ID to both create and retrieve the Context.\nexport const TodosContext = createContextId('Todos');\n\n// Example of providing context to child components.\nexport const App = component$(() => {\n useContextProvider(\n TodosContext,\n useStore({\n items: ['Learn Qwik', 'Build Qwik app', 'Profit'],\n })\n );\n\n return ;\n});\n\n// Example of retrieving the context provided by a parent component.\nexport const Items = component$(() => {\n const todos = useContext(TodosContext);\n return (\n
    \n {todos.items.map((item) => (\n
  • {item}
  • \n ))}\n
\n );\n});\n\n```\n\n\n```typescript\nuseContextProvider: (context: ContextId, newValue: STATE) => void\n```\n\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\ncontext\n\n\n\n\n[ContextId](#contextid)<STATE>\n\n\n\n\nThe context to assign a value to.\n\n\n
\n\nnewValue\n\n\n\n\nSTATE\n\n\n\n\nThe value to assign to the context.\n\n\n
\n**Returns:**\n\nvoid", + "content": "Assign a value to a Context.\n\nUse `useContextProvider()` to assign a value to a context. The assignment happens in the component's function. Once assigned, use `useContext()` in any child component to retrieve the value.\n\nContext is a way to pass stores to the child components without prop-drilling. Note that scalar values are allowed, but for reactivity you need signals or stores.\n\n\\#\\#\\# Example\n\n```tsx\n// Declare the Context type.\ninterface TodosStore {\n items: string[];\n}\n// Create a Context ID (no data is saved here.)\n// You will use this ID to both create and retrieve the Context.\nexport const TodosContext = createContextId('Todos');\n\n// Example of providing context to child components.\nexport const App = component$(() => {\n useContextProvider(\n TodosContext,\n useStore({\n items: ['Learn Qwik', 'Build Qwik app', 'Profit'],\n })\n );\n\n return ;\n});\n\n// Example of retrieving the context provided by a parent component.\nexport const Items = component$(() => {\n const todos = useContext(TodosContext);\n return (\n
    \n {todos.items.map((item) => (\n
  • {item}
  • \n ))}\n
\n );\n});\n\n```\n\n\n```typescript\nuseContextProvider: (context: ContextId, newValue: STATE) => void\n```\n\n\n\n\n\n
\n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
\n\ncontext\n\n\n\n\n[ContextId](#contextid)<STATE>\n\n\n\n\nThe context to assign a value to.\n\n\n
\n\nnewValue\n\n\n\n\nSTATE\n\n\n\n\n\n
\n**Returns:**\n\nvoid", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts", "mdFile": "core.usecontextprovider.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.md b/packages/docs/src/routes/api/qwik/index.md index 1c5314cd5ff..a82d3ab35fa 100644 --- a/packages/docs/src/routes/api/qwik/index.md +++ b/packages/docs/src/routes/api/qwik/index.md @@ -8631,8 +8631,6 @@ STATE -The value to assign to the context. - **Returns:** 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 29f888b564c..fe9e78e013f 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx +++ b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx @@ -146,7 +146,7 @@ export const QwikRouterProvider = component$((props) => { { deep: false } ); const navResolver: { r?: () => void } = {}; - const loaderState = _weakSerialize(useStore(env.response.loaders, { deep: false })); + const loaderState = useStore(_weakSerialize(env.response.loaders), { deep: false }); const routeInternal = useSignal({ type: 'initial', dest: url, diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index a087d76ea83..53c5f2c166e 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -210,7 +210,10 @@ export const routeLoaderQrl = (( } const data = untrack(() => state[id]); if (!data && isBrowser) { - throw loadClientData(location.url, iCtx.$hostElement$); + // TODO: fetch only loader with current id + throw loadClientData(location.url, iCtx.$hostElement$).then( + (data) => (state[id] = data?.loaders[id]) + ); } return _wrapStore(state, id); } diff --git a/packages/qwik/src/core/core.api.md b/packages/qwik/src/core/core.api.md index 3759911ea12..6563122532c 100644 --- a/packages/qwik/src/core/core.api.md +++ b/packages/qwik/src/core/core.api.md @@ -246,9 +246,11 @@ export { DomContainer as _DomContainer } // @internal (undocumented) export const _EFFECT_BACK_REF: unique symbol; +// Warning: (ae-forgotten-export) The symbol "VNodeFlags" needs to be exported by the entry point index.d.ts +// // @internal (undocumented) export type _ElementVNode = [ -_VNodeFlags.Element, +VNodeFlags.Element, ////////////// 0 - Flags _VNode | null, /////////////// 1 - Parent @@ -874,8 +876,6 @@ export abstract class _SharedContainer implements Container { abstract handleError(err: any, $host$: HostElement): void; // (undocumented) abstract resolveContext(host: HostElement, contextId: ContextId): T | undefined; - // (undocumented) - resolveContextForHost(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 // @@ -1579,7 +1579,7 @@ export type TaskFn = (ctx: TaskCtx) => ValueOrPromise void)>; // @internal (undocumented) export type _TextVNode = [ -_VNodeFlags.Text | _VNodeFlags.Inflated, +VNodeFlags.Text | VNodeFlags.Inflated, // 0 - Flags _VNode | null, ///////////////// 1 - Parent @@ -1748,7 +1748,7 @@ export const version: string; // @internal (undocumented) export type _VirtualVNode = [ -_VNodeFlags.Virtual, +VNodeFlags.Virtual, ///////////// 0 - Flags _VNode | null, /////////////// 1 - Parent @@ -1770,40 +1770,6 @@ export type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | ' // @internal (undocumented) export type _VNode = _ElementVNode | _TextVNode | _VirtualVNode; -// @internal -export const enum _VNodeFlags { - // (undocumented) - Deleted = 32, - // (undocumented) - Element = 1, - // (undocumented) - ELEMENT_OR_TEXT_MASK = 5, - // (undocumented) - ELEMENT_OR_VIRTUAL_MASK = 3, - // (undocumented) - Inflated = 8, - // (undocumented) - INFLATED_TYPE_MASK = 15, - // (undocumented) - NAMESPACE_MASK = 192, - // (undocumented) - NEGATED_NAMESPACE_MASK = -193, - // (undocumented) - NS_html = 0, - // (undocumented) - NS_math = 128, - // (undocumented) - NS_svg = 64, - // (undocumented) - Resolved = 16, - // (undocumented) - Text = 4,// http://www.w3.org/1999/xhtml - // (undocumented) - TYPE_MASK = 7,// http://www.w3.org/2000/svg - // (undocumented) - Virtual = 2 -} - // @internal (undocumented) export const _waitUntilRendered: (elm: Element) => Promise; diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 1a4f27e39e3..1bc2bd16157 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -40,7 +40,7 @@ import { isElement, isNode } from './utils/element'; import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight'; import { ELEMENT_ID } from './utils/markers'; import { isPromise } from './utils/promises'; -import { SerializerSymbol, fastSkipSerialize } from './utils/serialize-utils'; +import { SerializerSymbol, fastSkipSerialize, fastWeakSerialize } from './utils/serialize-utils'; import { _EFFECT_BACK_REF, EffectSubscriptionProp, @@ -387,12 +387,22 @@ const inflate = ( propsProxy[_VAR_PROPS] = data === 0 ? {} : (data as any)[0]; propsProxy[_CONST_PROPS] = (data as any)[1]; break; - case TypeIds.EffectData: { + case TypeIds.SubscriptionData: { const effectData = target as SubscriptionData; effectData.data.$scopedStyleIdPrefix$ = (data as any[])[0]; effectData.data.$isConst$ = (data as any[])[1]; break; } + case TypeIds.WeakObject: { + const objectKeys = data as string[]; + target = Object.fromEntries( + objectKeys.map((v) => + // initialize values with null + [v, null] + ) + ); + break; + } default: throw qError(QError.serializeErrorNotImplemented, [typeId]); } @@ -460,6 +470,7 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow case TypeIds.Array: return wrapDeserializerProxy(container as any, value as any[]); case TypeIds.Object: + case TypeIds.WeakObject: return {}; case TypeIds.QRL: case TypeIds.PreloadQRL: @@ -546,9 +557,8 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow } else { throw qError(QError.serializeErrorExpectedVNode, [typeof vNode]); } - case TypeIds.EffectData: + case TypeIds.SubscriptionData: return new SubscriptionData({} as NodePropData); - default: throw qError(QError.serializeErrorCannotAllocate, [typeId]); } @@ -848,7 +858,7 @@ const discoverValuesForVNodeData = (vnodeData: VNodeData, callback: (value: unkn if (isSsrAttrs(value)) { for (let i = 1; i < value.length; i += 2) { const attrValue = value[i]; - if (typeof attrValue === 'string') { + if (attrValue == null || typeof attrValue === 'string') { continue; } callback(attrValue); @@ -1065,7 +1075,7 @@ async function serialize(serializationContext: SerializationContext): Promise { it.todo(title(TypeIds.FormData)); it.todo(title(TypeIds.JSXNode)); it.todo(title(TypeIds.PropsProxy)); - it(title(TypeIds.EffectData), async () => { + it(title(TypeIds.SubscriptionData), async () => { expect(await dump(new SubscriptionData({ $isConst$: true, $scopedStyleIdPrefix$: null }))) .toMatchInlineSnapshot(` " @@ -751,7 +751,7 @@ describe('shared-serialization', () => { it.todo(title(TypeIds.ComputedSignal)); it.todo(title(TypeIds.SerializerSignal)); // this requires a domcontainer - it.skip(title(TypeIds.Store), async () => { + it(title(TypeIds.Store), async () => { const objs = await serialize(createStore(null, { a: { b: true } }, StoreFlags.RECURSIVE)); const store = deserialize(objs)[0] as any; expect(store).toHaveProperty('a'); @@ -761,7 +761,7 @@ describe('shared-serialization', () => { it.todo(title(TypeIds.FormData)); it.todo(title(TypeIds.JSXNode)); it.todo(title(TypeIds.PropsProxy)); - it(title(TypeIds.EffectData), async () => { + it(title(TypeIds.SubscriptionData), async () => { const objs = await serialize( new SubscriptionData({ $isConst$: true, $scopedStyleIdPrefix$: null }) ); diff --git a/packages/qwik/src/core/shared/utils/serialize-utils.ts b/packages/qwik/src/core/shared/utils/serialize-utils.ts index dbb309431a1..d54227b98f8 100644 --- a/packages/qwik/src/core/shared/utils/serialize-utils.ts +++ b/packages/qwik/src/core/shared/utils/serialize-utils.ts @@ -145,7 +145,15 @@ export const noSerialize = (input: T): NoSerialize /** @internal */ export const _weakSerialize = (input: T): Partial => { weakSerializeSet.add(input); - return input as any; + if (isObject(input)) { + for (const key in input) { + const value = input[key]; + if (isObject(value)) { + noSerializeSet.add(value); + } + } + } + return input; }; /** diff --git a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx index 53a37da632c..4a7f035c431 100644 --- a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx +++ b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx @@ -2,7 +2,7 @@ import { component$, useSignal } from "@qwik.dev/core"; import { routeLoader$ } from "@qwik.dev/router"; export const useTestLoader = routeLoader$(async () => { - return { test: "should not serialize this" }; + return { test: "some test value", notUsed: "should not serialize this" }; }); export default component$(() => { From 92fe96148d92c798fbcb35c5633ea172a1ae3caa Mon Sep 17 00:00:00 2001 From: Varixo Date: Sat, 1 Mar 2025 21:26:01 +0100 Subject: [PATCH 6/8] feat: get data for single loader --- .../resolve-request-handlers.ts | 11 +++++++- .../src/runtime/src/server-functions.ts | 7 +++-- .../src/runtime/src/use-endpoint.ts | 6 ++++- packages/qwik-router/src/runtime/src/utils.ts | 27 ++++++++++++++----- 4 files changed, 38 insertions(+), 13 deletions(-) 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 2aed2c925f1..80962a42ce6 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 @@ -578,8 +578,17 @@ export async function renderQData(requestEv: RequestEvent) { requestEv.request.headers.forEach((value, key) => (requestHeaders[key] = value)); requestEv.headers.set('Content-Type', 'application/json; charset=utf-8'); + const allLoaders = getRequestLoaders(requestEv); + const loaders: Record = {}; + for (const loaderId in allLoaders) { + const loader = allLoaders[loaderId]; + if (loader) { + loaders[loaderId] = loader; + } + } + const qData: ClientPageData = { - loaders: getRequestLoaders(requestEv), + loaders, action: requestEv.sharedMap.get(RequestEvSharedActionId), status: status !== 200 ? status : 200, href: getPathname(requestEv.url, trailingSlash), diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index 53c5f2c166e..2de9c1a1195 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -210,10 +210,9 @@ export const routeLoaderQrl = (( } const data = untrack(() => state[id]); if (!data && isBrowser) { - // TODO: fetch only loader with current id - throw loadClientData(location.url, iCtx.$hostElement$).then( - (data) => (state[id] = data?.loaders[id]) - ); + throw loadClientData(location.url, iCtx.$hostElement$, { + loaderIds: [id], + }).then((data) => (state[id] = data?.loaders[id])); } return _wrapStore(state, id); } diff --git a/packages/qwik-router/src/runtime/src/use-endpoint.ts b/packages/qwik-router/src/runtime/src/use-endpoint.ts index fc2ec8d9f31..f0d45ce4f1b 100644 --- a/packages/qwik-router/src/runtime/src/use-endpoint.ts +++ b/packages/qwik-router/src/runtime/src/use-endpoint.ts @@ -9,6 +9,7 @@ export const loadClientData = async ( element: unknown, opts?: { action?: RouteActionValue; + loaderIds?: string[]; clearCache?: boolean; prefetchSymbols?: boolean; isPrefetch?: boolean; @@ -16,7 +17,10 @@ export const loadClientData = async ( ) => { const pagePathname = url.pathname; const pageSearch = url.search; - const clientDataPath = getClientDataPath(pagePathname, pageSearch, opts?.action); + 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); diff --git a/packages/qwik-router/src/runtime/src/utils.ts b/packages/qwik-router/src/runtime/src/utils.ts index 23784d4ea77..b8f0ed733a0 100644 --- a/packages/qwik-router/src/runtime/src/utils.ts +++ b/packages/qwik-router/src/runtime/src/utils.ts @@ -1,6 +1,6 @@ -import type { RouteActionValue, SimpleURL } from './types'; +import type { SimpleURL } from './types'; -import { QACTION_KEY } from './constants'; +import { QACTION_KEY, QLOADER_KEY } from './constants'; /** Gets an absolute url path string (url.pathname + url.search + url.hash) */ export const toPath = (url: URL) => url.pathname + url.search + url.hash; @@ -31,13 +31,26 @@ export const isSameOriginDifferentPathname = (a: SimpleURL, b: SimpleURL) => export const getClientDataPath = ( pathname: string, pageSearch?: string, - action?: RouteActionValue + options?: { + actionId?: string; + loaderIds?: string[]; + } ) => { - let search = pageSearch ?? ''; - if (action) { - search += (search ? '&' : '?') + QACTION_KEY + '=' + encodeURIComponent(action.id); + const search = new URLSearchParams(pageSearch); + if (options?.actionId) { + search.set(QACTION_KEY, options.actionId); + } else if (options?.loaderIds) { + for (const id of options.loaderIds) { + search.append(QLOADER_KEY, id); + } } - return pathname + (pathname.endsWith('/') ? '' : '/') + 'q-data.json' + search; + const searchString = search.toString(); + return ( + pathname + + (pathname.endsWith('/') ? '' : '/') + + 'q-data.json' + + (searchString.length ? '?' + searchString : '') + ); }; export const getClientNavPath = (props: Record, baseUrl: { url: URL }) => { From db7770f975cc794347844af61116ae1181df987f Mon Sep 17 00:00:00 2001 From: Varixo Date: Sun, 2 Mar 2025 17:32:43 +0100 Subject: [PATCH 7/8] fix: loaders serialization --- .../request-handler/resolve-request-handlers.ts | 3 ++- packages/qwik-router/src/runtime/src/server-functions.ts | 7 +++++-- packages/qwik/src/core/core.api.md | 3 +++ packages/qwik/src/core/internal.ts | 2 +- packages/qwik/src/core/shared/shared-serialization.ts | 9 +++++++-- packages/qwik/src/core/shared/utils/constants.ts | 3 +++ scripts/qwik-router.ts | 2 +- starters/e2e/qwikrouter/nav.e2e.ts | 2 +- 8 files changed, 23 insertions(+), 8 deletions(-) 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 80962a42ce6..50586843634 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 @@ -29,6 +29,7 @@ import { import { getQwikRouterServerData } from './response-page'; import type { QwikSerializer, RequestEvent, RequestEventBase, RequestHandler } from './types'; import { IsQData, QDATA_JSON } from './user-response'; +import { _UNINITIALIZED } from '@qwik.dev/core/internal'; export const resolveRequestHandlers = ( serverPlugins: RouteModule[] | undefined, @@ -582,7 +583,7 @@ export async function renderQData(requestEv: RequestEvent) { const loaders: Record = {}; for (const loaderId in allLoaders) { const loader = allLoaders[loaderId]; - if (loader) { + if (loader !== _UNINITIALIZED) { loaders[loaderId] = loader; } } diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index 2de9c1a1195..ba516cb4e73 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -14,6 +14,7 @@ import { _getContextEvent, _serialize, _wrapStore, + _UNINITIALIZED, } from '@qwik.dev/core/internal'; import * as v from 'valibot'; @@ -209,10 +210,12 @@ export const routeLoaderQrl = (( For more information check: https://qwik.dev/docs/re-exporting-loaders/`); } const data = untrack(() => state[id]); - if (!data && isBrowser) { + if (data === _UNINITIALIZED && isBrowser) { throw loadClientData(location.url, iCtx.$hostElement$, { loaderIds: [id], - }).then((data) => (state[id] = data?.loaders[id])); + }).then((data) => { + state[id] = data?.loaders[id]; + }); } return _wrapStore(state, id); } diff --git a/packages/qwik/src/core/core.api.md b/packages/qwik/src/core/core.api.md index 6563122532c..d826cbe136c 100644 --- a/packages/qwik/src/core/core.api.md +++ b/packages/qwik/src/core/core.api.md @@ -1600,6 +1600,9 @@ export interface Tracker { (obj: T, prop: P): T[P]; } +// @internal (undocumented) +export const _UNINITIALIZED: unique symbol; + // @public export const untrack: (fn: () => T) => T; diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index f5c4417751a..9b47d7c4753 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -5,7 +5,7 @@ export { queueQRL as _run } from './client/queue-qrl'; export { scheduleTask as _task } from './use/use-task'; export { _wrapSignal, _wrapProp, _wrapStore } from './reactive-primitives/internal-api'; export { _restProps } from './shared/utils/prop'; -export { _IMMUTABLE } from './shared/utils/constants'; +export { _IMMUTABLE, _UNINITIALIZED } from './shared/utils/constants'; export { _CONST_PROPS, _VAR_PROPS } from './shared/utils/constants'; export { _weakSerialize } from './shared/utils/serialize-utils'; export { verifySerializable as _verifySerializable } from './shared/utils/serialize-utils'; diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 1bc2bd16157..b4dfc655182 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -35,7 +35,7 @@ import { isQrl, isSyncQrl } from './qrl/qrl-utils'; import type { QRL } from './qrl/qrl.public'; import { ChoreType } from './util-chore-type'; import type { DeserializeContainer, HostElement, ObjToProxyMap } from './types'; -import { _CONST_PROPS, _VAR_PROPS } from './utils/constants'; +import { _CONST_PROPS, _UNINITIALIZED, _VAR_PROPS } from './utils/constants'; import { isElement, isNode } from './utils/element'; import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight'; import { ELEMENT_ID } from './utils/markers'; @@ -398,7 +398,7 @@ const inflate = ( target = Object.fromEntries( objectKeys.map((v) => // initialize values with null - [v, null] + [v, _UNINITIALIZED] ) ); break; @@ -419,6 +419,7 @@ export const _constants = [ EMPTY_OBJ, NEEDS_COMPUTATION, STORE_ALL_PROPS, + _UNINITIALIZED, Slot, Fragment, NaN, @@ -438,6 +439,7 @@ const _constantNames = [ 'EMPTY_OBJ', 'NEEDS_COMPUTATION', 'STORE_ALL_PROPS', + '_UNINITIALIZED', 'Slot', 'Fragment', 'NaN', @@ -1033,6 +1035,8 @@ async function serialize(serializationContext: SerializationContext): Promise { +test.describe("nav", () => { test.describe("mpa", () => { test.use({ javaScriptEnabled: false }); tests(); From 5a5444e381f7f51452f057985028cf2bbb158ee3 Mon Sep 17 00:00:00 2001 From: Varixo Date: Sat, 29 Mar 2025 19:17:56 +0100 Subject: [PATCH 8/8] feat: use custom serialization --- .../request-handler/request-event.ts | 15 ++++--- .../resolve-request-handlers.ts | 7 ++- .../request-handler/response-page.ts | 22 ++++++++- .../qwik-router/src/runtime/src/constants.ts | 2 + .../src/runtime/src/qwik-router-component.tsx | 7 ++- .../src/runtime/src/server-functions.ts | 6 +-- packages/qwik-router/src/runtime/src/utils.ts | 19 +++----- packages/qwik/src/core/core.api.md | 45 +++++++++++++++---- packages/qwik/src/core/internal.ts | 2 +- .../src/core/shared/shared-serialization.ts | 17 +------ .../core/shared/shared-serialization.unit.ts | 23 +++++----- .../src/core/shared/utils/serialize-utils.ts | 19 -------- .../routes/loaders-serialization/index.tsx | 9 +++- 13 files changed, 106 insertions(+), 87 deletions(-) diff --git a/packages/qwik-router/src/middleware/request-handler/request-event.ts b/packages/qwik-router/src/middleware/request-handler/request-event.ts index 23ac2f4682c..ae92a8af7cf 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-event.ts @@ -1,12 +1,13 @@ import type { ValueOrPromise } from '@qwik.dev/core'; import type { QwikManifest, ResolvedManifest } from '@qwik.dev/core/optimizer'; import { QDATA_KEY } from '../../runtime/src/constants'; -import type { - ActionInternal, - FailReturn, - JSONValue, - LoadedRoute, - LoaderInternal, +import { + LoadedRouteProp, + type ActionInternal, + type FailReturn, + type JSONValue, + type LoadedRoute, + type LoaderInternal, } from '../../runtime/src/types'; import { isPromise } from '../../runtime/src/utils'; import { createCacheControl } from './cache-control'; @@ -149,7 +150,7 @@ export function createRequestEvent( env, method: request.method, signal: request.signal, - params: loadedRoute?.[1] ?? {}, + params: loadedRoute?.[LoadedRouteProp.Params] ?? {}, pathname: url.pathname, platform, query: url.searchParams, 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 50586843634..a6afa4a2329 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,4 +1,5 @@ -import type { QRL } from '@qwik.dev/core'; +import { type QRL } from '@qwik.dev/core'; +import { SerializerSymbol, _UNINITIALIZED } from '@qwik.dev/core/internal'; import type { Render, RenderToStringResult } from '@qwik.dev/core/server'; import { QACTION_KEY, QFN_KEY, QLOADER_KEY } from '../../runtime/src/constants'; import { @@ -29,7 +30,6 @@ import { import { getQwikRouterServerData } from './response-page'; import type { QwikSerializer, RequestEvent, RequestEventBase, RequestHandler } from './types'; import { IsQData, QDATA_JSON } from './user-response'; -import { _UNINITIALIZED } from '@qwik.dev/core/internal'; export const resolveRequestHandlers = ( serverPlugins: RouteModule[] | undefined, @@ -583,6 +583,9 @@ export async function renderQData(requestEv: RequestEvent) { const loaders: Record = {}; for (const loaderId in allLoaders) { const loader = allLoaders[loaderId]; + if (typeof loader === 'object' && loader !== null && SerializerSymbol in loader) { + delete (loader as any)[SerializerSymbol]; + } if (loader !== _UNINITIALIZED) { loaders[loaderId] = loader; } 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 7bc79f51a18..dca8f042167 100644 --- a/packages/qwik-router/src/middleware/request-handler/response-page.ts +++ b/packages/qwik-router/src/middleware/request-handler/response-page.ts @@ -1,3 +1,5 @@ +import { SerializerSymbol } from '@qwik.dev/core'; +import { _UNINITIALIZED } from '@qwik.dev/core/internal'; import type { QwikRouterEnvData } from '../../runtime/src/types'; import { getRequestLoaders, @@ -8,6 +10,7 @@ import { RequestRouteName, } from './request-event'; import type { RequestEvent } from './types'; +import { Q_ROUTE } from '../../runtime/src/constants'; export function getQwikRouterServerData(requestEv: RequestEvent) { const { url, params, request, status, locale } = requestEv; @@ -30,13 +33,28 @@ export function getQwikRouterServerData(requestEv: RequestEvent) { reconstructedUrl.protocol = protocol; } + const loaders = getRequestLoaders(requestEv); + + // shallow serialize loaders data + (loaders as any)[SerializerSymbol] = (loaders: Record) => { + const result: Record = {}; + for (const key in loaders) { + const loader = loaders[key]; + if (typeof loader === 'object' && loader !== null) { + (loader as any)[SerializerSymbol] = () => _UNINITIALIZED; + } + result[key] = _UNINITIALIZED; + } + return result; + }; + return { url: reconstructedUrl.href, requestHeaders, locale: locale(), nonce, containerAttributes: { - 'q:route': routeName, + [Q_ROUTE]: routeName, }, qwikrouter: { routeName, @@ -45,7 +63,7 @@ export function getQwikRouterServerData(requestEv: RequestEvent) { loadedRoute: getRequestRoute(requestEv), response: { status: status(), - loaders: getRequestLoaders(requestEv), + loaders, action, formData, }, diff --git a/packages/qwik-router/src/runtime/src/constants.ts b/packages/qwik-router/src/runtime/src/constants.ts index 8226d20cddf..93c5ca8818d 100644 --- a/packages/qwik-router/src/runtime/src/constants.ts +++ b/packages/qwik-router/src/runtime/src/constants.ts @@ -13,3 +13,5 @@ export const QLOADER_KEY = 'qloader'; export const QFN_KEY = 'qfunc'; export const QDATA_KEY = 'qdata'; + +export const Q_ROUTE = 'q:route'; 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 fe9e78e013f..e93b03c44fc 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx +++ b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx @@ -20,11 +20,10 @@ import { _getContextElement, _getQContainerElement, _waitUntilRendered, - _weakSerialize, type _ElementVNode, } from '@qwik.dev/core/internal'; import { clientNavigate } from './client-navigate'; -import { CLIENT_DATA_CACHE } from './constants'; +import { CLIENT_DATA_CACHE, Q_ROUTE } from './constants'; import { ContentContext, ContentInternalContext, @@ -146,7 +145,7 @@ export const QwikRouterProvider = component$((props) => { { deep: false } ); const navResolver: { r?: () => void } = {}; - const loaderState = useStore(_weakSerialize(env.response.loaders), { deep: false }); + const loaderState = useStore(env.response.loaders, { deep: false }); const routeInternal = useSignal({ type: 'initial', dest: url, @@ -641,7 +640,7 @@ export const QwikRouterProvider = component$((props) => { clientNavigate(window, navType, prevUrl, trackUrl, replaceState); _waitUntilRendered(elm as Element).then(() => { const container = _getQContainerElement(elm as _ElementVNode)!; - container.setAttribute('q:route', routeName); + container.setAttribute(Q_ROUTE, routeName); const scrollState = currentScrollState(scroller); saveScrollHistory(scrollState); win._qRouterScrollEnabled = true; diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index ba516cb4e73..fd3ba627bf1 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -7,6 +7,8 @@ import { type ValueOrPromise, untrack, isBrowser, + isDev, + isServer, } from '@qwik.dev/core'; import { _deserialize, @@ -14,6 +16,7 @@ import { _getContextEvent, _serialize, _wrapStore, + _useInvokeContext, _UNINITIALIZED, } from '@qwik.dev/core/internal'; @@ -54,9 +57,6 @@ import type { } from './types'; import { useAction, useLocation, useQwikRouterEnv } from './use-functions'; -import { isDev, isServer } from '@qwik.dev/core'; -import { _useInvokeContext } from '@qwik.dev/core/internal'; - import type { FormSubmitCompletedDetail } from './form-component'; import { deepFreeze } from './utils'; import { loadClientData } from './use-endpoint'; diff --git a/packages/qwik-router/src/runtime/src/utils.ts b/packages/qwik-router/src/runtime/src/utils.ts index b8f0ed733a0..40ed365bca4 100644 --- a/packages/qwik-router/src/runtime/src/utils.ts +++ b/packages/qwik-router/src/runtime/src/utils.ts @@ -36,21 +36,16 @@ export const getClientDataPath = ( loaderIds?: string[]; } ) => { - const search = new URLSearchParams(pageSearch); + let search = pageSearch ?? ''; if (options?.actionId) { - search.set(QACTION_KEY, options.actionId); - } else if (options?.loaderIds) { - for (const id of options.loaderIds) { - search.append(QLOADER_KEY, id); + search += (search ? '&' : '?') + QACTION_KEY + '=' + encodeURIComponent(options.actionId); + } + if (options?.loaderIds) { + for (const loaderId of options.loaderIds) { + search += (search ? '&' : '?') + QLOADER_KEY + '=' + encodeURIComponent(loaderId); } } - const searchString = search.toString(); - return ( - pathname + - (pathname.endsWith('/') ? '' : '/') + - 'q-data.json' + - (searchString.length ? '?' + searchString : '') - ); + return pathname + (pathname.endsWith('/') ? '' : '/') + 'q-data.json' + search; }; export const getClientNavPath = (props: Record, baseUrl: { url: URL }) => { diff --git a/packages/qwik/src/core/core.api.md b/packages/qwik/src/core/core.api.md index d826cbe136c..799fbd335b2 100644 --- a/packages/qwik/src/core/core.api.md +++ b/packages/qwik/src/core/core.api.md @@ -246,11 +246,9 @@ export { DomContainer as _DomContainer } // @internal (undocumented) export const _EFFECT_BACK_REF: unique symbol; -// Warning: (ae-forgotten-export) The symbol "VNodeFlags" needs to be exported by the entry point index.d.ts -// // @internal (undocumented) export type _ElementVNode = [ -VNodeFlags.Element, +_VNodeFlags.Element, ////////////// 0 - Flags _VNode | null, /////////////// 1 - Parent @@ -1579,7 +1577,7 @@ export type TaskFn = (ctx: TaskCtx) => ValueOrPromise void)>; // @internal (undocumented) export type _TextVNode = [ -VNodeFlags.Text | VNodeFlags.Inflated, +_VNodeFlags.Text | _VNodeFlags.Inflated, // 0 - Flags _VNode | null, ///////////////// 1 - Parent @@ -1751,7 +1749,7 @@ export const version: string; // @internal (undocumented) export type _VirtualVNode = [ -VNodeFlags.Virtual, +_VNodeFlags.Virtual, ///////////// 0 - Flags _VNode | null, /////////////// 1 - Parent @@ -1773,6 +1771,40 @@ export type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | ' // @internal (undocumented) export type _VNode = _ElementVNode | _TextVNode | _VirtualVNode; +// @internal +export const enum _VNodeFlags { + // (undocumented) + Deleted = 32, + // (undocumented) + Element = 1, + // (undocumented) + ELEMENT_OR_TEXT_MASK = 5, + // (undocumented) + ELEMENT_OR_VIRTUAL_MASK = 3, + // (undocumented) + Inflated = 8, + // (undocumented) + INFLATED_TYPE_MASK = 15, + // (undocumented) + NAMESPACE_MASK = 192, + // (undocumented) + NEGATED_NAMESPACE_MASK = -193, + // (undocumented) + NS_html = 0, + // (undocumented) + NS_math = 128, + // (undocumented) + NS_svg = 64, + // (undocumented) + Resolved = 16, + // (undocumented) + Text = 4,// http://www.w3.org/1999/xhtml + // (undocumented) + TYPE_MASK = 7,// http://www.w3.org/2000/svg + // (undocumented) + Virtual = 2 +} + // @internal (undocumented) export const _waitUntilRendered: (elm: Element) => Promise; @@ -1784,9 +1816,6 @@ export function _walkJSX(ssr: SSRContainer, value: JSXOutput, options: { parentComponentFrame: ISsrComponentFrame | null; }): Promise; -// @internal (undocumented) -export const _weakSerialize: (input: T) => Partial; - // @public export function withLocale(locale: string, fn: () => T): T; diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index 9b47d7c4753..c3fd4ed5319 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -7,7 +7,6 @@ export { _wrapSignal, _wrapProp, _wrapStore } from './reactive-primitives/intern export { _restProps } from './shared/utils/prop'; export { _IMMUTABLE, _UNINITIALIZED } from './shared/utils/constants'; export { _CONST_PROPS, _VAR_PROPS } from './shared/utils/constants'; -export { _weakSerialize } from './shared/utils/serialize-utils'; export { verifySerializable as _verifySerializable } from './shared/utils/serialize-utils'; export { _getContextElement, @@ -21,6 +20,7 @@ export { _fnSignal } from './shared/qrl/inlined-fn'; export type { ContainerElement as _ContainerElement, VNode as _VNode, + VNodeFlags as _VNodeFlags, VirtualVNode as _VirtualVNode, TextVNode as _TextVNode, QDocument as _QDocument, diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index b4dfc655182..04abe5071d1 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -40,7 +40,7 @@ import { isElement, isNode } from './utils/element'; import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight'; import { ELEMENT_ID } from './utils/markers'; import { isPromise } from './utils/promises'; -import { SerializerSymbol, fastSkipSerialize, fastWeakSerialize } from './utils/serialize-utils'; +import { SerializerSymbol, fastSkipSerialize } from './utils/serialize-utils'; import { _EFFECT_BACK_REF, EffectSubscriptionProp, @@ -393,16 +393,6 @@ const inflate = ( effectData.data.$isConst$ = (data as any[])[1]; break; } - case TypeIds.WeakObject: { - const objectKeys = data as string[]; - target = Object.fromEntries( - objectKeys.map((v) => - // initialize values with null - [v, _UNINITIALIZED] - ) - ); - break; - } default: throw qError(QError.serializeErrorNotImplemented, [typeId]); } @@ -472,7 +462,6 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow case TypeIds.Array: return wrapDeserializerProxy(container as any, value as any[]); case TypeIds.Object: - case TypeIds.WeakObject: return {}; case TypeIds.QRL: case TypeIds.PreloadQRL: @@ -1131,8 +1120,6 @@ async function serialize(serializationContext: SerializationContext): Promise { 6 Constant EMPTY_OBJ 7 Constant NEEDS_COMPUTATION 8 Constant STORE_ALL_PROPS - 9 Constant Slot - 10 Constant Fragment - 11 Constant NaN - 12 Constant Infinity - 13 Constant -Infinity - 14 Constant MAX_SAFE_INTEGER - 15 Constant MAX_SAFE_INTEGER-1 - 16 Constant MIN_SAFE_INTEGER - (76 chars)" + 9 Constant _UNINITIALIZED + 10 Constant Slot + 11 Constant Fragment + 12 Constant NaN + 13 Constant Infinity + 14 Constant -Infinity + 15 Constant MAX_SAFE_INTEGER + 16 Constant MAX_SAFE_INTEGER-1 + 17 Constant MIN_SAFE_INTEGER + (81 chars)" `); }); it(title(TypeIds.Number), async () => { @@ -550,7 +551,7 @@ describe('shared-serialization', () => { expect(await dump(new SubscriptionData({ $isConst$: true, $scopedStyleIdPrefix$: null }))) .toMatchInlineSnapshot(` " - 0 EffectData [ + 0 SubscriptionData [ Constant null Constant true ] diff --git a/packages/qwik/src/core/shared/utils/serialize-utils.ts b/packages/qwik/src/core/shared/utils/serialize-utils.ts index d54227b98f8..e47dcd01718 100644 --- a/packages/qwik/src/core/shared/utils/serialize-utils.ts +++ b/packages/qwik/src/core/shared/utils/serialize-utils.ts @@ -90,7 +90,6 @@ const _verifySerializable = ( return value; }; const noSerializeSet = /*#__PURE__*/ new WeakSet(); -const weakSerializeSet = /*#__PURE__*/ new WeakSet(); export const shouldSerialize = (obj: unknown): boolean => { if (isObject(obj) || isFunction(obj)) { @@ -103,10 +102,6 @@ export const fastSkipSerialize = (obj: object): boolean => { return typeof obj === 'object' && obj && (NoSerializeSymbol in obj || noSerializeSet.has(obj)); }; -export const fastWeakSerialize = (obj: object): boolean => { - return weakSerializeSet.has(obj); -}; - /** * Returned type of the `noSerialize()` function. It will be TYPE or undefined. * @@ -142,20 +137,6 @@ export const noSerialize = (input: T): NoSerialize return input as any; }; -/** @internal */ -export const _weakSerialize = (input: T): Partial => { - weakSerializeSet.add(input); - if (isObject(input)) { - for (const key in input) { - const value = input[key]; - if (isObject(value)) { - noSerializeSet.add(value); - } - } - } - return input; -}; - /** * If an object has this property, it will not be serialized. Use this on prototypes to avoid having * to call `noSerialize()` on every object. diff --git a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx index 4a7f035c431..6c51d241e94 100644 --- a/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx +++ b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx @@ -2,7 +2,7 @@ import { component$, useSignal } from "@qwik.dev/core"; import { routeLoader$ } from "@qwik.dev/router"; export const useTestLoader = routeLoader$(async () => { - return { test: "some test value", notUsed: "should not serialize this" }; + return { test: "some test value", abcd: "should not serialize this" }; }); export default component$(() => { @@ -21,5 +21,10 @@ export default component$(() => { export const Child = component$(() => { const testSignal = useTestLoader(); - return
{testSignal.value.test}
; + return ( + <> +
{testSignal.value.test}
+
{testSignal.value.abcd}
+ + ); });