@ecomiq/storefront 1.1.5 → 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.
Files changed (134) hide show
  1. package/README.md +26 -417
  2. package/bin/ecomiq.mjs +154 -0
  3. package/package.json +72 -84
  4. package/src/api-proxy.ts +194 -0
  5. package/src/auth/create-storefront-auth.ts +35 -0
  6. package/src/auth/ecomiq-oauth-password-client.ts +26 -0
  7. package/src/auth/ecomiq-oauth-password-constants.ts +26 -0
  8. package/src/auth/ecomiq-oauth-password-plugin.ts +299 -0
  9. package/src/auth/ecomiq-oauth-password-types.ts +25 -0
  10. package/src/auth/ecomiq-storefront-sign-in.ts +47 -0
  11. package/src/auth/env.ts +7 -0
  12. package/src/cart-session.ts +241 -0
  13. package/src/client.ts +48 -0
  14. package/src/codegen.mjs +603 -0
  15. package/src/components/checkout/cart-sync.ts +143 -0
  16. package/src/components/checkout/checkout-external-update-banner.tsx +15 -0
  17. package/src/components/checkout/checkout-form-controls.tsx +139 -0
  18. package/src/components/checkout/checkout-line-items.tsx +43 -0
  19. package/src/components/checkout/checkout-order-summary.tsx +156 -0
  20. package/src/components/checkout/checkout-payment-section.tsx +215 -0
  21. package/src/components/checkout/checkout-session-view.tsx +240 -0
  22. package/src/components/checkout/checkout-shipping-address-section.tsx +266 -0
  23. package/src/components/checkout/checkout-shipping-section.tsx +90 -0
  24. package/src/components/checkout/checkout-states.tsx +73 -0
  25. package/src/components/checkout/form-schema.ts +229 -0
  26. package/src/components/checkout/format.ts +27 -0
  27. package/src/components/checkout/index.ts +51 -0
  28. package/src/components/checkout/types.ts +43 -0
  29. package/src/components/checkout/use-checkout-geo.ts +50 -0
  30. package/src/components/checkout/use-checkout-session.ts +583 -0
  31. package/src/components/hero.tsx +291 -0
  32. package/src/config.d.ts +69 -0
  33. package/src/config.mjs +165 -0
  34. package/src/index.ts +274 -0
  35. package/src/link.tsx +2 -0
  36. package/src/pages/checkout.$checkoutId.tsx +60 -0
  37. package/src/pages/checkout.tsx +31 -0
  38. package/src/pages/thank-you.tsx +53 -0
  39. package/src/providers/auth.tsx +69 -0
  40. package/src/providers/cart.tsx +275 -0
  41. package/src/providers/ecomiq.ts +137 -0
  42. package/src/providers/index.ts +26 -0
  43. package/src/public-config.ts +44 -0
  44. package/src/review-submission.ts +89 -0
  45. package/src/runtime/context.ts +1 -0
  46. package/src/runtime/default-layout.tsx +80 -0
  47. package/src/runtime/head.ts +126 -0
  48. package/src/runtime/page.tsx +82 -0
  49. package/src/runtime/postcss-browser-stub.mjs +5 -0
  50. package/src/server-config-store.ts +26 -0
  51. package/src/server.ts +279 -0
  52. package/src/styles.css +76 -0
  53. package/src/ui/badge.tsx +32 -0
  54. package/src/ui/button.tsx +31 -0
  55. package/src/ui/card.tsx +31 -0
  56. package/src/ui/carousel.tsx +218 -0
  57. package/src/ui/cn.ts +6 -0
  58. package/src/ui/index.ts +8 -0
  59. package/src/ui/separator.tsx +6 -0
  60. package/src/ui/skeleton.tsx +6 -0
  61. package/src/ui/storefront-error.tsx +49 -0
  62. package/src/utils.ts +36 -0
  63. package/src/vite-inline.mjs +95 -0
  64. package/templates/__root.tsx +77 -0
  65. package/templates/router.tsx +19 -0
  66. package/dist/chunk-3QGB55R6.js +0 -157
  67. package/dist/chunk-3QGB55R6.js.map +0 -1
  68. package/dist/chunk-64GLOWMJ.js +0 -216
  69. package/dist/chunk-64GLOWMJ.js.map +0 -1
  70. package/dist/chunk-ASFU32ZM.js +0 -236
  71. package/dist/chunk-ASFU32ZM.js.map +0 -1
  72. package/dist/chunk-Q4U5OEUT.js +0 -21
  73. package/dist/chunk-Q4U5OEUT.js.map +0 -1
  74. package/dist/chunk-QRR7EPCH.js +0 -176
  75. package/dist/chunk-QRR7EPCH.js.map +0 -1
  76. package/dist/chunk-S7CPZ4UE.js +0 -6
  77. package/dist/chunk-S7CPZ4UE.js.map +0 -1
  78. package/dist/chunk-ZG2XUYGZ.js +0 -1281
  79. package/dist/chunk-ZG2XUYGZ.js.map +0 -1
  80. package/dist/chunks/client.css +0 -984
  81. package/dist/chunks/client.css.map +0 -1
  82. package/dist/chunks/client.d.ts +0 -10041
  83. package/dist/chunks/client.js +0 -5363
  84. package/dist/chunks/client.js.map +0 -1
  85. package/dist/chunks/config.d.ts +0 -31
  86. package/dist/chunks/config.js +0 -5
  87. package/dist/chunks/config.js.map +0 -1
  88. package/dist/chunks/core-public.d.ts +0 -130
  89. package/dist/chunks/core-public.js +0 -92
  90. package/dist/chunks/core-public.js.map +0 -1
  91. package/dist/chunks/server-nextjs.d.ts +0 -33
  92. package/dist/chunks/server-nextjs.js +0 -47
  93. package/dist/chunks/server-nextjs.js.map +0 -1
  94. package/dist/chunks/server-tanstack.d.ts +0 -22
  95. package/dist/chunks/server-tanstack.js +0 -34
  96. package/dist/chunks/server-tanstack.js.map +0 -1
  97. package/dist/collection-C6bWDLiw.d.ts +0 -3926
  98. package/dist/ecomiq-oauth-password-plugin-J5wDFqzL.d.ts +0 -3629
  99. package/dist/ecomiq-shell-ui-SWIFLQVM.js +0 -8
  100. package/dist/ecomiq-shell-ui-SWIFLQVM.js.map +0 -1
  101. package/dist/index.d.ts +0 -9
  102. package/dist/index.js +0 -1
  103. package/dist/index.js.map +0 -1
  104. package/dist/index.server.d.ts +0 -9
  105. package/dist/index.server.js +0 -8
  106. package/dist/index.server.js.map +0 -1
  107. package/dist/nextjs/api.d.ts +0 -45
  108. package/dist/nextjs/api.js +0 -32
  109. package/dist/nextjs/api.js.map +0 -1
  110. package/dist/nextjs/globals.css +0 -13
  111. package/dist/nextjs.d.ts +0 -2
  112. package/dist/nextjs.js +0 -2
  113. package/dist/nextjs.js.map +0 -1
  114. package/dist/nextjs.server.d.ts +0 -1
  115. package/dist/nextjs.server.js +0 -1
  116. package/dist/nextjs.server.js.map +0 -1
  117. package/dist/postcss.config.mjs +0 -6
  118. package/dist/react/checkout-page.css +0 -855
  119. package/dist/react/client.css +0 -984
  120. package/dist/react/payment-provider.css +0 -91
  121. package/dist/react/thank-you-page.css +0 -105
  122. package/dist/react.d.ts +0 -1
  123. package/dist/react.js +0 -2
  124. package/dist/react.js.map +0 -1
  125. package/dist/store-proxy-handler-CNzKlTFA.d.ts +0 -10
  126. package/dist/tanstack/api.d.ts +0 -41
  127. package/dist/tanstack/api.js +0 -30
  128. package/dist/tanstack/api.js.map +0 -1
  129. package/dist/tanstack.d.ts +0 -2
  130. package/dist/tanstack.js +0 -2
  131. package/dist/tanstack.js.map +0 -1
  132. package/dist/tanstack.server.d.ts +0 -1
  133. package/dist/tanstack.server.js +0 -1
  134. package/dist/tanstack.server.js.map +0 -1
