@zenky/storefront-vue 0.10.0 → 0.10.4

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 (77) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +66 -6
  3. package/dist/cart/index.d.ts +31 -2
  4. package/dist/cart/index.js +6 -9
  5. package/dist/catalog/index.d.ts +1 -11
  6. package/dist/catalog/index.js +2 -2
  7. package/dist/chunk-2CI4RW7V.js +21 -0
  8. package/dist/chunk-2CI4RW7V.js.map +1 -0
  9. package/dist/{chunk-GW2QNTAD.js → chunk-4IFPVEO7.js} +3 -113
  10. package/dist/chunk-4IFPVEO7.js.map +1 -0
  11. package/dist/{chunk-EYRCAUUP.js → chunk-E3G7NGCH.js} +2 -2
  12. package/dist/chunk-E3G7NGCH.js.map +1 -0
  13. package/dist/{chunk-IGDDWGS4.js → chunk-IVG4FQE5.js} +21 -13
  14. package/dist/chunk-IVG4FQE5.js.map +1 -0
  15. package/dist/{chunk-PJ2LEKW6.js → chunk-MZ66ARDL.js} +18 -3
  16. package/dist/chunk-MZ66ARDL.js.map +1 -0
  17. package/dist/{chunk-2OD7CLKD.js → chunk-NG7RUIWW.js} +3 -13
  18. package/dist/chunk-NG7RUIWW.js.map +1 -0
  19. package/dist/{chunk-VL3T36VM.js → chunk-NIQFYJRP.js} +53 -9
  20. package/dist/chunk-NIQFYJRP.js.map +1 -0
  21. package/dist/{chunk-AL5W5JSD.js → chunk-OTSAR32A.js} +6 -3
  22. package/dist/chunk-OTSAR32A.js.map +1 -0
  23. package/dist/{chunk-BIGTKVC6.js → chunk-SS7B2NLF.js} +3 -3
  24. package/dist/{chunk-BIGTKVC6.js.map → chunk-SS7B2NLF.js.map} +1 -1
  25. package/dist/{chunk-PWHIDGKW.js → chunk-TB5TBJ4U.js} +53 -6
  26. package/dist/chunk-TB5TBJ4U.js.map +1 -0
  27. package/dist/{chunk-4MJKN7PL.js → chunk-YREGCQSB.js} +4 -6
  28. package/dist/chunk-YREGCQSB.js.map +1 -0
  29. package/dist/{chunk-OQLU7FSR.js → chunk-YS5DXYYV.js} +228 -51
  30. package/dist/chunk-YS5DXYYV.js.map +1 -0
  31. package/dist/index.d.ts +6 -8
  32. package/dist/index.js +14 -16
  33. package/dist/order/index.d.ts +2 -0
  34. package/dist/order/index.js +2 -5
  35. package/dist/order/index.js.map +1 -1
  36. package/dist/pagination/index.js.map +1 -1
  37. package/dist/ports/index.d.ts +12 -1
  38. package/dist/ports/index.js +1 -1
  39. package/dist/products/index.js +3 -4
  40. package/dist/promotions/index.js +3 -5
  41. package/dist/seo/index.d.ts +5 -11
  42. package/dist/seo/index.js +17 -27
  43. package/dist/seo/index.js.map +1 -1
  44. package/dist/snippets/index.d.ts +21 -15
  45. package/dist/snippets/index.js +1 -1
  46. package/dist/stores/index.d.ts +1 -1
  47. package/dist/stores/index.js +3 -5
  48. package/dist/theme/index.d.ts +1 -11
  49. package/dist/theme/index.js +1 -1
  50. package/dist/utils/index.d.ts +28 -5
  51. package/dist/utils/index.js +1 -1
  52. package/dist/website-settings/index.d.ts +1 -1
  53. package/docs/consumer-setup.md +279 -0
  54. package/docs/ports.md +148 -0
  55. package/docs/releasing.md +53 -0
  56. package/package.json +27 -8
  57. package/dist/chunk-2OD7CLKD.js.map +0 -1
  58. package/dist/chunk-4MJKN7PL.js.map +0 -1
  59. package/dist/chunk-AL5W5JSD.js.map +0 -1
  60. package/dist/chunk-BWUKMWHL.js +0 -45
  61. package/dist/chunk-BWUKMWHL.js.map +0 -1
  62. package/dist/chunk-DKWDKVXQ.js +0 -91
  63. package/dist/chunk-DKWDKVXQ.js.map +0 -1
  64. package/dist/chunk-EYRCAUUP.js.map +0 -1
  65. package/dist/chunk-GW2QNTAD.js.map +0 -1
  66. package/dist/chunk-IGDDWGS4.js.map +0 -1
  67. package/dist/chunk-OQLU7FSR.js.map +0 -1
  68. package/dist/chunk-PJ2LEKW6.js.map +0 -1
  69. package/dist/chunk-PWHIDGKW.js.map +0 -1
  70. package/dist/chunk-V3VVDQIR.js +0 -27
  71. package/dist/chunk-V3VVDQIR.js.map +0 -1
  72. package/dist/chunk-VL3T36VM.js.map +0 -1
  73. package/dist/layout/index.d.ts +0 -54
  74. package/dist/layout/index.js +0 -102
  75. package/dist/layout/index.js.map +0 -1
  76. package/dist/schedule-Dd3zwHEb.d.ts +0 -27
  77. package/dist/snippets-BC5bkSjm.d.ts +0 -23
