@decocms/tanstack 7.38.0 → 7.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/sdk/workerEntry.ts +131 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/tanstack",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.40.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco framework binding for TanStack Start + Cloudflare Workers",
|
|
6
6
|
"repository": {
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"lint:unused": "knip"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@decocms/blocks": "7.
|
|
28
|
-
"@decocms/blocks-admin": "7.
|
|
29
|
-
"@decocms/blocks-cli": "7.
|
|
27
|
+
"@decocms/blocks": "7.40.0",
|
|
28
|
+
"@decocms/blocks-admin": "7.40.0",
|
|
29
|
+
"@decocms/blocks-cli": "7.40.0",
|
|
30
30
|
"@deco-cx/warp-node": "^0.3.16",
|
|
31
31
|
"fast-json-patch": "^3.1.0",
|
|
32
32
|
"ws": "^8.18.0"
|
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -27,16 +27,20 @@
|
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
29
|
getRevision,
|
|
30
|
+
getSectionOptions,
|
|
30
31
|
isBot,
|
|
31
32
|
loadBlocks,
|
|
32
33
|
type MatcherContext,
|
|
33
34
|
onChange,
|
|
34
35
|
resolveDecoPage,
|
|
36
|
+
resolveDeferredSectionFull,
|
|
35
37
|
runSectionLoaders,
|
|
36
38
|
runSingleSectionLoader,
|
|
39
|
+
serializeRenderJson,
|
|
37
40
|
} from "@decocms/blocks/cms";
|
|
38
41
|
import { DECO_MATCHERS_OVERRIDE_PARAM } from "@decocms/blocks/matchers/override";
|
|
39
42
|
import { clearLoaderCache } from "@decocms/blocks/sdk/cachedLoader";
|
|
43
|
+
import { djb2Hex } from "@decocms/blocks/sdk/djb2";
|
|
40
44
|
import {
|
|
41
45
|
type CacheProfileName,
|
|
42
46
|
cacheHeaders,
|
|
@@ -1736,6 +1740,133 @@ export function createDecoWorkerEntry(
|
|
|
1736
1740
|
});
|
|
1737
1741
|
}
|
|
1738
1742
|
|
|
1743
|
+
// ?renderJson — structured JSON of the page for the mobile app. Lean shape
|
|
1744
|
+
// ({ name, path, sections:[{ component, props }] }), each section projected
|
|
1745
|
+
// through its `renderJson` export. Wins over ?asJson when both are present.
|
|
1746
|
+
if (url.searchParams.has("renderJson") && request.method === "GET") {
|
|
1747
|
+
const basePath = url.pathname;
|
|
1748
|
+
const cookies: Record<string, string> = {};
|
|
1749
|
+
for (const pair of (request.headers.get("cookie") ?? "").split(";")) {
|
|
1750
|
+
const [k, ...v] = pair.split("=");
|
|
1751
|
+
if (k?.trim()) cookies[k.trim()] = v.join("=").trim();
|
|
1752
|
+
}
|
|
1753
|
+
const matcherCtx: MatcherContext = {
|
|
1754
|
+
userAgent: request.headers.get("user-agent") ?? "",
|
|
1755
|
+
url: url.toString(),
|
|
1756
|
+
path: basePath,
|
|
1757
|
+
cookies,
|
|
1758
|
+
request,
|
|
1759
|
+
};
|
|
1760
|
+
|
|
1761
|
+
// Reflect Origin (not literal "*") so credentialed mobile fetches work.
|
|
1762
|
+
const corsHeaders: Record<string, string> = {
|
|
1763
|
+
"Access-Control-Allow-Origin": request.headers.get("origin") ?? "*",
|
|
1764
|
+
"Access-Control-Allow-Credentials": "true",
|
|
1765
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
1766
|
+
"Access-Control-Allow-Headers": "Content-Type, If-None-Match, Authorization",
|
|
1767
|
+
"Access-Control-Expose-Headers": "ETag",
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
const page = await resolveDecoPage(basePath, matcherCtx);
|
|
1771
|
+
if (!page) {
|
|
1772
|
+
// Stable error envelope — the app gets a predictable "not found".
|
|
1773
|
+
return Response.json(
|
|
1774
|
+
{ status: 404, notFound: true },
|
|
1775
|
+
{ status: 404, headers: corsHeaders },
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// Drop web-only / app-owned sections BEFORE running their loaders so a
|
|
1780
|
+
// dropped section never triggers its (expensive) VTEX call. Dual-sourced:
|
|
1781
|
+
// - the section's own `export const renderJson = false`
|
|
1782
|
+
// - the website app's renderJson.sectionsToIgnore (resolveType suffix)
|
|
1783
|
+
const rjBlocks = loadBlocks();
|
|
1784
|
+
const rjSite = rjBlocks["Site"] as Record<string, unknown> | undefined;
|
|
1785
|
+
const rawIgnore = (rjSite?.renderJson as { sectionsToIgnore?: unknown })?.sectionsToIgnore;
|
|
1786
|
+
const ignoreSuffixes = (Array.isArray(rawIgnore) ? rawIgnore : [])
|
|
1787
|
+
.filter((s): s is string => typeof s === "string")
|
|
1788
|
+
.map((s) => s.trim())
|
|
1789
|
+
.filter((s) => s.length > 0);
|
|
1790
|
+
const isDropped = (component: string) =>
|
|
1791
|
+
getSectionOptions(component)?.renderJson === false ||
|
|
1792
|
+
ignoreSuffixes.some((suffix) => component.endsWith(suffix));
|
|
1793
|
+
const getSectionModule = (component: string) => ({
|
|
1794
|
+
renderJson: getSectionOptions(component)?.renderJson,
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
const jsonHeaders = {
|
|
1798
|
+
...corsHeaders,
|
|
1799
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1800
|
+
"Cache-Control": "public, max-age=0, must-revalidate",
|
|
1801
|
+
};
|
|
1802
|
+
const withEtag = (body: string): Response => {
|
|
1803
|
+
// Contract-stable ETag over the serialized body: identical → 304.
|
|
1804
|
+
const etag = `"rj-${djb2Hex(body)}"`;
|
|
1805
|
+
if (request.headers.get("if-none-match") === etag) {
|
|
1806
|
+
return new Response(null, { status: 304, headers: { ...corsHeaders, ETag: etag } });
|
|
1807
|
+
}
|
|
1808
|
+
return new Response(body, { headers: { ...jsonHeaders, ETag: etag } });
|
|
1809
|
+
};
|
|
1810
|
+
|
|
1811
|
+
// Lazy single-section fetch: `?renderJson&__section=<index>` resolves ONE
|
|
1812
|
+
// deferred section (the placeholder the whole-page envelope emitted) and
|
|
1813
|
+
// returns its serialized `{ component, props }`.
|
|
1814
|
+
const sectionParam = url.searchParams.get("__section");
|
|
1815
|
+
if (sectionParam !== null) {
|
|
1816
|
+
const sectionIndex = Number(sectionParam);
|
|
1817
|
+
const ds = page.deferredSections.find((d) => d.index === sectionIndex);
|
|
1818
|
+
if (!ds || isDropped(ds.component)) {
|
|
1819
|
+
return Response.json(
|
|
1820
|
+
{ status: 404, notFound: true },
|
|
1821
|
+
{ status: 404, headers: corsHeaders },
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
const resolved = await resolveDeferredSectionFull(ds, basePath, request, matcherCtx);
|
|
1825
|
+
if (!resolved) {
|
|
1826
|
+
return Response.json(
|
|
1827
|
+
{ status: 404, notFound: true },
|
|
1828
|
+
{ status: 404, headers: corsHeaders },
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
const [serialized] = serializeRenderJson(
|
|
1832
|
+
[{ component: resolved.component, props: resolved.props }],
|
|
1833
|
+
{ getSectionModule, sectionsToIgnore: ignoreSuffixes },
|
|
1834
|
+
);
|
|
1835
|
+
if (!serialized) {
|
|
1836
|
+
return Response.json(
|
|
1837
|
+
{ status: 404, notFound: true },
|
|
1838
|
+
{ status: 404, headers: corsHeaders },
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
return withEtag(JSON.stringify(serialized));
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// Whole page: eager sections are resolved + projected inline; deferred
|
|
1845
|
+
// sections become `{ component, lazyUrl }` placeholders the app fetches on
|
|
1846
|
+
// demand (interleaved by their position in the page).
|
|
1847
|
+
const keptSections = page.resolvedSections.filter((s) => !isDropped(s.component));
|
|
1848
|
+
const enrichedSections = await runSectionLoaders(keptSections, request);
|
|
1849
|
+
const lazyUrlFor = (ref: { index: number }): string => {
|
|
1850
|
+
const u = new URL(url.toString());
|
|
1851
|
+
u.searchParams.set("__section", String(ref.index));
|
|
1852
|
+
return u.pathname + u.search;
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
const sections = serializeRenderJson(
|
|
1856
|
+
enrichedSections.map((s) => ({ component: s.component, props: s.props, index: s.index })),
|
|
1857
|
+
{
|
|
1858
|
+
getSectionModule,
|
|
1859
|
+
// Belt-and-suspenders: keptSections already dropped these, but the
|
|
1860
|
+
// serializer re-checks so a section that slipped through is still cut.
|
|
1861
|
+
sectionsToIgnore: ignoreSuffixes,
|
|
1862
|
+
deferred: page.deferredSections.map((d) => ({ component: d.component, index: d.index })),
|
|
1863
|
+
lazyUrlFor,
|
|
1864
|
+
},
|
|
1865
|
+
);
|
|
1866
|
+
|
|
1867
|
+
return withEtag(JSON.stringify({ name: page.name, path: page.path, sections }));
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1739
1870
|
// ?asJson — return resolved page data as JSON (legacy deco compat)
|
|
1740
1871
|
if (url.searchParams.has("asJson") && request.method === "GET") {
|
|
1741
1872
|
const basePath = url.pathname;
|