package/README.md CHANGED
@@ -1,441 +1,50 @@
1
1
  # @ecomiq/storefront
2
2
 
3
- SDK **headless** para construir tu tienda con **Next.js** o **TanStack Start** y **React** sobre la API de Ecomiq.
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
- Instala también el peer de tu stack:
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
- En la **raíz de tu app** (`ecomiq.config.js`, `.cjs` o `.mjs`):
18
+ ## Uso
46
19
 
47
20
  ```js
48
- // @ts-check
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` | TanStack server + reexport client (misma API client que `/nextjs`) |
71
- | `@ecomiq/storefront/tanstack/api` | Proxy `/api/store/*` (server routes) |
72
-
73
- Importa hooks y `EcomiqProvider` desde `@ecomiq/storefront/react`. El build npm separa server (`react-server`), vanilla (raíz) y React (`/react`) para App Router.
74
-
75
- Los hooks (`useStoreProducts`, `useStoreCart`, …) viven en `/react`; `/nextjs` y `/tanstack` los reexportan en client por compatibilidad.
76
-
77
- ### Peers
78
-
79
- | Paquete | Cuándo |
80
- |---------|--------|
81
- | `react`, `react-dom` | Siempre |
82
- | `next` | Solo si usas `/nextjs` |
83
- | `@tanstack/react-start` | Solo si usas `/tanstack` |
84
-
85
- **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.
86
-
87
- ## Arquitectura
88
-
89
- ```
90
- ┌─────────────────────────────┐
91
- │ @ecomiq/storefront │ ecomiq.config.js
92
- └──────────────┬──────────────┘
93
- │
94
- ┌─────────────────────────┴─────────────────────────┐
95
- ▼ ▼
96
- /nextjs (+ /nextjs/api) /tanstack (+ /tanstack/api)
97
- getPublicStore · toNextJsStoreApiHandler getPublicStore · createTanStackStoreApiHandler
98
- │ │
99
- └─────────────────────────┬─────────────────────────┘
100
- ▼
101
- Capa React (`/react`) — hooks, EcomiqProvider, cliente BFF browser, auth
102
- │
103
- ▼
104
- core/server — fetchPublicStoreSettings · handleStoreProxyRequest
105
- │
106
- ▼
107
- Navegador → /api/store/* (proxy en tu app) → API Ecomiq
108
- ↑ contexto X-Store-Id (tras getPublicStore)
109
- ```
110
-
111
- El browser **no** llama al upstream directo; montas el proxy con el adapter de tu framework.
112
-
113
- ---
114
-
115
- ## Inicio rápido — Next.js
21
+ // ecomiq.config.js
22
+ import { defineConfig } from '@ecomiq/storefront/config'
116
23
 