@@ -1,5 +1,4 @@
1
- import { Phone } from '@zenky/storefront-api';
2
- export { D as DayScheduleInfo, S as SocialLink, T as TodayScheduleInfo, f as formatScheduleStatus, g as getEmailLink, a as getSocialLinks, b as getTodayInfo, c as getWebsiteLink, d as getWeeklySchedule } from '../schedule-Dd3zwHEb.js';
1
+ import { Phone, ContactType, Contact, Schedule } from '@zenky/storefront-api';
3
2
 
4
3
  declare function createDebounce(fn: () => void, delay: number): () => void;
5
4
 
@@ -17,12 +16,36 @@ declare function getPhoneTelLink(phone: Phone | null | undefined): string | null
17
16
  */
18
17
  declare function formatPhoneSpaced(formatted: string | null | undefined): string;
19
18
 
19
+ interface SocialLink {
20
+ type: ContactType;
21
+ url: string;
22
+ label: string;
23
+ }
24
+ declare function getSocialLinks(contacts: Contact[]): SocialLink[];
25
+ declare function getEmailLink(contacts: Contact[]): string | null;
26
+ declare function getWebsiteLink(contacts: Contact[]): string | null;
27
+
28
+ interface DayScheduleInfo {
29
+ dayKey: string;
30
+ name: string;
31
+ hours: string | null;
32
+ }
33
+ interface TodayScheduleInfo {
34
+ isOpenNow: boolean;
35
+ todayHours: string | null;
36
+ openingAt: string | null;
37
+ closingAt: string | null;
38
+ }
39
+ declare function getTodayInfo(schedule: Schedule | null | undefined): TodayScheduleInfo | null;
40
+ declare function getWeeklySchedule(schedule: Schedule | null | undefined, locale?: string): DayScheduleInfo[];
41
+ declare function formatScheduleStatus(schedule: Schedule | null | undefined, locale?: string): string | null;
42
+
20
43
  /**
21
44
  * Генерирует URL картинки QR-кода через Zenky media API.
22
45
  *
23
46
  * Контент кодируется в base64 и передаётся в query `content`.
24
- * `btoa` работает только с Latin-1 — для юникода предварительно прогоняем через
25
- * `encodeURIComponent` + `unescape`. На сервере используем Buffer.
47
+ * Реализация не зависит от `window`, `btoa` или Node.js `Buffer`, поэтому один
48
+ * и тот же код работает в браузере, Node SSR и edge runtimes.
26
49
  */
27
50
  declare function buildQrCodeUrl(target: string, apiBaseUrl: string, size?: number): string;
28
51
 
@@ -63,4 +86,4 @@ declare function getApiFieldErrors(e: unknown): Record<string, string[]> | null;
63
86
  */
