shop-client 3.14.0 → 3.14.1
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/dist/{chunk-RLVH7LEG.mjs → chunk-7IMI76JZ.mjs} +128 -5
- package/dist/collections.d.ts +2 -2
- package/dist/index.d.ts +18 -6
- package/dist/index.mjs +59 -5
- package/dist/products.d.ts +2 -2
- package/dist/store.d.ts +18 -6
- package/dist/store.mjs +3 -3
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
|
|
13
13
|
// src/client/get-info.ts
|
|
14
14
|
import { unique } from "remeda";
|
|
15
|
-
async function
|
|
15
|
+
async function getInfoForShop(args, options) {
|
|
16
16
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
|
|
17
17
|
const {
|
|
18
18
|
baseUrl,
|
|
@@ -206,7 +206,7 @@ async function getInfoForStore(args, options) {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
// src/store.ts
|
|
209
|
-
function
|
|
209
|
+
function createShopOperations(context) {
|
|
210
210
|
return {
|
|
211
211
|
/**
|
|
212
212
|
* Fetches comprehensive store information including metadata, social links, and showcase content.
|
|
@@ -242,7 +242,7 @@ function createStoreOperations(context) {
|
|
|
242
242
|
*/
|
|
243
243
|
info: async () => {
|
|
244
244
|
try {
|
|
245
|
-
const { info } = await
|
|
245
|
+
const { info } = await getInfoForShop({
|
|
246
246
|
baseUrl: context.baseUrl,
|
|
247
247
|
storeDomain: context.storeDomain,
|
|
248
248
|
validateProductExists: context.validateProductExists,
|
|
@@ -253,11 +253,134 @@ function createStoreOperations(context) {
|
|
|
253
253
|
} catch (error) {
|
|
254
254
|
context.handleFetchError(error, "fetching store info", context.baseUrl);
|
|
255
255
|
}
|
|
256
|
+
},
|
|
257
|
+
getMetaData: async () => {
|
|
258
|
+
try {
|
|
259
|
+
const response = await rateLimitedFetch(context.baseUrl, {
|
|
260
|
+
rateLimitClass: "store:metadata",
|
|
261
|
+
timeoutMs: 7e3
|
|
262
|
+
});
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
265
|
+
}
|
|
266
|
+
const html = await response.text();
|
|
267
|
+
const getPropertyMetaTag = (property) => {
|
|
268
|
+
const regex = new RegExp(
|
|
269
|
+
`<meta[^>]*property=["']${property}["'][^>]*content=["'](.*?)["']`
|
|
270
|
+
);
|
|
271
|
+
const match = html.match(regex);
|
|
272
|
+
return match ? match[1] : null;
|
|
273
|
+
};
|
|
274
|
+
const siteName = getPropertyMetaTag("og:site_name");
|
|
275
|
+
const title = getPropertyMetaTag("og:title");
|
|
276
|
+
const description = getPropertyMetaTag("og:description");
|
|
277
|
+
const url = getPropertyMetaTag("og:url");
|
|
278
|
+
const type = getPropertyMetaTag("og:type");
|
|
279
|
+
const image = getPropertyMetaTag("og:image");
|
|
280
|
+
const imageSecureUrl = getPropertyMetaTag("og:image:secure_url");
|
|
281
|
+
return {
|
|
282
|
+
siteName: siteName != null ? siteName : null,
|
|
283
|
+
title: title != null ? title : null,
|
|
284
|
+
description: description != null ? description : null,
|
|
285
|
+
url: url != null ? url : null,
|
|
286
|
+
type: type != null ? type : null,
|
|
287
|
+
image: image != null ? image : null,
|
|
288
|
+
imageSecureUrl: imageSecureUrl != null ? imageSecureUrl : null
|
|
289
|
+
};
|
|
290
|
+
} catch (error) {
|
|
291
|
+
context.handleFetchError(
|
|
292
|
+
error,
|
|
293
|
+
"fetching store metadata",
|
|
294
|
+
context.baseUrl
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
getJsonLd: async () => {
|
|
299
|
+
var _a, _b;
|
|
300
|
+
try {
|
|
301
|
+
const response = await rateLimitedFetch(context.baseUrl, {
|
|
302
|
+
rateLimitClass: "store:jsonld",
|
|
303
|
+
timeoutMs: 7e3
|
|
304
|
+
});
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
307
|
+
}
|
|
308
|
+
const html = await response.text();
|
|
309
|
+
const scripts = (_b = (_a = html.match(
|
|
310
|
+
/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/g
|
|
311
|
+
)) == null ? void 0 : _a.map(
|
|
312
|
+
(match) => match.replace(/^.*?>/, "").replace(/<\/script>$/i, "")
|
|
313
|
+
)) != null ? _b : [];
|
|
314
|
+
const parsed = scripts.map((json) => {
|
|
315
|
+
try {
|
|
316
|
+
return JSON.parse(json);
|
|
317
|
+
} catch {
|
|
318
|
+
return void 0;
|
|
319
|
+
}
|
|
320
|
+
}).filter((x) => !!x);
|
|
321
|
+
return parsed.length ? parsed : void 0;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
context.handleFetchError(
|
|
324
|
+
error,
|
|
325
|
+
"fetching store JSON-LD",
|
|
326
|
+
context.baseUrl
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
getHeaderLinks: async () => {
|
|
331
|
+
var _a;
|
|
332
|
+
try {
|
|
333
|
+
const response = await rateLimitedFetch(context.baseUrl, {
|
|
334
|
+
rateLimitClass: "store:header",
|
|
335
|
+
timeoutMs: 7e3
|
|
336
|
+
});
|
|
337
|
+
if (!response.ok) {
|
|
338
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
339
|
+
}
|
|
340
|
+
const html = await response.text();
|
|
341
|
+
const sections = (_a = html.match(
|
|
342
|
+
/<(header|nav|div|section)\b[^>]*\b(?:id|class)=["'][^"']*(?=.*shopify-section)(?=.*\b(header|navigation|nav|menu)\b)[^"']*["'][^>]*>[\s\S]*?<\/\1>/gi
|
|
343
|
+
)) != null ? _a : [];
|
|
344
|
+
const links = sections.flatMap((section) => {
|
|
345
|
+
var _a2;
|
|
346
|
+
return (_a2 = section.match(/href=["']([^"']+)["']/g)) != null ? _a2 : [];
|
|
347
|
+
}).map((link) => {
|
|
348
|
+
var _a2;
|
|
349
|
+
return (_a2 = link.match(/href=["']([^"']+)["']/)) == null ? void 0 : _a2[1];
|
|
350
|
+
}).filter((href) => !!href).filter(
|
|
351
|
+
(href) => href.includes("/products/") || href.includes("/collections/") || href.includes("/pages/")
|
|
352
|
+
).map((href) => {
|
|
353
|
+
try {
|
|
354
|
+
if (href.startsWith("//"))
|
|
355
|
+
return new URL(`https:${href}`).pathname;
|
|
356
|
+
if (href.startsWith("/"))
|
|
357
|
+
return new URL(href, context.storeDomain).pathname;
|
|
358
|
+
return new URL(href).pathname;
|
|
359
|
+
} catch {
|
|
360
|
+
return href;
|
|
361
|
+
}
|
|
362
|
+
}).map((p) => p.replace(/^\/|\/$/g, ""));
|
|
363
|
+
const seen = /* @__PURE__ */ new Set();
|
|
364
|
+
const deduped = [];
|
|
365
|
+
for (const l of links) {
|
|
366
|
+
if (!seen.has(l)) {
|
|
367
|
+
seen.add(l);
|
|
368
|
+
deduped.push(l);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return deduped;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
context.handleFetchError(
|
|
374
|
+
error,
|
|
375
|
+
"fetching header links",
|
|
376
|
+
context.baseUrl
|
|
377
|
+
);
|
|
378
|
+
}
|
|
256
379
|
}
|
|
257
380
|
};
|
|
258
381
|
}
|
|
259
382
|
|
|
260
383
|
export {
|
|
261
|
-
|
|
262
|
-
|
|
384
|
+
getInfoForShop,
|
|
385
|
+
createShopOperations
|
|
263
386
|
};
|
package/dist/collections.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ShopInfo } from './store.js';
|
|
2
2
|
import { C as Collection, f as CurrencyCode, P as Product, b as ShopifyCollection } from './types-luPg5O08.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -60,6 +60,6 @@ interface CollectionOperations {
|
|
|
60
60
|
declare function createCollectionOperations(baseUrl: string, storeDomain: string, fetchCollections: (page: number, limit: number) => Promise<Collection[] | null>, collectionsDto: (collections: ShopifyCollection[]) => Collection[], fetchPaginatedProductsFromCollection: (collectionHandle: string, options?: {
|
|
61
61
|
page?: number;
|
|
62
62
|
limit?: number;
|
|
63
|
-
}) => Promise<Product[] | null>, getStoreInfo: () => Promise<
|
|
63
|
+
}) => Promise<Product[] | null>, getStoreInfo: () => Promise<ShopInfo>, findCollection: (handle: string) => Promise<Collection | null>): CollectionOperations;
|
|
64
64
|
|
|
65
65
|
export { type CollectionOperations, createCollectionOperations };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { S as ShopifyProduct, P as Product, a as ShopifySingleProduct, b as ShopifyCollection, C as Collection, J as JsonLdEntry, c as StoreTypeBreakdown } from './types-luPg5O08.js';
|
|
2
|
+
export { d as CountryDetectionResult, e as CountryScores, f as CurrencyCode, L as LocalizedPricing, M as MetaTag, g as ProductImage, h as ProductOption, i as ProductVariant, j as ProductVariantImage } from './types-luPg5O08.js';
|
|
1
3
|
import { CheckoutOperations } from './checkout.js';
|
|
2
4
|
import { CollectionOperations } from './collections.js';
|
|
3
5
|
import { ProductOperations } from './products.js';
|
|
4
|
-
import {
|
|
5
|
-
import { S as ShopifyProduct, P as Product, a as ShopifySingleProduct, b as ShopifyCollection, C as Collection, c as StoreTypeBreakdown } from './types-luPg5O08.js';
|
|
6
|
-
export { d as CountryDetectionResult, e as CountryScores, f as CurrencyCode, L as LocalizedPricing, M as MetaTag, g as ProductImage, h as ProductOption, i as ProductVariant, j as ProductVariantImage } from './types-luPg5O08.js';
|
|
6
|
+
import { ShopOperations, ShopInfo, OpenGraphMeta } from './store.js';
|
|
7
7
|
export { classifyProduct, generateSEOContent } from './ai/enrich.js';
|
|
8
8
|
export { detectShopCountry } from './utils/detect-country.js';
|
|
9
9
|
export { calculateDiscount, extractDomainWithoutSuffix, genProductSlug, generateStoreSlug, safeParseDate, sanitizeDomain } from './utils/func.js';
|
|
@@ -40,10 +40,13 @@ declare class ShopClient {
|
|
|
40
40
|
private infoCacheValue?;
|
|
41
41
|
private infoCacheTimestamp?;
|
|
42
42
|
private infoInFlight?;
|
|
43
|
+
private metaCacheValue?;
|
|
44
|
+
private metaCacheTimestamp?;
|
|
45
|
+
private metaInFlight?;
|
|
43
46
|
products: ProductOperations;
|
|
44
47
|
collections: CollectionOperations;
|
|
45
48
|
checkout: CheckoutOperations;
|
|
46
|
-
|
|
49
|
+
shopOperations: ShopOperations;
|
|
47
50
|
/**
|
|
48
51
|
* Creates a new ShopClient instance for interacting with a Shopify store.
|
|
49
52
|
*
|
|
@@ -156,12 +159,21 @@ declare class ShopClient {
|
|
|
156
159
|
force?: boolean;
|
|
157
160
|
validateShowcase?: boolean;
|
|
158
161
|
validationBatchSize?: number;
|
|
159
|
-
}): Promise<
|
|
162
|
+
}): Promise<ShopInfo>;
|
|
160
163
|
/**
|
|
161
164
|
* Manually clear the cached store info.
|
|
162
165
|
* The next call to `getInfo()` will fetch fresh data regardless of TTL.
|
|
163
166
|
*/
|
|
164
167
|
clearInfoCache(): void;
|
|
168
|
+
/**
|
|
169
|
+
* Fetch OpenGraph metadata from the store homepage.
|
|
170
|
+
* Returns only `og:*` fields without additional parsing or validation.
|
|
171
|
+
*/
|
|
172
|
+
getMetaData(options?: {
|
|
173
|
+
force?: boolean;
|
|
174
|
+
}): Promise<OpenGraphMeta>;
|
|
175
|
+
getJsonLd(): Promise<JsonLdEntry[] | undefined>;
|
|
176
|
+
getHeaderLinks(): Promise<string[]>;
|
|
165
177
|
/**
|
|
166
178
|
* Determine the store's primary vertical and target audience.
|
|
167
179
|
* Uses `getInfo()` internally; no input required.
|
|
@@ -174,4 +186,4 @@ declare class ShopClient {
|
|
|
174
186
|
}): Promise<StoreTypeBreakdown>;
|
|
175
187
|
}
|
|
176
188
|
|
|
177
|
-
export { CheckoutOperations, Collection, CollectionOperations, Product, ProductOperations, ShopClient, type ShopClientOptions,
|
|
189
|
+
export { CheckoutOperations, Collection, CollectionOperations, OpenGraphMeta, Product, ProductOperations, ShopClient, type ShopClientOptions, ShopInfo, ShopOperations, StoreTypeBreakdown };
|
package/dist/index.mjs
CHANGED
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
createProductOperations
|
|
9
9
|
} from "./chunk-ZF4M6GMB.mjs";
|
|
10
10
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "./chunk-
|
|
11
|
+
createShopOperations,
|
|
12
|
+
getInfoForShop
|
|
13
|
+
} from "./chunk-7IMI76JZ.mjs";
|
|
14
14
|
import {
|
|
15
15
|
classifyProduct,
|
|
16
16
|
determineStoreType,
|
|
@@ -353,7 +353,7 @@ var ShopClient = class {
|
|
|
353
353
|
if (typeof (options == null ? void 0 : options.cacheTTL) === "number" && options.cacheTTL > 0) {
|
|
354
354
|
this.cacheExpiry = options.cacheTTL;
|
|
355
355
|
}
|
|
356
|
-
this.
|
|
356
|
+
this.shopOperations = createShopOperations({
|
|
357
357
|
baseUrl: this.baseUrl,
|
|
358
358
|
storeDomain: this.storeDomain,
|
|
359
359
|
validateProductExists: this.validateProductExists.bind(this),
|
|
@@ -688,7 +688,7 @@ var ShopClient = class {
|
|
|
688
688
|
return await this.infoInFlight;
|
|
689
689
|
}
|
|
690
690
|
this.infoInFlight = (async () => {
|
|
691
|
-
const { info, currencyCode } = await
|
|
691
|
+
const { info, currencyCode } = await getInfoForShop(
|
|
692
692
|
{
|
|
693
693
|
baseUrl: this.baseUrl,
|
|
694
694
|
storeDomain: this.storeDomain,
|
|
@@ -726,6 +726,60 @@ var ShopClient = class {
|
|
|
726
726
|
this.infoCacheValue = void 0;
|
|
727
727
|
this.infoCacheTimestamp = void 0;
|
|
728
728
|
}
|
|
729
|
+
/**
|
|
730
|
+
* Fetch OpenGraph metadata from the store homepage.
|
|
731
|
+
* Returns only `og:*` fields without additional parsing or validation.
|
|
732
|
+
*/
|
|
733
|
+
async getMetaData(options) {
|
|
734
|
+
try {
|
|
735
|
+
if ((options == null ? void 0 : options.force) === true) {
|
|
736
|
+
this.metaCacheValue = void 0;
|
|
737
|
+
this.metaCacheTimestamp = void 0;
|
|
738
|
+
}
|
|
739
|
+
if (this.metaCacheValue && this.metaCacheTimestamp !== void 0 && Date.now() - this.metaCacheTimestamp < this.cacheExpiry) {
|
|
740
|
+
return this.metaCacheValue;
|
|
741
|
+
}
|
|
742
|
+
if (this.metaInFlight) {
|
|
743
|
+
return await this.metaInFlight;
|
|
744
|
+
}
|
|
745
|
+
this.metaInFlight = (async () => {
|
|
746
|
+
const meta = await this.shopOperations.getMetaData();
|
|
747
|
+
this.metaCacheValue = meta;
|
|
748
|
+
this.metaCacheTimestamp = Date.now();
|
|
749
|
+
return meta;
|
|
750
|
+
})();
|
|
751
|
+
try {
|
|
752
|
+
const result = await this.metaInFlight;
|
|
753
|
+
return result;
|
|
754
|
+
} finally {
|
|
755
|
+
this.metaInFlight = void 0;
|
|
756
|
+
}
|
|
757
|
+
} catch (error) {
|
|
758
|
+
throw this.handleFetchError(
|
|
759
|
+
error,
|
|
760
|
+
"fetching store metadata",
|
|
761
|
+
this.baseUrl
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
async getJsonLd() {
|
|
766
|
+
try {
|
|
767
|
+
return await this.shopOperations.getJsonLd();
|
|
768
|
+
} catch (error) {
|
|
769
|
+
throw this.handleFetchError(
|
|
770
|
+
error,
|
|
771
|
+
"fetching store JSON-LD",
|
|
772
|
+
this.baseUrl
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async getHeaderLinks() {
|
|
777
|
+
try {
|
|
778
|
+
return await this.shopOperations.getHeaderLinks();
|
|
779
|
+
} catch (error) {
|
|
780
|
+
throw this.handleFetchError(error, "fetching header links", this.baseUrl);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
729
783
|
/**
|
|
730
784
|
* Determine the store's primary vertical and target audience.
|
|
731
785
|
* Uses `getInfo()` internally; no input required.
|
package/dist/products.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ShopInfo } from './store.js';
|
|
2
2
|
import { f as CurrencyCode, P as Product, k as ProductClassification, l as SEOContent, S as ShopifyProduct, a as ShopifySingleProduct } from './types-luPg5O08.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -77,6 +77,6 @@ interface ProductOperations {
|
|
|
77
77
|
/**
|
|
78
78
|
* Creates product operations for a store instance
|
|
79
79
|
*/
|
|
80
|
-
declare function createProductOperations(baseUrl: string, storeDomain: string, fetchProducts: (page: number, limit: number) => Promise<Product[] | null>, productsDto: (products: ShopifyProduct[]) => Product[] | null, productDto: (product: ShopifySingleProduct) => Product, getStoreInfo: () => Promise<
|
|
80
|
+
declare function createProductOperations(baseUrl: string, storeDomain: string, fetchProducts: (page: number, limit: number) => Promise<Product[] | null>, productsDto: (products: ShopifyProduct[]) => Product[] | null, productDto: (product: ShopifySingleProduct) => Product, getStoreInfo: () => Promise<ShopInfo>, findProduct: (handle: string) => Promise<Product | null>): ProductOperations;
|
|
81
81
|
|
|
82
82
|
export { type ProductOperations, createProductOperations };
|
package/dist/store.d.ts
CHANGED
|
@@ -4,14 +4,17 @@ import { J as JsonLdEntry, d as CountryDetectionResult, f as CurrencyCode } from
|
|
|
4
4
|
* Store operations interface for managing store-related functionality.
|
|
5
5
|
* Provides methods to fetch comprehensive store information and metadata.
|
|
6
6
|
*/
|
|
7
|
-
interface
|
|
8
|
-
info(): Promise<
|
|
7
|
+
interface ShopOperations {
|
|
8
|
+
info(): Promise<ShopInfo>;
|
|
9
|
+
getMetaData(): Promise<OpenGraphMeta>;
|
|
10
|
+
getJsonLd(): Promise<JsonLdEntry[] | undefined>;
|
|
11
|
+
getHeaderLinks(): Promise<string[]>;
|
|
9
12
|
}
|
|
10
13
|
/**
|
|
11
14
|
* Comprehensive store information structure returned by the info method.
|
|
12
15
|
* Contains all metadata, branding, social links, and showcase content for a Shopify store.
|
|
13
16
|
*/
|
|
14
|
-
interface
|
|
17
|
+
interface ShopInfo {
|
|
15
18
|
name: string;
|
|
16
19
|
domain: string;
|
|
17
20
|
slug: string;
|
|
@@ -38,17 +41,26 @@ interface StoreInfo {
|
|
|
38
41
|
country: CountryDetectionResult["country"];
|
|
39
42
|
currency: CurrencyCode | null;
|
|
40
43
|
}
|
|
44
|
+
interface OpenGraphMeta {
|
|
45
|
+
siteName: string | null;
|
|
46
|
+
title: string | null;
|
|
47
|
+
description: string | null;
|
|
48
|
+
url: string | null;
|
|
49
|
+
type: string | null;
|
|
50
|
+
image: string | null;
|
|
51
|
+
imageSecureUrl: string | null;
|
|
52
|
+
}
|
|
41
53
|
/**
|
|
42
54
|
* Creates store operations for a ShopClient instance.
|
|
43
55
|
* @param context - ShopClient context containing necessary methods and properties for store operations
|
|
44
56
|
*/
|
|
45
|
-
declare function
|
|
57
|
+
declare function createShopOperations(context: {
|
|
46
58
|
baseUrl: string;
|
|
47
59
|
storeDomain: string;
|
|
48
60
|
validateProductExists: (handle: string) => Promise<boolean>;
|
|
49
61
|
validateCollectionExists: (handle: string) => Promise<boolean>;
|
|
50
62
|
validateLinksInBatches: <T>(items: T[], validator: (item: T) => Promise<boolean>, batchSize?: number) => Promise<T[]>;
|
|
51
63
|
handleFetchError: (error: unknown, context: string, url: string) => never;
|
|
52
|
-
}):
|
|
64
|
+
}): ShopOperations;
|
|
53
65
|
|
|
54
|
-
export { type
|
|
66
|
+
export { type OpenGraphMeta, type ShopInfo, type ShopOperations, createShopOperations };
|
package/dist/store.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
createShopOperations
|
|
3
|
+
} from "./chunk-7IMI76JZ.mjs";
|
|
4
4
|
import "./chunk-D5MTUWFO.mjs";
|
|
5
5
|
import "./chunk-G7OCMGA6.mjs";
|
|
6
6
|
import "./chunk-U3RQRBXZ.mjs";
|
|
7
7
|
export {
|
|
8
|
-
|
|
8
|
+
createShopOperations
|
|
9
9
|
};
|