@ecomiq/storefront 2.0.20 → 2.0.22
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 +71 -5
- package/package.json +5 -2
- package/src/cloud-runtime-client.ts +218 -0
- package/src/codegen.mjs +11 -2
- package/src/components/checkout/format.ts +1 -8
- package/src/config.d.ts +26 -1
- package/src/config.mjs +20 -1
- package/src/format-money.ts +31 -0
- package/src/index.ts +12 -1
- package/src/server-config-store.ts +8 -0
- package/src/server.ts +6 -0
- package/src/storefront-definition/contract.ts +1 -0
- package/src/storefront-definition/index.ts +2 -0
- package/src/storefront-definition/renderer.tsx +905 -0
- package/src/storefront-layout.ts +37 -0
- package/src/storefront-runtime.ts +277 -0
- package/src/styles.css +13 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useRouterState } from '@tanstack/react-router'
|
|
2
|
+
import {
|
|
3
|
+
StorefrontDefinitionSchema,
|
|
4
|
+
type StorefrontDefinition,
|
|
5
|
+
} from '@ecomiq/storefront-definition'
|
|
6
|
+
|
|
7
|
+
export function definitionForRoute(
|
|
8
|
+
pathname: string,
|
|
9
|
+
loaderData: unknown,
|
|
10
|
+
): StorefrontDefinition | null {
|
|
11
|
+
if (
|
|
12
|
+
pathname !== '/' ||
|
|
13
|
+
loaderData === null ||
|
|
14
|
+
typeof loaderData !== 'object'
|
|
15
|
+
) {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
if (!('kind' in loaderData) || loaderData.kind !== 'definition') {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
if (!('definition' in loaderData)) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const definition = StorefrontDefinitionSchema.safeParse(loaderData.definition)
|
|
26
|
+
return definition.success ? definition.data : null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useDefinition(): StorefrontDefinition | null {
|
|
30
|
+
return useRouterState({
|
|
31
|
+
select: (state) =>
|
|
32
|
+
definitionForRoute(
|
|
33
|
+
state.location.pathname,
|
|
34
|
+
state.matches.at(-1)?.loaderData,
|
|
35
|
+
),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Collection,
|
|
3
|
+
Collections,
|
|
4
|
+
ProductsParamsInput,
|
|
5
|
+
ProductsResponse,
|
|
6
|
+
ProductSummary,
|
|
7
|
+
} from '@ecomiq/core'
|
|
8
|
+
import {
|
|
9
|
+
CloudRuntimeStoreSlugSchema,
|
|
10
|
+
getCloudRuntimeClient,
|
|
11
|
+
type ActiveStorefrontRuntime,
|
|
12
|
+
} from './cloud-runtime-client'
|
|
13
|
+
import { getStorefrontClient } from './client'
|
|
14
|
+
import {
|
|
15
|
+
getServerConfig,
|
|
16
|
+
type ServerEcomiqConfig,
|
|
17
|
+
} from './server-config-store'
|
|
18
|
+
import type {
|
|
19
|
+
CollectionGridStorefrontSection,
|
|
20
|
+
ProductGridStorefrontSection,
|
|
21
|
+
StorefrontDefinition,
|
|
22
|
+
} from '@ecomiq/storefront-definition'
|
|
23
|
+
import type { DefinitionData } from './storefront-definition/renderer'
|
|
24
|
+
|
|
25
|
+
const MAX_UNIQUE_PRODUCT_SOURCES = 12
|
|
26
|
+
const MANUAL_PRODUCT_PAGE_SIZE = 100
|
|
27
|
+
const MAX_MANUAL_PRODUCT_SCAN_PAGES = 5
|
|
28
|
+
|
|
29
|
+
export interface StorefrontRuntimeCatalogClient {
|
|
30
|
+
getProducts(params?: ProductsParamsInput): Promise<ProductsResponse>
|
|
31
|
+
getCollections(): Promise<Collections>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StorefrontRuntimeCloudClient {
|
|
35
|
+
resolveActiveStorefront(
|
|
36
|
+
storeSlug: string,
|
|
37
|
+
): Promise<ActiveStorefrontRuntime | null>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ResolvedStorefrontDefinitionData
|
|
41
|
+
extends DefinitionData {
|
|
42
|
+
productsBySectionId: Record<string, ProductSummary[]>
|
|
43
|
+
collectionsBySectionId: Record<string, Collection[]>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ActiveStorefrontHome =
|
|
47
|
+
| {
|
|
48
|
+
kind: 'legacy'
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
kind: 'definition'
|
|
52
|
+
definition: StorefrontDefinition
|
|
53
|
+
publication: ActiveStorefrontRuntime['publication']
|
|
54
|
+
data: ResolvedStorefrontDefinitionData
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function emptyRendererData(): ResolvedStorefrontDefinitionData {
|
|
58
|
+
return {
|
|
59
|
+
productsBySectionId: {},
|
|
60
|
+
collectionsBySectionId: {},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function productSourceKey(
|
|
65
|
+
section: ProductGridStorefrontSection,
|
|
66
|
+
): string | null {
|
|
67
|
+
if (section.source.kind === 'manual') return null
|
|
68
|
+
if (section.source.kind === 'collection') {
|
|
69
|
+
return `collection:${section.source.slug}`
|
|
70
|
+
}
|
|
71
|
+
return 'all'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function loadProductSource(
|
|
75
|
+
section: ProductGridStorefrontSection,
|
|
76
|
+
client: StorefrontRuntimeCatalogClient,
|
|
77
|
+
): Promise<ProductSummary[]> {
|
|
78
|
+
const params: ProductsParamsInput = {
|
|
79
|
+
page: 1,
|
|
80
|
+
pageSize: section.limit,
|
|
81
|
+
sort: 'newest',
|
|
82
|
+
}
|
|
83
|
+
if (section.source.kind === 'collection') {
|
|
84
|
+
params.collection = section.source.slug
|
|
85
|
+
}
|
|
86
|
+
const response = await client.getProducts(params)
|
|
87
|
+
return response.products.slice(0, section.limit)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function scanManualProducts(
|
|
91
|
+
productIds: ReadonlySet<string>,
|
|
92
|
+
client: StorefrontRuntimeCatalogClient,
|
|
93
|
+
): Promise<Map<string, ProductSummary>> {
|
|
94
|
+
const found = new Map<string, ProductSummary>()
|
|
95
|
+
|
|
96
|
+
for (let page = 1; page <= MAX_MANUAL_PRODUCT_SCAN_PAGES; page += 1) {
|
|
97
|
+
const response = await client.getProducts({
|
|
98
|
+
page,
|
|
99
|
+
pageSize: MANUAL_PRODUCT_PAGE_SIZE,
|
|
100
|
+
sort: 'newest',
|
|
101
|
+
})
|
|
102
|
+
for (const product of response.products) {
|
|
103
|
+
if (productIds.has(product.id)) found.set(product.id, product)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
found.size === productIds.size ||
|
|
108
|
+
page >= response.totalPages ||
|
|
109
|
+
response.products.length === 0
|
|
110
|
+
) {
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return found
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function resolveStorefrontDefinitionData(
|
|
119
|
+
definition: StorefrontDefinition,
|
|
120
|
+
client: StorefrontRuntimeCatalogClient,
|
|
121
|
+
): Promise<ResolvedStorefrontDefinitionData> {
|
|
122
|
+
const data = emptyRendererData()
|
|
123
|
+
const productSections = definition.sections.filter(
|
|
124
|
+
(section): section is ProductGridStorefrontSection =>
|
|
125
|
+
section.enabled && section.type === 'product-grid',
|
|
126
|
+
)
|
|
127
|
+
const collectionSections = definition.sections.filter(
|
|
128
|
+
(section): section is CollectionGridStorefrontSection =>
|
|
129
|
+
section.enabled && section.type === 'collection-grid',
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const sourceSections = new Map<string, ProductGridStorefrontSection>()
|
|
133
|
+
for (const section of productSections) {
|
|
134
|
+
const sourceKey = productSourceKey(section)
|
|
135
|
+
if (!sourceKey) continue
|
|
136
|
+
const current = sourceSections.get(sourceKey)
|
|
137
|
+
if (current) {
|
|
138
|
+
if (section.limit > current.limit) sourceSections.set(sourceKey, section)
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
if (sourceSections.size >= MAX_UNIQUE_PRODUCT_SOURCES) continue
|
|
142
|
+
sourceSections.set(sourceKey, section)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const sourceRequests = new Map<string, Promise<ProductSummary[]>>()
|
|
146
|
+
for (const [sourceKey, section] of sourceSections) {
|
|
147
|
+
sourceRequests.set(
|
|
148
|
+
sourceKey,
|
|
149
|
+
loadProductSource(section, client).catch(() => []),
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const manualIds = new Set(
|
|
154
|
+
productSections.flatMap((section) =>
|
|
155
|
+
section.source.kind === 'manual' ? section.source.productIds : [],
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
const manualProducts =
|
|
159
|
+
manualIds.size > 0
|
|
160
|
+
? scanManualProducts(manualIds, client).catch(
|
|
161
|
+
() => new Map<string, ProductSummary>(),
|
|
162
|
+
)
|
|
163
|
+
: Promise.resolve(new Map<string, ProductSummary>())
|
|
164
|
+
|
|
165
|
+
await Promise.all(
|
|
166
|
+
productSections.map(async (section) => {
|
|
167
|
+
if (section.source.kind === 'manual') {
|
|
168
|
+
const productsById = await manualProducts
|
|
169
|
+
data.productsBySectionId[section.id] = section.source.productIds
|
|
170
|
+
.map((productId) => productsById.get(productId))
|
|
171
|
+
.filter(
|
|
172
|
+
(product): product is ProductSummary => product !== undefined,
|
|
173
|
+
)
|
|
174
|
+
.slice(0, section.limit)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const sourceKey = productSourceKey(section)
|
|
179
|
+
const products = sourceKey
|
|
180
|
+
? await (sourceRequests.get(sourceKey) ?? Promise.resolve([]))
|
|
181
|
+
: []
|
|
182
|
+
data.productsBySectionId[section.id] = products.slice(0, section.limit)
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if (collectionSections.length > 0) {
|
|
187
|
+
const collections = await client.getCollections().catch(() => [])
|
|
188
|
+
const collectionsBySlug = new Map(
|
|
189
|
+
collections.map((collection) => [collection.slug, collection]),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
for (const section of collectionSections) {
|
|
193
|
+
if (section.source.kind === 'manual') {
|
|
194
|
+
data.collectionsBySectionId[section.id] = section.source.slugs
|
|
195
|
+
.map((slug) => collectionsBySlug.get(slug))
|
|
196
|
+
.filter(
|
|
197
|
+
(collection): collection is Collection =>
|
|
198
|
+
collection !== undefined,
|
|
199
|
+
)
|
|
200
|
+
.slice(0, section.limit)
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
data.collectionsBySectionId[section.id] = collections.slice(
|
|
204
|
+
0,
|
|
205
|
+
section.limit,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return data
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function deriveCloudStoreSlug(domain: string): string | null {
|
|
214
|
+
const normalized = domain.trim().toLowerCase()
|
|
215
|
+
if (!normalized) return null
|
|
216
|
+
|
|
217
|
+
let hostname: string
|
|
218
|
+
try {
|
|
219
|
+
hostname = new URL(
|
|
220
|
+
normalized.includes('://') ? normalized : `https://${normalized}`,
|
|
221
|
+
).hostname
|
|
222
|
+
} catch {
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const candidate = hostname.split('.')[0] ?? ''
|
|
227
|
+
const parsed = CloudRuntimeStoreSlugSchema.safeParse(candidate)
|
|
228
|
+
return parsed.success ? parsed.data : null
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface LoadActiveStorefrontHomeDependencies {
|
|
232
|
+
config?: ServerEcomiqConfig
|
|
233
|
+
cloudClient?: StorefrontRuntimeCloudClient
|
|
234
|
+
catalogClient?: StorefrontRuntimeCatalogClient
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function loadActiveStorefrontHome(
|
|
238
|
+
dependencies: LoadActiveStorefrontHomeDependencies = {},
|
|
239
|
+
): Promise<ActiveStorefrontHome> {
|
|
240
|
+
const config = dependencies.config ?? getServerConfig()
|
|
241
|
+
const cloudConfig = config.cloud
|
|
242
|
+
if (!cloudConfig?.baseUrl) return { kind: 'legacy' }
|
|
243
|
+
|
|
244
|
+
const storeSlug =
|
|
245
|
+
cloudConfig.storeSlug ?? deriveCloudStoreSlug(config.domain)
|
|
246
|
+
if (!storeSlug) return { kind: 'legacy' }
|
|
247
|
+
|
|
248
|
+
const cloudClient =
|
|
249
|
+
dependencies.cloudClient ??
|
|
250
|
+
getCloudRuntimeClient({
|
|
251
|
+
baseUrl: cloudConfig.baseUrl,
|
|
252
|
+
timeoutMs: cloudConfig.timeoutMs,
|
|
253
|
+
cacheTtlMs: cloudConfig.cacheTtlMs,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
let runtime: ActiveStorefrontRuntime | null
|
|
257
|
+
try {
|
|
258
|
+
runtime = await cloudClient.resolveActiveStorefront(storeSlug)
|
|
259
|
+
} catch {
|
|
260
|
+
return { kind: 'legacy' }
|
|
261
|
+
}
|
|
262
|
+
if (!runtime) return { kind: 'legacy' }
|
|
263
|
+
|
|
264
|
+
const catalogClient =
|
|
265
|
+
dependencies.catalogClient ?? getStorefrontClient()
|
|
266
|
+
const data = await resolveStorefrontDefinitionData(
|
|
267
|
+
runtime.definition,
|
|
268
|
+
catalogClient,
|
|
269
|
+
).catch(emptyRendererData)
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
kind: 'definition',
|
|
273
|
+
definition: runtime.definition,
|
|
274
|
+
publication: runtime.publication,
|
|
275
|
+
data,
|
|
276
|
+
}
|
|
277
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
/* Escanear los componentes UI del propio framework */
|
|
6
6
|
@source './ui';
|
|
7
7
|
@source './components';
|
|
8
|
+
@source './storefront-definition';
|
|
8
9
|
@source './runtime';
|
|
9
10
|
@source './pages';
|
|
10
11
|
@source '../templates';
|
|
@@ -74,3 +75,15 @@
|
|
|
74
75
|
transition-duration: 0.01ms !important;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
|
|
79
|
+
.storefront-definition {
|
|
80
|
+
font-family: var(--storefront-font-body);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.storefront-definition :where(h1, h2, h3, h4, h5, h6) {
|
|
84
|
+
font-family: var(--storefront-font-heading);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.storefront-definition-section {
|
|
88
|
+
padding-block: var(--storefront-section-padding);
|
|
89
|
+
}
|