64
87
  declare function formatAddressCityLast(raw: string): string;
65
88
 
66
- export { type OperationChain, buildQrCodeUrl, createDebounce, createOperationChain, formatAddressCityLast, formatDate, formatDateTime, formatPhoneNational, formatPhoneSpaced, getApiErrorMessage, getApiFieldErrors, getPhoneTelLink, pluralize, toDateInputValue, todayInTimezone };
89
+ export { type DayScheduleInfo, type OperationChain, type SocialLink, type TodayScheduleInfo, buildQrCodeUrl, createDebounce, createOperationChain, formatAddressCityLast, formatDate, formatDateTime, formatPhoneNational, formatPhoneSpaced, formatScheduleStatus, getApiErrorMessage, getApiFieldErrors, getEmailLink, getPhoneTelLink, getSocialLinks, getTodayInfo, getWebsiteLink, getWeeklySchedule, pluralize, toDateInputValue, todayInTimezone };
@@ -1,4 +1,4 @@
1
- export { buildQrCodeUrl, createOperationChain, formatAddressCityLast, formatDate, formatDateTime, formatPhoneNational, formatPhoneSpaced, formatScheduleStatus, getApiErrorMessage, getApiFieldErrors, getEmailLink, getPhoneTelLink, getSocialLinks, getTodayInfo, getWebsiteLink, getWeeklySchedule, pluralize, toDateInputValue, todayInTimezone } from '../chunk-PJ2LEKW6.js';
1
+ export { buildQrCodeUrl, createOperationChain, formatAddressCityLast, formatDate, formatDateTime, formatPhoneNational, formatPhoneSpaced, formatScheduleStatus, getApiErrorMessage, getApiFieldErrors, getEmailLink, getPhoneTelLink, getSocialLinks, getTodayInfo, getWebsiteLink, getWeeklySchedule, pluralize, toDateInputValue, todayInTimezone } from '../chunk-MZ66ARDL.js';
2
2
  export { createDebounce } from '../chunk-XUGXLJZB.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -1,6 +1,6 @@
1
1
  import { Media } from '@zenky/storefront-api';
2
2
  import { ZenkyIntegration } from '@zenky/integrations-sdk';
3
- import { a as SnippetGroup } from '../snippets-BC5bkSjm.js';
3
+ import { SnippetGroup } from '../snippets/index.js';
4
4
 
5
5
  type SettingValue = string | number | boolean | null;