117
- ### 1. Proxy BFF
118
-
119
- ```ts
120
- // app/api/store/[...path]/route.ts
121
- import { toNextJsStoreApiHandler } from "@ecomiq/storefront/nextjs/api";
122
-
123
- export const { GET, POST, PUT, PATCH, DELETE } = toNextJsStoreApiHandler({
124
- apiBaseUrl: process.env.STOREFRONT_API_URL!,
125
- });
126
- ```
127
-
128
- 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.
129
-
130
- ### 2. Layout y tienda
131
-
132
- ```tsx
133
- // app/ecomiq-provider.tsx
134
- "use client";
135
-
136
- import { EcomiqProvider } from "@ecomiq/storefront/react";
137
- import type { StorefrontSettings } from "@ecomiq/storefront";
138
-
139
- export function EcomiqRootProvider({
140
- store,
141
- children,
142
- }: {
143
- store: StorefrontSettings | null;
144
- children: React.ReactNode;
145
- }) {
146
- return <EcomiqProvider store={store}>{children}</EcomiqProvider>;
147
- }
148
- ```
149
-
150
- ```tsx
151
- // app/layout.tsx — Server Component
152
- import { getPublicStore } from "@ecomiq/storefront/nextjs";
153
- import { EcomiqRootProvider } from "./ecomiq-provider";
154
-
155
- export default async function RootLayout({ children }: { children: React.ReactNode }) {
156
- const { data: store, error } = await getPublicStore();
157
- if (error) {
158
- // tu página de error / tienda no encontrada
159
- }
160
- return (
161
- <html lang="es">
162
- <body>
163
- <EcomiqRootProvider store={store}>{children}</EcomiqRootProvider>
164
- </body>
165
- </html>
166
- );
167
- }
168
- ```
169
-
170
- `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"`.
171
-
172
- ### 3. Catálogo con hooks
173
-
174
- Importa hooks desde `@ecomiq/storefront/react`:
175
-
176
- ```tsx
177
- "use client";
178
-
179
- import { useStoreProducts } from "@ecomiq/storefront/react";
180
-
181
- export function ProductList() {
182
- const { useStorefrontProductsList } = useStoreProducts();
183
- const { data, isLoading } = useStorefrontProductsList({ page: 1, pageSize: 24 });
184
-
185
- if (isLoading) return <p>Cargando…</p>;
186
- return (
187
- <ul>
188
- {data?.products.map((p) => (
189
- <li key={p.id}>{p.name}</li>
190
- ))}
191
- </ul>
192
- );
193
- }
24
+ export default defineConfig({
25
+ store: { slug: 'mi-tienda' },
26
+ })
194
27
  ```
