@ecomiq/storefront 1.1.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -418
- package/bin/ecomiq.mjs +154 -0
- package/package.json +72 -94
- package/src/api-proxy.ts +194 -0
- package/src/auth/create-storefront-auth.ts +35 -0
- package/src/auth/ecomiq-oauth-password-client.ts +26 -0
- package/src/auth/ecomiq-oauth-password-constants.ts +26 -0
- package/src/auth/ecomiq-oauth-password-plugin.ts +299 -0
- package/src/auth/ecomiq-oauth-password-types.ts +25 -0
- package/src/auth/ecomiq-storefront-sign-in.ts +47 -0
- package/src/auth/env.ts +7 -0
- package/src/cart-session.ts +241 -0
- package/src/client.ts +48 -0
- package/src/codegen.mjs +603 -0
- package/src/components/checkout/cart-sync.ts +143 -0
- package/src/components/checkout/checkout-external-update-banner.tsx +15 -0
- package/src/components/checkout/checkout-form-controls.tsx +139 -0
- package/src/components/checkout/checkout-line-items.tsx +43 -0
- package/src/components/checkout/checkout-order-summary.tsx +156 -0
- package/src/components/checkout/checkout-payment-section.tsx +215 -0
- package/src/components/checkout/checkout-session-view.tsx +240 -0
- package/src/components/checkout/checkout-shipping-address-section.tsx +266 -0
- package/src/components/checkout/checkout-shipping-section.tsx +90 -0
- package/src/components/checkout/checkout-states.tsx +73 -0
- package/src/components/checkout/form-schema.ts +229 -0
- package/src/components/checkout/format.ts +27 -0
- package/src/components/checkout/index.ts +51 -0
- package/src/components/checkout/types.ts +43 -0
- package/src/components/checkout/use-checkout-geo.ts +50 -0
- package/src/components/checkout/use-checkout-session.ts +583 -0
- package/src/components/hero.tsx +291 -0
- package/src/config.d.ts +69 -0
- package/src/config.mjs +165 -0
- package/src/index.ts +274 -0
- package/src/link.tsx +2 -0
- package/src/pages/checkout.$checkoutId.tsx +60 -0
- package/src/pages/checkout.tsx +31 -0
- package/src/pages/thank-you.tsx +53 -0
- package/src/providers/auth.tsx +69 -0
- package/src/providers/cart.tsx +275 -0
- package/src/providers/ecomiq.ts +137 -0
- package/src/providers/index.ts +26 -0
- package/src/public-config.ts +44 -0
- package/src/review-submission.ts +89 -0
- package/src/runtime/context.ts +1 -0
- package/src/runtime/default-layout.tsx +80 -0
- package/src/runtime/head.ts +126 -0
- package/src/runtime/page.tsx +82 -0
- package/src/runtime/postcss-browser-stub.mjs +5 -0
- package/src/server-config-store.ts +26 -0
- package/src/server.ts +279 -0
- package/src/styles.css +76 -0
- package/src/ui/badge.tsx +32 -0
- package/src/ui/button.tsx +31 -0
- package/src/ui/card.tsx +31 -0
- package/src/ui/carousel.tsx +218 -0
- package/src/ui/cn.ts +6 -0
- package/src/ui/index.ts +8 -0
- package/src/ui/separator.tsx +6 -0
- package/src/ui/skeleton.tsx +6 -0
- package/src/ui/storefront-error.tsx +49 -0
- package/src/utils.ts +36 -0
- package/src/vite-inline.mjs +95 -0
- package/templates/__root.tsx +77 -0
- package/templates/router.tsx +19 -0
- package/dist/chunk-64GLOWMJ.js +0 -216
- package/dist/chunk-64GLOWMJ.js.map +0 -1
- package/dist/chunk-AKD5IDEB.js +0 -177
- package/dist/chunk-AKD5IDEB.js.map +0 -1
- package/dist/chunk-EFEVJJWF.js +0 -1345
- package/dist/chunk-EFEVJJWF.js.map +0 -1
- package/dist/chunk-NAJQB5DN.js +0 -155
- package/dist/chunk-NAJQB5DN.js.map +0 -1
- package/dist/chunk-OABDGTBI.js +0 -253
- package/dist/chunk-OABDGTBI.js.map +0 -1
- package/dist/chunk-Q4U5OEUT.js +0 -21
- package/dist/chunk-Q4U5OEUT.js.map +0 -1
- package/dist/chunk-S7CPZ4UE.js +0 -6
- package/dist/chunk-S7CPZ4UE.js.map +0 -1
- package/dist/chunks/client.css +0 -984
- package/dist/chunks/client.css.map +0 -1
- package/dist/chunks/client.d.ts +0 -10759
- package/dist/chunks/client.js +0 -5398
- package/dist/chunks/client.js.map +0 -1
- package/dist/chunks/config.d.ts +0 -31
- package/dist/chunks/config.js +0 -5
- package/dist/chunks/config.js.map +0 -1
- package/dist/chunks/core-public.d.ts +0 -130
- package/dist/chunks/core-public.js +0 -92
- package/dist/chunks/core-public.js.map +0 -1
- package/dist/chunks/server-nextjs.d.ts +0 -33
- package/dist/chunks/server-nextjs.js +0 -59
- package/dist/chunks/server-nextjs.js.map +0 -1
- package/dist/chunks/server-tanstack.d.ts +0 -22
- package/dist/chunks/server-tanstack.js +0 -40
- package/dist/chunks/server-tanstack.js.map +0 -1
- package/dist/collection-DU7gREXS.d.ts +0 -4062
- package/dist/ecomiq-oauth-password-plugin-B_gMdGuc.d.ts +0 -3631
- package/dist/ecomiq-shell-ui-SWIFLQVM.js +0 -8
- package/dist/ecomiq-shell-ui-SWIFLQVM.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.js +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.server.d.ts +0 -9
- package/dist/index.server.js +0 -8
- package/dist/index.server.js.map +0 -1
- package/dist/nextjs/api.d.ts +0 -45
- package/dist/nextjs/api.js +0 -32
- package/dist/nextjs/api.js.map +0 -1
- package/dist/nextjs/globals.css +0 -13
- package/dist/nextjs.d.ts +0 -2
- package/dist/nextjs.js +0 -2
- package/dist/nextjs.js.map +0 -1
- package/dist/nextjs.server.d.ts +0 -18
- package/dist/nextjs.server.js +0 -1
- package/dist/nextjs.server.js.map +0 -1
- package/dist/postcss.config.mjs +0 -6
- package/dist/react/checkout-page.css +0 -855
- package/dist/react/client.css +0 -984
- package/dist/react/payment-provider.css +0 -91
- package/dist/react/thank-you-page.css +0 -105
- package/dist/react.d.ts +0 -1
- package/dist/react.js +0 -2
- package/dist/react.js.map +0 -1
- package/dist/store-proxy-handler-CNzKlTFA.d.ts +0 -10
- package/dist/tanstack/api.d.ts +0 -41
- package/dist/tanstack/api.js +0 -30
- package/dist/tanstack/api.js.map +0 -1
- package/dist/tanstack.d.ts +0 -1
- package/dist/tanstack.js +0 -2
- package/dist/tanstack.js.map +0 -1
- package/dist/tanstack.server.d.ts +0 -18
- package/dist/tanstack.server.js +0 -1
- package/dist/tanstack.server.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,442 +1,50 @@
|
|
|
1
1
|
# @ecomiq/storefront
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Incluye tipos, schemas Zod, cliente BFF (`/api/store`), hooks, `EcomiqProvider` y bootstrap server por framework. **No incluye UI de tienda** (CMS, temas, checkout, etc.): la implementas en tu proyecto.
|
|
6
|
-
|
|
7
|
-
## Requisitos
|
|
8
|
-
|
|
9
|
-
- Node.js 20+
|
|
10
|
-
- React 19+
|
|
11
|
-
- **Next.js 15+** (App Router) *o* **TanStack Start** (`@tanstack/react-start`)
|
|
12
|
-
- Tailwind CSS 4+ (opcional; ver `/nextjs/css`)
|
|
3
|
+
Micro framework SSR de Ecomiq para storefronts. Páginas, datos y deploy sin
|
|
4
|
+
configurar TanStack Start, Vite ni Cloudflare a mano.
|
|
13
5
|
|
|
14
6
|
## Instalación
|
|
15
7
|
|
|
16
8
|
```bash
|
|
17
9
|
npm install @ecomiq/storefront
|
|
18
10
|
# o
|
|
11
|
+
yarn add @ecomiq/storefront
|
|
12
|
+
# o
|
|
19
13
|
pnpm add @ecomiq/storefront
|
|
20
14
|
```
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
# Next.js
|
|
26
|
-
pnpm add next react react-dom @tanstack/react-query better-auth
|
|
27
|
-
|
|
28
|
-
# TanStack Start
|
|
29
|
-
pnpm add @tanstack/react-start react react-dom @tanstack/react-query better-auth
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
**Next.js** — en `next.config`:
|
|
33
|
-
|
|
34
|
-
```js
|
|
35
|
-
/** @type {import('next').NextConfig} */
|
|
36
|
-
const nextConfig = {
|
|
37
|
-
transpilePackages: ["@ecomiq/storefront"],
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export default nextConfig;
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## `ecomiq.config.js`
|
|
16
|
+
Peer deps: `react` y `react-dom` ^19.
|
|
44
17
|
|
|
45
|
-
|
|
18
|
+
## Uso
|
|
46
19
|
|
|
47
20
|
```js
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
/** @type {import('@ecomiq/storefront').EcomiqConfig} */
|
|
51
|
-
const ecomiqConfig = {
|
|
52
|
-
slugStore: "mi-tienda",
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
module.exports = ecomiqConfig;
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Si defines `slugStore`, `getPublicStore` usa ese slug (útil en localhost). Si no, deriva del header `Host`. Plantilla: [ecomiq.config.example.cjs](./ecomiq.config.example.cjs).
|
|
59
|
-
|
|
60
|
-
## Puntos de entrada (npm)
|
|
61
|
-
|
|
62
|
-
| Import | Uso |
|
|
63
|
-
|--------|-----|
|
|
64
|
-
| `@ecomiq/storefront` | Tipos, schemas Zod, utilidades vanilla (`formatCurrency`, …). **Server** (`react-server`): config (`loadEcomiqConfig`, …) |
|
|
65
|
-
| `@ecomiq/storefront/react` | **Client** (`"use client"`): `EcomiqProvider`, hooks BFF, componentes reutilizables |
|
|
66
|
-
| `@ecomiq/storefront/nextjs` | **Server** (`react-server`): `getPublicStore`, cliente BFF server, auth helpers. **Client:** reexport de `/react` |
|
|
67
|
-
| `@ecomiq/storefront/nextjs/api` | Proxy `/api/store/*` (App Router) |
|
|
68
|
-
| `@ecomiq/storefront/nextjs/css` | Tailwind mínimo (opcional) |
|
|
69
|
-
| `@ecomiq/storefront/nextjs/postcss` | PostCSS de referencia |
|
|
70
|
-
| `@ecomiq/storefront/tanstack` | **Client:** reexport `/react` (hooks, provider) |
|
|
71
|
-
| `@ecomiq/storefront/tanstack/server` | **Server:** `getPublicStore`, BFF, auth (loaders, `createServerFn`) |
|
|
72
|
-
| `@ecomiq/storefront/tanstack/api` | Proxy `/api/store/*` (server routes) |
|
|
73
|
-
|
|
74
|
-
Importa hooks y `EcomiqProvider` desde `@ecomiq/storefront/react`. El build npm separa server (`react-server`), vanilla (raíz) y React (`/react`) para App Router.
|
|
75
|
-
|
|
76
|
-
Los hooks (`useStoreProducts`, `useStoreCart`, …) viven en `/react`; `/nextjs` y `/tanstack` los reexportan en client por compatibilidad.
|
|
77
|
-
|
|
78
|
-
### Peers
|
|
79
|
-
|
|
80
|
-
| Paquete | Cuándo |
|
|
81
|
-
|---------|--------|
|
|
82
|
-
| `react`, `react-dom` | Siempre |
|
|
83
|
-
| `next` | Solo si usas `/nextjs` |
|
|
84
|
-
| `@tanstack/react-start` | Solo si usas `/tanstack` |
|
|
85
|
-
|
|
86
|
-
**Cloudflare:** el SDK no incluye `@opennextjs/cloudflare`. Si despliegas en Cloudflare con settings vía KV (`PUBLIC_STORE_SETTINGS_SOURCE=kv`), instálalo y configura wrangler en **tu** proyecto.
|
|
87
|
-
|
|
88
|
-
## Arquitectura
|
|
89
|
-
|
|
90
|
-
```
|
|
91
|
-
┌─────────────────────────────┐
|
|
92
|
-
│ @ecomiq/storefront │ ecomiq.config.js
|
|
93
|
-
└──────────────┬──────────────┘
|
|
94
|
-
│
|
|
95
|
-
┌─────────────────────────┴─────────────────────────┐
|
|
96
|
-
▼ ▼
|
|
97
|
-
/nextjs (+ /nextjs/api) /tanstack (+ /tanstack/api)
|
|
98
|
-
getPublicStore · toNextJsStoreApiHandler getPublicStore · createTanStackStoreApiHandler
|
|
99
|
-
│ │
|
|
100
|
-
└─────────────────────────┬─────────────────────────┘
|
|
101
|
-
▼
|
|
102
|
-
Capa React (`/react`) — hooks, EcomiqProvider, cliente BFF browser, auth
|
|
103
|
-
│
|
|
104
|
-
▼
|
|
105
|
-
core/server — fetchPublicStoreSettings · handleStoreProxyRequest
|
|
106
|
-
│
|
|
107
|
-
▼
|
|
108
|
-
Navegador → /api/store/* (proxy en tu app) → API Ecomiq
|
|
109
|
-
↑ contexto X-Store-Id (tras getPublicStore)
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
El browser **no** llama al upstream directo; montas el proxy con el adapter de tu framework.
|
|
113
|
-
|
|
114
|
-
---
|
|
115
|
-
|
|
116
|
-
## Inicio rápido — Next.js
|
|
21
|
+
// ecomiq.config.js
|
|
22
|
+
import { defineConfig } from '@ecomiq/storefront/config'
|
|
117
23
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// app/api/store/[...path]/route.ts
|
|
122
|
-
import { toNextJsStoreApiHandler } from "@ecomiq/storefront/nextjs/api";
|
|
123
|
-
|
|
124
|
-
export const { GET, POST, PUT, PATCH, DELETE } = toNextJsStoreApiHandler({
|
|
125
|
-
apiBaseUrl: process.env.STOREFRONT_API_URL!,
|
|
126
|
-
});
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
Los hooks y `EcomiqProvider` son **Client Components** (`"use client"`). En el layout (Server Component) solo importes funciones server; el provider va en un archivo client aparte.
|
|
130
|
-
|
|
131
|
-
### 2. Layout y tienda
|
|
132
|
-
|
|
133
|
-
```tsx
|
|
134
|
-
// app/ecomiq-provider.tsx
|
|
135
|
-
"use client";
|
|
136
|
-
|
|
137
|
-
import { EcomiqProvider } from "@ecomiq/storefront/react";
|
|
138
|
-
import type { StorefrontSettings } from "@ecomiq/storefront";
|
|
139
|
-
|
|
140
|
-
export function EcomiqRootProvider({
|
|
141
|
-
store,
|
|
142
|
-
children,
|
|
143
|
-
}: {
|
|
144
|
-
store: StorefrontSettings | null;
|
|
145
|
-
children: React.ReactNode;
|
|
146
|
-
}) {
|
|
147
|
-
return <EcomiqProvider store={store}>{children}</EcomiqProvider>;
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
```tsx
|
|
152
|
-
// app/layout.tsx — Server Component
|
|
153
|
-
import { getPublicStore } from "@ecomiq/storefront/nextjs";
|
|
154
|
-
import { EcomiqRootProvider } from "./ecomiq-provider";
|
|
155
|
-
|
|
156
|
-
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
|
157
|
-
const { data: store, error } = await getPublicStore();
|
|
158
|
-
if (error) {
|
|
159
|
-
// tu página de error / tienda no encontrada
|
|
160
|
-
}
|
|
161
|
-
return (
|
|
162
|
-
<html lang="es">
|
|
163
|
-
<body>
|
|
164
|
-
<EcomiqRootProvider store={store}>{children}</EcomiqRootProvider>
|
|
165
|
-
</body>
|
|
166
|
-
</html>
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
`getPublicStore` corre en el servidor. `EcomiqProvider` hidrata `useStore`, configura React Query y el `storeId` del cliente BFF — debe vivir en un módulo con `"use client"`.
|
|
172
|
-
|
|
173
|
-
### 3. Catálogo con hooks
|
|
174
|
-
|
|
175
|
-
Importa hooks desde `@ecomiq/storefront/react`:
|
|
176
|
-
|
|
177
|
-
```tsx
|
|
178
|
-
"use client";
|
|
179
|
-
|
|
180
|
-
import { useStoreProducts } from "@ecomiq/storefront/react";
|
|
181
|
-
|
|
182
|
-
export function ProductList() {
|
|
183
|
-
const { useStorefrontProductsList } = useStoreProducts();
|
|
184
|
-
const { data, isLoading } = useStorefrontProductsList({ page: 1, pageSize: 24 });
|
|
185
|
-
|
|
186
|
-
if (isLoading) return <p>Cargando…</p>;
|
|
187
|
-
return (
|
|
188
|
-
<ul>
|
|
189
|
-
{data?.products.map((p) => (
|
|
190
|
-
<li key={p.id}>{p.name}</li>
|
|
191
|
-
))}
|
|
192
|
-
</ul>
|
|
193
|
-
);
|
|
194
|
-
}
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
store: { slug: 'mi-tienda' },
|
|
26
|
+
})
|
|
195
27
|
```
|
|
196
28
|
|
|
197
|
-
### 4. Datos en Server Components
|
|
198
|
-
|
|
199
29
|
```tsx
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
export default async function CatalogPage() {
|
|
203
|
-
const { products } = await getStorefrontProducts({ page: 1, pageSize: 24 });
|
|
204
|
-
return <YourGrid products={products} />;
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
---
|
|
209
|
-
|
|
210
|
-
## Inicio rápido — TanStack Start
|
|
211
|
-
|
|
212
|
-
### 1. Proxy BFF
|
|
30
|
+
// pages/index.tsx
|
|
31
|
+
import { definePage, serverApi } from '@ecomiq/storefront'
|
|
213
32
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const storeApi = createTanStackStoreApiHandler({
|
|
222
|
-
apiBaseUrl: process.env.STOREFRONT_API_URL,
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
export const Route = createFileRoute("/api/store/$")({
|
|
226
|
-
server: { handlers: storeApi },
|
|
227
|
-
});
|
|
33
|
+
export default definePage({
|
|
34
|
+
loader: async () => {
|
|
35
|
+
const products = await serverApi.products.list()
|
|
36
|
+
return { products }
|
|
37
|
+
},
|
|
38
|
+
component: ({ data }) => <pre>{JSON.stringify(data.products, null, 2)}</pre>,
|
|
39
|
+
})
|
|
228
40
|
```
|
|
229
41
|
|
|
230
|
-
|
|
42
|
+
CLI:
|
|
231
43
|
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
import { EcomiqProvider } from "@ecomiq/storefront/react";
|
|
237
|
-
import type { StorefrontSettings } from "@ecomiq/storefront/tanstack";
|
|
238
|
-
|
|
239
|
-
export function EcomiqRootProvider({
|
|
240
|
-
store,
|
|
241
|
-
children,
|
|
242
|
-
}: {
|
|
243
|
-
store: StorefrontSettings | null;
|
|
244
|
-
children: React.ReactNode;
|
|
245
|
-
}) {
|
|
246
|
-
return <EcomiqProvider store={store}>{children}</EcomiqProvider>;
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
```tsx
|
|
251
|
-
// src/routes/__root.tsx
|
|
252
|
-
import { createRootRoute, Outlet } from "@tanstack/react-router";
|
|
253
|
-
import { getPublicStore } from "@ecomiq/storefront/tanstack/server";
|
|
254
|
-
import { EcomiqRootProvider } from "../components/ecomiq-provider";
|
|
255
|
-
|
|
256
|
-
export const Route = createRootRoute({
|
|
257
|
-
loader: async () => getPublicStore(),
|
|
258
|
-
component: RootComponent,
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
function RootComponent() {
|
|
262
|
-
const { data: store } = Route.useLoaderData();
|
|
263
|
-
return (
|
|
264
|
-
<EcomiqRootProvider store={store}>
|
|
265
|
-
<Outlet />
|
|
266
|
-
</EcomiqRootProvider>
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
### 3. Hooks (Client Components)
|
|
272
|
-
|
|
273
|
-
```tsx
|
|
274
|
-
"use client";
|
|
275
|
-
|
|
276
|
-
import { useStoreProducts, useStoreCart } from "@ecomiq/storefront/react";
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
---
|
|
280
|
-
|
|
281
|
-
## Autenticación (Better Auth + SDK)
|
|
282
|
-
|
|
283
|
-
Instala [Better Auth](https://www.better-auth.com/).
|
|
284
|
-
|
|
285
|
-
**Servidor:**
|
|
286
|
-
|
|
287
|
-
```ts
|
|
288
|
-
// lib/auth.ts
|
|
289
|
-
import { betterAuth } from "better-auth";
|
|
290
|
-
import { ecomiqOAuthPassword } from "@ecomiq/storefront/nextjs";
|
|
291
|
-
// TanStack: import { ecomiqOAuthPassword } from "@ecomiq/storefront/tanstack";
|
|
292
|
-
|
|
293
|
-
export const auth = betterAuth({
|
|
294
|
-
plugins: [ecomiqOAuthPassword()],
|
|
295
|
-
});
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
**Ruta API (Next.js):**
|
|
299
|
-
|
|
300
|
-
```ts
|
|
301
|
-
// app/api/auth/[...all]/route.ts
|
|
302
|
-
import { toNextJsHandler } from "better-auth/next-js";
|
|
303
|
-
import { auth } from "@/lib/auth";
|
|
304
|
-
|
|
305
|
-
export const { GET, POST } = toNextJsHandler(auth);
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
**Cliente:**
|
|
309
|
-
|
|
310
|
-
```ts
|
|
311
|
-
// lib/auth-client.ts
|
|
312
|
-
"use client";
|
|
313
|
-
|
|
314
|
-
import { createAuthClient } from "better-auth/react";
|
|
315
|
-
import { ecomiqOAuthPasswordClient } from "@ecomiq/storefront/nextjs";
|
|
316
|
-
|
|
317
|
-
export const authClient = createAuthClient({
|
|
318
|
-
plugins: [ecomiqOAuthPasswordClient()],
|
|
319
|
-
});
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
**Layout** — pasa `authClient` al provider:
|
|
323
|
-
|
|
324
|
-
```tsx
|
|
325
|
-
<EcomiqProvider store={store} authClient={authClient}>
|
|
326
|
-
{children}
|
|
327
|
-
</EcomiqProvider>
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
### Login, registro y logout
|
|
331
|
-
|
|
332
|
-
```tsx
|
|
333
|
-
"use client";
|
|
334
|
-
|
|
335
|
-
import {
|
|
336
|
-
useStore,
|
|
337
|
-
useStorefrontAuthClient,
|
|
338
|
-
useStorefrontSession,
|
|
339
|
-
registerCustomer,
|
|
340
|
-
signInWithEcomiqCredentials,
|
|
341
|
-
} from "@ecomiq/storefront/nextjs";
|
|
342
|
-
|
|
343
|
-
export function AccountActions() {
|
|
344
|
-
const { data: store } = useStore();
|
|
345
|
-
const authClient = useStorefrontAuthClient();
|
|
346
|
-
const { isAuthenticated, user, signOut, refetch } = useStorefrontSession();
|
|
347
|
-
|
|
348
|
-
async function handleLogin(email: string, password: string) {
|
|
349
|
-
await signInWithEcomiqCredentials(authClient, store, { email, password });
|
|
350
|
-
await refetch();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async function handleRegister(input: {
|
|
354
|
-
email: string;
|
|
355
|
-
password: string;
|
|
356
|
-
firstName: string;
|
|
357
|
-
lastName: string;
|
|
358
|
-
countryIsoCode: string;
|
|
359
|
-
phone: string;
|
|
360
|
-
}) {
|
|
361
|
-
await registerCustomer(input);
|
|
362
|
-
await handleLogin(input.email, input.password);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (isAuthenticated) {
|
|
366
|
-
return (
|
|
367
|
-
<div>
|
|
368
|
-
<p>Hola, {user?.firstName ?? user?.email}</p>
|
|
369
|
-
<button type="button" onClick={() => signOut()}>Cerrar sesión</button>
|
|
370
|
-
</div>
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
return <div>{/* Tu UI */}</div>;
|
|
375
|
-
}
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
En TanStack, cambia el import server a `@ecomiq/storefront/tanstack` y los hooks a `@ecomiq/storefront/react`.
|
|
379
|
-
|
|
380
|
-
---
|
|
381
|
-
|
|
382
|
-
## Qué incluye cada entry
|
|
383
|
-
|
|
384
|
-
Desde `/nextjs` o `/tanstack` (**server**, condición `react-server`):
|
|
385
|
-
|
|
386
|
-
- `getPublicStore`, `getHostname`, `fetchPublicStoreSettings`
|
|
387
|
-
- Cliente BFF server: `getStorefrontProducts`, `registerCustomer`, carrito/cuenta, etc.
|
|
388
|
-
- Auth server: `ecomiqOAuthPassword`
|
|
389
|
-
|
|
390
|
-
Desde `@ecomiq/storefront/react` (**client**, `"use client"`):
|
|
391
|
-
|
|
392
|
-
- **Provider:** `EcomiqProvider`, `useStore`, `usePublicStore`
|
|
393
|
-
- **Hooks BFF:** `useStoreProducts`, `useStoreCollections`, `useStorePages`, `useStoreCart`
|
|
394
|
-
- **Auth client:** `ecomiqOAuthPasswordClient`, `signInWithEcomiqCredentials`, `useStorefrontSession`
|
|
395
|
-
- **Componentes:** selector de variantes de producto (más en futuras versiones)
|
|
396
|
-
|
|
397
|
-
Desde `@ecomiq/storefront` (raíz — vanilla, server o client):
|
|
398
|
-
|
|
399
|
-
- **Tipos y Zod:** `StorefrontSettings`, `StorefrontProduct`, schemas
|
|
400
|
-
- **Utilidades:** `formatCurrency`, `countryCurrency`, `sanitizeHtml`, `stripHtml`, labels de pedidos, `findStorefrontCmsPageBySlug`
|
|
401
|
-
- **Config** (solo server): `loadEcomiqConfig`, `EcomiqConfigSchema`, …
|
|
402
|
-
|
|
403
|
-
## Variables de entorno
|
|
404
|
-
|
|
405
|
-
| Variable | Descripción |
|
|
406
|
-
|----------|-------------|
|
|
407
|
-
| `STOREFRONT_API_URL` | URL base de la API de tienda (proxy) |
|
|
408
|
-
| `PUBLIC_STORE_SETTINGS_SOURCE` | `api` (default) o `kv` (Cloudflare KV; requiere `@opennextjs/cloudflare` en tu proyecto, no en el SDK) |
|
|
409
|
-
|
|
410
|
-
**Identificador de tienda:** `slugStore` en `ecomiq.config.*` si existe; si no, header `Host`.
|
|
411
|
-
|
|
412
|
-
## Dependencias recomendadas
|
|
413
|
-
|
|
414
|
-
**Next.js:**
|
|
415
|
-
|
|
416
|
-
```json
|
|
417
|
-
{
|
|
418
|
-
"dependencies": {
|
|
419
|
-
"@ecomiq/storefront": "^0.1.0",
|
|
420
|
-
"next": "^15.3.0",
|
|
421
|
-
"react": "^19.0.0",
|
|
422
|
-
"react-dom": "^19.0.0"
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
```
|
|
426
|
-
|
|
427
|
-
**TanStack Start:**
|
|
428
|
-
|
|
429
|
-
```json
|
|
430
|
-
{
|
|
431
|
-
"dependencies": {
|
|
432
|
-
"@ecomiq/storefront": "^0.1.0",
|
|
433
|
-
"@tanstack/react-start": "^1.0.0",
|
|
434
|
-
"react": "^19.0.0",
|
|
435
|
-
"react-dom": "^19.0.0"
|
|
436
|
-
}
|
|
437
|
-
}
|
|
44
|
+
```bash
|
|
45
|
+
npx ecomiq dev
|
|
46
|
+
npx ecomiq build
|
|
47
|
+
npx ecomiq deploy
|
|
438
48
|
```
|
|
439
49
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
Documentación y credenciales: [ecomiq.pe](https://ecomiq.pe) (o el portal de tu organización).
|
|
50
|
+
Ver el monorepo [ecomiq-storefront](https://github.com/ecomiq/ecomiq-storefront) para la app de referencia y más documentación.
|
package/bin/ecomiq.mjs
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI de Ecomiq.
|
|
4
|
+
*
|
|
5
|
+
* ecomiq dev → servidor de desarrollo con SSR y HMR
|
|
6
|
+
* ecomiq build → build de producción (target Cloudflare Workers)
|
|
7
|
+
* ecomiq deploy → build + publicación en Cloudflare Workers
|
|
8
|
+
* ecomiq clean → elimina la carpeta interna .ecomiq
|
|
9
|
+
*
|
|
10
|
+
* El usuario nunca interactúa con las herramientas internas: la CLI
|
|
11
|
+
* genera una app interna en `.ecomiq/` y delega en ella.
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from 'node:child_process'
|
|
14
|
+
import { rmSync, existsSync, realpathSync } from 'node:fs'
|
|
15
|
+
import { resolve, dirname } from 'node:path'
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
17
|
+
import { generateApp, watchPages } from '../src/codegen.mjs'
|
|
18
|
+
import { parseConfig } from '../src/config.mjs'
|
|
19
|
+
import { createViteConfig } from '../src/vite-inline.mjs'
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
const cwd = process.cwd()
|
|
23
|
+
|
|
24
|
+
const c = {
|
|
25
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
26
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
27
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
28
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function banner(msg) {
|
|
32
|
+
console.log(`\n ${c.bold('◈ ecomiq')} ${c.dim('·')} ${msg}\n`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function loadUserConfig(projectDirectory = process.cwd()) {
|
|
36
|
+
const configPath = resolve(projectDirectory, 'ecomiq.config.js')
|
|
37
|
+
if (!existsSync(configPath)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`No se encontró ${configPath}. Crea ecomiq.config.js en la raíz del proyecto.`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
const mod = await import(pathToFileURL(configPath).href)
|
|
43
|
+
return parseConfig(mod.default, { source: configPath })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function run(bin, args, opts = {}) {
|
|
47
|
+
return new Promise((resolvePromise, reject) => {
|
|
48
|
+
const child = spawn(bin, args, {
|
|
49
|
+
stdio: 'inherit',
|
|
50
|
+
shell: process.platform === 'win32',
|
|
51
|
+
...opts,
|
|
52
|
+
})
|
|
53
|
+
child.on('exit', (code) => (code === 0 ? resolvePromise() : reject(new Error(`${bin} salió con código ${code}`))))
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Los binarios internos viven en las dependencias del framework.
|
|
58
|
+
const frameworkRoot = resolve(__dirname, '..')
|
|
59
|
+
function internalBin(name) {
|
|
60
|
+
return resolve(frameworkRoot, 'node_modules', '.bin', name)
|
|
61
|
+
}
|
|
62
|
+
function resolveBin(name) {
|
|
63
|
+
const local = internalBin(name)
|
|
64
|
+
if (existsSync(local)) return local
|
|
65
|
+
// fallback: hoisted en el node_modules del proyecto
|
|
66
|
+
const hoisted = resolve(cwd, 'node_modules', '.bin', name)
|
|
67
|
+
return existsSync(hoisted) ? hoisted : name
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const appDir = resolve(cwd, '.ecomiq')
|
|
71
|
+
|
|
72
|
+
async function prepare() {
|
|
73
|
+
const config = await loadUserConfig()
|
|
74
|
+
await generateApp({ cwd, appDir, frameworkRoot, config })
|
|
75
|
+
return config
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function runCli(command = process.argv[2] ?? 'help') {
|
|
79
|
+
switch (command) {
|
|
80
|
+
case 'dev': {
|
|
81
|
+
const config = await prepare()
|
|
82
|
+
const port = config.dev?.port ?? 3000
|
|
83
|
+
banner(`dev · ${config.name ?? 'tienda'} ${c.dim('http://localhost:' + port)}`)
|
|
84
|
+
const { createServer } = await import('vite')
|
|
85
|
+
const server = await createServer({ ...createViteConfig({ appDir, config }), server: { port, fs: { allow: ['..'] } } })
|
|
86
|
+
await server.listen()
|
|
87
|
+
const pageWatcher = watchPages({
|
|
88
|
+
cwd,
|
|
89
|
+
appDir,
|
|
90
|
+
frameworkRoot,
|
|
91
|
+
config,
|
|
92
|
+
onGenerate() {
|
|
93
|
+
server.config.logger.info(c.dim(' ↻ rutas regeneradas'))
|
|
94
|
+
},
|
|
95
|
+
onError(error) {
|
|
96
|
+
server.config.logger.error(`No se pudieron regenerar las rutas: ${error.message}`)
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
server.httpServer?.once('close', () => pageWatcher.close())
|
|
100
|
+
server.printUrls()
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
case 'build': {
|
|
104
|
+
const config = await prepare()
|
|
105
|
+
banner(`build · Cloudflare Workers`)
|
|
106
|
+
const { createBuilder } = await import('vite')
|
|
107
|
+
const builder = await createBuilder(createViteConfig({ appDir, config }))
|
|
108
|
+
await builder.buildApp()
|
|
109
|
+
console.log(c.green('\n ✔ Build listo en .ecomiq/dist\n'))
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
case 'deploy': {
|
|
113
|
+
const config = await prepare()
|
|
114
|
+
banner(`deploy · Cloudflare Workers`)
|
|
115
|
+
const { createBuilder } = await import('vite')
|
|
116
|
+
const builder = await createBuilder(createViteConfig({ appDir, config }))
|
|
117
|
+
await builder.buildApp()
|
|
118
|
+
// @cloudflare/vite-plugin emite la config final del Worker junto al build
|
|
119
|
+
const built = resolve(appDir, 'dist', 'server', 'wrangler.json')
|
|
120
|
+
const fallback = resolve(appDir, 'wrangler.jsonc')
|
|
121
|
+
await run(resolveBin('wrangler'), ['deploy', '--config', existsSync(built) ? built : fallback], { cwd: appDir })
|
|
122
|
+
console.log(c.green('\n ✔ Publicado en Cloudflare Workers\n'))
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
case 'clean': {
|
|
126
|
+
rmSync(appDir, { recursive: true, force: true })
|
|
127
|
+
console.log(c.green(' ✔ .ecomiq eliminado'))
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
default: {
|
|
131
|
+
banner('framework de storefronts')
|
|
132
|
+
console.log(` ${c.bold('ecomiq dev')} servidor de desarrollo`)
|
|
133
|
+
console.log(` ${c.bold('ecomiq build')} build de producción`)
|
|
134
|
+
console.log(` ${c.bold('ecomiq deploy')} publicar en Cloudflare Workers`)
|
|
135
|
+
console.log(` ${c.bold('ecomiq clean')} limpiar archivos internos\n`)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const invokedPath = process.argv[1]
|
|
141
|
+
let invokedDirectly = false
|
|
142
|
+
if (invokedPath && existsSync(invokedPath)) {
|
|
143
|
+
invokedDirectly = realpathSync(invokedPath) === fileURLToPath(import.meta.url)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (invokedDirectly) {
|
|
147
|
+
try {
|
|
148
|
+
await runCli()
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : 'Error desconocido'
|
|
151
|
+
console.error(c.red(`\n ✖ ${message}\n`))
|
|
152
|
+
process.exitCode = 1
|
|
153
|
+
}
|
|
154
|
+
}
|