6
6
  interface WebsiteSettingsValues {
@@ -0,0 +1,279 @@
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.4
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`,
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
+ messages: {
84
+ format: (key, params) => i18n.t(key, params),
85
+ },
86
+ activity: {
87
+ begin: (kind) => busy.begin(kind),
88
+ end: (kind) => busy.end(kind),
89
+ },
90
+ }
91
+
92
+ app.use(pinia)
93
+ app.use(createStorefront(runtime))
94
+ app.mount('#app')
95
+ ```
96
+
97
+ Use `NOOP_NOTIFIER` and `NOOP_ACTIVITY_REPORTER` from the `ports` subpath when a
98
+ read-only consumer intentionally has no notifications or busy UI. Keep that decision
99
+ explicit.
100
+
101
+ ## 4. Nuxt adapter
102
+
103
+ Nuxt consumers should install the runtime in one plugin and keep Nuxt APIs in that
104
+ plugin, not in the package.
105
+
106
+ ```ts
107
+ import { createStorefront } from '@zenky/storefront-vue'
108
+ import type { StorefrontRuntime } from '@zenky/storefront-vue/ports'
109
+
110
+ export default defineNuxtPlugin((nuxtApp) => {
111
+ const config = useRuntimeConfig()
112
+
113
+ const runtime: StorefrontRuntime = {
114
+ sdk: useStorefrontApi(),
115
+ config: {
116
+ baseUrl: config.public.storefrontBaseUrl,
117
+ baseAuthUrl: config.public.storefrontAuthUrl,
118
+ },
119
+ storage: {
120
+ get: (key) => useCookie<string | null>(key).value ?? null,
121
+ set: (key, value, options) => {
122
+ useCookie<string>(key, options).value = value
123
+ },
124
+ remove: (key, options) => {
125
+ useCookie(key, options).value = null
126
+ },
127
+ },
128
+ notifier: createNotifierAdapter(),
129
+ messages: createMessageFormatter(),
130
+ activity: createActivityAdapter(),
131
+ }
132
+
133
+ nuxtApp.vueApp.use(createStorefront(runtime))
134
+ })
135
+ ```
136
+
137
+ Resolve optional integrations lazily when their initialization order is later than the
138
+ storefront plugin:
139
+
140
+ ```ts
141
+ runtime.events = {
142
+ get: () => useZenkyIntegrations(),
143
+ }
144
+
145
+ runtime.realtime = {
146
+ get: () => useRealtimeAdapter(),
147
+ }
148
+ ```
149
+
150
+ Do not capture an initial `null` integration instance during plugin setup.
151
+
152
+ ## 5. Storage and sensitive data
153
+
154
+ The storage adapter is synchronous because order and auth credentials are read while
155
+ composables are created.
156
+
157
+ For browser-only applications, `localStorage` or cookies are acceptable according to
158
+ the consumer's security model. For SSR, use request-scoped cookies.
159
+
160
+ Never:
161
+
162
+ - share in-memory storage between users;
163
+ - log storage values, order tokens, API tokens, cookies, confirmation codes, payment
164
+ tokens, phone numbers, addresses, or order comments;
165
+ - expose secret server configuration through public runtime config;
166
+ - persist real credentials in test fixtures.
167
+
168
+ See [Runtime Ports](ports.md) for the complete security boundary.
169
+
170
+ ## 6. SSR isolation
171
+
172
+ For every server request, create a new:
173
+
174
+ 1. Vue application;
175
+ 2. Pinia instance;
176
+ 3. `StorefrontRuntime`;
177
+ 4. storage adapter.
178
+
179
+ Do not keep runtime or Pinia instances in module-global server state. Runtime-owned
180
+ mutation versions, request deduplication, restriction gates, and related-product state
181
+ must not cross request or application boundaries.
182
+
183
+ ## 7. Local package development with yalc
184
+
185
+ Use `yalc` only when changing the package and consumer together.
186
+
187
+ In this repository:
188
+
189
+ ```bash
190
+ npm run build
191
+ npx yalc publish
192
+ ```
193
+
194
+ In the consumer:
195
+
196
+ ```bash
197
+ npx yalc add @zenky/storefront-vue
198
+ ```
199
+
200
+ After another package change:
201
+
202
+ ```bash
203
+ npm run build
204
+ npx yalc push
205
+ ```
206
+
207
+ Before committing the consumer:
208
+
209
+ ```bash
210
+ npx yalc remove @zenky/storefront-vue
211
+ npm install --save-exact @zenky/storefront-vue@<published-version>
212
+ ```
213
+
214
+ Verify that `package.json` and `package-lock.json` no longer reference `.yalc`.
215
+
216
+ ## 8. Consumer quality gate
217
+
218
+ Before requesting review:
219
+
220
+ ```bash
221
+ npm ci
222
+ npm run lint
223
+ npm run test
224
+ npm run build
225
+ ```
226
+
227
+ For Nuxt consumers, also verify:
228
+
229
+ - the first SSR request returns HTML without a storefront-runtime or Pinia error;
230
+ - hydration completes without mismatch warnings;
231
+ - a new browser session creates or restores the expected order;
232
+ - a second app or SSR request does not see the first user's state;
233
+ - optional integrations initialized after the runtime still receive events.
234
+
235
+ For cart or checkout changes, cover:
236
+
237
+ - add, increment, decrement, and clear;
238
+ - API failures and stale responses;
239
+ - delivery and payment selection;
240
+ - minimum-order behavior;
241
+ - product restrictions;
242
+ - cleanup of busy state.
243
+
244
+ ## 9. Upgrade checklist
245
+
246
+ 1. Read the package changelog and migration notes.
247
+ 2. Install the exact target version.
248
+ 3. Review changed ports and exported types.
249
+ 4. Run the package consumer's unit tests and production build.
250
+ 5. Run smoke tests against a dedicated development store.
251
+ 6. Deploy to development.
252
+ 7. Run the same smoke suite against the deployed URL.
253
+ 8. Only then widen the version range or promote a prerelease.
254
+
255
+ ## 10. Common failures
256
+
257
+ ### `createStorefront() is not installed`
258
+
259
+ The runtime plugin was not installed, was installed after a component used the package,
260
+ or a second Vue application did not receive its own runtime.
261
+
262
+ ### `getActivePinia()` or `No active Pinia`
263
+
264
+ Install Pinia before `createStorefront(runtime)` and before calling package stores.
265
+
266
+ ### The app builds locally but CI cannot install the package
267
+
268
+ Check `package.json` and the lockfile for `file:.yalc`, local paths, or an unpublished
269
+ version.
270
+
271
+ ### SSR users share cart or restriction state
272
+
273
+ A runtime, Pinia instance, or storage adapter is being reused between requests. Make all
274
+ three request-scoped.
275
+
276
+ ### An optional integration never receives events
277
+
278
+ Provide a lazy `{ get() }` adapter instead of capturing the integration instance before
279
+ it is initialized.
package/docs/ports.md ADDED
@@ -0,0 +1,148 @@
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
+ messages,
37
+ activity,
38
+ }
39
+
40
+ app.use(pinia)
41
+ app.use(createStorefront(runtime))
42
+ ```
43
+
44
+ For a locally isolated widget, `provideStorefront(runtime)` can be called inside its setup
45
+ scope instead of installing the plugin globally.
46
+
47
+ ## Required ports
48
+
49
+ | Port | Contract | Responsibility |
50
+ | --- | --- | --- |
51
+ | `sdk` | `ZenkyStorefront` | Typed access to Zenky Storefront API resources. The consumer owns configuration and request transport. |
52
+ | `config` | `StorefrontConfig` | Public storefront and auth base URLs plus an optional sales channel id. |
53
+ | `storage` | `StorageAdapter` | Synchronous key/value persistence used for order/auth state and lightweight preferences. |
54
+ | `notifier` | `Notifier` | User-facing `error`, `success`, `warning`, and generic message notifications. |
55
+ | `messages` | `MessageFormatter` | Maps stable domain message keys and parameters to localized consumer copy. |
56
+ | `activity` | `ActivityReporter` | Balanced `begin(kind)` / `end(kind)` signals for long-running operations. |
57
+
58
+ Required ports are intentionally small. If an application has no UI for notifications or
59
+ activity, use `NOOP_NOTIFIER` and `NOOP_ACTIVITY_REPORTER` explicitly.
60
+ For tests that do not render messages, use `KEY_MESSAGE_FORMATTER`. Production consumers
61
+ should provide a locale-aware formatter.
62
+
63
+ ## Optional ports
64
+
65
+ | Port | Contract | Behavior when absent |
66
+ | --- | --- | --- |
67
+ | `events` | Lazy `EventsPort.get()` returning `ZenkyIntegrationsSDK` | Integration events are skipped. |
68
+ | `realtime` | Lazy `RealtimePort.get()` returning `RealtimeAdapter` | Realtime order updates are skipped. |
69
+ | `catalogReloader` | `CatalogReloader.reload()` | Stock changes do not trigger an external catalog reload. |
70
+ | `rum` | `RumReporter.mark()` / `measure()` | Performance instrumentation is skipped. |
71
+ | `commerceEvents` | Commerce tracking plus anonymous/session identity | Commerce events and checkout identity enrichment are skipped. |
72
+
73
+ `events` and `realtime` are providers rather than captured values because integrations can
74
+ be initialized after the storefront runtime. The core resolves them at the moment an event
75
+ is emitted or a subscription is needed.
76
+
77
+ ## Storage adapter
78
+
79
+ The adapter is synchronous because cart credentials are needed while composables are being
80
+ created. Browser applications can wrap cookies or `localStorage`; SSR applications should
81
+ wrap request/response cookies. `createMemoryStorage()` is intended for tests and isolated,
82
+ request-scoped SSR work only.
83
+
84
+ ```ts
85
+ import type { StorageAdapter } from '@zenky/storefront-vue/ports'
86
+
87
+ export function createCookieStorage(cookies: CookieStore): StorageAdapter {
88
+ return {
89
+ get: (key) => cookies.get(key) ?? null,
90
+ set: (key, value, options) => {
91
+ cookies.set(key, value, {
92
+ path: options?.path ?? '/',
93
+ maxAge: options?.maxAge,
94
+ sameSite: options?.sameSite ?? 'lax',
95
+ secure: options?.secure ?? true,
96
+ })
97
+ },
98
+ remove: (key, options) => cookies.remove(key, { path: options?.path ?? '/' }),
99
+ }
100
+ }
101
+ ```
102
+
103
+ Do not use one in-memory adapter across users. Do not log storage values. Where the host
104
+ framework permits it, prefer secure, same-site cookies and narrowly scoped paths. Whether a
105
+ cookie can be `httpOnly` depends on where the browser-side SDK needs access to the value.
106
+
107
+ ## SSR and multi-app isolation
108
+
109
+ For every SSR request, create all three together:
110
+
111
+ 1. a new Pinia instance;
112
+ 2. a new `StorefrontRuntime` with request-scoped adapters;
113
+ 3. a new Vue application using those instances.
114
+
115
+ Runtime-owned mutation versions, request deduplication, and related-product state are held
116
+ per runtime. Reusing a runtime or Pinia across requests defeats that isolation and can leak
117
+ state between users.
118
+
119
+ Public entries are verified by CI through an import in a Node environment without browser
120
+ globals. Built output is also rejected if it contains CommonJS, Node built-in imports,
121
+ `Buffer`, or `process` references. Browser-only actions such as online-payment retry either
122
+ guard `window` access or accept the required URL from the consumer.
123
+
124
+ ## Security boundary
125
+
126
+ Ports can carry sensitive data even when their types look generic. Adapter implementations
127
+ must not log complete requests, orders, storage records, analytics payloads, or errors from
128
+ unknown SDK transports. The cart/checkout logger redacts common credential shapes as a
129
+ second line of defense, but callers should still pass allow-listed context such as
130
+ `orderId`, operation kind, counts, and booleans.
131
+
132
+ Never put these values in diagnostic context:
133
+
134
+ - order or API tokens;
135
+ - `Authorization` and `Cookie` headers;
136
+ - reCAPTCHA payloads and confirmation codes;
137
+ - payment/card token identifiers;
138
+ - customer names, phones, addresses, or free-form order comments.
139
+
140
+ ## Adding or changing a port
141
+
142
+ Before adding a port, verify that the capability cannot be represented by an existing one.
143
+ Keep the interface vendor-neutral and minimal, document absence/failure behavior, provide a
144
+ no-op implementation when that behavior is meaningful, and add an isolation test.
145
+
146
+ Adding an optional method is usually backward-compatible. Adding a required port or method,
147
+ changing semantics, or widening when sensitive data is emitted is a breaking change and
148
+ requires a major-version release note.
@@ -0,0 +1,53 @@
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
+
17
+ While the package is below `1.0.0`, SemVer does not reserve any component for
18
+ stability ([SemVer §4](https://semver.org/spec/v2.0.0.html#spec-item-4)). A breaking
19
+ release may stay inside the current `0.x.y` range when every consumer is known and
20
+ migrated in the same change. State it explicitly at the top of the changelog section
21
+ and write the migration steps. Once `1.0.0` ships, the table above is binding.
22
+ 5. Verify migrations and compatibility notes are explicit for any consumer-visible change.
23
+ 6. Update the version referenced in `docs/consumer-setup.md` to the one being released.
24
+ 7. Run `npm run check:release` and inspect `npm pack --dry-run` locally.
25
+
26
+ ## Publish
27
+
28
+ 1. Create a GitHub Release for the exact commit to publish.
29
+ 2. Tag it `vX.Y.Z`; prereleases may use `vX.Y.Z-beta.N` or `vX.Y.Z-rc.N`.
30
+ 3. Generate release notes, then edit them into a consumer-focused summary: outcome first,
31
+ migrations second, implementation detail last.
32
+ 4. Publish the GitHub Release.
33
+
34
+ The workflow validates the tag, installs with `npm ci`, injects the tag version without
35
+ creating a Git commit, runs the release quality gate, checks package contents, and publishes
36
+ through npm trusted publishing. Stable versions receive the `latest` dist-tag; prereleases
37
+ receive `next`.
38
+
39
+ The npm package must have a trusted publisher configured for this GitHub repository and the
40
+ `publish-npm.yml` workflow. No long-lived `NPM_TOKEN` should be added.
41
+
42
+ ## Verify
43
+
44
+ - Confirm the workflow completed and the package has npm provenance.
45
+ - Inspect `npm view @zenky/storefront-vue@X.Y.Z` metadata and dist-tags.
46
+ - Install the exact version into a clean consumer and import both the root and one subpath.
47
+ - Confirm the GitHub release links to the correct tag and changelog section.
48
+
49
+ ## Recovery
50
+
51
+ Never reuse or move a published version tag. npm versions are immutable. If a release is
52
+ bad, deprecate it with a clear message, fix forward, and publish a new patch or prerelease.
53
+ 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.4",
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": {
@@ -60,10 +70,6 @@
60
70
  "types": "./dist/seo/index.d.ts",
61
71
  "import": "./dist/seo/index.js"
62
72
  },
63
- "./layout": {
64
- "types": "./dist/layout/index.d.ts",
65
- "import": "./dist/layout/index.js"
66
- },
67
73
  "./pagination": {
68
74
  "types": "./dist/pagination/index.d.ts",
69
75
  "import": "./dist/pagination/index.js"
@@ -89,23 +95,36 @@
89
95
  "scripts": {
90
96
  "build": "tsup",
91
97
  "dev": "tsup --watch",
98
+ "format": "biome format --write .",
99
+ "format:check": "biome format .",
100
+ "lint": "biome lint .",
101
+ "lint:fix": "biome lint --write .",
92
102
  "typecheck": "tsc --noEmit",
93
103
  "test": "vitest run",
104
+ "test:coverage": "vitest run --coverage",
94
105
  "test:watch": "vitest",
95
106
  "check:exports": "publint --strict && attw --pack . --profile esm-only",
96
- "prepublishOnly": "npm run typecheck && npm run test && npm run build && npm run check:exports"
107
+ "check:boundaries": "node scripts/check-boundaries.mjs",
108
+ "check:runtime": "node scripts/check-runtime-compat.mjs",
109
+ "check:size": "node scripts/check-bundle-size.mjs",
110
+ "check": "npm run format:check && npm run lint && npm run typecheck && npm run test && npm run check:boundaries && npm run build && npm run check:exports && npm run check:runtime && npm run check:size",
111
+ "check:ci": "npm run format:check && npm run lint && npm run typecheck && npm run test:coverage && npm run check:boundaries && npm run build && npm run check:exports && npm run check:runtime && npm run check:size",
112
+ "check:release": "npm run typecheck && npm run test && npm run check:boundaries && npm run build && npm run check:exports && npm run check:runtime && npm run check:size",
113
+ "prepublishOnly": "npm run check:release"
97
114
  },
98
115
  "peerDependencies": {
99
116
  "@zenky/integrations-sdk": ">=0.1.3",
100
- "@zenky/storefront-api": ">=0.0.64",
117
+ "@zenky/storefront-api": "^1.0.1",
101
118
  "pinia": "^3.0.0",
102
119
  "vue": "^3.5.0"
103
120
  },
104
121
  "devDependencies": {
105
122
  "@arethetypeswrong/cli": "^0.18.2",
123
+ "@biomejs/biome": "^2.5.5",
106
124
  "@types/node": "^22.10.0",
125
+ "@vitest/coverage-v8": "^4.1.10",
107
126
  "@zenky/integrations-sdk": "^0.1.3",
108
- "@zenky/storefront-api": "^0.0.64",
127
+ "@zenky/storefront-api": "^1.0.1",
109
128
  "pinia": "^3.0.4",
110
129
  "publint": "^0.3.21",
111
130
  "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"]}