@ecomiq/storefront 2.0.16 → 2.0.18
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/package.json +3 -2
- package/src/codegen.mjs +6 -3
- package/src/health.ts +90 -0
- package/src/providers/auth.tsx +8 -3
- package/src/runtime/page.tsx +34 -5
- package/src/vite-inline.mjs +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecomiq/storefront",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"description": "Micro framework SSR de Ecomiq para storefronts. Páginas, datos y deploy sin configurar nada.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"./internal/page": "./src/runtime/page.tsx",
|
|
37
37
|
"./internal/context": "./src/runtime/context.ts",
|
|
38
38
|
"./internal/server-config": "./src/server-config-store.ts",
|
|
39
|
+
"./internal/health": "./src/health.ts",
|
|
39
40
|
"./internal/head": "./src/runtime/head.ts",
|
|
40
41
|
"./internal/default-layout": "./src/runtime/default-layout.tsx",
|
|
41
42
|
"./internal/checkout": "./src/pages/checkout.tsx",
|
|
@@ -78,7 +79,7 @@
|
|
|
78
79
|
"vite": "^8.1.0",
|
|
79
80
|
"wrangler": "^4.100.0",
|
|
80
81
|
"zod": "^3.25.0",
|
|
81
|
-
"@ecomiq/core": "^0.1.
|
|
82
|
+
"@ecomiq/core": "^0.1.6"
|
|
82
83
|
},
|
|
83
84
|
"peerDependencies": {
|
|
84
85
|
"react": "^19.0.0",
|
package/src/codegen.mjs
CHANGED
|
@@ -155,15 +155,18 @@ export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
|
|
|
155
155
|
HEADER +
|
|
156
156
|
`import startServer, { createServerEntry } from '@tanstack/react-start/server-entry'\n` +
|
|
157
157
|
`import config from '../../ecomiq.config.js'\n` +
|
|
158
|
-
`import { registerServerConfig } from '@ecomiq/storefront/internal/server-config'\n
|
|
158
|
+
`import { registerServerConfig } from '@ecomiq/storefront/internal/server-config'\n` +
|
|
159
|
+
`import { handleEcomiqHealth } from '@ecomiq/storefront/internal/health'\n\n` +
|
|
159
160
|
`export default createServerEntry({\n` +
|
|
160
|
-
` async fetch(...
|
|
161
|
+
` async fetch(request, ...rest) {\n` +
|
|
161
162
|
` let api = config.api\n` +
|
|
162
163
|
` if (import.meta.env.DEV && config.dev?.apiBaseUrl) {\n` +
|
|
163
164
|
` api = { ...api, baseUrl: config.dev.apiBaseUrl }\n` +
|
|
164
165
|
` }\n` +
|
|
165
166
|
` registerServerConfig({ domain: config.domain, api })\n` +
|
|
166
|
-
`
|
|
167
|
+
` const health = await handleEcomiqHealth(request)\n` +
|
|
168
|
+
` if (health) return health\n` +
|
|
169
|
+
` return startServer.fetch(request, ...rest)\n` +
|
|
167
170
|
` },\n` +
|
|
168
171
|
`})\n`,
|
|
169
172
|
)
|
package/src/health.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health interno del runtime storefront.
|
|
3
|
+
* GET /ping → liveness puro: "ok" 200 (target del probe de vm-manager).
|
|
4
|
+
* GET /__ecomiq/health → proceso up + estado de /v1/settings (sin SSR de páginas).
|
|
5
|
+
*/
|
|
6
|
+
import { getStorefrontClient } from './client'
|
|
7
|
+
|
|
8
|
+
export const ECOMIQ_HEALTH_PATH = '/__ecomiq/health'
|
|
9
|
+
export const ECOMIQ_PING_PATH = '/ping'
|
|
10
|
+
|
|
11
|
+
export type EcomiqHealthSettingsStatus = {
|
|
12
|
+
ok: boolean
|
|
13
|
+
latencyMs: number
|
|
14
|
+
storeId?: string
|
|
15
|
+
domain?: string | null
|
|
16
|
+
error?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type EcomiqHealthResponse = {
|
|
20
|
+
ok: true
|
|
21
|
+
settings: EcomiqHealthSettingsStatus
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function probeSettings(): Promise<EcomiqHealthSettingsStatus> {
|
|
25
|
+
const started = Date.now()
|
|
26
|
+
try {
|
|
27
|
+
const settings = await getStorefrontClient().getSettings()
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
latencyMs: Date.now() - started,
|
|
31
|
+
storeId: settings.store.id,
|
|
32
|
+
domain: settings.store.domain ?? settings.store.slug,
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return {
|
|
36
|
+
ok: false,
|
|
37
|
+
latencyMs: Date.now() - started,
|
|
38
|
+
error: error instanceof Error ? error.message : String(error),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Si el request es al health interno, responde JSON.
|
|
45
|
+
* Si no, `null` para que el caller siga con el fetch normal de TanStack.
|
|
46
|
+
*/
|
|
47
|
+
export async function handleEcomiqHealth(
|
|
48
|
+
request: Request,
|
|
49
|
+
): Promise<Response | null> {
|
|
50
|
+
const { pathname } = new URL(request.url)
|
|
51
|
+
if (pathname !== ECOMIQ_HEALTH_PATH && pathname !== ECOMIQ_PING_PATH) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
56
|
+
return new Response(null, {
|
|
57
|
+
status: 405,
|
|
58
|
+
headers: { allow: 'GET, HEAD' },
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (pathname === ECOMIQ_PING_PATH) {
|
|
63
|
+
return new Response(request.method === 'HEAD' ? null : 'ok', {
|
|
64
|
+
status: 200,
|
|
65
|
+
headers: {
|
|
66
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
67
|
+
'cache-control': 'no-store',
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const body: EcomiqHealthResponse = {
|
|
73
|
+
ok: true,
|
|
74
|
+
settings: await probeSettings(),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (request.method === 'HEAD') {
|
|
78
|
+
return new Response(null, {
|
|
79
|
+
status: 200,
|
|
80
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return Response.json(body, {
|
|
85
|
+
status: 200,
|
|
86
|
+
headers: {
|
|
87
|
+
'cache-control': 'no-store',
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}
|
package/src/providers/auth.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
signInWithEcomiqCredentials,
|
|
18
18
|
type EcomiqSignInCredentials,
|
|
19
19
|
} from '../auth/ecomiq-storefront-sign-in'
|
|
20
|
-
import {
|
|
20
|
+
import { AppEcomiqContext } from './ecomiq'
|
|
21
21
|
|
|
22
22
|
export type CreateStorefrontAuthClientOptions = {
|
|
23
23
|
baseURL?: string
|
|
@@ -55,7 +55,9 @@ export function useAuth() {
|
|
|
55
55
|
if (!client) throw new Error('useAuth debe usarse dentro de AuthProvider')
|
|
56
56
|
|
|
57
57
|
const sessionQuery = client.useSession()
|
|
58
|
-
|
|
58
|
+
// Mismo módulo que AppEcomiqProvider (no useSettings del barrel) + soft read
|
|
59
|
+
// para no tumbar el árbol si hay re-render concurrente post-sesión.
|
|
60
|
+
const ecomiq = useContext(AppEcomiqContext)
|
|
59
61
|
|
|
60
62
|
return {
|
|
61
63
|
client,
|
|
@@ -63,7 +65,10 @@ export function useAuth() {
|
|
|
63
65
|
isPending: sessionQuery.isPending,
|
|
64
66
|
error: sessionQuery.error,
|
|
65
67
|
signIn: async (credentials: EcomiqSignInCredentials) => {
|
|
66
|
-
|
|
68
|
+
if (!ecomiq) {
|
|
69
|
+
throw new Error('signIn requiere AppEcomiqProvider')
|
|
70
|
+
}
|
|
71
|
+
await signInWithEcomiqCredentials(client, ecomiq.settings, credentials)
|
|
67
72
|
const result = await client.getSession()
|
|
68
73
|
const session = (result as { data?: StorefrontSession | null })?.data ?? null
|
|
69
74
|
if (!session?.user) {
|
package/src/runtime/page.tsx
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime interno de Ecomiq (solo lo importa el código generado en
|
|
2
|
+
* Runtime interno de Ecomiq (solo lo importa el código generado en `.ecomiq`).
|
|
3
3
|
* Traduce el contrato público `definePage` a las opciones del router interno.
|
|
4
4
|
*/
|
|
5
|
+
import { useContext } from 'react'
|
|
6
|
+
import { useRouterState } from '@tanstack/react-router'
|
|
5
7
|
import type { StorefrontSettings } from '@ecomiq/core'
|
|
6
8
|
import type { EcomiqPage, PublicEcomiqConfig } from '../index'
|
|
7
|
-
import {
|
|
9
|
+
import { AppEcomiqContext } from '../providers/ecomiq'
|
|
8
10
|
import { serverApi } from '../server'
|
|
9
11
|
import { StorefrontError } from '../ui/storefront-error'
|
|
10
12
|
import { buildPageHead } from './head'
|
|
11
13
|
|
|
14
|
+
function isSettings(value: unknown): value is StorefrontSettings {
|
|
15
|
+
return Boolean(
|
|
16
|
+
value &&
|
|
17
|
+
typeof value === 'object' &&
|
|
18
|
+
value !== null &&
|
|
19
|
+
'store' in value,
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
async function resolveSettingsForMeta(
|
|
13
24
|
matches?: Array<{ loaderData?: unknown }>,
|
|
14
25
|
): Promise<StorefrontSettings | undefined> {
|
|
@@ -16,8 +27,8 @@ async function resolveSettingsForMeta(
|
|
|
16
27
|
// En SSR `matches` suele venir stale (updateMatch no muta el array), así que
|
|
17
28
|
// caemos a serverApi: el cliente core cachea settings y no hay HTTP extra.
|
|
18
29
|
const fromRoot = matches?.[0]?.loaderData
|
|
19
|
-
if (fromRoot
|
|
20
|
-
return fromRoot
|
|
30
|
+
if (isSettings(fromRoot)) {
|
|
31
|
+
return fromRoot
|
|
21
32
|
}
|
|
22
33
|
try {
|
|
23
34
|
return await serverApi.getSettings()
|
|
@@ -26,6 +37,18 @@ async function resolveSettingsForMeta(
|
|
|
26
37
|
}
|
|
27
38
|
}
|
|
28
39
|
|
|
40
|
+
function usePageSettings(): StorefrontSettings | undefined {
|
|
41
|
+
// Mismo módulo que AppEcomiqProvider (no el barrel) para evitar contextos duplicados.
|
|
42
|
+
const fromProvider = useContext(AppEcomiqContext)?.settings
|
|
43
|
+
const fromRootLoader = useRouterState({
|
|
44
|
+
select: (state) => {
|
|
45
|
+
const data = state.matches[0]?.loaderData
|
|
46
|
+
return isSettings(data) ? data : undefined
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
return fromProvider ?? fromRootLoader
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
export function pageToRouteOptions<
|
|
30
53
|
TData,
|
|
31
54
|
TParams extends Record<string, string>,
|
|
@@ -108,7 +131,13 @@ export function PageBoundary<
|
|
|
108
131
|
params: TParams
|
|
109
132
|
search: TSearch
|
|
110
133
|
}) {
|
|
111
|
-
const settings =
|
|
134
|
+
const settings = usePageSettings()
|
|
135
|
+
// Concurrent render / contexto duplicado: no tumbar el root.
|
|
136
|
+
// El re-render sync con provider o el loader del root suelen recuperar.
|
|
137
|
+
if (!settings) {
|
|
138
|
+
const Loading = page.loading
|
|
139
|
+
return Loading ? <Loading /> : null
|
|
140
|
+
}
|
|
112
141
|
const Component = page.component
|
|
113
142
|
return <Component data={data} params={params} search={search} settings={settings} />
|
|
114
143
|
}
|
package/src/vite-inline.mjs
CHANGED
|
@@ -198,12 +198,20 @@ export function createViteConfig({ appDir, config, projectRoot, runtime }) {
|
|
|
198
198
|
configFile: false,
|
|
199
199
|
publicDir: resolve(appDir, '../public'),
|
|
200
200
|
resolve: {
|
|
201
|
+
// Evita dos copias de React/context: el template resuelve desde
|
|
202
|
+
// node_modules del proyecto y `.ecomiq` desde symlinks — sin dedupe,
|
|
203
|
+
// AppEcomiqProvider y useSettings no comparten el mismo createContext
|
|
204
|
+
// (falla al re-render post useSession de Better Auth).
|
|
205
|
+
// No aliasar `@ecomiq/storefront` → rompe subpaths (`/internal/*`).
|
|
201
206
|
dedupe: [
|
|
202
207
|
'react',
|
|
203
208
|
'react-dom',
|
|
209
|
+
'@ecomiq/storefront',
|
|
210
|
+
'@ecomiq/core',
|
|
204
211
|
'@tanstack/react-router',
|
|
205
212
|
'@tanstack/react-start',
|
|
206
213
|
'@tanstack/react-query',
|
|
214
|
+
'better-auth',
|
|
207
215
|
],
|
|
208
216
|
},
|
|
209
217
|
plugins,
|