@zenky/storefront-vue 0.10.0 → 0.10.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +52 -2
  3. package/dist/cart/index.d.ts +31 -2
  4. package/dist/cart/index.js +5 -6
  5. package/dist/catalog/index.js +2 -2
  6. package/dist/{chunk-BIGTKVC6.js → chunk-57IOMDAM.js} +3 -3
  7. package/dist/{chunk-BIGTKVC6.js.map → chunk-57IOMDAM.js.map} +1 -1
  8. package/dist/{chunk-DKWDKVXQ.js → chunk-7FRMMCR5.js} +12 -10
  9. package/dist/chunk-7FRMMCR5.js.map +1 -0
  10. package/dist/{chunk-EYRCAUUP.js → chunk-E3G7NGCH.js} +2 -2
  11. package/dist/chunk-E3G7NGCH.js.map +1 -0
  12. package/dist/{chunk-VL3T36VM.js → chunk-FFBZKQRV.js} +14 -7
  13. package/dist/chunk-FFBZKQRV.js.map +1 -0
  14. package/dist/{chunk-OQLU7FSR.js → chunk-G2X4SBCO.js} +199 -29
  15. package/dist/chunk-G2X4SBCO.js.map +1 -0
  16. package/dist/{chunk-AL5W5JSD.js → chunk-GLTOJTZO.js} +2 -2
  17. package/dist/{chunk-AL5W5JSD.js.map → chunk-GLTOJTZO.js.map} +1 -1
  18. package/dist/{chunk-IGDDWGS4.js → chunk-JLFBAAFT.js} +20 -11
  19. package/dist/chunk-JLFBAAFT.js.map +1 -0
  20. package/dist/{chunk-PJ2LEKW6.js → chunk-MZ66ARDL.js} +18 -3
  21. package/dist/chunk-MZ66ARDL.js.map +1 -0
  22. package/dist/{chunk-4MJKN7PL.js → chunk-N3NOTJUC.js} +4 -4
  23. package/dist/{chunk-4MJKN7PL.js.map → chunk-N3NOTJUC.js.map} +1 -1
  24. package/dist/{chunk-2OD7CLKD.js → chunk-RYYY7MVP.js} +2 -2
  25. package/dist/chunk-RYYY7MVP.js.map +1 -0
  26. package/dist/{chunk-PWHIDGKW.js → chunk-TEG7EL4N.js} +12 -3
  27. package/dist/chunk-TEG7EL4N.js.map +1 -0
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +11 -11
  30. package/dist/layout/index.js +8 -4
  31. package/dist/layout/index.js.map +1 -1
  32. package/dist/order/index.js.map +1 -1
  33. package/dist/pagination/index.js.map +1 -1
  34. package/dist/ports/index.js +1 -1
  35. package/dist/products/index.js +2 -2
  36. package/dist/promotions/index.js +3 -3
  37. package/dist/seo/index.js +5 -5
  38. package/dist/seo/index.js.map +1 -1
  39. package/dist/snippets/index.js +1 -1
  40. package/dist/stores/index.js +3 -3
  41. package/dist/utils/index.d.ts +2 -2
  42. package/dist/utils/index.js +1 -1
  43. package/docs/consumer-setup.md +275 -0
  44. package/docs/ports.md +144 -0
  45. package/docs/releasing.md +46 -0
  46. package/package.json +26 -4
  47. package/dist/chunk-2OD7CLKD.js.map +0 -1
  48. package/dist/chunk-DKWDKVXQ.js.map +0 -1
  49. package/dist/chunk-EYRCAUUP.js.map +0 -1
  50. package/dist/chunk-IGDDWGS4.js.map +0 -1
  51. package/dist/chunk-OQLU7FSR.js.map +0 -1
  52. package/dist/chunk-PJ2LEKW6.js.map +0 -1
  53. package/dist/chunk-PWHIDGKW.js.map +0 -1
  54. package/dist/chunk-VL3T36VM.js.map +0 -1
