@scayle/storefront-cms-contentstack 0.2.0-alpha.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/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # @scayle/storefront-cms-contentstack
2
+
3
+ Contentstack CMS provider integration for the SCAYLE Storefront Application V3.
4
+
5
+ > **Requires Storefront Application V3.** This package implements `CMSProviderService` from
6
+ > `@scayle/storefront`. Controllers resolve the constructed provider from the `ServiceRegistry` and call
7
+ > `fetchPageData`/`fetchListingPageData` directly. It has no standalone use outside a Storefront
8
+ > Application V3 project.
9
+
10
+ ## Package entrypoint
11
+
12
+ | Export | Use |
13
+ | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
14
+ | `ContentstackCMSService` | Concrete `CMSProviderService` implementation, usable directly since Contentstack's runtime guards are hand-written, not generated. |
15
+ | `ContentstackCMSServiceConfig` | Constructor config shape: `{apiKey, deliveryToken, environment, region?, branch?, previewAccessToken?}`. |
16
+ | `cspConfig` | `CMSCspConfig` for the Contentstack Visual Builder: iframe embedding and Live Preview API connect rules, using wildcard subdomains for region-agnostic support. |
17
+
18
+ See [Contentstack's Content Delivery API documentation](https://www.contentstack.com/docs/developers/apis/content-delivery-api/)
19
+ and the [TypeScript Delivery SDK docs](https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/typescript/get-started-with-typescript-delivery-sdk)
20
+ for the underlying client.
21
+
22
+ ## Configuration
23
+
24
+ The boilerplate's `src/server/cms/providers/contentstack/service.ts` reads configuration values from environment variables
25
+ via `getRequiredEnv` and `process.env`, passing them to the `ContentstackCMSService` constructor. See `.env.example` for a
26
+ complete list of environment variables.
27
+
28
+ | Config field | Required | Purpose | Location in Contentstack | Documentation |
29
+ | --------------------- | -------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
30
+ | `apiKey` | Yes | Stack API key used to authenticate content requests to the Delivery API. | Settings > Stack > API Keys (under "Stack API Key") | [API credentials](https://www.contentstack.com/docs/developers/apis/content-delivery-api#how-to-get-api-credentials) |
31
+ | `deliveryToken` | Yes | Delivery token used to authenticate requests for published content. | Settings > Tokens > Delivery Tokens | [API credentials](https://www.contentstack.com/docs/developers/apis/content-delivery-api#how-to-get-api-credentials) |
32
+ | `environment` | Yes | Environment name the service queries (e.g. `production`, `staging`). | Settings > Environments | [Manage environments](https://www.contentstack.com/docs/developers/set-up-contentstack/manage-multiple-environments) |
33
+ | `region` | No | Stack region determining API endpoints (e.g. `US`, `EU`, `AZURE_NA`). Defaults to `US`. | Settings > Stack > region shown in API URL or Org settings | [About regions](https://www.contentstack.com/docs/developers/set-up-contentstack/about-region) |
34
+ | `branch` | No | Content branch name for multi-branch workflows (only needed if your stack uses branching). | Settings > Branches | [Branches guide](https://www.contentstack.com/docs/developers/set-up-contentstack/about-branches) |
35
+ | `previewAccessToken` | No | Preview token for live preview and Visual Builder requests (required only if using preview/live-preview mode). | Settings > Tokens > Delivery Tokens (preview tokens managed alongside delivery tokens) | [API credentials](https://www.contentstack.com/docs/developers/apis/content-delivery-api#how-to-get-api-credentials) |
36
+ | `draftContentEnabled` | Yes | Whether draft/unpublished content may be served during an editor session | `STOREFRONT_CMS_ALLOW_DRAFTS` (read by boilerplate) | |
37
+
38
+ Environment variable mapping:
39
+
40
+ - `CONTENTSTACK_CMS_STACK_API_KEY` → `apiKey`
41
+ - `CONTENTSTACK_CMS_DELIVERY_TOKEN` → `deliveryToken`
42
+ - `CONTENTSTACK_CMS_ENVIRONMENT` → `environment`
43
+ - `CONTENTSTACK_CMS_REGION` → `region`
44
+ - `CONTENTSTACK_CMS_BRANCH` → `branch`
45
+ - `CONTENTSTACK_CMS_PREVIEW_ACCESS_TOKEN` → `previewAccessToken`
46
+ - `STOREFRONT_CMS_ALLOW_DRAFTS` → `draftContentEnabled`
47
+
48
+ ## Usage
49
+
50
+ Contentstack's page and listing guards are hand-written against a fixed `seo` field shape, not generated
51
+ per-tenant, so `ContentstackCMSService` needs no subclass to fill an abstract method. The boilerplate's
52
+ `src/server/cms/providers/contentstack/service.ts` still wraps it in a thin subclass for consistency with
53
+ the other providers and as the tenant's override point:
54
+
55
+ ```ts
56
+ import { ContentstackCMSService as BaseContentstackCMSService } from '@scayle/storefront-cms-contentstack'
57
+
58
+ export class ContentstackCMSService extends BaseContentstackCMSService {}
59
+
60
+ export const createCMSService = (storefront: StorefrontContext) =>
61
+ new ContentstackCMSService(
62
+ {
63
+ apiKey: getRequiredEnv('CONTENTSTACK_CMS_STACK_API_KEY'),
64
+ deliveryToken: getRequiredEnv('CONTENTSTACK_CMS_DELIVERY_TOKEN'),
65
+ environment: getRequiredEnv('CONTENTSTACK_CMS_ENVIRONMENT'),
66
+ region: process.env.CONTENTSTACK_CMS_REGION,
67
+ branch: process.env.CONTENTSTACK_CMS_BRANCH,
68
+ previewAccessToken: process.env.CONTENTSTACK_CMS_PREVIEW_ACCESS_TOKEN,
69
+ draftContentEnabled:
70
+ process.env.STOREFRONT_CMS_ALLOW_DRAFTS?.toLowerCase() === 'true',
71
+ },
72
+ storefront,
73
+ )
74
+ ```
75
+
76
+ The package never reads `process.env` itself — the boilerplate is the sole place reading environment
77
+ variables, and passes plain values into the constructor.
78
+
79
+ The boilerplate registers `createCMSService(storefront)` as the `cms` slot on the `ServiceRegistry`
80
+ (`src/server/registries.ts`), alongside every other domain. A controller resolves it from there, calls
81
+ `resolveCmsRequest()` (`src/server/cms/utils/request.ts`) for editor-mode detection and draft handling,
82
+ then calls `fetchPageData`/`fetchListingPageData` directly with the raw request:
83
+
84
+ ```ts
85
+ const { isCmsEditorSession, cmsEditorData } = resolveCmsRequest(
86
+ this.services.cms,
87
+ this.ctx.req,
88
+ )
89
+ const pageData = await this.services.cms.fetchPageData(
90
+ slug,
91
+ locale,
92
+ this.ctx.req,
93
+ )
94
+ ```
95
+
96
+ See `@scayle/storefront`'s CMS module for the `CMSProviderService` contract this package implements.
97
+
98
+ ## Architecture
99
+
100
+ `ContentstackCMSService` implements `CMSProviderService` directly — there is no shared base class.
101
+ Each provider package owns its own caching, error handling, and constructor validation, so this package
102
+ can diverge from Storyblok/Contentful without a `@scayle/storefront` release touching them:
103
+
104
+ - **`fetchPageData`** and **`fetchListingPageData`** wrap the raw fetch with application-level caching
105
+ (`context.cache.getOrSet(...)`, 5-minute TTL, bypassed entirely in draft-content mode) under the cache-key
106
+ patterns `cms:contentstack:page:${slug}:${locale}` and `cms:contentstack:plp:${categoryId}:${locale}`.
107
+ `fetchListingPageData` also converts a `CMSContentNotFoundError` from the raw fetch into `undefined`
108
+ (missing PLP content is expected, not an error), logging any other error before rethrowing it.
109
+ - The raw fetches query the `page-component` and `productlistingpage-component` content types by `url`,
110
+ throwing `CMSContentNotFoundError` when no entry matches. The content-type UID and URL come from
111
+ `buildPageQuery`/`buildListingQuery`, both `protected` so a subclass can point at a different content
112
+ type or URL shape without touching live-preview tagging or the shared entry-query chain.
113
+ - A delivery stack is a lazily initialized static singleton, reused across requests. A fresh preview stack
114
+ is constructed per preview request, since `livePreviewQuery()` mutates the stack's internal config and
115
+ cannot be shared. Both go through `@scayle/storefront/cms`'s `wrapClientInit`, so a bad API key or
116
+ environment name fails with a message naming the stack and the config field to check, not a bare SDK
117
+ error. Constructing a preview stack without a configured `previewAccessToken` throws immediately.
118
+ - Live preview requests are tagged with `contentstack.Utils.addEditableTags(...)` so the Visual Builder can
119
+ identify editable fields inline.
120
+ - **`isEditorMode`** detects the Contentstack Live Preview / Visual Builder via the `live_preview` query
121
+ parameter.
122
+ - **`getAdditionalEditorData`** returns the access token, environment, branch, and region the client-side
123
+ Live Preview SDK needs to connect, read from the constructor's config object, since (unlike Storyblok or
124
+ Contentful) Contentstack's editor bridge runs entirely client-side.
125
+ - **Draft-content decision**: controlled by the `draftContentEnabled` boolean in the service config, read from
126
+ the `STOREFRONT_CMS_ALLOW_DRAFTS` environment variable in the boilerplate factory. When both `isEditorMode()`
127
+ returns true and `draftContentEnabled` is true, the service fetches draft content; otherwise it fetches
128
+ published content.
129
+
130
+ **No generated-guard requirement.** Unlike Contentful, Contentstack's page/listing detection
131
+ (`isPageComponent`, `isProductlistingpageComponent` in this package) checks for a fixed `seo` field shape
132
+ rather than delegating to per-tenant generated types, so there is nothing a tenant subclass is required to
133
+ fill in.
134
+
135
+ ## Extending and customizing
136
+
137
+ Within a tenant project, all customization happens in the boilerplate's
138
+ `src/server/cms/providers/contentstack/service.ts`, by subclassing `ContentstackCMSService`:
139
+
140
+ - **Content type and URL shape**: override `buildPageQuery(slug)` or `buildListingQuery(categoryId)` to
141
+ query a different content-type UID or URL field value.
142
+ - **Live preview payload**: override `getAdditionalEditorData` to change which fields the client-side Live
143
+ Preview SDK receives.
144
+ - **CSP rules**: re-export a modified `cspConfig` (imported from this package as a base) if the tenant needs
145
+ additional directives beyond the default wildcard Contentstack domains.
146
+
147
+ Do not reimplement `fetchPageData` or `fetchListingPageData` from scratch. Override only the method that
148
+ needs to change and call `super` for everything else, so the tenant subclass tracks fixes made to this
149
+ package.
@@ -0,0 +1,172 @@
1
+ import { CMSCspConfig, CMSEditorData, CMSPagePayload, CMSProviderService, CMSRequestLike } from "@scayle/storefront/cms";
2
+ import { StorefrontContext } from "@scayle/storefront/types";
3
+ declare module "@scayle/storefront/cms" {
4
+ interface CMSEditorData {
5
+ accessToken?: string;
6
+ environment?: string;
7
+ branch?: string;
8
+ region?: string;
9
+ }
10
+ }
11
+ /**
12
+ * Configuration accepted by {@link ContentstackCMSService}'s constructor.
13
+ * The boilerplate reads these from environment variables and passes plain
14
+ * values in, so this package never touches `process.env` directly.
15
+ */
16
+ interface ContentstackCMSServiceConfig {
17
+ /** Stack API key (Stack > Settings > Tokens). Used as `apiKey` in the Delivery SDK. */
18
+ apiKey: string;
19
+ /** Content Delivery Token for the target environment. */
20
+ deliveryToken: string;
21
+ /** Environment name (e.g. `production`, `staging`). */
22
+ environment: string;
23
+ /**
24
+ * Stack region: `na`, `eu`, `au`, `azure-na`, `azure-eu`, `gcp-na`, or
25
+ * `gcp-eu`. The aliases `us`, `aws-na`, `aws-eu`, and `aws-au` are also
26
+ * accepted. Defaults to `na` when unset or empty. An unrecognized value
27
+ * throws at initialization.
28
+ */
29
+ region?: string;
30
+ /** Optional content branch name. */
31
+ branch?: string;
32
+ /** Preview Token, read lazily on first preview request. */
33
+ previewAccessToken?: string;
34
+ /** Whether to fetch draft content when the request is in editor mode. */
35
+ draftContentEnabled: boolean;
36
+ }
37
+ /**
38
+ * Contentstack CMS provider service.
39
+ * Uses a static singleton for delivery (read-only, safe across requests)
40
+ * and creates a fresh Stack instance per preview request because
41
+ * `livePreviewQuery()` mutates the internal config.
42
+ *
43
+ * Unlike Storyblok and Contentful, this class needs no tenant subclass
44
+ * today — `isPageComponent`/`isProductlistingpageComponent` check a fixed
45
+ * `seo` field shape rather than delegating to generated guards. The
46
+ * boilerplate still keeps a thin `service.ts` wrapper per tenant fork
47
+ * (constructed from env vars via `createCMSService()`), since that file is
48
+ * what `pnpm cms:sync` and future tenant customization would touch, even
49
+ * though it currently has no override to add.
50
+ *
51
+ * @see https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/typescript/get-started-with-typescript-delivery-sdk
52
+ */
53
+ declare class ContentstackCMSService implements CMSProviderService {
54
+ private static deliveryStack;
55
+ readonly cspConfig: CMSCspConfig;
56
+ private readonly config;
57
+ private readonly draftContentEnabled;
58
+ private readonly context;
59
+ constructor(config: ContentstackCMSServiceConfig, context: StorefrontContext);
60
+ /**
61
+ * Builds editor data for the current request.
62
+ * Contentstack's Live Preview SDK runs client-side and needs its own
63
+ * access token, environment, branch, and region to connect.
64
+ *
65
+ * @param request Incoming request, used to detect editor mode
66
+ * @returns Editor data fields for Inertia page props, or undefined outside an editor session
67
+ */
68
+ getCMSEditorData(request: CMSRequestLike): CMSEditorData | undefined;
69
+ private getDeliveryStack;
70
+ private createPreviewStack;
71
+ /**
72
+ * Checks whether the current request originates from the Contentstack
73
+ * Live Preview / Visual Builder. Detects editor mode via the
74
+ * `live_preview` query parameter.
75
+ *
76
+ * @param request Incoming HTTP request
77
+ * @returns True when the request is from Contentstack's live preview
78
+ */
79
+ isEditorMode(request: CMSRequestLike): boolean;
80
+ /**
81
+ * Content-type UID and URL slug queried by `fetchPageData`.
82
+ * Override to look up a different content type or URL shape.
83
+ *
84
+ * @param slug Page slug to look up
85
+ * @returns Content-type UID and the `url` field value to match
86
+ */
87
+ protected buildPageQuery(slug: string): {
88
+ contentTypeUid: string;
89
+ url: string;
90
+ };
91
+ /**
92
+ * Runs a Contentstack entry query for the given content type and URL,
93
+ * returning the first matching entry. Shared by `fetchPageData` and
94
+ * `fetchListingPageData`; override `buildPageQuery`/`buildListingQuery`
95
+ * to change what gets queried, not this method.
96
+ *
97
+ * @param stack Delivery or preview stack
98
+ * @param contentTypeUid Content-type UID to query
99
+ * @param url URL field value to match
100
+ * @param locale Storefront locale code
101
+ * @returns First matching entry, or undefined when none match
102
+ */
103
+ private findEntry;
104
+ /**
105
+ * Retrieves CMS page content from Contentstack by slug.
106
+ * Uses the delivery singleton for normal requests and a fresh preview
107
+ * instance for live preview requests. Tags entries with editable-field
108
+ * metadata when the request is an active live preview session.
109
+ *
110
+ * @param slug Page slug to look up
111
+ * @param locale Storefront locale code
112
+ * @param useDraftContent Whether to fetch draft content instead of published content
113
+ * @param request Incoming request, used to read editor-specific query params
114
+ * @returns Contentstack page data
115
+ * @throws {CMSContentNotFoundError} When no entry matches the slug
116
+ */
117
+ private fetchRawPageData;
118
+ /**
119
+ * Retrieves CMS page data with application-level caching.
120
+ * Preview requests bypass the cache entirely.
121
+ *
122
+ * @param slug CMS slug
123
+ * @param locale Storefront locale code
124
+ * @param request Incoming request, used to detect editor mode and read editor-specific query params
125
+ * @returns Contentstack page data or undefined
126
+ */
127
+ fetchPageData(slug: string, locale: string, request: CMSRequestLike): Promise<CMSPagePayload | undefined>;
128
+ /**
129
+ * Content-type UID and URL slug queried by `fetchListingPageData`.
130
+ * Override to look up a different content type or URL shape.
131
+ *
132
+ * @param categoryId SCAYLE category ID
133
+ * @returns Content-type UID and the `url` field value to match
134
+ */
135
+ protected buildListingQuery(categoryId: number): {
136
+ contentTypeUid: string;
137
+ url: string;
138
+ };
139
+ /**
140
+ * Retrieves CMS content for a product listing page.
141
+ * Throws {@link CMSContentNotFoundError} when no entry matches the
142
+ * category; {@link fetchListingPageData} converts that into `undefined`
143
+ * for the caller.
144
+ *
145
+ * @param categoryId SCAYLE category ID
146
+ * @param locale Storefront locale code
147
+ * @param useDraftContent Whether to fetch draft content instead of published content
148
+ * @returns Contentstack listing page data
149
+ * @throws {CMSContentNotFoundError} When no entry matches the category
150
+ */
151
+ private fetchRawListingPageData;
152
+ /**
153
+ * Retrieves CMS content for a product listing page, with caching.
154
+ * Converts a missing entry into `undefined` rather than throwing, since
155
+ * missing PLP content is expected, not an error condition.
156
+ *
157
+ * @param categoryId SCAYLE category ID
158
+ * @param locale Storefront locale code
159
+ * @param request Incoming request, used to detect editor mode and read editor-specific query params
160
+ * @returns Contentstack listing page data or undefined
161
+ */
162
+ fetchListingPageData(categoryId: number, locale: string, request: CMSRequestLike): Promise<CMSPagePayload | undefined>;
163
+ }
164
+ /**
165
+ * CSP configuration for the Contentstack provider.
166
+ * Allows the Contentstack Visual Builder to embed the storefront in an
167
+ * iframe and the Live Preview SDK to communicate with the Contentstack API.
168
+ * Uses wildcard subdomains for region-agnostic support (EU, US, Azure, GCP).
169
+ */
170
+ declare const cspConfig: CMSCspConfig;
171
+ export { ContentstackCMSService, cspConfig };
172
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/ContentstackCMSService.ts","../src/csp.ts"],"mappings":";;;YAkCmB;IACf;IACA;IACA;IACA;;;;;;;;UAqCa;;EAEf;;EAEA;;EAEA;;;;;;;EAOA;;EAEA;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;cAmBW,kCAAkC;iBAC9B;WAEN,WAAW;mBAEH;mBAKA;mBAEA;EAEjB,YACE,QAAQ,8BACR,SAAS;;;;;;;;;EAoCX,iBAAiB,SAAS,iBAAiB;UAanC;UAsBA;;;;;;;;;EAsCR,aAAa,SAAS;;;;;;;;YAWZ,eAAe;IACvB;IACA;;;;;;;;;;;;;;UAiBY;;;;;;;;;;;;;;UAqCA;;;;;;;;;;EAyEd,cACE,cACA,gBACA,SAAS,iBACR,QAAQ;;;;;;;;YA6BD,kBAAkB;IAC1B;IACA;;;;;;;;;;;;;;UAoBY;;;;;;;;;;;EAsDd,qBACE,oBACA,gBACA,SAAS,iBACR,QAAQ;;;;;;;;cCneA,WAAW"}
package/dist/index.mjs ADDED
@@ -0,0 +1,204 @@
1
+ import contentstack, { QueryOperation } from "@contentstack/delivery-sdk";
2
+ import { getContentstackEndpoints, getRegionForString } from "@timbenniks/contentstack-endpoints";
3
+ import { CMSContentNotFoundError, extractErrorStatus, wrapClientInit } from "@scayle/storefront/cms";
4
+ import { createLogger } from "@scayle/storefront/shared";
5
+ const cspConfig = {
6
+ isPreviewRequest: (req) => Boolean(req.query("live_preview")),
7
+ directives: {
8
+ "frame-ancestors": "'self' https://*.contentstack.com",
9
+ "script-src": "'self' 'unsafe-inline' https://*.contentstack.com",
10
+ "connect-src": "'self' https://*.contentstack.io https://*.contentstack.com"
11
+ }
12
+ };
13
+ const log = createLogger("cms");
14
+ const CACHE_TTL_SECONDS = 300;
15
+ function resolveRegion(region) {
16
+ const resolved = getRegionForString(region);
17
+ if (!resolved) throw new Error(`Contentstack CMS initialization failed: invalid region "${region}". Set CONTENTSTACK_CMS_REGION to one of: na, eu, au, azure-na, azure-eu, gcp-na, gcp-eu. The aliases "us", "aws-na", "aws-eu", and "aws-au" are also accepted. See https://www.contentstack.com/docs/developers/contentstack-regions/api-endpoints`);
18
+ return resolved;
19
+ }
20
+ function getEndpoints(region) {
21
+ return getContentstackEndpoints(resolveRegion(region), true);
22
+ }
23
+ var ContentstackCMSService = class ContentstackCMSService {
24
+ static deliveryStack;
25
+ cspConfig = cspConfig;
26
+ config;
27
+ draftContentEnabled;
28
+ context;
29
+ constructor(config, context) {
30
+ if (!config.apiKey) throw new Error("Contentstack CMS initialization failed: missing or empty API key. Check that CONTENTSTACK_CMS_STACK_API_KEY is set.");
31
+ if (!config.deliveryToken) throw new Error("Contentstack CMS initialization failed: missing or empty delivery token. Check that CONTENTSTACK_CMS_DELIVERY_TOKEN is set.");
32
+ if (!config.environment) throw new Error("Contentstack CMS initialization failed: missing or empty environment. Check that CONTENTSTACK_CMS_ENVIRONMENT is set.");
33
+ this.config = {
34
+ ...config,
35
+ region: resolveRegion(config.region || "na")
36
+ };
37
+ this.draftContentEnabled = config.draftContentEnabled;
38
+ this.context = context;
39
+ }
40
+ getCMSEditorData(request) {
41
+ if (!this.isEditorMode(request)) return;
42
+ return {
43
+ accessToken: this.config.deliveryToken,
44
+ environment: this.config.environment,
45
+ branch: this.config.branch,
46
+ region: this.config.region
47
+ };
48
+ }
49
+ getDeliveryStack() {
50
+ if (ContentstackCMSService.deliveryStack) return ContentstackCMSService.deliveryStack;
51
+ const { apiKey, deliveryToken, environment, region } = this.config;
52
+ const endpoints = getEndpoints(region);
53
+ ContentstackCMSService.deliveryStack = wrapClientInit(() => contentstack.stack({
54
+ apiKey,
55
+ deliveryToken,
56
+ environment,
57
+ region,
58
+ host: endpoints.contentDelivery
59
+ }), "Contentstack delivery stack initialization failed. Check that CONTENTSTACK_CMS_STACK_API_KEY is a valid API key, CONTENTSTACK_CMS_DELIVERY_TOKEN is a valid delivery token, and CONTENTSTACK_CMS_ENVIRONMENT is a valid environment name");
60
+ return ContentstackCMSService.deliveryStack;
61
+ }
62
+ createPreviewStack() {
63
+ const { apiKey, deliveryToken, environment, region, previewAccessToken } = this.config;
64
+ if (!previewAccessToken) throw new Error("Contentstack CMS initialization failed: missing or empty preview token. Check that CONTENTSTACK_CMS_PREVIEW_ACCESS_TOKEN is set.");
65
+ const endpoints = getEndpoints(region);
66
+ return wrapClientInit(() => contentstack.stack({
67
+ apiKey,
68
+ deliveryToken,
69
+ environment,
70
+ region,
71
+ host: endpoints.contentDelivery,
72
+ live_preview: {
73
+ enable: true,
74
+ preview_token: previewAccessToken,
75
+ host: endpoints.preview
76
+ }
77
+ }), "Contentstack preview stack initialization failed. Check that CONTENTSTACK_CMS_STACK_API_KEY is a valid API key, CONTENTSTACK_CMS_DELIVERY_TOKEN is a valid delivery token, CONTENTSTACK_CMS_PREVIEW_ACCESS_TOKEN is a valid preview token, and CONTENTSTACK_CMS_ENVIRONMENT is a valid environment name");
78
+ }
79
+ isEditorMode(request) {
80
+ return Boolean(request.query("live_preview"));
81
+ }
82
+ buildPageQuery(slug) {
83
+ return {
84
+ contentTypeUid: "page-component",
85
+ url: `/${slug}`
86
+ };
87
+ }
88
+ async findEntry(stack, contentTypeUid, url, locale) {
89
+ return (await stack.contentType(contentTypeUid).entry().locale(locale.toLowerCase()).includeFallback().query().where("url", QueryOperation.EQUALS, url).addParams({
90
+ include_all: "true",
91
+ include_all_depth: "5",
92
+ include_dimension: "true"
93
+ }).limit(1).find()).entries?.at(0);
94
+ }
95
+ async fetchRawPageData(slug, locale, useDraftContent, request) {
96
+ const stack = useDraftContent ? this.createPreviewStack() : this.getDeliveryStack();
97
+ const { contentTypeUid, url } = this.buildPageQuery(slug);
98
+ const livePreview = useDraftContent && request.query("live_preview");
99
+ if (livePreview) stack.livePreviewQuery({
100
+ live_preview: livePreview,
101
+ content_type_uid: request.query("content_type_uid") ?? contentTypeUid
102
+ });
103
+ try {
104
+ const entry = await this.findEntry(stack, contentTypeUid, url, locale);
105
+ if (!entry) throw new CMSContentNotFoundError("Contentstack content not found", { details: {
106
+ slug,
107
+ locale,
108
+ contentTypeUid,
109
+ url
110
+ } });
111
+ if (livePreview) try {
112
+ contentstack.Utils.addEditableTags(entry, contentTypeUid, true, locale.toLowerCase());
113
+ } catch {}
114
+ return { entry };
115
+ } catch (error) {
116
+ if (error instanceof CMSContentNotFoundError) throw error;
117
+ if (extractErrorStatus(error) === 404) throw new CMSContentNotFoundError("Contentstack content not found", {
118
+ cause: error,
119
+ details: {
120
+ slug,
121
+ locale,
122
+ contentTypeUid,
123
+ url
124
+ }
125
+ });
126
+ const cause = error instanceof Error ? error : new Error(String(error));
127
+ throw new Error(`Contentstack API request failed (slug: "${slug}", locale: "${locale}", content-type: "${contentTypeUid}"): ${cause.message}`, { cause });
128
+ }
129
+ }
130
+ async fetchPageData(slug, locale, request) {
131
+ const useDraftContent = this.isEditorMode(request) && this.draftContentEnabled;
132
+ if (useDraftContent) {
133
+ log.debug({
134
+ message: "Bypassing CMS cache in preview mode",
135
+ slug,
136
+ locale
137
+ });
138
+ return await this.fetchRawPageData(slug, locale, useDraftContent, request);
139
+ }
140
+ return await this.context.cache.getOrSet(`cms:contentstack:page:${slug}:${locale}`, () => this.fetchRawPageData(slug, locale, useDraftContent, request), CACHE_TTL_SECONDS);
141
+ }
142
+ buildListingQuery(categoryId) {
143
+ return {
144
+ contentTypeUid: "productlistingpage-component",
145
+ url: `/c/c-${categoryId}`
146
+ };
147
+ }
148
+ async fetchRawListingPageData(categoryId, locale, useDraftContent) {
149
+ const stack = useDraftContent ? this.createPreviewStack() : this.getDeliveryStack();
150
+ const { contentTypeUid, url } = this.buildListingQuery(categoryId);
151
+ try {
152
+ const entry = await this.findEntry(stack, contentTypeUid, url, locale);
153
+ if (!entry) throw new CMSContentNotFoundError("Contentstack listing content not found", { details: {
154
+ categoryId,
155
+ slug: url,
156
+ locale,
157
+ contentTypeUid
158
+ } });
159
+ return { entry };
160
+ } catch (error) {
161
+ if (error instanceof CMSContentNotFoundError) throw error;
162
+ if (extractErrorStatus(error) === 404) throw new CMSContentNotFoundError("Contentstack listing content not found", {
163
+ cause: error,
164
+ details: {
165
+ categoryId,
166
+ slug: url,
167
+ locale,
168
+ contentTypeUid
169
+ }
170
+ });
171
+ const cause = error instanceof Error ? error : new Error(String(error));
172
+ throw new Error(`Contentstack API request failed (category: ${categoryId}, url: "${url}", locale: "${locale}", content-type: "${contentTypeUid}"): ${cause.message}`, { cause });
173
+ }
174
+ }
175
+ async fetchListingPageData(categoryId, locale, request) {
176
+ const useDraftContent = this.isEditorMode(request) && this.draftContentEnabled;
177
+ const fetchListing = async () => {
178
+ try {
179
+ return await this.fetchRawListingPageData(categoryId, locale, useDraftContent);
180
+ } catch (error) {
181
+ if (error instanceof CMSContentNotFoundError) return;
182
+ log.error({
183
+ message: "Failed to fetch CMS listing page data",
184
+ err: error instanceof Error ? error : new Error(String(error)),
185
+ categoryId,
186
+ locale
187
+ });
188
+ throw error;
189
+ }
190
+ };
191
+ if (useDraftContent) {
192
+ log.debug({
193
+ message: "Bypassing CMS PLP cache in preview mode",
194
+ categoryId,
195
+ locale
196
+ });
197
+ return await fetchListing();
198
+ }
199
+ return await this.context.cache.getOrSet(`cms:contentstack:plp:${categoryId}:${locale}`, () => fetchListing(), CACHE_TTL_SECONDS);
200
+ }
201
+ };
202
+ export { ContentstackCMSService, cspConfig };
203
+
204
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/csp.ts","../src/ContentstackCMSService.ts"],"sourcesContent":["import type { CMSCspConfig } from '@scayle/storefront/cms'\n\n/**\n * CSP configuration for the Contentstack provider.\n * Allows the Contentstack Visual Builder to embed the storefront in an\n * iframe and the Live Preview SDK to communicate with the Contentstack API.\n * Uses wildcard subdomains for region-agnostic support (EU, US, Azure, GCP).\n */\nexport const cspConfig: CMSCspConfig = {\n isPreviewRequest: (req) => Boolean(req.query('live_preview')),\n directives: {\n 'frame-ancestors': \"'self' https://*.contentstack.com\",\n 'script-src': \"'self' 'unsafe-inline' https://*.contentstack.com\",\n 'connect-src':\n \"'self' https://*.contentstack.io https://*.contentstack.com\",\n },\n}\n","import contentstack, { QueryOperation } from '@contentstack/delivery-sdk'\nimport type { Stack } from '@contentstack/delivery-sdk'\nimport {\n getContentstackEndpoints,\n getRegionForString,\n} from '@timbenniks/contentstack-endpoints'\nimport {\n CMSContentNotFoundError,\n extractErrorStatus,\n wrapClientInit,\n} from '@scayle/storefront/cms'\nimport type {\n CMSCspConfig,\n CMSEditorData,\n CMSPagePayload,\n CMSProviderService,\n CMSRequestLike,\n} from '@scayle/storefront/cms'\nimport { createLogger } from '@scayle/storefront/shared'\nimport type { StorefrontContext } from '@scayle/storefront/types'\nimport { cspConfig } from './csp'\n\nconst log = createLogger('cms')\n\n/** Cache TTL for published CMS content: 5 minutes. */\nconst CACHE_TTL_SECONDS = 5 * 60\n\ndeclare module '@scayle/storefront/cms' {\n /**\n * Fields added by `ContentstackCMSService.getCMSEditorData()` for the\n * client-side Live Preview SDK. A tenant project can layer further fields\n * onto `CMSEditorData` with its own `declare module` block; TypeScript\n * merges augmentations from every file in the program.\n */\n export interface CMSEditorData {\n accessToken?: string\n environment?: string\n branch?: string\n region?: string\n }\n}\n\n/**\n * Resolves a configured region string to a Contentstack region identifier.\n *\n * `getContentstackEndpoints` returns an empty object rather than throwing for an\n * unknown region, which would leave `host` undefined and let the Delivery SDK\n * silently fall back to its North America default. Resolving up front turns a\n * misconfigured region into a startup error instead of wrong-region content.\n *\n * @param region Configured region value\n * @returns Canonical region identifier\n * @throws Error when the region is not a recognized Contentstack region\n */\nfunction resolveRegion(region: string): string {\n const resolved = getRegionForString(region)\n\n if (!resolved) {\n throw new Error(\n `Contentstack CMS initialization failed: invalid region \"${region}\". Set CONTENTSTACK_CMS_REGION to one of: na, eu, au, azure-na, azure-eu, gcp-na, gcp-eu. The aliases \"us\", \"aws-na\", \"aws-eu\", and \"aws-au\" are also accepted. See https://www.contentstack.com/docs/developers/contentstack-regions/api-endpoints`,\n )\n }\n\n return resolved\n}\n\nfunction getEndpoints(region: string) {\n return getContentstackEndpoints(resolveRegion(region), true)\n}\n\n/**\n * Configuration accepted by {@link ContentstackCMSService}'s constructor.\n * The boilerplate reads these from environment variables and passes plain\n * values in, so this package never touches `process.env` directly.\n */\nexport interface ContentstackCMSServiceConfig {\n /** Stack API key (Stack > Settings > Tokens). Used as `apiKey` in the Delivery SDK. */\n apiKey: string\n /** Content Delivery Token for the target environment. */\n deliveryToken: string\n /** Environment name (e.g. `production`, `staging`). */\n environment: string\n /**\n * Stack region: `na`, `eu`, `au`, `azure-na`, `azure-eu`, `gcp-na`, or\n * `gcp-eu`. The aliases `us`, `aws-na`, `aws-eu`, and `aws-au` are also\n * accepted. Defaults to `na` when unset or empty. An unrecognized value\n * throws at initialization.\n */\n region?: string\n /** Optional content branch name. */\n branch?: string\n /** Preview Token, read lazily on first preview request. */\n previewAccessToken?: string\n /** Whether to fetch draft content when the request is in editor mode. */\n draftContentEnabled: boolean\n}\n\n/**\n * Contentstack CMS provider service.\n * Uses a static singleton for delivery (read-only, safe across requests)\n * and creates a fresh Stack instance per preview request because\n * `livePreviewQuery()` mutates the internal config.\n *\n * Unlike Storyblok and Contentful, this class needs no tenant subclass\n * today — `isPageComponent`/`isProductlistingpageComponent` check a fixed\n * `seo` field shape rather than delegating to generated guards. The\n * boilerplate still keeps a thin `service.ts` wrapper per tenant fork\n * (constructed from env vars via `createCMSService()`), since that file is\n * what `pnpm cms:sync` and future tenant customization would touch, even\n * though it currently has no override to add.\n *\n * @see https://www.contentstack.com/docs/developers/sdks/content-delivery-sdk/typescript/get-started-with-typescript-delivery-sdk\n */\nexport class ContentstackCMSService implements CMSProviderService {\n private static deliveryStack: Stack | undefined\n\n readonly cspConfig: CMSCspConfig = cspConfig\n\n private readonly config: Required<\n Omit<ContentstackCMSServiceConfig, 'branch' | 'previewAccessToken'>\n > &\n Pick<ContentstackCMSServiceConfig, 'branch' | 'previewAccessToken'>\n\n private readonly draftContentEnabled: boolean\n\n private readonly context: StorefrontContext\n\n constructor(\n config: ContentstackCMSServiceConfig,\n context: StorefrontContext,\n ) {\n if (!config.apiKey) {\n throw new Error(\n 'Contentstack CMS initialization failed: missing or empty API key. Check that CONTENTSTACK_CMS_STACK_API_KEY is set.',\n )\n }\n\n if (!config.deliveryToken) {\n throw new Error(\n 'Contentstack CMS initialization failed: missing or empty delivery token. Check that CONTENTSTACK_CMS_DELIVERY_TOKEN is set.',\n )\n }\n\n if (!config.environment) {\n throw new Error(\n 'Contentstack CMS initialization failed: missing or empty environment. Check that CONTENTSTACK_CMS_ENVIRONMENT is set.',\n )\n }\n\n // `||` not `??`: a blanked `CONTENTSTACK_CMS_REGION=` arrives as an empty\n // string, which is \"not set\" rather than a typo. This matches the `region ||\n // 'na'` in the setup CLI and `${CONTENTSTACK_CMS_REGION:-na}` in sync-cms.sh.\n this.config = { ...config, region: resolveRegion(config.region || 'na') }\n this.draftContentEnabled = config.draftContentEnabled\n this.context = context\n }\n\n /**\n * Builds editor data for the current request.\n * Contentstack's Live Preview SDK runs client-side and needs its own\n * access token, environment, branch, and region to connect.\n *\n * @param request Incoming request, used to detect editor mode\n * @returns Editor data fields for Inertia page props, or undefined outside an editor session\n */\n getCMSEditorData(request: CMSRequestLike): CMSEditorData | undefined {\n if (!this.isEditorMode(request)) {\n return undefined\n }\n\n return {\n accessToken: this.config.deliveryToken,\n environment: this.config.environment,\n branch: this.config.branch,\n region: this.config.region,\n }\n }\n\n private getDeliveryStack(): Stack {\n if (ContentstackCMSService.deliveryStack) {\n return ContentstackCMSService.deliveryStack\n }\n\n const { apiKey, deliveryToken, environment, region } = this.config\n const endpoints = getEndpoints(region)\n\n ContentstackCMSService.deliveryStack = wrapClientInit(\n () =>\n contentstack.stack({\n apiKey,\n deliveryToken,\n environment,\n region,\n host: endpoints.contentDelivery,\n }),\n 'Contentstack delivery stack initialization failed. Check that CONTENTSTACK_CMS_STACK_API_KEY is a valid API key, CONTENTSTACK_CMS_DELIVERY_TOKEN is a valid delivery token, and CONTENTSTACK_CMS_ENVIRONMENT is a valid environment name',\n )\n return ContentstackCMSService.deliveryStack\n }\n\n private createPreviewStack(): Stack {\n const { apiKey, deliveryToken, environment, region, previewAccessToken } =\n this.config\n\n if (!previewAccessToken) {\n throw new Error(\n 'Contentstack CMS initialization failed: missing or empty preview token. Check that CONTENTSTACK_CMS_PREVIEW_ACCESS_TOKEN is set.',\n )\n }\n\n const endpoints = getEndpoints(region)\n\n return wrapClientInit(\n () =>\n contentstack.stack({\n apiKey,\n deliveryToken,\n environment,\n region,\n host: endpoints.contentDelivery,\n live_preview: {\n enable: true,\n preview_token: previewAccessToken,\n host: endpoints.preview,\n },\n }),\n 'Contentstack preview stack initialization failed. Check that CONTENTSTACK_CMS_STACK_API_KEY is a valid API key, CONTENTSTACK_CMS_DELIVERY_TOKEN is a valid delivery token, CONTENTSTACK_CMS_PREVIEW_ACCESS_TOKEN is a valid preview token, and CONTENTSTACK_CMS_ENVIRONMENT is a valid environment name',\n )\n }\n\n /**\n * Checks whether the current request originates from the Contentstack\n * Live Preview / Visual Builder. Detects editor mode via the\n * `live_preview` query parameter.\n *\n * @param request Incoming HTTP request\n * @returns True when the request is from Contentstack's live preview\n */\n isEditorMode(request: CMSRequestLike): boolean {\n return Boolean(request.query('live_preview'))\n }\n\n /**\n * Content-type UID and URL slug queried by `fetchPageData`.\n * Override to look up a different content type or URL shape.\n *\n * @param slug Page slug to look up\n * @returns Content-type UID and the `url` field value to match\n */\n protected buildPageQuery(slug: string): {\n contentTypeUid: string\n url: string\n } {\n return { contentTypeUid: 'page-component', url: `/${slug}` }\n }\n\n /**\n * Runs a Contentstack entry query for the given content type and URL,\n * returning the first matching entry. Shared by `fetchPageData` and\n * `fetchListingPageData`; override `buildPageQuery`/`buildListingQuery`\n * to change what gets queried, not this method.\n *\n * @param stack Delivery or preview stack\n * @param contentTypeUid Content-type UID to query\n * @param url URL field value to match\n * @param locale Storefront locale code\n * @returns First matching entry, or undefined when none match\n */\n private async findEntry(\n stack: Stack,\n contentTypeUid: string,\n url: string,\n locale: string,\n ): Promise<unknown | undefined> {\n const result = await stack\n .contentType(contentTypeUid)\n .entry()\n .locale(locale.toLowerCase())\n .includeFallback()\n .query()\n .where('url', QueryOperation.EQUALS, url)\n .addParams({\n include_all: 'true',\n include_all_depth: '5',\n include_dimension: 'true',\n })\n .limit(1)\n .find()\n\n return result.entries?.at(0)\n }\n\n /**\n * Retrieves CMS page content from Contentstack by slug.\n * Uses the delivery singleton for normal requests and a fresh preview\n * instance for live preview requests. Tags entries with editable-field\n * metadata when the request is an active live preview session.\n *\n * @param slug Page slug to look up\n * @param locale Storefront locale code\n * @param useDraftContent Whether to fetch draft content instead of published content\n * @param request Incoming request, used to read editor-specific query params\n * @returns Contentstack page data\n * @throws {CMSContentNotFoundError} When no entry matches the slug\n */\n private async fetchRawPageData(\n slug: string,\n locale: string,\n useDraftContent: boolean,\n request: CMSRequestLike,\n ): Promise<CMSPagePayload | undefined> {\n const stack = useDraftContent\n ? this.createPreviewStack()\n : this.getDeliveryStack()\n const { contentTypeUid, url } = this.buildPageQuery(slug)\n const livePreview = useDraftContent && request.query('live_preview')\n\n if (livePreview) {\n stack.livePreviewQuery({\n live_preview: livePreview,\n content_type_uid: request.query('content_type_uid') ?? contentTypeUid,\n })\n }\n\n try {\n const entry = await this.findEntry(stack, contentTypeUid, url, locale)\n\n if (!entry) {\n throw new CMSContentNotFoundError('Contentstack content not found', {\n details: { slug, locale, contentTypeUid, url },\n })\n }\n\n if (livePreview) {\n try {\n contentstack.Utils.addEditableTags(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n entry as any,\n contentTypeUid,\n true,\n locale.toLowerCase(),\n )\n } catch {\n // Editable tags only add inline edit markers for the Visual\n // Builder; a tagging failure must not fail the page request itself.\n }\n }\n\n return { entry }\n } catch (error) {\n if (error instanceof CMSContentNotFoundError) {\n throw error\n }\n\n if (extractErrorStatus(error) === 404) {\n throw new CMSContentNotFoundError('Contentstack content not found', {\n cause: error,\n details: { slug, locale, contentTypeUid, url },\n })\n }\n\n const cause = error instanceof Error ? error : new Error(String(error))\n throw new Error(\n `Contentstack API request failed (slug: \"${slug}\", locale: \"${locale}\", content-type: \"${contentTypeUid}\"): ${cause.message}`,\n { cause },\n )\n }\n }\n\n /**\n * Retrieves CMS page data with application-level caching.\n * Preview requests bypass the cache entirely.\n *\n * @param slug CMS slug\n * @param locale Storefront locale code\n * @param request Incoming request, used to detect editor mode and read editor-specific query params\n * @returns Contentstack page data or undefined\n */\n async fetchPageData(\n slug: string,\n locale: string,\n request: CMSRequestLike,\n ): Promise<CMSPagePayload | undefined> {\n const useDraftContent =\n this.isEditorMode(request) && this.draftContentEnabled\n\n if (useDraftContent) {\n log.debug({\n message: 'Bypassing CMS cache in preview mode',\n slug,\n locale,\n })\n return await this.fetchRawPageData(slug, locale, useDraftContent, request)\n }\n\n return await this.context.cache.getOrSet(\n `cms:contentstack:page:${slug}:${locale}`,\n // Cast required: StorageValue excludes undefined, but the fetch can return undefined for missing content\n () =>\n this.fetchRawPageData(slug, locale, useDraftContent, request) as never,\n CACHE_TTL_SECONDS,\n )\n }\n\n /**\n * Content-type UID and URL slug queried by `fetchListingPageData`.\n * Override to look up a different content type or URL shape.\n *\n * @param categoryId SCAYLE category ID\n * @returns Content-type UID and the `url` field value to match\n */\n protected buildListingQuery(categoryId: number): {\n contentTypeUid: string\n url: string\n } {\n return {\n contentTypeUid: 'productlistingpage-component',\n url: `/c/c-${categoryId}`,\n }\n }\n\n /**\n * Retrieves CMS content for a product listing page.\n * Throws {@link CMSContentNotFoundError} when no entry matches the\n * category; {@link fetchListingPageData} converts that into `undefined`\n * for the caller.\n *\n * @param categoryId SCAYLE category ID\n * @param locale Storefront locale code\n * @param useDraftContent Whether to fetch draft content instead of published content\n * @returns Contentstack listing page data\n * @throws {CMSContentNotFoundError} When no entry matches the category\n */\n private async fetchRawListingPageData(\n categoryId: number,\n locale: string,\n useDraftContent: boolean,\n ): Promise<CMSPagePayload | undefined> {\n const stack = useDraftContent\n ? this.createPreviewStack()\n : this.getDeliveryStack()\n const { contentTypeUid, url } = this.buildListingQuery(categoryId)\n\n try {\n const entry = await this.findEntry(stack, contentTypeUid, url, locale)\n\n if (!entry) {\n throw new CMSContentNotFoundError(\n 'Contentstack listing content not found',\n { details: { categoryId, slug: url, locale, contentTypeUid } },\n )\n }\n\n return { entry }\n } catch (error) {\n if (error instanceof CMSContentNotFoundError) {\n throw error\n }\n\n if (extractErrorStatus(error) === 404) {\n throw new CMSContentNotFoundError(\n 'Contentstack listing content not found',\n {\n cause: error,\n details: { categoryId, slug: url, locale, contentTypeUid },\n },\n )\n }\n\n const cause = error instanceof Error ? error : new Error(String(error))\n throw new Error(\n `Contentstack API request failed (category: ${categoryId}, url: \"${url}\", locale: \"${locale}\", content-type: \"${contentTypeUid}\"): ${cause.message}`,\n { cause },\n )\n }\n }\n\n /**\n * Retrieves CMS content for a product listing page, with caching.\n * Converts a missing entry into `undefined` rather than throwing, since\n * missing PLP content is expected, not an error condition.\n *\n * @param categoryId SCAYLE category ID\n * @param locale Storefront locale code\n * @param request Incoming request, used to detect editor mode and read editor-specific query params\n * @returns Contentstack listing page data or undefined\n */\n async fetchListingPageData(\n categoryId: number,\n locale: string,\n request: CMSRequestLike,\n ): Promise<CMSPagePayload | undefined> {\n const useDraftContent =\n this.isEditorMode(request) && this.draftContentEnabled\n const fetchListing = async () => {\n try {\n return await this.fetchRawListingPageData(\n categoryId,\n locale,\n useDraftContent,\n )\n } catch (error) {\n if (error instanceof CMSContentNotFoundError) {\n return undefined\n }\n\n log.error({\n message: 'Failed to fetch CMS listing page data',\n err: error instanceof Error ? error : new Error(String(error)),\n categoryId,\n locale,\n })\n\n throw error\n }\n }\n\n if (useDraftContent) {\n log.debug({\n message: 'Bypassing CMS PLP cache in preview mode',\n categoryId,\n locale,\n })\n return await fetchListing()\n }\n\n return await this.context.cache.getOrSet(\n `cms:contentstack:plp:${categoryId}:${locale}`,\n // Cast required: StorageValue excludes undefined, but the fetch can return undefined for missing content\n () => fetchListing() as never,\n CACHE_TTL_SECONDS,\n )\n }\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@scayle/storefront-cms-contentstack",
3
+ "version": "0.2.0-alpha.0",
4
+ "description": "Contentstack CMS provider integration for the SCAYLE Storefront Application V3",
5
+ "author": "SCAYLE Commerce Engine",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "private": false,
9
+ "sideEffects": false,
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ }
15
+ },
16
+ "main": "./dist/index.mjs",
17
+ "files": [
18
+ "CHANGELOG.md",
19
+ "dist"
20
+ ],
21
+ "engines": {
22
+ "node": ">= 24.0.0"
23
+ },
24
+ "peerDependencies": {
25
+ "@contentstack/delivery-sdk": "^5.4.0",
26
+ "@timbenniks/contentstack-endpoints": "^3.0.2",
27
+ "@scayle/storefront": "1.0.0-alpha.2"
28
+ },
29
+ "devDependencies": {
30
+ "@arethetypeswrong/cli": "0.18.5",
31
+ "@contentstack/delivery-sdk": "^5.4.0",
32
+ "@timbenniks/contentstack-endpoints": "^3.0.2",
33
+ "@types/node": "^24",
34
+ "@vitest/coverage-v8": "4.1.10",
35
+ "eslint": "10.8.1",
36
+ "eslint-formatter-gitlab": "7.2.0",
37
+ "publint": "0.3.23",
38
+ "typescript": "6.0.3",
39
+ "obuild": "0.4.38",
40
+ "vitest": "4.1.10",
41
+ "@scayle/eslint-config-storefront": "4.8.3-alpha.0",
42
+ "@scayle/storefront": "1.0.0-alpha.2",
43
+ "@scayle/vitest-config-storefront": "1.0.0"
44
+ },
45
+ "scripts": {
46
+ "build": "obuild",
47
+ "lint": "eslint .",
48
+ "lint:ci": "eslint . --format gitlab",
49
+ "lint:fix": "eslint . --fix",
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "vitest run",
52
+ "package:lint": "publint",
53
+ "verify-packaging": "attw --pack . --profile esm-only"
54
+ }
55
+ }