diff --git a/eslint.config.mjs b/eslint.config.mjs index dcc4bcb7897..238e944172d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -109,7 +109,7 @@ export default tseslint.config( }, }, { - files: ['packages/docs/**/*.{ts,tsx}'], + files: ['packages/docs/demo/**/*.{ts,tsx}'], rules: { 'no-console': 'off', }, diff --git a/package.json b/package.json index 87efcc892e3..9ca2a288707 100644 --- a/package.json +++ b/package.json @@ -177,6 +177,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/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index 3ac5286798e..28ae3ec1c85 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -672,20 +672,6 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/jsx-runtime.ts", "mdFile": "core.h.md" }, - { - "name": "HTMLElementAttrs", - "id": "htmlelementattrs", - "hierarchy": [ - { - "name": "HTMLElementAttrs", - "id": "htmlelementattrs" - } - ], - "kind": "Interface", - "content": "```typescript\nexport interface HTMLElementAttrs extends HTMLAttributesBase, FilterBase \n```\n**Extends:** HTMLAttributesBase, FilterBase<HTMLElement>", - "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts", - "mdFile": "core.htmlelementattrs.md" - }, { "name": "implicit$FirstArg", "id": "implicit_firstarg", @@ -1318,7 +1304,7 @@ } ], "kind": "TypeAlias", - "content": "The DOM props without plain handlers, for use inside functions\n\n\n```typescript\nexport type QwikHTMLElements = {\n [tag in keyof HTMLElementTagNameMap]: Augmented & HTMLElementAttrs & QwikAttributes;\n};\n```\n**References:** [HTMLElementAttrs](#htmlelementattrs), [QwikAttributes](#qwikattributes)", + "content": "The DOM props without plain handlers, for use inside functions\n\n\n```typescript\nexport type QwikHTMLElements = {\n [tag in keyof HTMLElementTagNameMap]: Augmented & HTMLElementAttrs & QwikAttributes;\n};\n```\n**References:** [QwikAttributes](#qwikattributes)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts", "mdFile": "core.qwikhtmlelements.md" }, @@ -1458,7 +1444,7 @@ } ], "kind": "TypeAlias", - "content": "The SVG props without plain handlers, for use inside functions\n\n\n```typescript\nexport type QwikSVGElements = {\n [K in keyof Omit]: SVGProps;\n};\n```\n**References:** [SVGProps](#svgprops)", + "content": "The SVG props without plain handlers, for use inside functions\n\n\n```typescript\nexport type QwikSVGElements = {\n [K in keyof Omit]: SVGProps;\n};\n```", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts", "mdFile": "core.qwiksvgelements.md" }, @@ -2008,20 +1994,6 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts", "mdFile": "core.svgattributes.md" }, - { - "name": "SVGProps", - "id": "svgprops", - "hierarchy": [ - { - "name": "SVGProps", - "id": "svgprops" - } - ], - "kind": "Interface", - "content": "```typescript\nexport interface SVGProps extends SVGAttributes, QwikAttributes \n```\n**Extends:** [SVGAttributes](#svgattributes), [QwikAttributes](#qwikattributes)<T>", - "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts", - "mdFile": "core.svgprops.md" - }, { "name": "sync$", "id": "sync_", diff --git a/packages/docs/src/routes/api/qwik/index.mdx b/packages/docs/src/routes/api/qwik/index.mdx index bb1de806426..04abe6e5ef4 100644 --- a/packages/docs/src/routes/api/qwik/index.mdx +++ b/packages/docs/src/routes/api/qwik/index.mdx @@ -1333,16 +1333,6 @@ any[] [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/jsx-runtime.ts) -## HTMLElementAttrs - -```typescript -export interface HTMLElementAttrs extends HTMLAttributesBase, FilterBase -``` - -**Extends:** HTMLAttributesBase, FilterBase<HTMLElement> - -[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts) - ## implicit$FirstArg Create a `____$(...)` convenience method from `___(...)`. @@ -2586,7 +2576,7 @@ export type QwikHTMLElements = { }; ``` -**References:** [HTMLElementAttrs](#htmlelementattrs), [QwikAttributes](#qwikattributes) +**References:** [QwikAttributes](#qwikattributes) [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts) @@ -2784,8 +2774,6 @@ export type QwikSVGElements = { }; ``` -**References:** [SVGProps](#svgprops) - [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts) ## QwikSymbolEvent @@ -8121,16 +8109,6 @@ _(Optional)_ [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts) -## SVGProps - -```typescript -export interface SVGProps extends SVGAttributes, QwikAttributes -``` - -**Extends:** [SVGAttributes](#svgattributes), [QwikAttributes](#qwikattributes)<T> - -[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts) - ## sync$ Extract function into a synchronously loadable QRL. diff --git a/packages/qwik-router/src/buildtime/vite/dev-server.ts b/packages/qwik-router/src/buildtime/vite/dev-server.ts index cf95cd6c1a7..c9a1b655e09 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/index.ts b/packages/qwik-router/src/middleware/request-handler/index.ts index a5d10b3195a..931c4562200 100644 --- a/packages/qwik-router/src/middleware/request-handler/index.ts +++ b/packages/qwik-router/src/middleware/request-handler/index.ts @@ -2,6 +2,7 @@ export { getErrorHtml, ServerError } from './error-handler'; export { mergeHeadersCookies } from './cookie'; export { AbortMessage, RedirectMessage } from './redirect-handler'; export { requestHandler } from './request-handler'; +export { RequestEvShareQData } from './request-event'; export { _TextEncoderStream_polyfill } from './polyfill'; export type { CacheControl, diff --git a/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md b/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md index 4198462608e..69fa7f0fd9f 100644 --- a/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md +++ b/packages/qwik-router/src/middleware/request-handler/middleware.request-handler.api.md @@ -1,240 +1,245 @@ -## API Report File for "@qwik.dev/router" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import type { Action } from '@qwik.dev/router'; -import type { _deserialize } from '@qwik.dev/core/internal'; -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'; -import type { QwikCityPlan } from '@qwik.dev/router'; -import type { QwikIntrinsicElements } from '@qwik.dev/core'; -import type { QwikRouterConfig } from '@qwik.dev/router'; -import type { Render } from '@qwik.dev/core/server'; -import type { RenderOptions } from '@qwik.dev/core/server'; -import { RequestEvent as RequestEvent_2 } from '@qwik.dev/router/middleware/request-handler'; -import type { RequestHandler as RequestHandler_2 } from '@qwik.dev/router/middleware/request-handler'; -import type { ResolveSyncValue as ResolveSyncValue_2 } from '@qwik.dev/router/middleware/request-handler'; -import type { _serialize } from '@qwik.dev/core/internal'; -import type { ValueOrPromise } from '@qwik.dev/core'; -import type { _verifySerializable } from '@qwik.dev/core/internal'; - -// @public (undocumented) -export class AbortMessage { -} - -// Warning: (ae-forgotten-export) The symbol "CacheControlOptions" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export type CacheControl = CacheControlOptions | number | 'day' | 'week' | 'month' | 'year' | 'no-cache' | 'immutable' | 'private'; - -// @public (undocumented) -export interface ClientConn { - // (undocumented) - country?: string; - // (undocumented) - ip?: string; -} - -// @public (undocumented) -export interface Cookie { - append(name: string, value: string | number | Record, options?: CookieOptions): void; - delete(name: string, options?: Pick): void; - get(name: string): CookieValue | null; - getAll(): Record; - has(name: string): boolean; - headers(): string[]; - set(name: string, value: string | number | Record, options?: CookieOptions): void; -} - -// @public -export interface CookieOptions { - domain?: string; - expires?: Date | string; - httpOnly?: boolean; - maxAge?: number | [number, 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks']; - path?: string; - sameSite?: 'strict' | 'lax' | 'none' | 'Strict' | 'Lax' | 'None' | boolean; - secure?: boolean; -} - -// @public (undocumented) -export interface CookieValue { - // (undocumented) - json: () => T; - // (undocumented) - number: () => number; - // (undocumented) - value: string; -} - -// @public (undocumented) -export type DeferReturn = () => Promise; - -// @public (undocumented) -export interface EnvGetter { - // (undocumented) - get(key: string): string | undefined; -} - -// @public (undocumented) -export function getErrorHtml(status: number, e: any): string; - -// @public (undocumented) -export const mergeHeadersCookies: (headers: Headers, cookies: Cookie) => Headers; - -// @public (undocumented) -export class RedirectMessage extends AbortMessage { -} - -// @public (undocumented) -export interface RequestEvent extends RequestEventCommon { - readonly exited: boolean; - readonly getWritableStream: () => WritableStream; - readonly headersSent: boolean; - readonly next: () => Promise; -} - -// @public (undocumented) -export interface RequestEventAction extends RequestEventCommon { - // (undocumented) - fail: >(status: number, returnData: T) => FailReturn; -} - -// @public (undocumented) -export interface RequestEventBase { - readonly basePathname: string; - // Warning: (ae-forgotten-export) The symbol "CacheControlTarget" needs to be exported by the entry point index.d.ts - readonly cacheControl: (cacheControl: CacheControl, target?: CacheControlTarget) => void; - readonly clientConn: ClientConn; - readonly cookie: Cookie; - readonly env: EnvGetter; - readonly headers: Headers; - readonly method: string; - readonly params: Readonly>; - readonly parseBody: () => Promise; - readonly pathname: string; - readonly platform: PLATFORM; - readonly query: URLSearchParams; - readonly request: Request; - readonly sharedMap: Map; - readonly signal: AbortSignal; - readonly url: URL; -} - -// @public (undocumented) -export interface RequestEventCommon extends RequestEventBase { - // Warning: (ae-forgotten-export) The symbol "ErrorCodes" needs to be exported by the entry point index.d.ts - readonly error: (statusCode: ErrorCodes, message: T) => ServerError; - // (undocumented) - readonly exit: () => AbortMessage; - readonly html: (statusCode: StatusCodes, html: string) => AbortMessage; - readonly json: (statusCode: StatusCodes, data: any) => AbortMessage; - readonly locale: (local?: string) => string; - // Warning: (ae-forgotten-export) The symbol "RedirectCode" needs to be exported by the entry point index.d.ts - readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage; - // Warning: (ae-forgotten-export) The symbol "SendMethod" needs to be exported by the entry point index.d.ts - readonly send: SendMethod; - // Warning: (ae-forgotten-export) The symbol "StatusCodes" needs to be exported by the entry point index.d.ts - readonly status: (statusCode?: StatusCodes) => number; - readonly text: (statusCode: StatusCodes, text: string) => AbortMessage; -} - -// @public (undocumented) -export interface RequestEventLoader extends RequestEventAction { - // (undocumented) - defer: (returnData: Promise | (() => Promise)) => DeferReturn; - // (undocumented) - resolveValue: ResolveValue; -} - -// @public (undocumented) -export type RequestHandler = (ev: RequestEvent) => Promise | void; - -// Warning: (ae-forgotten-export) The symbol "QwikSerializer" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "QwikRouterRun" needs to be exported by the entry point index.d.ts -// -// @public -export function requestHandler(serverRequestEv: ServerRequestEvent, opts: ServerRenderOptions, qwikSerializer: QwikSerializer): Promise | null>; - -// @public (undocumented) -export interface ResolveSyncValue { - // (undocumented) - (loader: Loader_2): Awaited extends () => any ? never : Awaited; - // (undocumented) - (action: Action): Awaited | undefined; -} - -// @public (undocumented) -export interface ResolveValue { - // (undocumented) - (loader: Loader_2): Awaited extends () => any ? never : Promise; - // (undocumented) - (action: Action): Promise; -} - -// @public (undocumented) -export class ServerError extends Error { - constructor(status: number, data: T); - // (undocumented) - data: T; - // (undocumented) - status: number; -} - -// @public (undocumented) -export interface ServerRenderOptions extends RenderOptions { - checkOrigin?: boolean; - // @deprecated (undocumented) - qwikCityPlan?: QwikCityPlan; - // (undocumented) - qwikRouterConfig?: QwikRouterConfig; - // (undocumented) - render: Render; -} - -// @public -export interface ServerRequestEvent { - // (undocumented) - env: EnvGetter; - // (undocumented) - getClientConn: () => ClientConn; - // (undocumented) - getWritableStream: ServerResponseHandler; - // (undocumented) - locale: string | undefined; - // (undocumented) - mode: ServerRequestMode; - // (undocumented) - platform: QwikRouterPlatform; - // (undocumented) - request: Request; - // (undocumented) - url: URL; -} - -// @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 = (status: number, headers: Headers, cookies: Cookie, resolve: (response: T) => void, requestEv: RequestEventInternal) => WritableStream; - -// @internal (undocumented) -export class _TextEncoderStream_polyfill { - // (undocumented) - get [Symbol.toStringTag](): string; - // (undocumented) - get encoding(): string; - // (undocumented) - get readable(): ReadableStream>; - // (undocumented) - get writable(): WritableStream; -} - -// (No @packageDocumentation comment for this package) - -``` +## API Report File for "@qwik.dev/router" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { Action } from '@qwik.dev/router'; +import type { _deserialize } from '@qwik.dev/core/internal'; +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'; +import type { QwikCityPlan } from '@qwik.dev/router'; +import type { QwikIntrinsicElements } from '@qwik.dev/core'; +import type { QwikRouterConfig } from '@qwik.dev/router'; +import type { Render } from '@qwik.dev/core/server'; +import type { RenderOptions } from '@qwik.dev/core/server'; +import { RequestEvent as RequestEvent_2 } from '@qwik.dev/router/middleware/request-handler'; +import type { RequestHandler as RequestHandler_2 } from '@qwik.dev/router/middleware/request-handler'; +import type { ResolveSyncValue as ResolveSyncValue_2 } from '@qwik.dev/router/middleware/request-handler'; +import type { _serialize } from '@qwik.dev/core/internal'; +import type { ValueOrPromise } from '@qwik.dev/core'; +import type { _verifySerializable } from '@qwik.dev/core/internal'; + +// @public (undocumented) +export class AbortMessage { +} + +// Warning: (ae-forgotten-export) The symbol "CacheControlOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type CacheControl = CacheControlOptions | number | 'day' | 'week' | 'month' | 'year' | 'no-cache' | 'immutable' | 'private'; + +// @public (undocumented) +export interface ClientConn { + // (undocumented) + country?: string; + // (undocumented) + ip?: string; +} + +// @public (undocumented) +export interface Cookie { + append(name: string, value: string | number | Record, options?: CookieOptions): void; + delete(name: string, options?: Pick): void; + get(name: string): CookieValue | null; + getAll(): Record; + has(name: string): boolean; + headers(): string[]; + set(name: string, value: string | number | Record, options?: CookieOptions): void; +} + +// @public +export interface CookieOptions { + domain?: string; + expires?: Date | string; + httpOnly?: boolean; + maxAge?: number | [number, 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks']; + path?: string; + sameSite?: 'strict' | 'lax' | 'none' | 'Strict' | 'Lax' | 'None' | boolean; + secure?: boolean; +} + +// @public (undocumented) +export interface CookieValue { + // (undocumented) + json: () => T; + // (undocumented) + number: () => number; + // (undocumented) + value: string; +} + +// @public (undocumented) +export type DeferReturn = () => Promise; + +// @public (undocumented) +export interface EnvGetter { + // (undocumented) + get(key: string): string | undefined; +} + +// @public (undocumented) +export function getErrorHtml(status: number, e: any): string; + +// @public (undocumented) +export const mergeHeadersCookies: (headers: Headers, cookies: Cookie) => Headers; + +// @public (undocumented) +export class RedirectMessage extends AbortMessage { +} + +// @public (undocumented) +export interface RequestEvent extends RequestEventCommon { + readonly exited: boolean; + readonly getWritableStream: () => WritableStream; + readonly headersSent: boolean; + readonly next: () => Promise; +} + +// @public (undocumented) +export interface RequestEventAction extends RequestEventCommon { + // (undocumented) + fail: >(status: number, returnData: T) => FailReturn; +} + +// @public (undocumented) +export interface RequestEventBase { + readonly basePathname: string; + // Warning: (ae-forgotten-export) The symbol "CacheControlTarget" needs to be exported by the entry point index.d.ts + readonly cacheControl: (cacheControl: CacheControl, target?: CacheControlTarget) => void; + readonly clientConn: ClientConn; + readonly cookie: Cookie; + readonly env: EnvGetter; + readonly headers: Headers; + readonly method: string; + readonly params: Readonly>; + readonly parseBody: () => Promise; + readonly pathname: string; + readonly platform: PLATFORM; + readonly query: URLSearchParams; + readonly request: Request; + readonly sharedMap: Map; + readonly signal: AbortSignal; + readonly url: URL; +} + +// @public (undocumented) +export interface RequestEventCommon extends RequestEventBase { + // Warning: (ae-forgotten-export) The symbol "ErrorCodes" needs to be exported by the entry point index.d.ts + readonly error: (statusCode: ErrorCodes, message: T) => ServerError; + // (undocumented) + readonly exit: () => AbortMessage; + readonly html: (statusCode: StatusCodes, html: string) => AbortMessage; + readonly json: (statusCode: StatusCodes, data: any) => AbortMessage; + readonly locale: (local?: string) => string; + // Warning: (ae-forgotten-export) The symbol "RedirectCode" needs to be exported by the entry point index.d.ts + readonly redirect: (statusCode: RedirectCode, url: string) => RedirectMessage; + // Warning: (ae-forgotten-export) The symbol "SendMethod" needs to be exported by the entry point index.d.ts + readonly send: SendMethod; + // Warning: (ae-forgotten-export) The symbol "StatusCodes" needs to be exported by the entry point index.d.ts + readonly status: (statusCode?: StatusCodes) => number; + readonly text: (statusCode: StatusCodes, text: string) => AbortMessage; +} + +// @public (undocumented) +export interface RequestEventLoader extends RequestEventAction { + // (undocumented) + defer: (returnData: Promise | (() => Promise)) => DeferReturn; + // (undocumented) + resolveValue: ResolveValue; +} + +// Warning: (ae-internal-missing-underscore) The name "RequestEvShareQData" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export const RequestEvShareQData = "qData"; + +// @public (undocumented) +export type RequestHandler = (ev: RequestEvent) => Promise | void; + +// Warning: (ae-forgotten-export) The symbol "QwikSerializer" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "QwikRouterRun" needs to be exported by the entry point index.d.ts +// +// @public +export function requestHandler(serverRequestEv: ServerRequestEvent, opts: ServerRenderOptions, qwikSerializer: QwikSerializer): Promise | null>; + +// @public (undocumented) +export interface ResolveSyncValue { + // (undocumented) + (loader: Loader_2): Awaited extends () => any ? never : Awaited; + // (undocumented) + (action: Action): Awaited | undefined; +} + +// @public (undocumented) +export interface ResolveValue { + // (undocumented) + (loader: Loader_2): Awaited extends () => any ? never : Promise; + // (undocumented) + (action: Action): Promise; +} + +// @public (undocumented) +export class ServerError extends Error { + constructor(status: number, data: T); + // (undocumented) + data: T; + // (undocumented) + status: number; +} + +// @public (undocumented) +export interface ServerRenderOptions extends RenderOptions { + checkOrigin?: boolean; + // @deprecated (undocumented) + qwikCityPlan?: QwikCityPlan; + // (undocumented) + qwikRouterConfig?: QwikRouterConfig; + // (undocumented) + render: Render; +} + +// @public +export interface ServerRequestEvent { + // (undocumented) + env: EnvGetter; + // (undocumented) + getClientConn: () => ClientConn; + // (undocumented) + getWritableStream: ServerResponseHandler; + // (undocumented) + locale: string | undefined; + // (undocumented) + mode: ServerRequestMode; + // (undocumented) + platform: QwikRouterPlatform; + // (undocumented) + request: Request; + // (undocumented) + url: URL; +} + +// @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 = (status: number, headers: Headers, cookies: Cookie, resolve: (response: T) => void, requestEv: RequestEventInternal) => WritableStream; + +// @internal (undocumented) +export class _TextEncoderStream_polyfill { + // (undocumented) + get [Symbol.toStringTag](): string; + // (undocumented) + get encoding(): string; + // (undocumented) + get readable(): ReadableStream>; + // (undocumented) + get writable(): WritableStream; +} + +// (No @packageDocumentation comment for this package) + +``` 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 23cc56c821c..086a37cc417 100644 --- a/packages/qwik-router/src/middleware/request-handler/request-event.ts +++ b/packages/qwik-router/src/middleware/request-handler/request-event.ts @@ -1,11 +1,12 @@ import type { ValueOrPromise } from '@qwik.dev/core'; 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'; @@ -36,6 +37,9 @@ export const RequestRouteName = '@routeName'; export const RequestEvSharedActionId = '@actionId'; export const RequestEvSharedActionFormData = '@actionFormData'; export const RequestEvSharedNonce = '@nonce'; +export const RequestEvShareServerTiming = '@serverTiming'; +/** @internal */ +export const RequestEvShareQData = 'qData'; export function createRequestEvent( serverRequestEv: ServerRequestEvent, @@ -140,7 +144,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, @@ -264,9 +268,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 75eca7e4dbe..69a604d72be 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 QRL } from '@qwik.dev/core'; import type { Render, RenderToStringResult } from '@qwik.dev/core/server'; -import { QACTION_KEY, QFN_KEY } from '../../runtime/src/constants'; -import type { - ActionInternal, - ClientPageData, - DataValidator, - JSONObject, - LoadedRoute, - LoaderInternal, - PageModule, - RouteModule, - ValidatorReturn, +import { QACTION_KEY, QFN_KEY, QLOADER_KEY } from '../../runtime/src/constants'; +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 { @@ -45,7 +48,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, @@ -58,7 +61,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') @@ -74,7 +77,7 @@ export const resolveRequestHandlers = ( requestHandlers.push(fixTrailingSlash); requestHandlers.push(renderQData); } - const routeModules = route[2]; + const routeModules = route[LoadedRouteProp.Mods]; requestHandlers.push(handleRedirect); _resolveRequestHandlers( routeLoaders, @@ -89,7 +92,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); } } @@ -167,8 +171,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; @@ -218,51 +223,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, @@ -413,7 +458,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}`; } @@ -485,7 +530,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(); @@ -547,8 +592,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 !== null) { + 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), @@ -559,7 +613,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(); } @@ -590,9 +644,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/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/middleware/request-handler/response-page.ts b/packages/qwik-router/src/middleware/request-handler/response-page.ts index 7bc79f51a18..0319962a3e1 100644 --- a/packages/qwik-router/src/middleware/request-handler/response-page.ts +++ b/packages/qwik-router/src/middleware/request-handler/response-page.ts @@ -8,6 +8,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 +31,15 @@ export function getQwikRouterServerData(requestEv: RequestEvent) { reconstructedUrl.protocol = protocol; } + const loaders = getRequestLoaders(requestEv); + return { url: reconstructedUrl.href, requestHeaders, locale: locale(), nonce, containerAttributes: { - 'q:route': routeName, + [Q_ROUTE]: routeName, }, qwikrouter: { routeName, @@ -45,7 +48,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 f5d9842f2cc..f7dfedd8ad3 100644 --- a/packages/qwik-router/src/runtime/src/constants.ts +++ b/packages/qwik-router/src/runtime/src/constants.ts @@ -6,6 +6,10 @@ export const CLIENT_DATA_CACHE = new Map>('qc-s'); +export const RouteStateContext = + /*#__PURE__*/ createContextId>>('qc-s'); export const ContentContext = /*#__PURE__*/ createContextId('qc-c'); export const ContentInternalContext = diff --git a/packages/qwik-router/src/runtime/src/link-component.tsx b/packages/qwik-router/src/runtime/src/link-component.tsx index 979528eadf2..28c34dd4546 100644 --- a/packages/qwik-router/src/runtime/src/link-component.tsx +++ b/packages/qwik-router/src/runtime/src/link-component.tsx @@ -61,7 +61,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/qwik-router-component.tsx b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx index 29f888b564c..15f1a5604b3 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx +++ b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx @@ -19,12 +19,16 @@ import { import { _getContextElement, _getQContainerElement, + _UNINITIALIZED, _waitUntilRendered, - _weakSerialize, + SerializerSymbol, + _serializationWeakRef, type _ElementVNode, + createSignal, + type Signal, } 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 +150,21 @@ export const QwikRouterProvider = component$((props) => { { deep: false } ); const navResolver: { r?: () => void } = {}; - const loaderState = _weakSerialize(useStore(env.response.loaders, { deep: false })); + const loaderState = Object.fromEntries( + Object.entries(env.response.loaders).map(([k, v]) => { + const value = createSignal(v); + return [k, value]; + }) + ); + + (loaderState as any)[SerializerSymbol] = async (o: Record) => { + const resultPs = Object.entries(o).map(async ([k, val]) => { + const v = await val; + return [k, _serializationWeakRef(v)]; + }); + return Object.fromEntries(await Promise.all(resultPs)); + }; + const routeInternal = useSignal({ type: 'initial', dest: url, @@ -279,7 +297,7 @@ export const QwikRouterProvider = component$((props) => { let scroller = document.getElementById(QWIK_ROUTER_SCROLLER); if (!scroller) { scroller = document.getElementById(QWIK_CITY_SCROLLER); - if (scroller) { + if (scroller && isDev) { console.warn( `Please update your scroller ID to "${QWIK_ROUTER_SCROLLER}" as "${QWIK_CITY_SCROLLER}" is deprecated and will be removed in V3` ); @@ -461,12 +479,17 @@ export const QwikRouterProvider = component$((props) => { } const loaders = clientPageData?.loaders; - const win = window as ClientSPAWindow; if (loaders) { - Object.assign(loaderState, loaders); + for (const [key, value] of Object.entries(loaders)) { + const signal = loaderState[key] as Signal | typeof _UNINITIALIZED; + if (signal && signal !== _UNINITIALIZED) { + signal.value = value; + } + } } CLIENT_DATA_CACHE.clear(); + const win = window as ClientSPAWindow; if (!win._qRouterSPA) { // only add event listener once win._qRouterSPA = true; @@ -641,7 +664,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; @@ -699,7 +722,7 @@ export const QwikRouterMockProvider = component$((props) => { deep: false } ); - const loaderState = useSignal({}); + const loaderState = {}; const routeInternal = useSignal({ type: 'initial', dest: url }); const goto: RouteNavigate = 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/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index d065522cd54..ee976c2b324 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -2,24 +2,28 @@ import { $, implicit$FirstArg, noSerialize, - useContext, useStore, type QRL, type ValueOrPromise, + untrack, + isBrowser, + isDev, + isServer, } from '@qwik.dev/core'; import { _deserialize, _getContextElement, _getContextEvent, _serialize, - _wrapStore, + _useInvokeContext, + _UNINITIALIZED, } from '@qwik.dev/core/internal'; 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, @@ -52,10 +56,9 @@ import type { } from './types'; import { useAction, useLocation, useQwikRouterEnv } from './use-functions'; -import { isDev, isServer } from '@qwik.dev/core'; - import type { FormSubmitCompletedDetail } from './form-component'; import { deepFreeze } from './utils'; +import { loadClientData } from './use-endpoint'; /** @internal */ export const routeActionQrl = (( @@ -193,17 +196,29 @@ 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 loaderData = untrack(() => state[id].value); + if (loaderData === _UNINITIALIZED && isBrowser) { + // Request the loader data from the server and throw the Promise + // so the client can load it synchronously. + throw loadClientData(location.url, iCtx.$hostElement$, { + loaderIds: [id], + }).then((clientData) => { + state[id].value = clientData?.loaders[id]; + }); + } + return state[id]; } loader.__brand = 'server_loader' as const; loader.__qrl = loaderQrl; diff --git a/packages/qwik-router/src/runtime/src/types.ts b/packages/qwik-router/src/runtime/src/types.ts index 522673f081e..8ad79a346ec 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; @@ -391,6 +401,7 @@ export type GetValidatorType = export interface CommonLoaderActionOptions { readonly id?: string; readonly validation?: DataValidator[]; + readonly persist?: boolean; } /** @public */ @@ -665,8 +676,6 @@ export type LoaderConstructorQRL = { ): Loader>>>; }; -export type LoaderStateHolder = Record>; - /** @public */ export type ActionReturn = { readonly status?: number; diff --git a/packages/qwik-router/src/runtime/src/use-endpoint.ts b/packages/qwik-router/src/runtime/src/use-endpoint.ts index cd0d26632a7..a429e1b9527 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 c540b61e136..33c727ff875 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,11 +31,19 @@ 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); + if (options?.actionId) { + search += (search ? '&' : '?') + QACTION_KEY + '=' + encodeURIComponent(options.actionId); + } + if (options?.loaderIds) { + for (const loaderId of options.loaderIds) { + search += (search ? '&' : '?') + QLOADER_KEY + '=' + encodeURIComponent(loaderId); + } } return pathname + (pathname.endsWith('/') ? '' : '/') + 'q-data.json' + search; }; 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..497ea2dfb53 100644 --- a/packages/qwik-router/src/static/worker-thread.ts +++ b/packages/qwik-router/src/static/worker-thread.ts @@ -1,6 +1,6 @@ import { _deserialize, _serialize, _verifySerializable } from '@qwik.dev/core/internal'; import type { ServerRequestEvent } from '@qwik.dev/router/middleware/request-handler'; -import { requestHandler } from '@qwik.dev/router/middleware/request-handler'; +import { requestHandler, RequestEvShareQData } from '@qwik.dev/router/middleware/request-handler'; import { WritableStream } from 'node:stream/web'; import { pathToFileURL } from 'node:url'; import type { QwikSerializer } from '../middleware/request-handler/types'; @@ -178,7 +178,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); diff --git a/packages/qwik/src/core/index.ts b/packages/qwik/src/core/index.ts index 921188abe73..50c3e232e4f 100644 --- a/packages/qwik/src/core/index.ts +++ b/packages/qwik/src/core/index.ts @@ -85,8 +85,6 @@ export type { QwikHTMLElements, QwikSVGElements, SVGAttributes, - HTMLElementAttrs, - SVGProps, } from './shared/jsx/types/jsx-generated'; export { render } from './client/dom-render'; export { getDomContainer, _getQContainerElement } from './client/dom-container'; diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index 766b94aa3c1..6825f4b34dd 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -16,7 +16,7 @@ export type { VNodeFlags as _VNodeFlags, } from './client/types'; export { vnode_toString as _vnode_toString } from './client/vnode'; -export { _wrapProp, _wrapSignal, _wrapStore } from './reactive-primitives/internal-api'; +export { _wrapProp, _wrapSignal } from './reactive-primitives/internal-api'; export { SubscriptionData as _SubscriptionData } from './reactive-primitives/subscription-data'; export { _EFFECT_BACK_REF } from './reactive-primitives/types'; export { @@ -37,20 +37,19 @@ export { _deserialize, dumpState as _dumpState, preprocessState as _preprocessState, + _serializationWeakRef, _serialize, } from './shared/shared-serialization'; -export { _CONST_PROPS, _IMMUTABLE, _VAR_PROPS } from './shared/utils/constants'; +export { _CONST_PROPS, _IMMUTABLE, _VAR_PROPS, _UNINITIALIZED } from './shared/utils/constants'; export { EMPTY_ARRAY as _EMPTY_ARRAY } from './shared/utils/flyweight'; export { _restProps } from './shared/utils/prop'; -export { - verifySerializable as _verifySerializable, - _weakSerialize, -} from './shared/utils/serialize-utils'; +export { verifySerializable as _verifySerializable } from './shared/utils/serialize-utils'; export { _walkJSX } from './ssr/ssr-render-jsx'; export { _getContextElement, _getContextEvent, _jsxBranch, + useInvokeContext as _useInvokeContext, _waitUntilRendered, } from './use/use-core'; export { scheduleTask as _task } from './use/use-task'; diff --git a/packages/qwik/src/core/qwik.core.api.md b/packages/qwik/src/core/qwik.core.api.md index 2df8dbce463..c45d6d6f7c9 100644 --- a/packages/qwik/src/core/qwik.core.api.md +++ b/packages/qwik/src/core/qwik.core.api.md @@ -336,13 +336,6 @@ function h, PROPS extends {} = {} export { h as createElement } export { h } -// Warning: (ae-forgotten-export) The symbol "HTMLAttributesBase" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "FilterBase" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export interface HTMLElementAttrs extends HTMLAttributesBase, FilterBase { -} - // @internal @deprecated (undocumented) export const _IMMUTABLE: unique symbol; @@ -637,6 +630,7 @@ export type QwikFocusEvent = NativeFocusEvent; // Warning: (ae-forgotten-export) The symbol "Augmented" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SpecialAttrs" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "HTMLElementAttrs" needs to be exported by the entry point index.d.ts // // @public export type QwikHTMLElements = { @@ -693,6 +687,8 @@ export type QwikPointerEvent = NativePointerEvent; // @public @deprecated (undocumented) export type QwikSubmitEvent = SubmitEvent; +// Warning: (ae-forgotten-export) The symbol "SVGProps" needs to be exported by the entry point index.d.ts +// // @public export type QwikSVGElements = { [K in keyof Omit]: SVGProps; @@ -842,6 +838,11 @@ export const _restProps: (props: PropsProxy, omit: string[], target?: Props) => // @internal export const _run: (...args: unknown[]) => ValueOrPromise_2; +// Warning: (ae-forgotten-export) The symbol "SerializationWeakRef" needs to be exported by the entry point index.d.ts +// +// @internal (undocumented) +export const _serializationWeakRef: (obj: unknown) => SerializationWeakRef; + // @internal export function _serialize(data: unknown[]): Promise; @@ -1559,10 +1560,6 @@ export interface SVGAttributes extends AriaAttribut zoomAndPan?: string | undefined; } -// @public (undocumented) -export interface SVGProps extends SVGAttributes, QwikAttributes { -} - // @public export const sync$: (fn: T) => SyncQRL; @@ -1614,6 +1611,9 @@ export interface Tracker { (obj: T, prop: P): T[P]; } +// @internal (undocumented) +export const _UNINITIALIZED: unique symbol; + // @public export const untrack: (fn: () => T) => T; @@ -1645,6 +1645,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 @@ -1827,9 +1832,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; @@ -1839,9 +1841,6 @@ export const _wrapProp: , P extends keyof T>(...args: // @internal @deprecated (undocumented) export const _wrapSignal: , P extends keyof T>(obj: T, prop: P) => any; -// @internal (undocumented) -export const _wrapStore: , P extends keyof T>(obj: T, prop: P) => Signal; - // (No @packageDocumentation comment for this package) ``` diff --git a/packages/qwik/src/core/reactive-primitives/impl/store.ts b/packages/qwik/src/core/reactive-primitives/impl/store.ts index f5e7202ee38..6b5dbaf64bd 100644 --- a/packages/qwik/src/core/reactive-primitives/impl/store.ts +++ b/packages/qwik/src/core/reactive-primitives/impl/store.ts @@ -1,7 +1,7 @@ import { pad, qwikDebugToString } from '../../debug'; import { assertTrue } from '../../shared/error/assert'; import { tryGetInvokeContext } from '../../use/use-core'; -import { isSerializableObject } from '../../shared/utils/types'; +import { isObject, isSerializableObject } from '../../shared/utils/types'; import type { Container } from '../../shared/types'; import { addQrlToSerializationCtx, @@ -124,8 +124,7 @@ export class StoreHandler implements ProxyHandler { const flags = this.$flags$; if ( flags & StoreFlags.RECURSIVE && - typeof value === 'object' && - value !== null && + isObject(value) && !Object.isFrozen(value) && !isStore(value) && !Object.isFrozen(target) diff --git a/packages/qwik/src/core/reactive-primitives/internal-api.ts b/packages/qwik/src/core/reactive-primitives/internal-api.ts index 17c3bdae903..cf3523aecb8 100644 --- a/packages/qwik/src/core/reactive-primitives/internal-api.ts +++ b/packages/qwik/src/core/reactive-primitives/internal-api.ts @@ -1,10 +1,10 @@ import { _CONST_PROPS, _IMMUTABLE } from '../shared/utils/constants'; import { assertEqual } from '../shared/error/assert'; import { isObject } from '../shared/utils/types'; -import { isSignal, type Signal } from './signal.public'; +import { isSignal } from './signal.public'; import { getStoreTarget } from './impl/store'; import { isPropsProxy } from '../shared/jsx/jsx-runtime'; -import { SignalFlags, WrappedSignalFlags } from './types'; +import { WrappedSignalFlags } from './types'; import { WrappedSignalImpl } from './impl/wrapped-signal-impl'; // Keep these properties named like this so they're the same as from wrapSignal @@ -60,20 +60,6 @@ export const _wrapProp = , P extends keyof T>(...args return getWrapped(args); }; -/** @internal */ -export const _wrapStore = , P extends keyof T>( - obj: T, - prop: P -): Signal => { - const target = getStoreTarget(obj)!; - const value = target[prop]; - if (isSignal(value)) { - return value; - } else { - return new WrappedSignalImpl(null, getProp, [obj, prop], null, SignalFlags.INVALID); - } -}; - /** @internal @deprecated v1 compat */ export const _wrapSignal = , P extends keyof T>( obj: T, diff --git a/packages/qwik/src/core/reactive-primitives/utils.ts b/packages/qwik/src/core/reactive-primitives/utils.ts index fe898ce337c..b93dd0c2f5a 100644 --- a/packages/qwik/src/core/reactive-primitives/utils.ts +++ b/packages/qwik/src/core/reactive-primitives/utils.ts @@ -9,6 +9,7 @@ import type { Container, HostElement } from '../shared/types'; import { ChoreType } from '../shared/util-chore-type'; import { ELEMENT_PROPS, OnRenderProp } from '../shared/utils/markers'; import { SerializerSymbol } from '../shared/utils/serialize-utils'; +import { isObject } from '../shared/utils/types'; import type { ISsrNode, SSRContainer } from '../ssr/ssr-types'; import { TaskFlags, isTask } from '../use/use-task'; import { ComputedSignalImpl } from './impl/computed-signal-impl'; @@ -147,7 +148,5 @@ export const triggerEffects = ( export const isSerializerObj = any }, S>( obj: unknown ): obj is CustomSerializable => { - return ( - typeof obj === 'object' && obj !== null && typeof (obj as any)[SerializerSymbol] === 'function' - ); + return isObject(obj) && typeof (obj as any)[SerializerSymbol] === 'function'; }; diff --git a/packages/qwik/src/core/shared/error/error.ts b/packages/qwik/src/core/shared/error/error.ts index 52386099afa..5f8eb848ff4 100644 --- a/packages/qwik/src/core/shared/error/error.ts +++ b/packages/qwik/src/core/shared/error/error.ts @@ -1,5 +1,6 @@ import { logErrorAndStop } from '../utils/log'; import { qDev } from '../utils/qdev'; +import { isObject } from '../utils/types'; export const codeToText = (code: number, ...parts: any[]): string => { if (qDev) { @@ -61,7 +62,7 @@ export const codeToText = (code: number, ...parts: any[]): string => { if (parts.length) { text = text.replaceAll(/{{(\d+)}}/g, (_, index) => { let v = parts[index]; - if (v && typeof v === 'object' && v.constructor === Object) { + if (v && isObject(v) && v.constructor === Object) { v = JSON.stringify(v).slice(0, 50); } return v; diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index 3909d563550..9065b36d415 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'; @@ -56,13 +56,14 @@ import { SignalImpl } from '../reactive-primitives/impl/signal-impl'; import { ComputedSignalImpl } from '../reactive-primitives/impl/computed-signal-impl'; import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl'; import { SerializerSignalImpl } from '../reactive-primitives/impl/serializer-signal-impl'; +import { isObject } from './utils/types'; const deserializedProxyMap = new WeakMap(); type DeserializerProxy = T & { [SERIALIZER_PROXY_UNWRAP]: object }; export const isDeserializerProxy = (value: unknown): value is DeserializerProxy => { - return typeof value === 'object' && value !== null && SERIALIZER_PROXY_UNWRAP in value; + return isObject(value) && SERIALIZER_PROXY_UNWRAP in value; }; export const SERIALIZER_PROXY_UNWRAP = Symbol('UNWRAP'); @@ -387,7 +388,7 @@ 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]; @@ -409,6 +410,7 @@ export const _constants = [ EMPTY_OBJ, NEEDS_COMPUTATION, STORE_ALL_PROPS, + _UNINITIALIZED, Slot, Fragment, NaN, @@ -428,6 +430,7 @@ const _constantNames = [ 'EMPTY_OBJ', 'NEEDS_COMPUTATION', 'STORE_ALL_PROPS', + '_UNINITIALIZED', 'Slot', 'Fragment', 'NaN', @@ -450,7 +453,12 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow if (!container.$forwardRefs$) { throw qError(QError.serializeErrorCannotAllocate, ['forward ref']); } - return container.$getObjectById$(container.$forwardRefs$[value as number]); + const rootRef = container.$forwardRefs$[value as number]; + if (rootRef === -1) { + return _UNINITIALIZED; + } else { + return container.$getObjectById$(rootRef); + } case TypeIds.ForwardRefs: return value; case TypeIds.Constant: @@ -546,9 +554,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]); } @@ -613,8 +620,13 @@ type DomRef = { }; type SeenRef = { - $parent$: unknown | null; + // Parent used for RootRef path calculation (null for roots) + $refParent$: unknown | null; + // The actual parent in the object graph, roots can also have a structural parent + $structuralParents$: (unknown | null)[]; + // Index within $structuralParent$ $index$: number; + // Index in the roots array (-1 if not a root) $rootIndex$: number; }; @@ -649,20 +661,11 @@ export interface SerializationContext { */ $addRoot$: (obj: unknown, parent?: unknown) => number; - /** - * Get root path of the object without creating a new root. - * - * This is used during serialization, as new roots can't be created during serialization. - * - * The function throws if the root was not found. - */ - $addRootPath$: (obj: any) => string | number; - $seen$: (obj: unknown, parent: unknown | null, index: number) => void; $roots$: unknown[]; - $pathMap$: Map; - + $objectPathStringCache$: Map; + $outputRootRefs$: boolean; $addSyncFn$($funcStr$: string | null, argsCount: number, fn: Function): number; $isSsrNode$: (obj: unknown) => obj is SsrNode; @@ -712,39 +715,53 @@ export const createSerializationContext = ( } as StreamWriter; } const seenObjsMap = new Map(); - const rootsPathMap = new Map(); + const objectPathStringCache = new Map(); const syncFnMap = new Map(); const syncFns: string[] = []; const roots: unknown[] = []; const $wasSeen$ = (obj: unknown) => seenObjsMap.get(obj); const $seen$ = (obj: unknown, parent: unknown | null, index: number) => { - return seenObjsMap.set(obj, { $parent$: parent, $index$: index, $rootIndex$: -1 }); + return seenObjsMap.set(obj, { + $refParent$: parent, + $structuralParents$: parent ? [parent] : [], // Initialize as array + $index$: index, + $rootIndex$: -1, + }); }; - const $addRootPath$ = (obj: unknown) => { - const rootPath = rootsPathMap.get(obj); - if (rootPath) { - return rootPath; - } - const seen = seenObjsMap.get(obj); - if (!seen) { - throw qError(QError.serializeErrorMissingRootId, [obj]); - } + const $getObjectIndexPath$ = ( + seen: SeenRef, + getParent: (seen: SeenRef) => unknown | null + ): number[] => { const path = []; let current: typeof seen | undefined = seen; // Traverse up through parent references to build a path while (current && current.$index$ >= 0) { path.unshift(current.$index$); - if (typeof current.$parent$ !== 'object' || current.$parent$ === null) { + const parent = getParent(current); + if (typeof parent !== 'object' || parent === null) { break; } - current = seenObjsMap.get(current.$parent$); + current = seenObjsMap.get(parent); } + return path; + }; + + const $getObjectPathString$ = (obj: unknown) => { + const rootPath = objectPathStringCache.get(obj); + if (rootPath) { + return rootPath; + } + const seen = seenObjsMap.get(obj); + if (!seen) { + throw qError(QError.serializeErrorMissingRootId, [obj]); + } + const path = $getObjectIndexPath$(seen, (seen) => seen.$refParent$); const pathStr = path.length > 1 ? path.join(' ') : path.length ? path[0] : seen.$index$; - rootsPathMap.set(obj, pathStr); + objectPathStringCache.set(obj, pathStr); return pathStr; }; @@ -752,14 +769,23 @@ export const createSerializationContext = ( let seen = seenObjsMap.get(obj); if (!seen) { const rootIndex = roots.length; - seen = { $parent$: parent, $index$: rootIndex, $rootIndex$: rootIndex }; + seen = { + $refParent$: parent, + $structuralParents$: parent ? [parent] : [], + $index$: rootIndex, + $rootIndex$: rootIndex, + }; seenObjsMap.set(obj, seen); roots.push(obj); } else if (seen.$rootIndex$ === -1) { seen.$rootIndex$ = roots.length; roots.push(obj); } - $addRootPath$(obj); + // If a parent is provided and not already in the list, add it. + if (parent !== null && !seen.$structuralParents$.includes(parent)) { + seen.$structuralParents$.push(parent); + } + $getObjectPathString$(obj); return seen.$rootIndex$; }; @@ -779,13 +805,14 @@ export const createSerializationContext = ( $symbolToChunkResolver$: symbolToChunkResolver, $wasSeen$, $roots$: roots, + $objectPathStringCache$: objectPathStringCache, + $outputRootRefs$: true, $seen$, $hasRootId$: (obj: any) => { const id = seenObjsMap.get(obj); - return id?.$parent$ === null ? id.$index$ : undefined; + return id?.$refParent$ === null ? id.$index$ : undefined; }, $addRoot$, - $addRootPath$, $syncFns$: syncFns, $addSyncFn$: (funcStr: string | null, argCount: number, fn: Function) => { const isFullFn = funcStr == null; @@ -817,7 +844,6 @@ export const createSerializationContext = ( $getProp$: getProp, $setProp$: setProp, $prepVNodeData$: prepVNodeData, - $pathMap$: rootsPathMap, }; }; @@ -848,7 +874,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); @@ -869,6 +895,14 @@ class PromiseResult { public $qrl$: QRLInternal | null = null ) {} } + +class SerializationWeakRef { + constructor(public $obj$: unknown) {} +} + +/** @internal */ +export const _serializationWeakRef = (obj: unknown) => new SerializationWeakRef(obj); + /** * Format: * @@ -879,13 +913,22 @@ class PromiseResult { * - Therefore root indexes need to be doubled to get the actual index. */ async function serialize(serializationContext: SerializationContext): Promise { - const { $writer$, $isSsrNode$, $isDomRef$, $storeProxyMap$, $addRoot$, $pathMap$, $wasSeen$ } = - serializationContext; + const { + $writer$, + $isSsrNode$, + $isDomRef$, + $storeProxyMap$, + $addRoot$, + $objectPathStringCache$, + $wasSeen$, + } = serializationContext; let depth = 0; + let rootIdx = 0; const forwardRefs: number[] = []; let forwardRefsId = 0; const promises: Set> = new Set(); const preloadQrls = new Set(); + const s11nWeakRefs = new Map(); let parent: unknown = null; const isRootObject = () => depth === 0; @@ -930,19 +973,42 @@ async function serialize(serializationContext: SerializationContext): Promise { preloadQrls.add(qrl); - serializationContext.$addRoot$(qrl, null); + serializationContext.$addRoot$(qrl); }; - const outputRootRef = (value: unknown, elseCallback: () => void) => { + const outputAsRootRef = (value: unknown, rootDepth = 0): boolean => { const seen = $wasSeen$(value); - const rootRefPath = $pathMap$.get(value); - if (isRootObject() && seen && seen.$parent$ !== null && rootRefPath) { + const rootRefPath = $objectPathStringCache$.get(value); + + // Objects are the only way to create circular dependencies. + // So the first thing to to is to see if we have a circular dependency. + // (NOTE: For root objects we need to serialize them regardless if we have seen + // them before, otherwise the root object reference will point to itself.) + // Also note that depth will be 1 for objects in root + if (rootDepth === depth && seen && seen.$refParent$ !== null && rootRefPath) { output(TypeIds.RootRef, rootRefPath); - } else if (depth > 0 && seen && seen.$rootIndex$ !== -1) { + return true; + } else if (depth > rootDepth && seen && seen.$rootIndex$ !== -1) { + // We have seen this object before, so we can serialize it as a reference. + // Otherwise serialize as normal output(TypeIds.RootRef, seen.$rootIndex$); - } else { - elseCallback(); + return true; + } else if (s11nWeakRefs.has(value)) { + const forwardRefId = s11nWeakRefs.get(value)!; + // We see the object again, we must now make it a root and update the forward ref + if (rootDepth === depth) { + // It's already a root + forwardRefs[forwardRefId] = rootIdx; + } else { + // ref + const rootRef = $addRoot$(value); + output(TypeIds.RootRef, rootRef); + forwardRefs[forwardRefId] = rootRef; + return true; + } } + + return false; }; const writeValue = (value: unknown) => { @@ -958,7 +1024,7 @@ async function serialize(serializationContext: SerializationContext): Promise { + if (!outputAsRootRef(value)) { const qrl = qrlToString(serializationContext, value); const type = preloadQrls.has(value) ? TypeIds.PreloadQRL : TypeIds.QRL; if (isRootObject()) { @@ -967,7 +1033,7 @@ async function serialize(serializationContext: SerializationContext): Promise { + if (!outputAsRootRef(value)) { output(TypeIds.String, value); - }); + } } } else if (typeof value === 'undefined') { output(TypeIds.Constant, Constants.Undefined); @@ -1023,6 +1089,8 @@ async function serialize(serializationContext: SerializationContext): Promise 1) { - const seen = $wasSeen$(value); - if (seen && seen.$rootIndex$ !== -1) { - // We have seen this object before, so we can serialize it as a reference. - // Otherwise serialize as normal - output(TypeIds.RootRef, seen.$rootIndex$); - return; - } + if (outputAsRootRef(value, 1)) { + return; } + // handle custom serializers + // add to the seen map + if (isPropsProxy(value)) { const varProps = value[_VAR_PROPS]; const constProps = value[_CONST_PROPS]; @@ -1065,7 +1117,7 @@ async function serialize(serializationContext: SerializationContext): Promise { return new PromiseResult(TypeIds.SerializerSignal, resolved, resolvedValue, null, null); @@ -1286,6 +1338,11 @@ async function serialize(serializationContext: SerializationContext): Promise { $writer$.write('['); - let lastRootsLength = 0; let rootsLength = serializationContext.$roots$.length; - while (lastRootsLength < rootsLength || promises.size) { - if (lastRootsLength !== 0) { + while (rootIdx < rootsLength || promises.size) { + if (rootIdx !== 0) { $writer$.write(','); } let separator = false; - for (let i = lastRootsLength; i < rootsLength; i++) { + for (; rootIdx < rootsLength; rootIdx++) { if (separator) { $writer$.write(','); } else { separator = true; } - writeValue(serializationContext.$roots$[i]); + writeValue(serializationContext.$roots$[rootIdx]); } if (promises.size) { @@ -1342,7 +1398,6 @@ async function serialize(serializationContext: SerializationContext): Promise(value: object): value is ResourceReturnInternal const frameworkType = (obj: any) => { return ( - (typeof obj === 'object' && - obj !== null && - (obj instanceof SignalImpl || obj instanceof Task || isJSXNode(obj))) || + (isObject(obj) && (obj instanceof SignalImpl || obj instanceof Task || isJSXNode(obj))) || isQrl(obj) ); }; @@ -1845,7 +1898,7 @@ export const enum TypeIds { FormData, JSXNode, PropsProxy, - EffectData, + SubscriptionData, } export const _typeIdNames = [ 'RootRef', @@ -1882,7 +1935,7 @@ export const _typeIdNames = [ 'FormData', 'JSXNode', 'PropsProxy', - 'EffectData', + 'SubscriptionData', ]; export const enum Constants { @@ -1895,6 +1948,7 @@ export const enum Constants { EMPTY_OBJ, NEEDS_COMPUTATION, STORE_ALL_PROPS, + UNINITIALIZED, Slot, Fragment, NaN, @@ -1910,8 +1964,8 @@ const circularProofJson = (obj: unknown, indent?: string | number) => { const seen = new WeakSet(); return JSON.stringify( obj, - (key, value) => { - if (typeof value === 'object' && value !== null) { + (_, value) => { + if (isObject(value)) { if (seen.has(value)) { return `[Circular ${value.constructor.name}]`; } @@ -1956,7 +2010,7 @@ export const dumpState = ( if (key === undefined) { hasRaw = true; out.push( - `${RED}[raw${typeof value === 'object' && value ? ` ${value.constructor.name}` : ''}]${RESET} ${printRaw(value, `${prefix} `)}` + `${RED}[raw${isObject(value) ? ` ${value.constructor.name}` : ''}]${RESET} ${printRaw(value, `${prefix} `)}` ); } else { if (key === TypeIds.Constant) { @@ -1987,6 +2041,6 @@ export const typeIdToName = (code: TypeIds) => { return _typeIdNames[code] || `Unknown(${code})`; }; -const constantToName = (code: Constants) => { +export const constantToName = (code: Constants) => { return _constantNames[code] || `Unknown(${code})`; }; diff --git a/packages/qwik/src/core/shared/shared-serialization.unit.ts b/packages/qwik/src/core/shared/shared-serialization.unit.ts index bf3b6822f0b..7be7972b00c 100644 --- a/packages/qwik/src/core/shared/shared-serialization.unit.ts +++ b/packages/qwik/src/core/shared/shared-serialization.unit.ts @@ -1,6 +1,6 @@ import { $, componentQrl, noSerialize } from '@qwik.dev/core'; import { describe, expect, it, vi } from 'vitest'; -import { _fnSignal, _wrapProp } from '../internal'; +import { _fnSignal, _serializationWeakRef, _UNINITIALIZED, _wrapProp } from '../internal'; import { type SignalImpl } from '../reactive-primitives/impl/signal-impl'; import { createComputedQrl, @@ -14,10 +14,12 @@ import { Task } from '../use/use-task'; import { inlinedQrl } from './qrl/qrl'; import { createQRL, type QRLInternal } from './qrl/qrl-class'; import { + Constants, TypeIds, _constants, _createDeserializeContainer, _typeIdNames, + constantToName, createSerializationContext, dumpState, } from './shared-serialization'; @@ -25,7 +27,7 @@ import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight'; import { isQrl } from './qrl/qrl-utils'; import { NoSerializeSymbol, SerializerSymbol } from './utils/serialize-utils'; import { SubscriptionData } from '../reactive-primitives/subscription-data'; -import { StoreFlags } from '../reactive-primitives/types'; +import { StoreFlags, type CustomSerializable } from '../reactive-primitives/types'; const DEBUG = false; @@ -65,17 +67,140 @@ describe('shared-serialization', () => { 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)" `); }); + describe(constantToName(Constants.UNINITIALIZED), () => { + it('should not serialize object', async () => { + const parent = { + child: { should: 'serialize' }, + }; + + (parent as any)[SerializerSymbol] = () => ({ + child: _serializationWeakRef(parent.child), + }); + + expect(await dump(parent)).toMatchInlineSnapshot(` + " + 0 Object [ + String "child" + ForwardRef 0 + ] + 1 ForwardRefs [ + -1 + ] + (27 chars)" + `); + }); + it('should serialize object before qrl', async () => { + const parent = { + child: { should: 'serialize' }, + }; + + (parent as any)[SerializerSymbol] = () => ({ + child: _serializationWeakRef(parent.child), + }); + + const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent.child]); + expect(await dump(parent, qrl)).toMatchInlineSnapshot(` + " + 0 Object [ + String "child" + ForwardRef 0 + ] + 1 QRL "mock-chunk#dump_qrl[2]" + 2 Object [ + String "should" + String "serialize" + ] + 3 ForwardRefs [ + 2 + ] + (84 chars)" + `); + }); + it('should serialize object after qrl', async () => { + const parent = { + child: { should: 'serialize' }, + }; + + (parent as any)[SerializerSymbol] = () => ({ + child: _serializationWeakRef(parent.child), + }); + + const qrl = inlinedQrl(() => parent.child.should, 'dump_qrl', [parent.child]); + expect(await dump(qrl, parent)).toMatchInlineSnapshot(` + " + 0 QRL "mock-chunk#dump_qrl[2]" + 1 Object [ + String "child" + ForwardRef 0 + ] + 2 Object [ + String "should" + String "serialize" + ] + 3 ForwardRefs [ + 2 + ] + (84 chars)" + `); + }); + + // it.only('should serialize wrapped signal args', async () => { + // const parent = createStore( + // null, + // { + // child: { should: 'serialize' }, + // }, + // StoreFlags.RECURSIVE + // ); + + // (parent as any)[SerializerSymbol] = () => ({ + // child: _serializationWeakRef(parent.child), + // }); + + // const wrappedSignal = _wrapStore(parent, 'child'); + // expect(await dump(parent, wrappedSignal)).toMatchInlineSnapshot(` + // " + // 0 Store [ + // Object [ + // String "child" + // ForwardRef 0 + // ] + // Number 1 + // ] + // 1 WrappedSignal [ + // Number 0 + // Array [ + // RootRef 0 + // RootRef 2 + // ] + // Constant null + // Number 1 + // Constant null + // ] + // 2 String "child" + // 3 Object [ + // String "should" + // String "serialize" + // ] + // 4 ForwardRefs [ + // 3 + // ] + // (79 chars)" + // `); + // }); + }); it(title(TypeIds.Number), async () => { expect(await dump(123)).toMatchInlineSnapshot(` " @@ -546,11 +671,11 @@ 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 () => { expect(await dump(new SubscriptionData({ $isConst$: true, $scopedStyleIdPrefix$: null }))) .toMatchInlineSnapshot(` " - 0 EffectData [ + 0 SubscriptionData [ Constant null Constant true ] @@ -630,7 +755,7 @@ describe('shared-serialization', () => { it.todo(title(TypeIds.VNode)); it(title(TypeIds.BigInt), async () => { const objs = await serialize(BigInt('12345678901234567890')); - const bi = deserialize(objs)[0] as BigInt; + const bi = deserialize(objs)[0] as bigint; expect(bi).toBeTypeOf('bigint'); expect(bi.toString()).toBe('12345678901234567890'); }); @@ -751,7 +876,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 +886,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 }) ); @@ -769,6 +894,39 @@ describe('shared-serialization', () => { expect(effect).toBeInstanceOf(SubscriptionData); expect(effect.data).toEqual({ $isConst$: true, $scopedStyleIdPrefix$: null }); }); + + describe('UNINITIALIZED', () => { + it(title(TypeIds.Constant) + ' - UNINITIALIZED, not serialized object', async () => { + const uninitializedObject = { + shouldNot: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + + const objs = await serialize(uninitializedObject); + const effect = deserialize(objs)[0] as any; + expect(effect).toBe(_UNINITIALIZED); + }); + it(title(TypeIds.Constant) + ' - UNINITIALIZED, serialized object', async () => { + const uninitializedObject = { + should: 'serialize', + }; + (uninitializedObject as unknown as CustomSerializable)[SerializerSymbol] = () => { + return _UNINITIALIZED; + }; + const qrl = inlinedQrl(() => uninitializedObject.should, 'dump_qrl', [uninitializedObject]); + const objs = await serialize(uninitializedObject, qrl); + const state = deserialize(objs); + delete (uninitializedObject as any)[SerializerSymbol]; + const deserializedObject = state[0]; + expect(deserializedObject).toEqual(uninitializedObject); + + const deserializedQrl = state[1] as QRLInternal; + expect(isQrl(deserializedQrl)).toBeTruthy(); + expect(await (deserializedQrl.getFn() as any)()).toBe(uninitializedObject.should); + }); + }); }); describe('special cases', () => { @@ -1068,7 +1226,6 @@ async function serialize(...roots: any[]): Promise { } await sCtx.$serialize$(); const objs = JSON.parse(sCtx.$writer$.toString()); - // eslint-disable-next-line no-console DEBUG && console.log(objs); return objs; } diff --git a/packages/qwik/src/core/shared/utils/constants.ts b/packages/qwik/src/core/shared/utils/constants.ts index 8414be910bc..1dac1773baf 100644 --- a/packages/qwik/src/core/shared/utils/constants.ts +++ b/packages/qwik/src/core/shared/utils/constants.ts @@ -5,3 +5,6 @@ export const _VAR_PROPS = Symbol('VAR'); /** @internal @deprecated v1 compat */ export const _IMMUTABLE = Symbol('IMMUTABLE'); + +/** @internal */ +export const _UNINITIALIZED = Symbol('UNINITIALIZED'); diff --git a/packages/qwik/src/core/shared/utils/serialize-utils.ts b/packages/qwik/src/core/shared/utils/serialize-utils.ts index dbb309431a1..621a25b8d4a 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)) { @@ -100,11 +99,7 @@ export const shouldSerialize = (obj: unknown): boolean => { }; 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); + return obj && isObject(obj) && (NoSerializeSymbol in obj || noSerializeSet.has(obj)); }; /** @@ -142,12 +137,6 @@ export const noSerialize = (input: T): NoSerialize return input as any; }; -/** @internal */ -export const _weakSerialize = (input: T): Partial => { - weakSerializeSet.add(input); - return input as any; -}; - /** * 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/packages/qwik/src/core/shared/utils/types.ts b/packages/qwik/src/core/shared/utils/types.ts index 20567d85822..c3114840e9a 100644 --- a/packages/qwik/src/core/shared/utils/types.ts +++ b/packages/qwik/src/core/shared/utils/types.ts @@ -9,7 +9,7 @@ export const isSerializableObject = (v: unknown): v is Record = }; export const isObject = (v: unknown): v is object => { - return !!v && typeof v === 'object'; + return typeof v === 'object' && v !== null; }; export const isArray = (v: unknown): v is unknown[] => { diff --git a/packages/qwik/src/core/use/use-core.ts b/packages/qwik/src/core/use/use-core.ts index bcb1721ef9e..bba4d8af006 100644 --- a/packages/qwik/src/core/use/use-core.ts +++ b/packages/qwik/src/core/use/use-core.ts @@ -5,7 +5,7 @@ import type { QRLInternal } from '../shared/qrl/qrl-class'; import type { QRL } from '../shared/qrl/qrl.public'; import { ComputedEvent, RenderEvent, ResourceEvent, TaskEvent } from '../shared/utils/markers'; import { seal } from '../shared/utils/qdev'; -import { isArray } from '../shared/utils/types'; +import { isArray, isObject } from '../shared/utils/types'; import { setLocale } from './use-locale'; import type { Container, HostElement } from '../shared/types'; import { vnode_getNode, vnode_isElementVNode, vnode_isVNode, vnode_locate } from '../client/vnode'; @@ -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) { @@ -159,7 +159,7 @@ export const newInvokeContext = ( ): InvokeContext => { // ServerRequestEvent has .locale, but it's not always defined. const $locale$ = - locale || (typeof event === 'object' && event && 'locale' in event ? event.locale : undefined); + locale || (event && isObject(event) && 'locale' in event ? event.locale : undefined); const ctx: InvokeContext = { $url$: url, $i$: 0, diff --git a/scripts/qwik-router.ts b/scripts/qwik-router.ts index 58ad6c1ea5b..1b9bc208f90 100644 --- a/scripts/qwik-router.ts +++ b/scripts/qwik-router.ts @@ -82,6 +82,7 @@ async function buildVite(config: BuildConfig) { 'typescript', 'vite-imagetools', 'svgo', + '@qwik.dev/core', ]; const swRegisterPath = join(config.srcQwikRouterDir, 'runtime', 'src', 'sw-register.ts'); @@ -102,7 +103,6 @@ async function buildVite(config: BuildConfig) { format: 'esm', external, alias: { - '@qwik.dev/core': 'noop', '@qwik.dev/core/optimizer': 'noop', }, plugins: [serviceWorkerRegisterBuild(swRegisterCode)], 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..6c51d241e94 --- /dev/null +++ b/starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx @@ -0,0 +1,30 @@ +import { component$, useSignal } from "@qwik.dev/core"; +import { routeLoader$ } from "@qwik.dev/router"; + +export const useTestLoader = routeLoader$(async () => { + return { test: "some test value", abcd: "should not serialize this" }; +}); + +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}
+
{testSignal.value.abcd}
+ + ); +}); diff --git a/starters/e2e/qwikrouter/nav.e2e.ts b/starters/e2e/qwikrouter/nav.e2e.ts index 044e4f01052..f3fc15726f2 100644 --- a/starters/e2e/qwikrouter/nav.e2e.ts +++ b/starters/e2e/qwikrouter/nav.e2e.ts @@ -10,7 +10,7 @@ import { scrollTo, } from "./util.js"; -test.describe("actions", () => { +test.describe("nav", () => { test.describe("mpa", () => { test.use({ javaScriptEnabled: false }); tests();