From 6779d77264522c5c024d69710305fbf25aab79c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 07:22:08 +0000 Subject: [PATCH] chore: sync storefront from monorepo Source: coded-letter/coded-letter-monorepo@83fde1488f6a18d3bfdf024c39d0587f00208d77 --- .../storefront/scripts/cms-tailwind-index.mjs | 37 ++++++++++++++----- .../scripts/cms-tailwind-index.test.mjs | 13 +++++++ .../prerender-language-fallback.test.mjs | 4 +- apps/storefront/scripts/prerender.mjs | 15 ++++---- apps/storefront/scripts/route-query.mjs | 31 +++++++++++----- apps/storefront/scripts/route-query.test.mjs | 18 +++++++++ 6 files changed, 91 insertions(+), 27 deletions(-) diff --git a/apps/storefront/scripts/cms-tailwind-index.mjs b/apps/storefront/scripts/cms-tailwind-index.mjs index 5212048..82735c0 100644 --- a/apps/storefront/scripts/cms-tailwind-index.mjs +++ b/apps/storefront/scripts/cms-tailwind-index.mjs @@ -42,7 +42,7 @@ function endpointUrl(baseUrl, path) { return base.href; } -async function readBoundedJsonResponse(response, label, maxBytes = MAX_RESPONSE_BYTES) { +async function readBoundedTextResponse(response, label, maxBytes = MAX_RESPONSE_BYTES) { const declaredLength = Number.parseInt(response.headers.get("content-length") || "", 10); if (Number.isFinite(declaredLength) && declaredLength > maxBytes) { throw new Error(`${label} exceeded the ${maxBytes}-byte response limit`); @@ -51,8 +51,13 @@ async function readBoundedJsonResponse(response, label, maxBytes = MAX_RESPONSE_ if (bytes.byteLength > maxBytes) { throw new Error(`${label} exceeded the ${maxBytes}-byte response limit`); } + return new TextDecoder().decode(bytes); +} + +async function readBoundedJsonResponse(response, label, maxBytes = MAX_RESPONSE_BYTES) { + const responseText = await readBoundedTextResponse(response, label, maxBytes); try { - return JSON.parse(new TextDecoder().decode(bytes)); + return JSON.parse(responseText); } catch { throw new Error(`${label} returned invalid JSON`); } @@ -382,17 +387,31 @@ export async function fetchPreviousTailwindSourceIndex({ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); if (response.status === 404 && allowBootstrap) return emptyIndex(); - if ( - response.ok - && allowBootstrap - && (response.headers.get("content-type") || "").toLowerCase().includes("text/html") - ) { - return emptyIndex(); - } if (response.status === 404) { throw new Error("previous Tailwind source index is missing; enable CMS_TAILWIND_BOOTSTRAP for the first deployment"); } if (!response.ok) throw new Error(`previous Tailwind source index returned HTTP ${response.status}`); + if (allowBootstrap) { + const responseText = await readBoundedTextResponse( + response, + "previous Tailwind source index", + MAX_INDEX_BYTES, + ); + const contentType = (response.headers.get("content-type") || "").toLowerCase(); + if ( + contentType.includes("text/html") + || /^\s*(?: }); +test("bootstrap treats a mislabeled Netlify SPA fallback as a missing deployed index", async () => { + const result = await fetchPreviousTailwindSourceIndex({ + indexUrl: `${siteUrl}/.well-known/funkycommerce-tailwind-index.json`, + allowBootstrap: true, + fetchImpl: async () => new Response("Storefront", { + headers: { "content-type": "application/json; charset=UTF-8" }, + }), + }); + + assert.equal(result.generatedAt, "1970-01-01T00:00:00.000Z"); + assert.deepEqual(result.sources, {}); +}); + test("bootstrap resumes from a validated local index without refetching source bodies", async () => { await withOutput(async ({ outputPath, indexOutputPath }) => { const sourceVersion = version("page-7-v1"); diff --git a/apps/storefront/scripts/prerender-language-fallback.test.mjs b/apps/storefront/scripts/prerender-language-fallback.test.mjs index 0507047..00fbf21 100644 --- a/apps/storefront/scripts/prerender-language-fallback.test.mjs +++ b/apps/storefront/scripts/prerender-language-fallback.test.mjs @@ -23,7 +23,7 @@ test("prerender uses the WordPress site language when Polylang is unavailable", test("prerender retries commerce route discovery without multilingual WooCommerce metadata", () => { assert.match( prerenderSource, - /!backendLanguageFieldsAvailable[\s\S]*?WPGraphQL route discovery failed with status 500/, + /!backendLanguageFieldsAvailable[\s\S]*?error instanceof GraphqlHttpError[\s\S]*?error\.status >= 500/, ); assert.match( prerenderSource, @@ -35,7 +35,7 @@ test("prerender retries commerce route discovery without multilingual WooCommerc ); assert.match( prerenderSource, - /WPGraphQL route discovery without commerce metadata failed with status 500[\s\S]*?discoverStandardWordPressRoutes/, + /compatibilityError instanceof GraphqlHttpError[\s\S]*?compatibilityError\.status < 500[\s\S]*?discoverStandardWordPressRoutes/, ); assert.match( prerenderSource, diff --git a/apps/storefront/scripts/prerender.mjs b/apps/storefront/scripts/prerender.mjs index 4c05af4..5f8fe1e 100644 --- a/apps/storefront/scripts/prerender.mjs +++ b/apps/storefront/scripts/prerender.mjs @@ -692,10 +692,11 @@ class GraphqlResponseError extends Error { } class GraphqlHttpError extends Error { - constructor(message, retryable) { + constructor(message, status) { super(message); this.name = "GraphqlHttpError"; - this.retryable = retryable; + this.status = status; + this.retryable = status === 429 || status >= 500; } } @@ -757,7 +758,7 @@ async function requestGraphql( if (!response.ok) { throw new GraphqlHttpError( `${operationLabel} failed with status ${response.status}`, - response.status === 429 || response.status >= 500, + response.status, ); } @@ -1357,8 +1358,8 @@ async function discoverCmsRoutes({ ) || ( !backendLanguageFieldsAvailable - && error instanceof Error - && /WPGraphQL route discovery failed with status 500/.test(error.message) + && error instanceof GraphqlHttpError + && error.status >= 500 ) ); if (commerceMetadataUnavailable) { @@ -1383,8 +1384,8 @@ async function discoverCmsRoutes({ ); } catch (compatibilityError) { if ( - !(compatibilityError instanceof Error) - || !/WPGraphQL route discovery without commerce metadata failed with status 500/.test(compatibilityError.message) + !(compatibilityError instanceof GraphqlHttpError) + || compatibilityError.status < 500 ) { throw compatibilityError; } diff --git a/apps/storefront/scripts/route-query.mjs b/apps/storefront/scripts/route-query.mjs index 405376a..67fa1c5 100644 --- a/apps/storefront/scripts/route-query.mjs +++ b/apps/storefront/scripts/route-query.mjs @@ -263,6 +263,13 @@ export function buildCoreRoutesQuery({ seo = false, } = {}) { const selectedConnections = new Set(connections); + const variables = [ + selectedConnections.has("pages") ? "$pageAfter: String" : "", + selectedConnections.has("posts") ? "$postAfter: String" : "", + selectedConnections.has("categories") ? "$categoryAfter: String" : "", + selectedConnections.has("tags") ? "$tagAfter: String" : "", + selectedConnections.has("users") ? "$userAfter: String" : "", + ].filter(Boolean).join(", "); const languageField = multilingual ? "language { code }" : ""; const pageLanguageFields = multilingual ? "language { code }\n translations { databaseId uri }" @@ -272,15 +279,20 @@ export function buildCoreRoutesQuery({ ? "funkycommercePublicRobots { noindex nofollow }" : ""; const taxonomySeoField = seo ? "seo { ...StorefrontTaxonomyRouteSeo }" : ""; + const usesImageFragment = selectedConnections.has("pages") + || selectedConnections.has("posts") + || (seo && ( + selectedConnections.has("categories") + || selectedConnections.has("tags") + || selectedConnections.has("users") + )); + const usesPostTypeSeoFragment = seo + && (selectedConnections.has("pages") || selectedConnections.has("posts")); + const usesTaxonomySeoFragment = seo + && (selectedConnections.has("categories") || selectedConnections.has("tags")); return ` - query StorefrontCoreBuildRoutes( - $pageAfter: String - $postAfter: String - $categoryAfter: String - $tagAfter: String - $userAfter: String - ) { + query StorefrontCoreBuildRoutes(${variables}) { readingSettings { showOnFront pageOnFront @@ -352,7 +364,8 @@ export function buildCoreRoutesQuery({ pageInfo { hasNextPage endCursor } }` : ""} } - ${ROUTE_IMAGE_FRAGMENT} - ${seo ? `${POST_TYPE_SEO_FRAGMENT}\n${TAXONOMY_SEO_FRAGMENT}` : ""} + ${usesImageFragment ? ROUTE_IMAGE_FRAGMENT : ""} + ${usesPostTypeSeoFragment ? POST_TYPE_SEO_FRAGMENT : ""} + ${usesTaxonomySeoFragment ? TAXONOMY_SEO_FRAGMENT : ""} `; } diff --git a/apps/storefront/scripts/route-query.test.mjs b/apps/storefront/scripts/route-query.test.mjs index b40bf78..a694cc0 100644 --- a/apps/storefront/scripts/route-query.test.mjs +++ b/apps/storefront/scripts/route-query.test.mjs @@ -78,10 +78,28 @@ test("core fallback paginates standard routes without the generic connections", test("core fallback can isolate a connection when combined resolvers fail", () => { const query = buildCoreRoutesQuery({ connections: ["posts"] }); + assert.match(query, /query StorefrontCoreBuildRoutes\(\$postAfter: String\)/); assert.match(query, /posts\(first: 25/); for (const connection of ["pages", "categories", "tags", "users"]) { assert.doesNotMatch(query, new RegExp(`${connection}\\(first: 25`)); } + assert.doesNotMatch( + query, + /\$pageAfter|\$categoryAfter|\$tagAfter|\$userAfter|StorefrontTaxonomyRouteSeo/, + ); +}); + +test("isolated core taxonomy fallback omits unrelated variables and fragments", () => { + const query = buildCoreRoutesQuery({ connections: ["categories"], seo: true }); + + assert.match(query, /query StorefrontCoreBuildRoutes\(\$categoryAfter: String\)/); + assert.match(query, /categories\(first: 25/); + assert.match(query, /fragment StorefrontRouteImage on MediaItem/); + assert.match(query, /fragment StorefrontTaxonomyRouteSeo on TaxonomySEO/); + assert.doesNotMatch( + query, + /\$pageAfter|\$postAfter|\$tagAfter|\$userAfter|StorefrontPostTypeRouteSeo/, + ); }); test("generic route discovery can isolate one bounded connection", () => {