195
28
 
196
- ### 4. Datos en Server Components
197
-
198
29
  ```tsx
199
- import { getStorefrontProducts } from "@ecomiq/storefront/nextjs";
200
-
201
- export default async function CatalogPage() {
202
- const { products } = await getStorefrontProducts({ page: 1, pageSize: 24 });
203
- return <YourGrid products={products} />;
204
- }
205
- ```
206
-
207
- ---
208
-
209
- ## Inicio rápido — TanStack Start
210
-
211
- ### 1. Proxy BFF
30
+ // pages/index.tsx
31
+ import { definePage, serverApi } from '@ecomiq/storefront'
212
32
 
213
- Ruta splat: `/api/store/*` → `routes/api/store/$.ts`
214
-
215
- ```ts
216
- // src/routes/api/store/$.ts
217
- import { createFileRoute } from "@tanstack/react-router";
218
- import { createTanStackStoreApiHandler } from "@ecomiq/storefront/tanstack/api";
219
-
220
- const storeApi = createTanStackStoreApiHandler({
221
- apiBaseUrl: process.env.STOREFRONT_API_URL,
222
- });
223
-
224
- export const Route = createFileRoute("/api/store/$")({
225
- server: { handlers: storeApi },
226
- });
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
+ })
227
40
  ```
228
41
 
229
- ### 2. Tienda en root route
42
+ CLI:
230
43
 
