@ecomiq/storefront 2.0.22 → 2.1.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomiq/storefront",
3
- "version": "2.0.22",
3
+ "version": "2.1.3",
4
4
  "description": "Micro framework SSR de Ecomiq para storefronts. Páginas, datos y deploy sin configurar nada.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,6 +38,8 @@
38
38
  "./internal/page": "./src/runtime/page.tsx",
39
39
  "./internal/context": "./src/runtime/context.ts",
40
40
  "./internal/server-config": "./src/server-config-store.ts",
41
+ "./internal/runtime": "./src/storefront-runtime.ts",
42
+ "./internal/definition-data": "./src/storefront-definition/data.ts",
41
43
  "./internal/health": "./src/health.ts",
42
44
  "./internal/head": "./src/runtime/head.ts",
43
45
  "./internal/default-layout": "./src/runtime/default-layout.tsx",
@@ -84,7 +86,7 @@
84
86
  "vite": "^8.1.0",
85
87
  "wrangler": "^4.100.0",
86
88
  "zod": "^3.25.0",
87
- "@ecomiq/core": "^0.1.8",
89
+ "@ecomiq/core": "^0.1.9",
88
90
  "@ecomiq/storefront-definition": "^1.0.0"
89
91
  },
90
92
  "peerDependencies": {
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  ApiError,
3
- cartVariantIdFromProduct,
4
3
  type RemoveCartItemParams,
5
4
  type StorefrontCart,
6
5
  type StorefrontClient,
@@ -11,7 +10,7 @@ import { z } from 'zod'
11
10
  type CartClient = Pick<StorefrontClient, 'getCart' | 'createCart' | 'addCartItem'>
12
11
 
13
12
  export interface LoadActiveCartInput {
14
- cartId: string
13
+ cartToken: string
15
14
  }
16
15
 
17
16
  export type ActiveCartLoadResult =
@@ -35,14 +34,14 @@ export type CartItemMutationResult =
35
34
  }
36
35
 
37
36
  export interface AddProductToCartInput {
38
- cartId?: string
37
+ cartToken?: string
39
38
  currencyCode: string
40
39
  variantId: string
41
40
  quantity: number
42
41
  }
43
42
 
44
43
  export interface AddProductToCartResult {
45
- cartId: string
44
+ cartToken: string
46
45
  cart: StorefrontCart
47
46
  created: boolean
48
47
  }
