diff --git a/deno.json b/deno.json index 194d62d32..22179c096 100644 --- a/deno.json +++ b/deno.json @@ -9,7 +9,7 @@ "std/": "https://deno.land/std@0.204.0/", "partytown/": "https://deno.land/x/partytown@0.4.8/", "deco-sites/std/": "https://denopkg.com/deco-sites/std@1.26.7/", - "deco/": "https://cdn.jsdelivr.net/gh/deco-cx/deco@1.78.0/" + "deco/": "https://cdn.jsdelivr.net/gh/deco-cx/deco@30aa5339af59f53d5452b29eebde196a1df66a16/" }, "lock": false, "tasks": { diff --git a/htmx/sections/Deferred.tsx b/htmx/sections/Deferred.tsx index ce6379477..0c3401765 100644 --- a/htmx/sections/Deferred.tsx +++ b/htmx/sections/Deferred.tsx @@ -2,7 +2,7 @@ import type { Section } from "deco/blocks/section.ts"; import { useSection } from "deco/hooks/useSection.ts"; import { asResolved, isDeferred } from "deco/mod.ts"; import { AppContext } from "../mod.ts"; -import { renderSection, shouldForceRender } from "../../utils/deferred.tsx"; +import { shouldForceRender } from "../../utils/deferred.ts"; /** * @titleBy type @@ -10,7 +10,8 @@ import { renderSection, shouldForceRender } from "../../utils/deferred.tsx"; */ interface Load { type: "load"; - /** @hide true */ + + // TODO: @gimenes add delay delay?: number; } @@ -34,19 +35,18 @@ interface Intersect { export interface Props { sections: Section[]; - /** @hide true */ - fallbacks?: Section[]; + fallback?: Section; trigger?: Load | Revealed | Intersect; loading?: "lazy" | "eager"; } const Deferred = (props: Props) => { - const { sections, loading, trigger } = props; + const { sections, loading, trigger, fallback } = props; if (loading === "eager") { return ( <> - {sections.map(renderSection)} + {sections.map(({ Component, props }) => )} ); } @@ -61,16 +61,15 @@ const Deferred = (props: Props) => { } return ( - <> -
- {props.fallbacks?.map(renderSection)} - +
+ {fallback && } +
); }; diff --git a/utils/deferred.tsx b/utils/deferred.ts similarity index 51% rename from utils/deferred.tsx rename to utils/deferred.ts index dd10cb224..2a555cfb6 100644 --- a/utils/deferred.tsx +++ b/utils/deferred.ts @@ -1,10 +1,5 @@ -import { Section } from "deco/mod.ts"; -import { __DECO_FBT } from "../website/handlers/fresh.ts"; +export const __DECO_FBT = "__decoFBT"; export const shouldForceRender = ( { ctx, searchParams }: { ctx: Ctx; searchParams: URLSearchParams }, ): boolean => ctx.isBot || searchParams.get(__DECO_FBT) === "0"; - -export const renderSection = ({ Component, props }: Section) => ( - -); diff --git a/website/handlers/fresh.ts b/website/handlers/fresh.ts index e6f2f89e0..8be8814cb 100644 --- a/website/handlers/fresh.ts +++ b/website/handlers/fresh.ts @@ -10,10 +10,9 @@ import { DecoState } from "deco/types.ts"; import { allowCorsFor } from "deco/utils/http.ts"; import { getSetCookies } from "std/http/cookie.ts"; import { ConnInfo } from "std/http/server.ts"; +import { __DECO_FBT } from "../../utils/deferred.ts"; import { AppContext } from "../mod.ts"; -export const __DECO_FBT = "__decoFBT"; - /** * @title Fresh Config */ diff --git a/website/manifest.gen.ts b/website/manifest.gen.ts index bc05df6cd..0432f8631 100644 --- a/website/manifest.gen.ts +++ b/website/manifest.gen.ts @@ -48,9 +48,10 @@ import * as $$$$$$$14 from "./matchers/userAgent.ts"; import * as $$$$$0 from "./pages/Page.tsx"; import * as $$$$$$0 from "./sections/Analytics/Analytics.tsx"; import * as $$$$$$1 from "./sections/Rendering/Deferred.tsx"; -import * as $$$$$$2 from "./sections/Rendering/SingleDeferred.tsx"; -import * as $$$$$$3 from "./sections/Seo/Seo.tsx"; -import * as $$$$$$4 from "./sections/Seo/SeoV2.tsx"; +import * as $$$$$$2 from "./sections/Rendering/Lazy.tsx"; +import * as $$$$$$3 from "./sections/Rendering/SingleDeferred.tsx"; +import * as $$$$$$4 from "./sections/Seo/Seo.tsx"; +import * as $$$$$$5 from "./sections/Seo/SeoV2.tsx"; const manifest = { "functions": { @@ -84,9 +85,10 @@ const manifest = { "sections": { "website/sections/Analytics/Analytics.tsx": $$$$$$0, "website/sections/Rendering/Deferred.tsx": $$$$$$1, - "website/sections/Rendering/SingleDeferred.tsx": $$$$$$2, - "website/sections/Seo/Seo.tsx": $$$$$$3, - "website/sections/Seo/SeoV2.tsx": $$$$$$4, + "website/sections/Rendering/Lazy.tsx": $$$$$$2, + "website/sections/Rendering/SingleDeferred.tsx": $$$$$$3, + "website/sections/Seo/Seo.tsx": $$$$$$4, + "website/sections/Seo/SeoV2.tsx": $$$$$$5, }, "matchers": { "website/matchers/always.ts": $$$$$$$0, diff --git a/website/mod.ts b/website/mod.ts index a7985fbd3..73a9cb542 100644 --- a/website/mod.ts +++ b/website/mod.ts @@ -107,8 +107,9 @@ export interface Props { caching?: Caching; /** - * @title Async Rendering - * @description Async sections will be deferred to the client-side + * @title Global Async Rendering (Deprecated) + * @description Please disable this setting and enable each section individually. More info at https://deco.cx/en/blog/async-render-default + * @deprecated true * @default false */ firstByteThresholdMS?: boolean; diff --git a/website/sections/Rendering/Deferred.tsx b/website/sections/Rendering/Deferred.tsx index 71e4a8b67..fb55b2d9f 100644 --- a/website/sections/Rendering/Deferred.tsx +++ b/website/sections/Rendering/Deferred.tsx @@ -4,7 +4,7 @@ import { useScriptAsDataURI } from "deco/hooks/useScript.ts"; import { asResolved, isDeferred } from "deco/mod.ts"; import { useId } from "preact/hooks"; import { AppContext } from "../../mod.ts"; -import { renderSection, shouldForceRender } from "../../../utils/deferred.tsx"; +import { shouldForceRender } from "../../../utils/deferred.ts"; /** @titleBy type */ export interface Scroll { @@ -43,8 +43,6 @@ export interface Props { sections: Section[]; display?: boolean; behavior?: Scroll | Intersection | Load; - /** @hide true */ - fallbacks?: Section[]; } const script = ( @@ -103,7 +101,7 @@ const Deferred = (props: Props) => { if (display) { return ( <> - {sections.map(renderSection)} + {sections.map(({ Component, props }) => )} ); } @@ -125,7 +123,6 @@ const Deferred = (props: Props) => { behavior?.payload.toString() || "", )} /> - {props.fallbacks?.map(renderSection)} ); }; diff --git a/website/sections/Rendering/Lazy.tsx b/website/sections/Rendering/Lazy.tsx new file mode 100644 index 000000000..b2d35559b --- /dev/null +++ b/website/sections/Rendering/Lazy.tsx @@ -0,0 +1,145 @@ +import type { Section } from "deco/blocks/section.ts"; +import { SectionContext } from "deco/components/section.tsx"; +import { asResolved, context } from "deco/mod.ts"; +import { useContext } from "preact/hooks"; +import { shouldForceRender } from "../../../utils/deferred.ts"; +import type { AppContext } from "../../mod.ts"; + +const useSectionContext = () => useContext(SectionContext); + +interface Props { + /** @label hidden */ + section: Section; + + /** + * @description htmx/Deferred.tsx prop + * @hide true + */ + loading?: "eager" | "lazy"; +} + +const defaultFallbackFor = (section: string) => () => ( +
+ {!context.isDeploy && ( + <> +
+ Async Rendering not implemented for section{" "} + {section} +
+
+ If you are a developer, export a{" "} + LoadingFallback{" "} + component in this section. To learn more, check out our{" "} + + guide + +
+
+ If you are NOT a developer, you can tweak Async Rendering. To learn + more, check out our{" "} + + blog post + +
+ + )} +
+); + +export const loader = async (props: Props, req: Request, ctx: AppContext) => { + const { section, loading } = props; + const url = new URL(req.url); + + const shouldRender = loading === "eager" || shouldForceRender({ + ctx, + searchParams: url.searchParams, + }); + + if (shouldRender) { + return { + loading: "eager", + section: await ctx.get
(section), + }; + } + + const resolvingMatchers: Record = {}; + const resolvedSection = await ctx.get
(section, { + propagateOptions: true, + hooks: { + onPropsResolveStart: ( + resolve, + _props, + resolver, + _resolveType, + ctx, + ) => { + if (resolvingMatchers[ctx.resolveId]) { + return resolve(); + } + if (resolver?.type === "matchers") { // matchers should not have a timeout. + const id = crypto.randomUUID(); + resolvingMatchers[id] = true; + return resolve(id); + } + if (resolver?.type === "loaders") { + // deno-lint-ignore no-explicit-any + return undefined as any; + } + return resolve(); + }, + }, + }); + + return { + loading: shouldRender ? "eager" : "lazy", + section: { + Component: resolvedSection.LoadingFallback ?? + defaultFallbackFor(resolvedSection.metadata?.component ?? "unknown"), + props: {}, + }, + }; +}; + +type SectionProps = Awaited>; + +function Lazy({ section, loading }: SectionProps) { + const ctx = useSectionContext(); + + if (!ctx) { + throw new Error("Missing SectionContext"); + } + + if (loading === "lazy") { + return ( + + + + ); + } + + return ; +} + +export const onBeforeResolveProps = (props: Props) => ({ + ...props, + section: asResolved(props.section), +}); + +export default Lazy; diff --git a/website/sections/Rendering/SingleDeferred.tsx b/website/sections/Rendering/SingleDeferred.tsx index 399e574af..94dc9152f 100644 --- a/website/sections/Rendering/SingleDeferred.tsx +++ b/website/sections/Rendering/SingleDeferred.tsx @@ -1,107 +1,2 @@ -import type { Section } from "deco/blocks/section.ts"; -import { SectionContext } from "deco/components/section.tsx"; -import { RequestContext } from "deco/deco.ts"; -import { asResolved, isDeferred } from "deco/mod.ts"; -import { useContext } from "preact/hooks"; -import HTMXDeferred from "../../../htmx/sections/Deferred.tsx"; -import { shouldForceRender } from "../../../utils/deferred.tsx"; -import type { AppContext } from "../../mod.ts"; -import PartialDeferred from "./Deferred.tsx"; - -const isHtmx = ( - props: SectionProps, -): props is SectionProps & { framework: "htmx" } => props.framework === "htmx"; - -interface Props { - /** @label hidden */ - section: Section; - /** - * @description fresh/Deferred.tsx props - * @hide true - */ - display?: boolean; - /** - * @description htmx/Deferred.tsx prop - * @hide true - */ - loading?: "eager"; -} - -export const loader = async ( - props: Props, - _req: Request, - ctx: AppContext, -) => { - const isFreshSyncRender = "display" in props && props.display === true; - const isHtmxSyncRender = "loading" in props && props.loading === "eager"; - const url = new URL(_req.url); - const _shouldForceRender = shouldForceRender({ - ctx, - searchParams: url.searchParams, - }); - const shouldRender = isFreshSyncRender || isHtmxSyncRender || - _shouldForceRender; - const framework = ctx.flavor?.framework || "fresh"; - - if (shouldRender) { - const section = isDeferred
(props.section) - ? await props.section() - : props.section; - return { ...props, framework, section, fallback: null }; - } - - // How to improve it - const abortControler = new AbortController(); - abortControler.abort(); - const section = isDeferred
(props.section) - ? await RequestContext.bind( - { signal: abortControler.signal }, - props.section, - )() - : props.section; - - return { ...props, framework, section: null, fallback: section }; -}; - -type SectionProps = Awaited>; - -function SingleDeferred(props: SectionProps) { - const ctx = useContext(SectionContext)!; - const idxOrNaN = Number(ctx?.resolveChain.at(-2)?.value); - const delay = 25 * (Math.min(Number.isNaN(idxOrNaN) ? 1 : idxOrNaN, 10) || 1); - const sections = props.section ? [props.section] : []; - const fallbacks = props.fallback ? [props.fallback] : undefined; - - return ( - - {isHtmx(props) - ? ( - - ) - : ( - - )} - - ); -} - -export const onBeforeResolveProps = (props: Props) => { - return { - ...props, - section: asResolved(props.section, true), - }; -}; - -export default SingleDeferred; +export * from "./Lazy.tsx"; +export { default } from "./Lazy.tsx";