@decocms/apps-nuvemshop 7.75.0 → 7.77.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/__tests__/account.test.ts +250 -0
- package/src/__tests__/cart.test.ts +118 -0
- package/src/__tests__/loaders.test.ts +22 -3
- package/src/__tests__/mod.test.ts +38 -4
- package/src/actions/account/login.ts +30 -0
- package/src/actions/account/logout.ts +10 -0
- package/src/actions/account/register.ts +83 -0
- package/src/actions/createCheckout.ts +37 -9
- package/src/admin.ts +23 -0
- package/src/client.ts +34 -2
- package/src/hooks/__tests__/useCart.test.ts +37 -0
- package/src/hooks/useCart.ts +51 -5
- package/src/index.ts +7 -0
- package/src/loaders/cart.ts +108 -0
- package/src/loaders/user.ts +30 -0
- package/src/manifest.gen.ts +10 -0
- package/src/mod.ts +31 -0
- package/src/store.ts +88 -0
- package/src/utils/__tests__/sitemap.test.ts +57 -0
- package/src/utils/operationRouter.ts +7 -1
- package/src/utils/sitemap.ts +54 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import categoriesFixture from "../../__fixtures__/categories.json";
|
|
3
|
+
import { clearNuvemshopCache, configureNuvemshop, setNuvemshopFetch } from "../../client";
|
|
4
|
+
import { nuvemshopSitemap } from "../sitemap";
|
|
5
|
+
|
|
6
|
+
const json = (body: unknown) =>
|
|
7
|
+
new Response(JSON.stringify(body), { headers: { "content-type": "application/json" } });
|
|
8
|
+
const page = (n: number, total: number, handles: string[]) => ({
|
|
9
|
+
data: handles.map((handle, i) => ({
|
|
10
|
+
id: n * 100 + i,
|
|
11
|
+
handle,
|
|
12
|
+
updated_at: `2026-10-0${n}T10:00:00+0000`,
|
|
13
|
+
})),
|
|
14
|
+
pagination: { page: n, per_page: 200, total: 3, total_pages: total },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const fetchMock = vi.fn(async (input: string | URL | Request) => {
|
|
18
|
+
const url = new URL(String(input));
|
|
19
|
+
if (url.pathname.endsWith("/categories")) return json(categoriesFixture);
|
|
20
|
+
const p = Number(url.searchParams.get("page") ?? 1);
|
|
21
|
+
return json(p === 1 ? page(1, 2, ["camisa-a", "camisa-b&c"]) : page(2, 2, ["tenis-x"]));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
fetchMock.mockClear();
|
|
26
|
+
clearNuvemshopCache();
|
|
27
|
+
configureNuvemshop({ storeId: "8336778" });
|
|
28
|
+
setNuvemshopFetch(fetchMock as unknown as typeof fetch);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("nuvemshopSitemap", () => {
|
|
32
|
+
it("pages through every product and lists categories with nested URLs", async () => {
|
|
33
|
+
const xml = await nuvemshopSitemap({ origin: "https://www.loja.example" });
|
|
34
|
+
|
|
35
|
+
const productCalls = fetchMock.mock.calls
|
|
36
|
+
.map(([u]) => new URL(String(u)))
|
|
37
|
+
.filter((u) => u.pathname.endsWith("/products"));
|
|
38
|
+
expect(productCalls.map((u) => u.searchParams.get("page"))).toEqual(["1", "2"]);
|
|
39
|
+
expect(productCalls[0].searchParams.get("per_page")).toBe("200");
|
|
40
|
+
expect(productCalls[0].searchParams.get("fields")).toBe("handle,updated_at");
|
|
41
|
+
|
|
42
|
+
expect(
|
|
43
|
+
xml.startsWith(
|
|
44
|
+
'<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
45
|
+
),
|
|
46
|
+
).toBe(true);
|
|
47
|
+
expect(xml).toContain(
|
|
48
|
+
"<url><loc>https://www.loja.example/produtos/camisa-a</loc><lastmod>2026-10-01</lastmod></url>",
|
|
49
|
+
);
|
|
50
|
+
expect(xml).toContain("<loc>https://www.loja.example/produtos/tenis-x</loc>");
|
|
51
|
+
expect(xml).toContain("<loc>https://www.loja.example/produtos/camisa-b&c</loc>"); // XML-escaped
|
|
52
|
+
expect(xml).toContain("<url><loc>https://www.loja.example/calcados/masculino</loc></url>");
|
|
53
|
+
expect(xml).toContain("<url><loc>https://www.loja.example/</loc></url>");
|
|
54
|
+
expect(xml.match(/<url>/g)).toHaveLength(1 + categoriesFixture.data.length + 3);
|
|
55
|
+
expect(xml.trimEnd().endsWith("</urlset>")).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* URL → operation name for Nuvemshop Storefront API
|
|
2
|
+
* URL → operation name for Nuvemshop egress (Storefront API, the store's own
|
|
3
|
+
* session endpoints, the Admin API and Turnstile), plugged into
|
|
3
4
|
* `createInstrumentedFetch`'s `resolveOperation`. Returns `undefined` when
|
|
4
5
|
* nothing matches so the framework falls back to `nuvemshop.fetch`.
|
|
5
6
|
*/
|
|
@@ -13,6 +14,11 @@ const MATCHERS: ReadonlyArray<[RegExp, string]> = [
|
|
|
13
14
|
[/\/stores\/\d+\/categories\/[^/]+\/?$/, "categories.get"],
|
|
14
15
|
[/\/stores\/\d+\/shipping-options\/?$/, "shipping.options"],
|
|
15
16
|
[/\/stores\/\d+\/checkouts\/?$/, "checkout.create"],
|
|
17
|
+
[/^\/account\/login\/?$/, "store.account.login"],
|
|
18
|
+
[/^\/account\/logout\/?$/, "store.account.logout"],
|
|
19
|
+
[/^\/account\/?$/, "store.account.page"],
|
|
20
|
+
[/^\/v1\/\d+\/customers(\/\d+)?\/?$/, "admin.customers"],
|
|
21
|
+
[/^\/turnstile\/v0\/siteverify$/, "turnstile.verify"],
|
|
16
22
|
];
|
|
17
23
|
|
|
18
24
|
export function nuvemshopOperationRouter(url: string, _method: string): string | undefined {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product + category sitemap built from the Storefront API (which has no
|
|
3
|
+
* sitemap endpoint). URLs use the same paths as the loaders (`/produtos/<handle>`,
|
|
4
|
+
* `/<cat>/<sub>`), so they resolve without redirects. Serve it from a site route:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // src/routes/sitemap[.]xml.ts
|
|
8
|
+
* GET: async ({ request }) => new Response(await nuvemshopSitemap({ origin: new URL(request.url).origin }),
|
|
9
|
+
* { headers: { "content-type": "application/xml; charset=utf-8" } })
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { nuvemshopGet } from "../client";
|
|
13
|
+
import { categoryChain, categoryPath, productPath } from "./transform";
|
|
14
|
+
import type { NuvemshopCategory, NuvemshopList } from "./types";
|
|
15
|
+
|
|
16
|
+
// ponytail: one request per 200 products, sequential; split into a sitemap index if a catalog
|
|
17
|
+
// ever outgrows the 50k-URL sitemap limit.
|
|
18
|
+
const PER_PAGE = 200;
|
|
19
|
+
|
|
20
|
+
const escapeXml = (s: string) =>
|
|
21
|
+
s
|
|
22
|
+
.replace(/&/g, "&")
|
|
23
|
+
.replace(/</g, "<")
|
|
24
|
+
.replace(/>/g, ">")
|
|
25
|
+
.replace(/"/g, """)
|
|
26
|
+
.replace(/'/g, "'");
|
|
27
|
+
const entry = (loc: string, lastmod?: string) =>
|
|
28
|
+
`<url><loc>${escapeXml(loc)}</loc>${lastmod ? `<lastmod>${lastmod.slice(0, 10)}</lastmod>` : ""}</url>`;
|
|
29
|
+
|
|
30
|
+
export async function nuvemshopSitemap({ origin }: { origin: string }): Promise<string> {
|
|
31
|
+
const urls = [entry(`${origin}/`)];
|
|
32
|
+
|
|
33
|
+
const categories =
|
|
34
|
+
(await nuvemshopGet<NuvemshopList<NuvemshopCategory>>("/categories", { per_page: PER_PAGE }))
|
|
35
|
+
?.data ?? [];
|
|
36
|
+
for (const c of categories)
|
|
37
|
+
urls.push(entry(`${origin}${categoryPath(categoryChain(c, categories))}`));
|
|
38
|
+
|
|
39
|
+
for (let page = 1, pages = 1; page <= pages; page++) {
|
|
40
|
+
const list = await nuvemshopGet<NuvemshopList<{ handle: string; updated_at?: string }>>(
|
|
41
|
+
"/products",
|
|
42
|
+
{
|
|
43
|
+
fields: "handle,updated_at",
|
|
44
|
+
per_page: PER_PAGE,
|
|
45
|
+
page,
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
if (!list) break;
|
|
49
|
+
pages = list.pagination.total_pages;
|
|
50
|
+
for (const p of list.data) urls.push(entry(`${origin}${productPath(p.handle)}`, p.updated_at));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.join("\n")}\n</urlset>\n`;
|
|
54
|
+
}
|