@@ -0,0 +1,275 @@
1
+ # Consumer Setup
2
+
3
+ This guide is for teams integrating `@zenky/storefront-vue` into a Vue or Nuxt
4
+ storefront. The package owns reusable storefront state and business flows. The consumer
5
+ owns framework glue, visual components, routing, server endpoints, and environment
6
+ configuration.
7
+
8
+ ## 1. Install an exact version
9
+
10
+ Use an exact version while integrating or testing a prerelease. Upgrade deliberately
11
+ after the consumer quality gate is green.
12
+
13
+ ```bash
14
+ npm install --save-exact @zenky/storefront-vue@0.10.0
15
+ ```
16
+
17
+ The consumer must also provide compatible peer dependencies:
18
+
19
+ ```bash
20
+ npm install vue@^3.5 pinia@^3 @zenky/storefront-api@^1
21
+ ```
22
+
23
+ Install `@zenky/integrations-sdk` when the consumer sends Zenky integration events.
24
+
25
+ Do not commit a `file:.yalc/...` dependency. `yalc` is only for local development; CI
26
+ and deployed environments must install a published package version.
27
+
28
+ ## 2. Decide which domains the consumer needs
29
+
30
+ Import public subpaths instead of application source files:
31
+
32
+ ```ts
33
+ import { createStorefront } from '@zenky/storefront-vue'
34
+ import { getRemoteCatalog } from '@zenky/storefront-vue/catalog'
35
+ import { useProductPricing } from '@zenky/storefront-vue/products'
36
+ import { useCartStore } from '@zenky/storefront-vue/stores'
37
+ ```
38
+
39
+ A read-only menu normally needs `catalog`, `products`, `theme`, `utils`, and selected
40
+ stores. It does not need cart, checkout, auth, delivery, or payment modules.
41
+
42
+ A full storefront can additionally use `cart`, `order`, `promotions`, `seo`, `layout`,
43
+ `pagination`, `snippets`, and `website-settings`.
44
+
45
+ Never import from `@zenky/storefront-vue/dist/*` or from this repository's `src/*`.
46
+ Only documented package exports are public API.
47
+
48
+ ## 3. Create one composition root
49
+
50
+ The consumer creates external adapters once and installs the storefront plugin after
51
+ Pinia.
52
+
53
+ ```ts
54
+ import { createApp } from 'vue'
55
+ import { createPinia } from 'pinia'
56
+ import { createStorefront } from '@zenky/storefront-vue'
57
+ import type { StorefrontRuntime } from '@zenky/storefront-vue/ports'
58
+
59
+ import App from './App.vue'
60
+ import { sdk } from './services/zenky'
61
+
62
+ const app = createApp(App)
63
+ const pinia = createPinia()
64
+
65
+ const runtime: StorefrontRuntime = {
66
+ sdk,
67
+ config: {
68
+ baseUrl: import.meta.env.VITE_STOREFRONT_URL,
69
+ baseAuthUrl: import.meta.env.VITE_AUTH_URL,
70
+ salesChannelId: import.meta.env.VITE_SALES_CHANNEL_ID || undefined,
71
+ },
72
+ storage: {
73
+ get: (key) => localStorage.getItem(key),
74
+ set: (key, value) => localStorage.setItem(key, value),
75
+ remove: (key) => localStorage.removeItem(key),
76
+ },
77
+ notifier: {
78
+ error: (message) => notifications.error(message),
79
+ success: (message) => notifications.success(message),
80
+ warning: (message) => notifications.warning(message),
81
+ message: (title, options) => notifications.message(title, options),
82
+ },
83
+ activity: {
84
+ begin: (kind) => busy.begin(kind),
85
+ end: (kind) => busy.end(kind),
86
+ },
87
+ }
88
+
89
+ app.use(pinia)
90
+ app.use(createStorefront(runtime))
91
+ app.mount('#app')
92
+ ```
93
+
94
+ Use `NOOP_NOTIFIER` and `NOOP_ACTIVITY_REPORTER` from the `ports` subpath when a
95
+ read-only consumer intentionally has no notifications or busy UI. Keep that decision
96
+ explicit.
97
+
98
+ ## 4. Nuxt adapter
99
+
100
+ Nuxt consumers should install the runtime in one plugin and keep Nuxt APIs in that
101
+ plugin, not in the package.
102
+
103
+ ```ts
104
+ import { createStorefront } from '@zenky/storefront-vue'
105
+ import type { StorefrontRuntime } from '@zenky/storefront-vue/ports'
106
+
107
+ export default defineNuxtPlugin((nuxtApp) => {
108
+ const config = useRuntimeConfig()
109
+
110
+ const runtime: StorefrontRuntime = {
111
+ sdk: useStorefrontApi(),
112
+ config: {
113
+ baseUrl: config.public.storefrontBaseUrl,
114
+ baseAuthUrl: config.public.storefrontAuthUrl,
115
+ },
116
+ storage: {
117
+ get: (key) => useCookie<string | null>(key).value ?? null,
118
+ set: (key, value, options) => {
119
+ useCookie<string>(key, options).value = value
120
+ },
121
+ remove: (key, options) => {
122
+ useCookie(key, options).value = null
123
+ },
124
+ },
125
+ notifier: createNotifierAdapter(),
126
+ activity: createActivityAdapter(),
127
+ }
128
+
129
+ nuxtApp.vueApp.use(createStorefront(runtime))
130
+ })
131
+ ```
132
+
133
+ Resolve optional integrations lazily when their initialization order is later than the
134
+ storefront plugin:
135
+
136
+ ```ts
137
+ runtime.events = {
138
+ get: () => useZenkyIntegrations(),
139
+ }
140
+
141
+ runtime.realtime = {
142
+ get: () => useRealtimeAdapter(),
143
+ }
144
+ ```
145
+
146
+ Do not capture an initial `null` integration instance during plugin setup.
147
+
148
+ ## 5. Storage and sensitive data
149
+
150
+ The storage adapter is synchronous because order and auth credentials are read while
151
+ composables are created.
152
+
153
+ For browser-only applications, `localStorage` or cookies are acceptable according to
154
+ the consumer's security model. For SSR, use request-scoped cookies.
155
+
156
+ Never:
157
+
158
+ - share in-memory storage between users;
159
+ - log storage values, order tokens, API tokens, cookies, confirmation codes, payment
160
+ tokens, phone numbers, addresses, or order comments;
161
+ - expose secret server configuration through public runtime config;
162
+ - persist real credentials in test fixtures.
163
+
164
+ See [Runtime Ports](ports.md) for the complete security boundary.
165
+
166
+ ## 6. SSR isolation
167
+
168
+ For every server request, create a new:
169
+
170
+ 1. Vue application;
171
+ 2. Pinia instance;
172
+ 3. `StorefrontRuntime`;
173
+ 4. storage adapter.
174
+
175
+ Do not keep runtime or Pinia instances in module-global server state. Runtime-owned
176
+ mutation versions, request deduplication, restriction gates, and related-product state
177
+ must not cross request or application boundaries.
178
+
179
+ ## 7. Local package development with yalc
180
+
181
+ Use `yalc` only when changing the package and consumer together.
182
+
183
+ In this repository:
184
+
185
+ ```bash
186
+ npm run build
187
+ npx yalc publish
188
+ ```
189
+
190
+ In the consumer:
191
+
192
+ ```bash
193
+ npx yalc add @zenky/storefront-vue
194
+ ```
195
+
196
+ After another package change:
197
+
198
+ ```bash
199
+ npm run build
200
+ npx yalc push
201
+ ```
202
+
203
+ Before committing the consumer:
204
+
205
+ ```bash
206
+ npx yalc remove @zenky/storefront-vue
207
+ npm install --save-exact @zenky/storefront-vue@<published-version>
208
+ ```
209
+
210
+ Verify that `package.json` and `package-lock.json` no longer reference `.yalc`.
211
+
212
+ ## 8. Consumer quality gate
213
+
214
+ Before requesting review:
215
+
216
+ ```bash
217
+ npm ci
218
+ npm run lint
219
+ npm run test
220
+ npm run build
221
+ ```
222
+
223
+ For Nuxt consumers, also verify:
224
+
225
+ - the first SSR request returns HTML without a storefront-runtime or Pinia error;
226
+ - hydration completes without mismatch warnings;
227
+ - a new browser session creates or restores the expected order;
228
+ - a second app or SSR request does not see the first user's state;
229
+ - optional integrations initialized after the runtime still receive events.
230
+
231
+ For cart or checkout changes, cover:
232
+
233
+ - add, increment, decrement, and clear;
234
+ - API failures and stale responses;
235
+ - delivery and payment selection;
236
+ - minimum-order behavior;
237
+ - product restrictions;
238
+ - cleanup of busy state.
239
+
240
+ ## 9. Upgrade checklist
241
+
242
+ 1. Read the package changelog and migration notes.
243
+ 2. Install the exact target version.
244
+ 3. Review changed ports and exported types.
245
+ 4. Run the package consumer's unit tests and production build.
246
+ 5. Run smoke tests against a dedicated development store.
247
+ 6. Deploy to development.
248
+ 7. Run the same smoke suite against the deployed URL.
249
+ 8. Only then widen the version range or promote a prerelease.
250
+
251
+ ## 10. Common failures
252
+
253
+ ### `createStorefront() is not installed`
254
+
255
+ The runtime plugin was not installed, was installed after a component used the package,
256
+ or a second Vue application did not receive its own runtime.
257
+
258
+ ### `getActivePinia()` or `No active Pinia`
259
+
260
+ Install Pinia before `createStorefront(runtime)` and before calling package stores.
261
+
262
+ ### The app builds locally but CI cannot install the package
263
+
264
+ Check `package.json` and the lockfile for `file:.yalc`, local paths, or an unpublished
265
+ version.
266
+
267
+ ### SSR users share cart or restriction state
268
+
269
+ A runtime, Pinia instance, or storage adapter is being reused between requests. Make all
270
+ three request-scoped.
271
+
272
+ ### An optional integration never receives events
273
+
274
+ Provide a lazy `{ get() }` adapter instead of capturing the integration instance before
275
+ it is initialized.
package/docs/ports.md ADDED
@@ -0,0 +1,144 @@
1
+ # Runtime Ports
2
+
3
+ `@zenky/storefront-vue` follows Ports & Adapters. Domain composables declare the external
4
+ capabilities they need; a storefront supplies concrete adapters once at its composition
5
+ root. This keeps the package portable across Vue SPA, Nuxt SSR, embedded menus, edge
6
+ runtimes, tests, and future storefront templates.
7
+
8
+ ## Dependency direction
9
+
10
+ ```text
11
+ Vue/Nuxt storefront ── creates adapters ──▶ StorefrontRuntime
12
+ │
13
+ UI/components ── call composables ──▶ domain logic ── calls ports ──▶ Zenky SDK/services
14
+ ```
15
+
16
+ The core may know a port interface. It must not know which router, cookie library, toast
17
+ system, WebSocket provider, analytics vendor, or server framework implements that port.
18
+
19
+ ## Runtime contract
20
+
21
+ Install one runtime per Vue application:
22
+
23
+ ```ts
24
+ import { createStorefront } from '@zenky/storefront-vue'
25
+ import type { StorefrontRuntime } from '@zenky/storefront-vue/ports'
26
+
27
+ const runtime: StorefrontRuntime = {
28
+ sdk,
29
+ config: {
30
+ baseUrl: 'https://shop.example.com',
31
+ baseAuthUrl: 'https://auth.example.com',
32
+ salesChannelId: 'web',
33
+ },
34
+ storage,
35
+ notifier,
36
+ activity,
37
+ }
38
+
39
+ app.use(pinia)
40
+ app.use(createStorefront(runtime))
41
+ ```
42
+
43
+ For a locally isolated widget, `provideStorefront(runtime)` can be called inside its setup
44
+ scope instead of installing the plugin globally.
45
+
46
+ ## Required ports
47
+
48
+ | Port | Contract | Responsibility |
49
+ | --- | --- | --- |
50
+ | `sdk` | `ZenkyStorefront` | Typed access to Zenky Storefront API resources. The consumer owns configuration and request transport. |
51
+ | `config` | `StorefrontConfig` | Public storefront and auth base URLs plus an optional sales channel id. |
52
+ | `storage` | `StorageAdapter` | Synchronous key/value persistence used for order/auth state and lightweight preferences. |
53
+ | `notifier` | `Notifier` | User-facing `error`, `success`, `warning`, and generic message notifications. |
54
+ | `activity` | `ActivityReporter` | Balanced `begin(kind)` / `end(kind)` signals for long-running operations. |
55
+
56
+ Required ports are intentionally small. If an application has no UI for notifications or
57
+ activity, use `NOOP_NOTIFIER` and `NOOP_ACTIVITY_REPORTER` explicitly.
58
+
59
+ ## Optional ports
60
+
61
+ | Port | Contract | Behavior when absent |
62
+ | --- | --- | --- |
63
+ | `events` | Lazy `EventsPort.get()` returning `ZenkyIntegrationsSDK` | Integration events are skipped. |
64
+ | `realtime` | Lazy `RealtimePort.get()` returning `RealtimeAdapter` | Realtime order updates are skipped. |
65
+ | `catalogReloader` | `CatalogReloader.reload()` | Stock changes do not trigger an external catalog reload. |
66
+ | `rum` | `RumReporter.mark()` / `measure()` | Performance instrumentation is skipped. |
67
+ | `commerceEvents` | Commerce tracking plus anonymous/session identity | Commerce events and checkout identity enrichment are skipped. |
68
+
69
+ `events` and `realtime` are providers rather than captured values because integrations can
70
+ be initialized after the storefront runtime. The core resolves them at the moment an event
71
+ is emitted or a subscription is needed.
72
+
73
+ ## Storage adapter
74
+
75
+ The adapter is synchronous because cart credentials are needed while composables are being
76
+ created. Browser applications can wrap cookies or `localStorage`; SSR applications should
77
+ wrap request/response cookies. `createMemoryStorage()` is intended for tests and isolated,
78
+ request-scoped SSR work only.
79
+
80
+ ```ts
81
+ import type { StorageAdapter } from '@zenky/storefront-vue/ports'
82
+
83
+ export function createCookieStorage(cookies: CookieStore): StorageAdapter {
84
+ return {
85
+ get: (key) => cookies.get(key) ?? null,
86
+ set: (key, value, options) => {
87
+ cookies.set(key, value, {
88
+ path: options?.path ?? '/',
89
+ maxAge: options?.maxAge,
90
+ sameSite: options?.sameSite ?? 'lax',
91
+ secure: options?.secure ?? true,
92
+ })
93
+ },
94
+ remove: (key, options) => cookies.remove(key, { path: options?.path ?? '/' }),
95
+ }
96
+ }
97
+ ```
98
+
99
+ Do not use one in-memory adapter across users. Do not log storage values. Where the host
100
+ framework permits it, prefer secure, same-site cookies and narrowly scoped paths. Whether a
101
+ cookie can be `httpOnly` depends on where the browser-side SDK needs access to the value.
102
+
103
+ ## SSR and multi-app isolation
104
+
105
+ For every SSR request, create all three together:
106
+
107
+ 1. a new Pinia instance;
108
+ 2. a new `StorefrontRuntime` with request-scoped adapters;
109
+ 3. a new Vue application using those instances.
110
+
111
+ Runtime-owned mutation versions, request deduplication, and related-product state are held
112
+ per runtime. Reusing a runtime or Pinia across requests defeats that isolation and can leak
113
+ state between users.
114
+
115
+ Public entries are verified by CI through an import in a Node environment without browser
116
+ globals. Built output is also rejected if it contains CommonJS, Node built-in imports,
117
+ `Buffer`, or `process` references. Browser-only actions such as online-payment retry either
118
+ guard `window` access or accept the required URL from the consumer.
119
+
120
+ ## Security boundary
121
+
122
+ Ports can carry sensitive data even when their types look generic. Adapter implementations
123
+ must not log complete requests, orders, storage records, analytics payloads, or errors from
124
+ unknown SDK transports. The cart/checkout logger redacts common credential shapes as a
125
+ second line of defense, but callers should still pass allow-listed context such as
126
+ `orderId`, operation kind, counts, and booleans.
127
+
128
+ Never put these values in diagnostic context:
129
+
130
+ - order or API tokens;
131
+ - `Authorization` and `Cookie` headers;
132
+ - reCAPTCHA payloads and confirmation codes;
133
+ - payment/card token identifiers;
134
+ - customer names, phones, addresses, or free-form order comments.
135
+
136
+ ## Adding or changing a port
137
+
138
+ Before adding a port, verify that the capability cannot be represented by an existing one.
139
+ Keep the interface vendor-neutral and minimal, document absence/failure behavior, provide a
140
+ no-op implementation when that behavior is meaningful, and add an isolation test.
141
+
142
+ Adding an optional method is usually backward-compatible. Adding a required port or method,
143
+ changing semantics, or widening when sensitive data is emitted is a breaking change and
144
+ requires a major-version release note.
@@ -0,0 +1,46 @@
1
+ # Release Process
2
+
3
+ Releases are driven by GitHub Releases. Source control keeps
4
+ `"version": "0.0.0-development"`; the publish workflow derives the immutable npm version
5
+ from the release tag.
6
+
7
+ ## Before creating a release
8
+
9
+ 1. Confirm all required CI checks pass on `master`.
10
+ 2. Review Dependabot and security alerts.
11
+ 3. Move relevant `Unreleased` changelog entries into a new `X.Y.Z - YYYY-MM-DD` section.
12
+ 4. Choose the version using Semantic Versioning:
13
+ - patch for compatible fixes and security hardening;
14
+ - minor for backward-compatible public features;
15
+ - major for breaking exports, port contracts, behavior, or support changes.
16
+ 5. Verify migrations and compatibility notes are explicit for any consumer-visible change.
17
+ 6. Run `npm run check:release` and inspect `npm pack --dry-run` locally.
18
+
19
+ ## Publish
20
+
21
+ 1. Create a GitHub Release for the exact commit to publish.
22
+ 2. Tag it `vX.Y.Z`; prereleases may use `vX.Y.Z-beta.N` or `vX.Y.Z-rc.N`.
23
+ 3. Generate release notes, then edit them into a consumer-focused summary: outcome first,
24
+ migrations second, implementation detail last.
25
+ 4. Publish the GitHub Release.
26
+
27
+ The workflow validates the tag, installs with `npm ci`, injects the tag version without
28
+ creating a Git commit, runs the release quality gate, checks package contents, and publishes
29
+ through npm trusted publishing. Stable versions receive the `latest` dist-tag; prereleases
30
+ receive `next`.
31
+
32
+ The npm package must have a trusted publisher configured for this GitHub repository and the
33
+ `publish-npm.yml` workflow. No long-lived `NPM_TOKEN` should be added.
34
+
35
+ ## Verify
36
+
37
+ - Confirm the workflow completed and the package has npm provenance.
38
+ - Inspect `npm view @zenky/storefront-vue@X.Y.Z` metadata and dist-tags.
39
+ - Install the exact version into a clean consumer and import both the root and one subpath.
40
+ - Confirm the GitHub release links to the correct tag and changelog section.
41
+
42
+ ## Recovery
43
+
44
+ Never reuse or move a published version tag. npm versions are immutable. If a release is
45
+ bad, deprecate it with a clear message, fix forward, and publish a new patch or prerelease.
46
+ Reserve unpublishing for exceptional security/legal cases and follow npm policy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenky/storefront-vue",
3
- "version": "0.10.0",
3
+ "version": "0.10.3",
4
4
  "description": "Zenky Storefront SDK for Vue 3.",