231
- ```tsx
232
- // src/components/ecomiq-provider.tsx
233
- "use client";
234
-
235
- import { EcomiqProvider } from "@ecomiq/storefront/react";
236
- import type { StorefrontSettings } from "@ecomiq/storefront/tanstack";
237
-
238
- export function EcomiqRootProvider({
239
- store,
240
- children,
241
- }: {
242
- store: StorefrontSettings | null;
243
- children: React.ReactNode;
244
- }) {
245
- return <EcomiqProvider store={store}>{children}</EcomiqProvider>;
246
- }
247
- ```
248
-
249
- ```tsx
250
- // src/routes/__root.tsx
251
- import { createRootRoute, Outlet } from "@tanstack/react-router";
252
- import { getPublicStore } from "@ecomiq/storefront/tanstack";
253
- import { EcomiqRootProvider } from "../components/ecomiq-provider";
254
-
255
- export const Route = createRootRoute({
256
- loader: async () => getPublicStore(),
257
- component: RootComponent,
258
- });
259
-
260
- function RootComponent() {
261
- const { data: store } = Route.useLoaderData();
262
- return (
263
- <EcomiqRootProvider store={store}>
264
- <Outlet />
265
- </EcomiqRootProvider>
266
- );
267
- }
268
- ```
269
-
270
- ### 3. Hooks (Client Components)
271
-
272
- ```tsx
273
- "use client";
274
-
275
- import { useStoreProducts, useStoreCart } from "@ecomiq/storefront/react";
276
- ```
277
-
278
- ---
279
-
280
- ## Autenticación (Better Auth + SDK)
281
-
282
- Instala [Better Auth](https://www.better-auth.com/).
283
-
284
- **Servidor:**
285
-
286
- ```ts
287
- // lib/auth.ts
288
- import { betterAuth } from "better-auth";
289
- import { ecomiqOAuthPassword } from "@ecomiq/storefront/nextjs";
290
- // TanStack: import { ecomiqOAuthPassword } from "@ecomiq/storefront/tanstack";
291
-
292
- export const auth = betterAuth({
293
- plugins: [ecomiqOAuthPassword()],
294
- });
295
- ```
296
-
297
- **Ruta API (Next.js):**
298
-
299
- ```ts
300
- // app/api/auth/[...all]/route.ts
301
- import { toNextJsHandler } from "better-auth/next-js";
302
- import { auth } from "@/lib/auth";
303
-
304
- export const { GET, POST } = toNextJsHandler(auth);
305
- ```
306
-
307
- **Cliente:**
308
-
309
- ```ts
310
- // lib/auth-client.ts
311
- "use client";
312
-
313
- import { createAuthClient } from "better-auth/react";
314
- import { ecomiqOAuthPasswordClient } from "@ecomiq/storefront/nextjs";
315
-
316
- export const authClient = createAuthClient({
317
- plugins: [ecomiqOAuthPasswordClient()],
318
- });
319
- ```
320
-
321
- **Layout** — pasa `authClient` al provider:
322
-
323
- ```tsx
324
- <EcomiqProvider store={store} authClient={authClient}>
325
- {children}
326
- </EcomiqProvider>
327
- ```
328
-
329
- ### Login, registro y logout
330
-
331
- ```tsx
332
- "use client";
333
-
334
- import {
335
- useStore,
336
- useStorefrontAuthClient,
337
- useStorefrontSession,
338
- registerCustomer,
339
- signInWithEcomiqCredentials,
340
- } from "@ecomiq/storefront/nextjs";
341
-
342
- export function AccountActions() {
343
- const { data: store } = useStore();
344
- const authClient = useStorefrontAuthClient();
345
- const { isAuthenticated, user, signOut, refetch } = useStorefrontSession();
346
-
347
- async function handleLogin(email: string, password: string) {
348
- await signInWithEcomiqCredentials(authClient, store, { email, password });
349
- await refetch();
350
- }
351
-
352
- async function handleRegister(input: {
353
- email: string;
354
- password: string;
355
- firstName: string;
356
- lastName: string;
357
- countryIsoCode: string;
358
- phone: string;
359
- }) {
360
- await registerCustomer(input);
361
- await handleLogin(input.email, input.password);
362
- }
363
-
364
- if (isAuthenticated) {
365
- return (
366
- <div>
367
- <p>Hola, {user?.firstName ?? user?.email}</p>
368
- <button type="button" onClick={() => signOut()}>Cerrar sesión</button>
369
- </div>
370
- );
371
- }
372
-
373
- return <div>{/* Tu UI */}</div>;
374
- }
375
- ```
376
-
377
- En TanStack, cambia el import server a `@ecomiq/storefront/tanstack` y los hooks a `@ecomiq/storefront/react`.
378
-
379
- ---
380
-
381
- ## Qué incluye cada entry
382
-
383
- Desde `/nextjs` o `/tanstack` (**server**, condición `react-server`):
384
-
385
- - `getPublicStore`, `getHostname`, `fetchPublicStoreSettings`
386
- - Cliente BFF server: `getStorefrontProducts`, `registerCustomer`, carrito/cuenta, etc.
387
- - Auth server: `ecomiqOAuthPassword`
388
-
389
- Desde `@ecomiq/storefront/react` (**client**, `"use client"`):
390
-
391
- - **Provider:** `EcomiqProvider`, `useStore`, `usePublicStore`
392
- - **Hooks BFF:** `useStoreProducts`, `useStoreCollections`, `useStorePages`, `useStoreCart`
393
- - **Auth client:** `ecomiqOAuthPasswordClient`, `signInWithEcomiqCredentials`, `useStorefrontSession`
394
- - **Componentes:** selector de variantes de producto (más en futuras versiones)
395
-
396
- Desde `@ecomiq/storefront` (raíz — vanilla, server o client):
397
-
398
- - **Tipos y Zod:** `StorefrontSettings`, `StorefrontProduct`, schemas
399
- - **Utilidades:** `formatCurrency`, `countryCurrency`, `sanitizeHtml`, `stripHtml`, labels de pedidos, `findStorefrontCmsPageBySlug`
400
- - **Config** (solo server): `loadEcomiqConfig`, `EcomiqConfigSchema`, …
401
-
402
- ## Variables de entorno
403
-
404
- | Variable | Descripción |
405
- |----------|-------------|
406
- | `STOREFRONT_API_URL` | URL base de la API de tienda (proxy) |
407
- | `PUBLIC_STORE_SETTINGS_SOURCE` | `api` (default) o `kv` (Cloudflare KV; requiere `@opennextjs/cloudflare` en tu proyecto, no en el SDK) |
408
-
409
- **Identificador de tienda:** `slugStore` en `ecomiq.config.*` si existe; si no, header `Host`.
410
-
411
- ## Dependencias recomendadas
412
-
413
- **Next.js:**
414
-
415
- ```json
416
- {
417
- "dependencies": {
418
- "@ecomiq/storefront": "^0.1.0",
419
- "next": "^15.3.0",
420
- "react": "^19.0.0",
421
- "react-dom": "^19.0.0"
422
- }
423
- }
424
- ```
425
-
426
- **TanStack Start:**
427
-
428
- ```json
429
- {
430
- "dependencies": {
431
- "@ecomiq/storefront": "^0.1.0",
432
- "@tanstack/react-start": "^1.0.0",
433
- "react": "^19.0.0",
434
- "react-dom": "^19.0.0"
435
- }
436
- }
44
+ ```bash
45
+ npx ecomiq dev
46
+ npx ecomiq build
47
+ npx ecomiq deploy
437
48
  ```
438
49
 
439
- ## Soporte
440
-
441
- 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
+ }