@@ -118,7 +117,7 @@ export async function loadActiveCart(
118
117
  input: LoadActiveCartInput,
119
118
  ): Promise<ActiveCartLoadResult> {
120
119
  try {
121
- const cart = await client.getCart({ cartId: input.cartId })
120
+ const cart = await client.getCart({ cartToken: input.cartToken })
122
121
  return { ok: true as const, value: cartIsActive(cart) ? cart : null }
123
122
  } catch (error) {
124
123
  if (cartIsUnavailable(error)) return { ok: true as const, value: null }
@@ -165,13 +164,12 @@ export async function addProductToCart(
165
164
  client: CartClient,
166
165
  input: AddProductToCartInput,
167
166
  ): Promise<AddProductToCartResult> {
168
- const variantId = cartVariantIdFromProduct(input.variantId)
169
167
  let current: StorefrontCart | undefined
170
168
  let created = false
171
169
 
172
- if (input.cartId) {
170
+ if (input.cartToken) {
173
171
  try {
174
- const persisted = await client.getCart({ cartId: input.cartId })
172
+ const persisted = await client.getCart({ cartToken: input.cartToken })
175
173
  if (cartIsActive(persisted)) current = persisted
176
174
  } catch (error) {
177
175
  if (!cartIsUnavailable(error)) throw error
@@ -183,20 +181,20 @@ export async function addProductToCart(
183
181
  created = true
184
182
  }
185
183
 
186
- const add = (cartId: string) => client.addCartItem({
187
- cartId,
188
- variantId,
184
+ const add = (cartToken: string) => client.addCartItem({
185
+ cartToken,
186
+ variantId: input.variantId,
189
187
  quantity: input.quantity,
190
188
  })
191
189
 
192
190
  try {
193
- const cart = await add(current.id)
194
- return { cartId: cart.id, cart, created }
191
+ const cart = await add(current.token)
192
+ return { cartToken: cart.token, cart, created }
195
193
  } catch (error) {
196
194
  if (!cartIsUnavailable(error)) throw error
197
195
  const replacement = await client.createCart({ currencyCode: input.currencyCode })
198
- const cart = await add(replacement.id)
199
- return { cartId: cart.id, cart, created: true }
196
+ const cart = await add(replacement.token)
197
+ return { cartToken: cart.token, cart, created: true }
200
198
  }
201
199
  }
202
200
 
package/src/client.ts CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  createClient as createCoreClient,
3
3
  type CreateApiOptions,
4
4
  type StorefrontClient,
5
- } from '@ecomiq/core'
5
+ } from '@ecomiq/core/client'
6
6
  import { getServerConfig } from './server-config-store'
7
7
 
8
8
  let sharedClient: StorefrontClient | null = null
@@ -27,6 +27,8 @@ export const ActiveStorefrontRuntimeSchema = z
27
27
  sequence: z.number().int().nonnegative(),
28
28
  releaseSequence: z.number().int().nonnegative(),
29
29
  publishedAt: z.string().datetime({ offset: true }),
30
+ runtimeReleaseId: z.string().optional(),
31
+ artifactDigest: z.string().optional(),
30
32
  })
31
33
  .strict(),
32
34
  definition: StorefrontDefinitionSchema,
@@ -37,6 +39,32 @@ export type ActiveStorefrontRuntime = z.infer<
37
39
  typeof ActiveStorefrontRuntimeSchema
38
40
  >
39
41
 
42
+ export const EnvironmentPointerSchema = z
43
+ .object({
44
+ schemaVersion: z.literal(1),
45
+ storeSlug: CloudRuntimeStoreSlugSchema,
46
+ projectId: z.string().min(1),
47
+ environmentId: z.string().min(1),
48
+ generation: z.number().int().nonnegative(),
49
+ releaseSequence: z.number().int().nonnegative(),
50
+ deploymentSequence: z.number().int().nonnegative(),
51
+ artifactDigest: z.string().min(1),
52
+ artifactKey: z.string().min(1),
53
+ manifestDigest: z.string().min(1),
54
+ manifestKey: z.string().min(1),
55
+ runtimeReleaseId: z.string().min(1),
56
+ publishedAt: z.string().datetime({ offset: true }),
57
+ cachePolicy: z
58
+ .object({
59
+ pointerMaxAgeSeconds: z.number().int().nonnegative(),
60
+ staleIfErrorSeconds: z.number().int().nonnegative(),
61
+ })
62
+ .strict(),
63
+ })
64
+ .strict()
65
+
66
+ export type EnvironmentPointer = z.infer<typeof EnvironmentPointerSchema>
67
+
40
68
  export type CloudRuntimeFetch = (
41
69
  input: RequestInfo | URL,
42
70
  init?: RequestInit,
@@ -44,6 +72,7 @@ export type CloudRuntimeFetch = (
44
72
 
45
73
  export interface CloudRuntimeClientOptions {
46
74
  baseUrl: string
75
+ artifactBaseUrl?: string
47
76
  timeoutMs?: number
48
77
  cacheTtlMs?: number
49
78
  fetch?: CloudRuntimeFetch
@@ -70,14 +99,22 @@ const DEFAULT_TIMEOUT_MS = 3_000
70
99
  const DEFAULT_CACHE_TTL_MS = 15_000
71
100
  const MAX_CACHE_ENTRIES = 100
72
101
 
102
+ interface LastKnownGood {
103
+ runtime: ActiveStorefrontRuntime
104
+ generation: number
105
+ }
106
+
73
107
  export class CloudRuntimeClient {
74
108
  readonly baseUrl: string
109
+ readonly artifactBaseUrl: string | null
75
110
  readonly timeoutMs: number
76
111
  readonly cacheTtlMs: number
77
112
 
78
113
  private readonly fetchRuntime: CloudRuntimeFetch
79
114
  private readonly now: () => number
80
115
  private readonly cache = new Map<string, RuntimeCacheEntry>()
116
+ private readonly lastKnownGood = new Map<string, LastKnownGood>()
117
+ private readonly artifactCache = new Map<string, ActiveStorefrontRuntime>()
81
118
  private readonly inFlight = new Map<
82
119
  string,
83
120
  Promise<ActiveStorefrontRuntime | null>
@@ -85,6 +122,9 @@ export class CloudRuntimeClient {
85
122
 
86
123
  constructor(options: CloudRuntimeClientOptions) {
87
124
  this.baseUrl = new URL(options.baseUrl).toString().replace(/\/$/, '')
125
+ this.artifactBaseUrl = options.artifactBaseUrl
126
+ ? new URL(options.artifactBaseUrl).toString().replace(/\/$/, '')
127
+ : null
88
128
  this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
89
129
  this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS
90
130
  this.fetchRuntime = options.fetch ?? globalThis.fetch
@@ -103,20 +143,139 @@ export class CloudRuntimeClient {
103
143
  const activeRequest = this.inFlight.get(storeSlug)
104
144
  if (activeRequest) return activeRequest
105
145
 
106
- const request = this.requestActiveStorefront(storeSlug, cached).finally(
107
- () => {
108
- this.inFlight.delete(storeSlug)
109
- },
110
- )
146
+ const request = (
147
+ this.artifactBaseUrl
148
+ ? this.requestFromArtifactStore(storeSlug)
149
+ : this.requestActiveStorefront(storeSlug, cached)
150
+ ).finally(() => {
151
+ this.inFlight.delete(storeSlug)
152
+ })
111
153
  this.inFlight.set(storeSlug, request)
112
154
  return request
113
155
  }
114
156
 
115
157
  clearCache(): void {
116
158
  this.cache.clear()
159
+ this.lastKnownGood.clear()
160
+ this.artifactCache.clear()
117
161
  this.inFlight.clear()
118
162
  }
119
163
 
164
+ private async requestFromArtifactStore(
165
+ storeSlug: string,
166
+ ): Promise<ActiveStorefrontRuntime | null> {
167
+ const survivor = this.lastKnownGood.get(storeSlug)
168
+ let pointer: EnvironmentPointer | null
169
+ try {
170
+ pointer = await this.fetchPointer(storeSlug)
171
+ } catch {
172
+ if (survivor) return survivor.runtime
173
+ throw new CloudRuntimeResponseError(503)
174
+ }
175
+
176
+ if (pointer === null) {
177
+ if (survivor) return survivor.runtime
178
+ this.setCache(storeSlug, {
179
+ value: null,
180
+ expiresAt: this.now() + this.cacheTtlMs,
181
+ })
182
+ return null
183
+ }
184
+
185
+ if (survivor && pointer.generation < survivor.generation) {
186
+ this.setCache(storeSlug, {
187
+ value: survivor.runtime,
188
+ expiresAt: this.now() + this.cacheTtlMs,
189
+ })
190
+ return survivor.runtime
191
+ }
192
+
193
+ let runtime: ActiveStorefrontRuntime
194
+ try {
195
+ runtime = await this.fetchArtifactRuntime(pointer)
196
+ } catch {
197
+ if (survivor) return survivor.runtime
198
+ throw new CloudRuntimeResponseError(503)
199
+ }
200
+
201
+ if (runtime.store.slug !== storeSlug) {
202
+ throw new Error(
203
+ `El pointer de ${storeSlug} apunta a un artifact de ${runtime.store.slug}`,
204
+ )
205
+ }
206
+
207
+ const ttlMs = pointer.cachePolicy.pointerMaxAgeSeconds * 1_000
208
+ this.setCache(storeSlug, {
209
+ value: runtime,
210
+ expiresAt: this.now() + (ttlMs > 0 ? ttlMs : this.cacheTtlMs),
211
+ })
212
+ this.lastKnownGood.set(storeSlug, {
213
+ runtime,
214
+ generation: pointer.generation,
215
+ })
216
+ return runtime
217
+ }
218
+
219
+ private async fetchPointer(
220
+ storeSlug: string,
221
+ ): Promise<EnvironmentPointer | null> {
222
+ const response = await this.fetchWithTimeout(
223
+ `${this.artifactBaseUrl}/stores/${encodeURIComponent(storeSlug)}/pointer.json`,
224
+ )
225
+ if (response.status === 404) return null
226
+ if (!response.ok) throw new CloudRuntimeResponseError(response.status)
227
+ return EnvironmentPointerSchema.parse(await response.json())
228
+ }
229
+
230
+ private async fetchArtifactRuntime(
231
+ pointer: EnvironmentPointer,
232
+ ): Promise<ActiveStorefrontRuntime> {
233
+ const cached = this.artifactCache.get(pointer.artifactKey)
234
+ const base =
235
+ cached ?? (await this.requestArtifactBody(pointer.artifactKey))
236
+ if (!cached) {
237
+ if (this.artifactCache.size >= MAX_CACHE_ENTRIES) {
238
+ const oldestKey = this.artifactCache.keys().next().value
239
+ if (oldestKey !== undefined) this.artifactCache.delete(oldestKey)
240
+ }
241
+ this.artifactCache.set(pointer.artifactKey, base)
242
+ }
243
+ return {
244
+ ...base,
245
+ publication: {
246
+ sequence: pointer.deploymentSequence,
247
+ releaseSequence: pointer.releaseSequence,
248
+ publishedAt: pointer.publishedAt,
249
+ runtimeReleaseId: pointer.runtimeReleaseId,
250
+ artifactDigest: pointer.artifactDigest,
251
+ },
252
+ }
253
+ }
254
+
255
+ private async requestArtifactBody(
256
+ artifactKey: string,
257
+ ): Promise<ActiveStorefrontRuntime> {
258
+ const response = await this.fetchWithTimeout(
259
+ `${this.artifactBaseUrl}/${artifactKey}`,
260
+ )
261
+ if (!response.ok) throw new CloudRuntimeResponseError(response.status)
262
+ return ActiveStorefrontRuntimeSchema.parse(await response.json())
263
+ }
264
+
265
+ private async fetchWithTimeout(url: string): Promise<Response> {
266
+ const controller = new AbortController()
267
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs)
268
+ try {
269
+ return await this.fetchRuntime(url, {
270
+ method: 'GET',
271
+ headers: { accept: 'application/json' },
272
+ signal: controller.signal,
273
+ })
274
+ } finally {
275
+ clearTimeout(timeout)
276
+ }
277
+ }
278
+
120
279
  private setCache(storeSlug: string, entry: RuntimeCacheEntry): void {
121
280
  if (!this.cache.has(storeSlug) && this.cache.size >= MAX_CACHE_ENTRIES) {
122
281
  const oldestKey = this.cache.keys().next().value
@@ -197,6 +356,7 @@ export function getCloudRuntimeClient(
197
356
  ): CloudRuntimeClient {
198
357
  const key = [
199
358
  new URL(options.baseUrl).toString(),
359
+ options.artifactBaseUrl ? new URL(options.artifactBaseUrl).toString() : '',
200
360
  options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
201
361
  options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
202
362
  ].join('|')
package/src/codegen.mjs CHANGED
@@ -545,7 +545,6 @@ function assertNoFrameworkPageOverrides(pageEntries) {
545
545
  )
546
546
  // Overrides / alias legados
547
547
  reserved.set('/checkout', 'checkout.tsx')
548
- reserved.set('/checkout/$cartId', 'checkout.$cartId.tsx')
549
548
 
550
549
  for (const entry of pageEntries) {
551
550
  const routeId = fileToRouteId(entry.rel)
@@ -45,11 +45,11 @@ import { useCheckoutGeo } from './use-checkout-geo'
45
45
  import { useCheckoutCustomerPrefill } from './use-checkout-customer-prefill'
46
46
  import { useCheckoutSession } from './use-checkout-session'
47
47
 
48
- export function CheckoutSessionView({ cartId }: { cartId: string }) {
48
+ export function CheckoutSessionView({ cartToken }: { cartToken: string }) {
49
49
  const { store } = useSettings()
50
50
  const { session } = useAuth()
51
51
  const isAuthenticated = Boolean(session?.user)
52
- const checkout = useCheckoutSession(cartId)
52
+ const checkout = useCheckoutSession(cartToken)
53
53
  const [stepFlow, setStepFlow] = useState<CheckoutStepFlow>(() => createCheckoutStepFlow())
54
54
  const [fieldErrors, setFieldErrors] = useState<
55
55
  Partial<Record<CheckoutFieldId, string>>
@@ -72,7 +72,7 @@ export function CheckoutSessionView({ cartId }: { cartId: string }) {
72
72
 
73
73
  const customerPrefill = useCheckoutCustomerPrefill({
74
74
  enabled: isAuthenticated && checkout.loadStatus === 'ready',
75
- cartId,
75
+ cartToken,
76
76
  currency: store.currency,
77
77
  contact: checkout.contact,
78
78
  setContact: checkout.setContact,
@@ -298,7 +298,7 @@ export function CheckoutSessionView({ cartId }: { cartId: string }) {
298
298
  return
299
299
  }
300
300
  window.location.assign(
301
- thankYouUrl(cartId, result.payment?.transactionId ?? undefined),
301
+ thankYouUrl(cartToken, result.payment?.transactionId ?? undefined),
302
302
  )
303
303
  } catch {
304
304
  /* formError en sesión */
@@ -2,8 +2,8 @@ import type { CartCheckoutResponse } from '@ecomiq/core'
2
2
 
3
3
  export { formatMoney } from '../../format-money'
4
4
 
5
- export function thankYouUrl(cartId: string, transactionId?: string) {
6
- const q = new URLSearchParams({ cartId })
5
+ export function thankYouUrl(cartToken: string, transactionId?: string) {
6
+ const q = new URLSearchParams({ cartToken })
7
7
  if (transactionId) q.set('transactionId', transactionId)
8
8
  return `/thank-you?${q.toString()}`
9
9
  }
@@ -42,7 +42,7 @@ type ShippingFlatItem = { key: string }
42
42
 
43
43
  type UseCheckoutCustomerPrefillArgs = {
44
44
  enabled: boolean
45
- cartId: string
45
+ cartToken: string
46
46
  currency: string
47
47
  contact: CheckoutContactForm
48
48
  setContact: Dispatch<SetStateAction<CheckoutContactForm>>
@@ -66,7 +66,7 @@ type UseCheckoutCustomerPrefillArgs = {
66
66
 
67
67
  export function useCheckoutCustomerPrefill({
68
68
  enabled,
69
- cartId,
69
+ cartToken,
70
70
  currency,
71
71
  contact,
72
72
  setContact,
@@ -111,7 +111,7 @@ export function useCheckoutCustomerPrefill({
111
111
  setProfile(null)
112
112
  setSavedAddresses([])
113
113
  setSelectedSavedAddressId(null)
114
- }, [cartId])
114
+ }, [cartToken])
115
115
 
116
116
  useEffect(() => {
117
117
  if (!enabled) return
@@ -139,7 +139,7 @@ export function useCheckoutCustomerPrefill({
139
139
  setIsLoadingCustomer(false)
140
140
  }
141
141
  })()
142
- }, [enabled, cartId])
142
+ }, [enabled, cartToken])
143
143
 
144
144
  // Prefill perfil (campos vacíos).
145
145
  useEffect(() => {