@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.
Files changed (134) hide show
  1. package/README.md +26 -418
  2. package/bin/ecomiq.mjs +154 -0
  3. package/package.json +72 -94
  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-64GLOWMJ.js +0 -216
  67. package/dist/chunk-64GLOWMJ.js.map +0 -1
  68. package/dist/chunk-AKD5IDEB.js +0 -177
  69. package/dist/chunk-AKD5IDEB.js.map +0 -1
  70. package/dist/chunk-EFEVJJWF.js +0 -1345
  71. package/dist/chunk-EFEVJJWF.js.map +0 -1
  72. package/dist/chunk-NAJQB5DN.js +0 -155
  73. package/dist/chunk-NAJQB5DN.js.map +0 -1
  74. package/dist/chunk-OABDGTBI.js +0 -253
  75. package/dist/chunk-OABDGTBI.js.map +0 -1
  76. package/dist/chunk-Q4U5OEUT.js +0 -21
  77. package/dist/chunk-Q4U5OEUT.js.map +0 -1
  78. package/dist/chunk-S7CPZ4UE.js +0 -6
  79. package/dist/chunk-S7CPZ4UE.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 -10759
  83. package/dist/chunks/client.js +0 -5398
  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 -59
  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 -40
  96. package/dist/chunks/server-tanstack.js.map +0 -1
  97. package/dist/collection-DU7gREXS.d.ts +0 -4062
  98. package/dist/ecomiq-oauth-password-plugin-B_gMdGuc.d.ts +0 -3631
  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 -18
  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 -1
  130. package/dist/tanstack.js +0 -2
  131. package/dist/tanstack.js.map +0 -1
  132. package/dist/tanstack.server.d.ts +0 -18
  133. package/dist/tanstack.server.js +0 -1
  134. package/dist/tanstack.server.js.map +0 -1
package/README.md CHANGED
@@ -1,442 +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` | **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
- ### 1. Proxy BFF
119
-
120
- ```ts
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
- import { getStorefrontProducts } from "@ecomiq/storefront/nextjs";
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
- Ruta splat: `/api/store/*` → `routes/api/store/$.ts`
215
-
216
- ```ts
217
- // src/routes/api/store/$.ts
218
- import { createFileRoute } from "@tanstack/react-router";
219
- import { createTanStackStoreApiHandler } from "@ecomiq/storefront/tanstack/api";
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
- ### 2. Tienda en root route
42
+ CLI:
231
43
 
232
- ```tsx
233
- // src/components/ecomiq-provider.tsx
234
- "use client";
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
- ## Soporte
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
+ }