5
5
  "keywords": [
6
6
  "zenky",
@@ -12,11 +12,21 @@
12
12
  ],
13
13
  "author": "Zenky",
14
14
  "license": "MIT",
15
+ "homepage": "https://github.com/zenky/storefront-vue#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/zenky/storefront-vue/issues"
18
+ },
19
+ "engines": {
20
+ "node": ">=20.19.0",
21
+ "npm": ">=10"
22
+ },
15
23
  "type": "module",
16
24
  "sideEffects": false,
17
25
  "files": [
18
26
  "dist",
27
+ "docs",
19
28
  "README.md",
29
+ "CHANGELOG.md",
20
30
  "LICENSE.md"
21
31
  ],
22
32
  "exports": {
@@ -89,23 +99,35 @@
89
99
  "scripts": {
90
100
  "build": "tsup",
91
101
  "dev": "tsup --watch",
102
+ "format": "biome format --write .",
103
+ "format:check": "biome format .",
104
+ "lint": "biome lint .",
105
+ "lint:fix": "biome lint --write .",
92
106
  "typecheck": "tsc --noEmit",
93
107
  "test": "vitest run",
108
+ "test:coverage": "vitest run --coverage",
94
109
  "test:watch": "vitest",
95
110
  "check:exports": "publint --strict && attw --pack . --profile esm-only",
96
- "prepublishOnly": "npm run typecheck && npm run test && npm run build && npm run check:exports"
111
+ "check:runtime": "node scripts/check-runtime-compat.mjs",
112
+ "check:size": "node scripts/check-bundle-size.mjs",
113
+ "check": "npm run format:check && npm run lint && npm run typecheck && npm run test && npm run build && npm run check:exports && npm run check:runtime && npm run check:size",
114
+ "check:ci": "npm run format:check && npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm run check:exports && npm run check:runtime && npm run check:size",
115
+ "check:release": "npm run typecheck && npm run test && npm run build && npm run check:exports && npm run check:runtime && npm run check:size",
116
+ "prepublishOnly": "npm run check:release"
97
117
  },
98
118
  "peerDependencies": {
99
119
  "@zenky/integrations-sdk": ">=0.1.3",
100
- "@zenky/storefront-api": ">=0.0.64",
120
+ "@zenky/storefront-api": ">=0.0.68",
101
121
  "pinia": "^3.0.0",
102
122
  "vue": "^3.5.0"
103
123
  },
104
124
  "devDependencies": {
105
125
  "@arethetypeswrong/cli": "^0.18.2",
126
+ "@biomejs/biome": "^2.5.5",
106
127
  "@types/node": "^22.10.0",
128
+ "@vitest/coverage-v8": "^4.1.10",
107
129
  "@zenky/integrations-sdk": "^0.1.3",
108
- "@zenky/storefront-api": "^0.0.64",
130
+ "@zenky/storefront-api": "^0.0.68",
109
131
  "pinia": "^3.0.4",
110
132
  "publint": "^0.3.21",
111
133
  "tsup": "^8.3.5",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/catalog/findCategory.ts","../src/catalog/groupBySubcategory.ts","../src/catalog/useCollections.ts","../src/catalog/routes.ts"],"names":[],"mappings":"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","file":"chunk-2OD7CLKD.js","sourcesContent":["import type { NestedCategory } from '@zenky/storefront-api'\n\n/**\n * Рекурсивный поиск категории в дереве по `short_id`.\n *\n * `NestedCategory[]` — это дерево: корневые категории на верхнем уровне,\n * вложенные — внутри `children[]`. Простой `tree.find()` найдёт только\n * корневые. Для вложенных (например «Куртки» внутри «Мужская» внутри «Одежда»)\n * нужно рекурсивно обойти все ветки.\n *\n * Возвращает первое совпадение (DFS pre-order).\n */\nexport function findCategoryByShortId(\n tree: NestedCategory[],\n shortId: string\n): NestedCategory | null {\n for (const cat of tree) {\n if (cat.short_id === shortId) return cat\n if (cat.children && cat.children.length > 0) {\n const found = findCategoryByShortId(cat.children, shortId)\n if (found) return found\n }\n }\n return null\n}\n\n/**\n * Находит корневого предка категории по `parent_id`.\n *\n * Для вложенной «Куртки» (parent: «Мужская») вернёт «Одежда» (корень ветки).\n * Для корневой «Одежда» вернёт саму себя.\n *\n * Используется в `CategoryTabs`, чтобы подсветить корневой таб когда юзер\n * находится на вложенной категории.\n */\nexport function findRootAncestor(\n tree: NestedCategory[],\n category: NestedCategory\n): NestedCategory | null {\n if (!category.parent_id) {\n // Уже корневая — возвращаем её саму если она есть в tree на верхнем уровне\n return tree.find((c) => c.id === category.id) ?? category\n }\n\n // Ищем родителя\n const parent = findCategoryById(tree, category.parent_id)\n if (!parent) return null\n return findRootAncestor(tree, parent)\n}\n\n/** Рекурсивный поиск по `id` (не `short_id`). Используется в findRootAncestor. */\nexport function findCategoryById(tree: NestedCategory[], id: string): NestedCategory | null {\n for (const cat of tree) {\n if (cat.id === id) return cat\n if (cat.children && cat.children.length > 0) {\n const found = findCategoryById(cat.children, id)\n if (found) return found\n }\n }\n return null\n}\n\n/**\n * Возвращает путь от корневой категории до указанной (включительно).\n *\n * Пример для «Куртки» в ветке «Одежда → Мужская → Куртки»:\n * → [Одежда, Мужская, Куртки]\n *\n * Используется для построения хлебных крошек.\n */\nexport function getCategoryPath(\n tree: NestedCategory[],\n target: NestedCategory\n): NestedCategory[] {\n const path: NestedCategory[] = []\n let current: NestedCategory | null = target\n\n while (current) {\n path.unshift(current)\n if (!current.parent_id) break\n current = findCategoryById(tree, current.parent_id)\n }\n\n return path\n}\n","import type { NestedCategory, Product } from '@zenky/storefront-api'\n\nexport interface SubcategoryGroup {\n category: NestedCategory\n products: Product[]\n}\n\nexport interface GroupedProducts {\n rootDirect: Product[]\n groups: SubcategoryGroup[]\n}\n\nexport function collectDescendantIds(category: NestedCategory): string[] {\n const ids = [category.id]\n for (const child of category.children ?? []) ids.push(...collectDescendantIds(child))\n return ids\n}\n\nfunction productCategoryIds(product: Product): string[] {\n const categories = (product as unknown as { categories?: Array<{ id: string }> }).categories\n return categories?.map((c) => c.id) ?? []\n}\n\n/**\n * Товары поддерева в порядке дерева (DFS): сперва товары самой категории\n * (в порядке каталога), затем рекурсивно товары дочерних категорий.\n * Дубли (товар в нескольких категориях) отбрасываем, оставляя первое вхождение.\n */\nfunction collectSubtreeProducts(\n category: NestedCategory,\n products: Product[],\n order: Map<string, number>\n): Product[] {\n const seen = new Set<string>()\n const out: Product[] = []\n\n const pushUnique = (list: Product[]): void => {\n for (const p of list) {\n if (!seen.has(p.id)) {\n seen.add(p.id)\n out.push(p)\n }\n }\n }\n\n pushUnique(\n products\n .filter((p) => productCategoryIds(p).includes(category.id))\n .sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0))\n )\n for (const child of category.children ?? []) {\n pushUnique(collectSubtreeProducts(child, products, order))\n }\n\n return out\n}\n\nexport function groupProductsBySubcategory(\n rootCategory: NestedCategory,\n products: Product[]\n): GroupedProducts {\n const order = new Map(products.map((p, i) => [p.id, i]))\n\n const rootDirect = products.filter((p) => productCategoryIds(p).includes(rootCategory.id))\n\n const groups: SubcategoryGroup[] = []\n for (const child of rootCategory.children ?? []) {\n const groupProducts = collectSubtreeProducts(child, products, order)\n if (groupProducts.length > 0) groups.push({ category: child, products: groupProducts })\n }\n\n return { rootDirect, groups }\n}\n","import type { CollectionsListRequest, ProductsCollection, ZenkyStorefront } from '@zenky/storefront-api'\n\n/**\n * Загрузка списка коллекций товаров магазина.\n * Pure composable — SDK передаётся параметром (DI), Nuxt не нужен.\n *\n * NOTE: одиночный `getCollection(id)` в storefront API не предусмотрен,\n * для получения товаров коллекции используется `GET /products?collection_id=...`.\n * Чтобы получить метаданные коллекции — ищем в списке через `loadCollections`.\n */\nexport function useCollections(sdk: ZenkyStorefront): {\n loadCollections: (\n storeId: string,\n request?: CollectionsListRequest\n ) => Promise<ProductsCollection[]>\n} {\n async function loadCollections(\n storeId: string,\n request?: CollectionsListRequest\n ): Promise<ProductsCollection[]> {\n const response = await sdk.collections.getCollections(storeId, request)\n return response.items\n }\n\n return { loadCollections }\n}\n","/** URL-хелперы каталога — единая точка правды для путей категорий/коллекций. */\nexport const catalogRoutes = {\n /** Корневая страница каталога: список категорий + коллекций */\n index: (): string => '/catalog',\n\n /** Страница конкретной категории */\n category: (shortId: string, slug: string | null): string =>\n `/catalog/${shortId}/${slug || shortId}`,\n\n /** Страница конкретной коллекции */\n collection: (id: string): string => `/collections/${id}`,\n} as const\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/snippets/snippets.ts","../src/snippets/buildSnippetHead.ts"],"names":[],"mappings":";AAwBO,SAAS,kBAAkB,MAAA,EAAsD;AACpF,EAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,SAAU,EAAC;AAEpC,EAAA,OAAO,MAAA,CAAO,OAAA;AAAA,IAAQ,CAAC,OAAO,UAAA,KAAA,CACzB,KAAA,CAAM,YAAY,EAAC,EACf,OAAO,CAAC,IAAA,KAAS,CAAC,CAAC,IAAA,CAAK,SAAS,IAAA,EAAM,EACvC,GAAA,CAAI,CAAC,MAAM,SAAA,KAAuB;AAC/B,MAAA,MAAM,OAAA,GAAmB;AAAA,QACrB,IAAI,CAAA,EAAG,KAAA,CAAM,MAAM,CAAA,CAAA,EAAI,UAAU,IAAI,SAAS,CAAA,CAAA;AAAA,QAC9C,UAAU,KAAA,CAAM,MAAA;AAAA,QAChB,UAAU,IAAA,CAAK,QAAA;AAAA,QACf,MAAM,IAAA,CAAK,OAAA;AAAA,QACX,OAAA,EAAS,KAAK,OAAA,IAAW;AAAA,OAC7B;AAEA,MAAA,IAAI,IAAA,CAAK,IAAA,KAAS,MAAA,EAAW,OAAA,CAAQ,OAAO,IAAA,CAAK,IAAA;AACjD,MAAA,OAAO,OAAA;AAAA,IACX,CAAC;AAAA,GACT;AACJ;;;AChCA,SAAS,cAAc,OAAA,EAA0B;AAC7C,EAAA,MAAM,EAAA,GAAK,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,EAAE,CAAA;AACpC,EAAA,OAAO,CAAA;AAAA;AAAA,QAAA,EAED,QAAQ,IAAI;AAAA;AAAA,+CAAA,EAE2B,EAAE,CAAA;AAAA;AAAA,0BAAA,EAEvB,EAAE,CAAA;AAAA;AAAA;AAAA,KAAA,CAAA;AAI9B;AAUA,SAAS,mBAAmB,GAAA,EAAwB;AAChD,EAAA,MAAM,QAAmB,EAAC;AAC1B,EAAA,MAAM,EAAA,GAAK,sEAAA;AACX,EAAA,IAAI,KAAA;AACJ,EAAA,OAAA,CAAQ,KAAA,GAAQ,EAAA,CAAG,IAAA,CAAK,GAAG,OAAO,IAAA,EAAM;AACpC,IAAA,MAAM,IAAA,GAAO,MAAM,CAAC,CAAA;AACpB,IAAA,IAAI,IAAA,EAAM,KAAA,CAAM,IAAI,CAAA,GAAI,KAAA,CAAM,CAAC,CAAA,IAAK,KAAA,CAAM,CAAC,CAAA,IAAK,KAAA,CAAM,CAAC,CAAA,IAAK,IAAA;AAAA,EAChE;AACA,EAAA,OAAO,KAAA;AACX;AAEA,SAAS,cAAc,IAAA,EAA8B;AACjD,EAAA,MAAM,MAAA,GAAyB,EAAE,IAAA,EAAM,EAAC,EAAG,MAAM,EAAC,EAAG,MAAA,EAAQ,EAAC,EAAE;AAChE,EAAA,IAAI,CAAA;AAEJ,EAAA,MAAM,MAAA,GAAS,mBAAA;AACf,EAAA,OAAA,CAAQ,CAAA,GAAI,MAAA,CAAO,IAAA,CAAK,IAAI,OAAO,IAAA,EAAM,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,kBAAA,CAAmB,CAAA,CAAE,CAAC,CAAA,IAAK,EAAE,CAAC,CAAA;AAExF,EAAA,MAAM,MAAA,GAAS,mBAAA;AACf,EAAA,OAAA,CAAQ,CAAA,GAAI,MAAA,CAAO,IAAA,CAAK,IAAI,OAAO,IAAA,EAAM,MAAA,CAAO,IAAA,CAAK,IAAA,CAAK,kBAAA,CAAmB,CAAA,CAAE,CAAC,CAAA,IAAK,EAAE,CAAC,CAAA;AAExF,EAAA,MAAM,QAAA,GAAW,yCAAA;AACjB,EAAA,OAAA,CAAQ,CAAA,GAAI,QAAA,CAAS,IAAA,CAAK,IAAI,OAAO,IAAA,EAAM;AACvC,IAAA,MAAM,KAAA,GAAQ,kBAAA,CAAmB,CAAA,CAAE,CAAC,KAAK,EAAE,CAAA;AAC3C,IAAA,MAAM,KAAA,GAAA,CAAS,CAAA,CAAE,CAAC,CAAA,IAAK,IAAI,IAAA,EAAK;AAChC,IAAA,MAAA,CAAO,MAAA,CAAO,KAAK,KAAA,GAAQ,EAAE,GAAG,KAAA,EAAO,SAAA,EAAW,KAAA,EAAM,GAAI,KAAK,CAAA;AAAA,EACrE;AAEA,EAAA,OAAO,MAAA;AACX;AAMO,SAAS,iBAAiB,QAAA,EAAkC;AAC/D,EAAA,MAAM,UAAU,QAAA,CAAS,MAAA,CAAO,CAAC,CAAA,KAAM,EAAE,OAAO,CAAA;AAEhD,EAAA,MAAM,aAAa,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,aAAa,YAAY,CAAA;AACpE,EAAA,MAAM,cAAc,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,aAAa,KAAK,CAAA;AAC9D,EAAA,MAAM,mBAAmB,OAAA,CAAQ,MAAA;AAAA,IAC7B,CAAC,MAAM,CAAA,CAAE,QAAA,KAAa,UAAU,CAAA,CAAE,QAAA,CAAS,WAAW,MAAM;AAAA,GAChE;AAEA,EAAA,MAAM,UAAA,GAAa,iBAAiB,GAAA,CAAI,CAAC,MAAM,aAAA,CAAc,CAAA,CAAE,IAAI,CAAC,CAAA;AACpE,EAAA,MAAM,WAAW,UAAA,CAAW,OAAA,CAAQ,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AACjD,EAAA,MAAM,WAAW,UAAA,CAAW,OAAA,CAAQ,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AACjD,EAAA,MAAM,cAAc,UAAA,CAAW,OAAA,CAAQ,CAAC,CAAA,KAAM,EAAE,MAAM,CAAA;AAEtD,EAAA,OAAO;AAAA,IACH,IAAA,EAAM,QAAA;AAAA,IACN,IAAA,EAAM,QAAA;AAAA,IACN,MAAA,EAAQ;AAAA,MACJ,GAAG,UAAA,CAAW,GAAA,CAAI,CAAC,OAAA,MAAa;AAAA,QAC5B,GAAA,EAAK,CAAA,WAAA,EAAc,OAAA,CAAQ,EAAE,CAAA,CAAA;AAAA,QAC7B,SAAA,EAAW,cAAc,OAAO,CAAA;AAAA,QAChC,IAAA,EAAM,OAAA,CAAQ,QAAA,CAAS,UAAA,CAAW,MAAM,CAAA;AAAA,QACxC,mBAAmB,OAAA,CAAQ;AAAA,OAC/B,CAAE,CAAA;AAAA,MACF,GAAG;AAAA,KACP;AAAA,IACA,KAAA,EAAO,WAAA,CAAY,GAAA,CAAI,CAAC,OAAA,MAAa;AAAA,MACjC,GAAA,EAAK,CAAA,YAAA,EAAe,OAAA,CAAQ,EAAE,CAAA,CAAA;AAAA,MAC9B,WAAW,OAAA,CAAQ,IAAA;AAAA,MACnB,mBAAmB,OAAA,CAAQ;AAAA,KAC/B,CAAE;AAAA,GACN;AACJ","file":"chunk-DKWDKVXQ.js","sourcesContent":["export type SnippetPosition = 'head_start' | 'head_end' | 'body_start' | 'body_end'\nexport type SnippetLanguage = 'html' | 'css' | 'javascript'\n\nexport interface Snippet {\n id: string\n name?: string\n position: SnippetPosition\n language: SnippetLanguage\n code: string\n enabled: boolean\n}\n\nexport interface RawSnippetItem {\n language: SnippetLanguage\n content: string\n enabled?: boolean\n name?: string\n}\n\nexport interface SnippetGroup {\n region: SnippetPosition\n snippets: RawSnippetItem[]\n}\n\nexport function normalizeSnippets(groups: SnippetGroup[] | null | undefined): Snippet[] {\n if (!Array.isArray(groups)) return []\n\n return groups.flatMap((group, groupIndex) =>\n (group.snippets ?? [])\n .filter((item) => !!item.content?.trim())\n .map((item, itemIndex): Snippet => {\n const snippet: Snippet = {\n id: `${group.region}-${groupIndex}-${itemIndex}`,\n position: group.region,\n language: item.language,\n code: item.content,\n enabled: item.enabled ?? true,\n }\n // exactOptionalPropertyTypes: name задаём только если оно есть\n if (item.name !== undefined) snippet.name = item.name\n return snippet\n })\n )\n}\n","import type { Snippet } from './snippets'\n\n/** Head-нагрузка для инъекции сниппетов (потребитель отдаёт её в useHead). */\nexport interface SnippetHead {\n meta: Record<string, string | boolean>[]\n link: Record<string, string | boolean>[]\n script: Record<string, unknown>[]\n style: Record<string, string | boolean>[]\n}\n\n/** JS-сниппет в try/catch с dispatch `zenky:snippet-error` при падении. */\nfunction wrapSnippetJs(snippet: Snippet): string {\n const id = JSON.stringify(snippet.id)\n return `;(() => {\n try {\n ${snippet.code}\n } catch (error) {\n console.error('[snippet:error]', { id: ${id}, error });\n window.dispatchEvent(new CustomEvent('zenky:snippet-error', {\n detail: { id: ${id}, message: error instanceof Error ? error.message : String(error) },\n }));\n }\n})();`\n}\n\ntype HeadAttrs = Record<string, string | boolean>\n\ninterface ParsedHeadHtml {\n meta: HeadAttrs[]\n link: HeadAttrs[]\n script: Array<HeadAttrs & { innerHTML?: string }>\n}\n\nfunction parseTagAttributes(raw: string): HeadAttrs {\n const attrs: HeadAttrs = {}\n const re = /([a-zA-Z][\\w:-]*)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+)))?/g\n let match: RegExpExecArray | null\n while ((match = re.exec(raw)) !== null) {\n const name = match[1]\n if (name) attrs[name] = match[2] ?? match[3] ?? match[4] ?? true\n }\n return attrs\n}\n\nfunction parseHeadHtml(html: string): ParsedHeadHtml {\n const result: ParsedHeadHtml = { meta: [], link: [], script: [] }\n let m: RegExpExecArray | null\n\n const metaRe = /<meta\\b([^>]*)>/gi\n while ((m = metaRe.exec(html)) !== null) result.meta.push(parseTagAttributes(m[1] ?? ''))\n\n const linkRe = /<link\\b([^>]*)>/gi\n while ((m = linkRe.exec(html)) !== null) result.link.push(parseTagAttributes(m[1] ?? ''))\n\n const scriptRe = /<script\\b([^>]*)>([\\s\\S]*?)<\\/script>/gi\n while ((m = scriptRe.exec(html)) !== null) {\n const attrs = parseTagAttributes(m[1] ?? '')\n const inner = (m[2] ?? '').trim()\n result.script.push(inner ? { ...attrs, innerHTML: inner } : attrs)\n }\n\n return result\n}\n\n/**\n * Раскладывает сниппеты магазина в head-нагрузку: JS (обёрнут в try/catch),\n * CSS (style), head-HTML (распарсенные meta/link/script). Инъекция — у потребителя.\n */\nexport function buildSnippetHead(snippets: Snippet[]): SnippetHead {\n const enabled = snippets.filter((s) => s.enabled)\n\n const jsSnippets = enabled.filter((s) => s.language === 'javascript')\n const cssSnippets = enabled.filter((s) => s.language === 'css')\n const headHtmlSnippets = enabled.filter(\n (s) => s.language === 'html' && s.position.startsWith('head')\n )\n\n const parsedHead = headHtmlSnippets.map((s) => parseHeadHtml(s.code))\n const headMeta = parsedHead.flatMap((p) => p.meta)\n const headLink = parsedHead.flatMap((p) => p.link)\n const headScripts = parsedHead.flatMap((p) => p.script)\n\n return {\n meta: headMeta,\n link: headLink,\n script: [\n ...jsSnippets.map((snippet) => ({\n key: `snippet-js-${snippet.id}`,\n innerHTML: wrapSnippetJs(snippet),\n body: snippet.position.startsWith('body'),\n 'data-snippet-id': snippet.id,\n })),\n ...headScripts,\n ],\n style: cssSnippets.map((snippet) => ({\n key: `snippet-css-${snippet.id}`,\n innerHTML: snippet.code,\n 'data-snippet-id': snippet.id,\n })),\n }\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/catalog/catalogResolver.ts"],"names":[],"mappings":";AAEO,SAAS,OAAA,CAAQ,OAAc,MAAA,EAAqC;AACvE,EAAA,IAAI,CAAC,KAAA,CAAM,MAAA,CAAO,MAAA,EAAQ,OAAO,IAAA;AAEjC,EAAA,MAAM,WAAA,GAAc,KAAA,CAAM,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,UAAU,CAAA,IAAK,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,IAAK,IAAA;AAEjF,EAAA,IAAI,CAAC,QAAQ,OAAO,WAAA;AAEpB,EAAA,OAAO,KAAA,CAAM,OAAO,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,EAAA,KAAO,MAAM,CAAA,IAAK,WAAA;AACxD;AAEO,SAAS,QAAA,CAAS,MAAY,OAAA,EAAuC;AACxE,EAAA,IAAI,CAAC,IAAA,CAAK,MAAA,EAAQ,MAAA,EAAQ,OAAO,IAAA;AAEjC,EAAA,MAAM,YAAA,GAAe,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,IAAK,IAAA;AAEvC,EAAA,IAAI,CAAC,SAAS,OAAO,YAAA;AAErB,EAAA,OAAO,IAAA,CAAK,OAAO,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,EAAA,KAAO,OAAO,CAAA,IAAK,YAAA;AACxD;AAEO,SAAS,gBAAA,CAAiB,KAAA,EAAc,IAAA,EAAY,KAAA,EAA2C;AAClG,EAAA,IAAI,CAAC,KAAA,CAAM,QAAA,CAAS,MAAA,EAAQ;AACxB,IAAA,OAAA,CAAQ,KAAK,4CAAA,EAA8C;AAAA,MACvD,SAAS,KAAA,CAAM,EAAA;AAAA,MACf,QAAQ,IAAA,CAAK,EAAA;AAAA,MACb,OAAA,EAAS,OAAO,EAAA,IAAM;AAAA,KACzB,CAAA;AACD,IAAA,OAAO,IAAA;AAAA,EACX;AAGA,EAAA,IAAI,KAAA,EAAO;AACP,IAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,OAAA,KAAY,IAAA,CAAK,EAAA,IAAM,CAAA,CAAE,QAAA,KAAa,MAAM,EAAE,CAAA;AACzF,IAAA,IAAI,OAAO,OAAO,KAAA;AAAA,EACtB;AAEA,EAAA,MAAM,QAAA,GAAW,KAAA,CAAM,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,OAAA,KAAY,IAAA,CAAK,EAAA,IAAM,CAAA,CAAE,QAAA,KAAa,IAAI,CAAA;AACxF,EAAA,IAAI,UAAU,OAAO,QAAA;AAErB,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,QAAA,CAAS,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,OAAA,KAAY,IAAA,IAAQ,CAAA,CAAE,QAAA,KAAa,IAAI,CAAA;AACnF,EAAA,IAAI,QAAQ,OAAO,MAAA;AAEnB,EAAA,OAAA,CAAQ,KAAK,gEAAA,EAAkE;AAAA,IAC3E,SAAS,KAAA,CAAM,EAAA;AAAA,IACf,QAAQ,IAAA,CAAK,EAAA;AAAA,IACb,OAAA,EAAS,OAAO,EAAA,IAAM,IAAA;AAAA,IACtB,iBAAA,EAAmB,KAAA,CAAM,QAAA,CAAS,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,MAC1C,SAAS,CAAA,CAAE,OAAA;AAAA,MACX,UAAU,CAAA,CAAE;AAAA,KAChB,CAAE;AAAA,GACL,CAAA;AAED,EAAA,OAAO,IAAA;AACX","file":"chunk-EYRCAUUP.js","sourcesContent":["import type { City, RemoteCatalog, Stock, Store } from '@zenky/storefront-api'\n\nexport function getCity(store: Store, cityId?: string | null): City | null {\n if (!store.cities.length) return null\n\n const defaultCity = store.cities.find((c) => c.is_default) ?? store.cities[0] ?? null\n\n if (!cityId) return defaultCity\n\n return store.cities.find((c) => c.id === cityId) ?? defaultCity\n}\n\nexport function getStock(city: City, stockId?: string | null): Stock | null {\n if (!city.stocks?.length) return null\n\n const defaultStock = city.stocks[0] ?? null\n\n if (!stockId) return defaultStock\n\n return city.stocks.find((s) => s.id === stockId) ?? defaultStock\n}\n\nexport function getRemoteCatalog(store: Store, city: City, stock: Stock | null): RemoteCatalog | null {\n if (!store.catalogs.length) {\n console.warn('[catalog-resolver] store.catalogs is empty', {\n storeId: store.id,\n cityId: city.id,\n stockId: stock?.id ?? null,\n })\n return null\n }\n\n // Приоритет: точное совпадение city+stock → city-only → глобальный.\n if (stock) {\n const exact = store.catalogs.find((c) => c.city_id === city.id && c.stock_id === stock.id)\n if (exact) return exact\n }\n\n const cityOnly = store.catalogs.find((c) => c.city_id === city.id && c.stock_id === null)\n if (cityOnly) return cityOnly\n\n const global = store.catalogs.find((c) => c.city_id === null && c.stock_id === null)\n if (global) return global\n\n console.warn('[catalog-resolver] no catalog matched (exact/city-only/global)', {\n storeId: store.id,\n cityId: city.id,\n stockId: stock?.id ?? null,\n availableCatalogs: store.catalogs.map((c) => ({\n city_id: c.city_id,\n stock_id: c.stock_id,\n })),\n })\n\n return null\n}\n"]}