@ecomiq/storefront 2.0.17 → 2.0.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomiq/storefront",
3
- "version": "2.0.17",
3
+ "version": "2.0.19",
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",
@@ -49,6 +50,7 @@
49
50
  "bin",
50
51
  "src",
51
52
  "templates",
53
+ "public",
52
54
  "README.md"
53
55
  ],
54
56
  "sideEffects": [
@@ -78,7 +80,7 @@
78
80
  "vite": "^8.1.0",
79
81
  "wrangler": "^4.100.0",
80
82
  "zod": "^3.25.0",
81
- "@ecomiq/core": "^0.1.5"
83
+ "@ecomiq/core": "^0.1.6"
82
84
  },
83
85
  "peerDependencies": {
84
86
  "react": "^19.0.0",
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Puente de modo design (solo dev). El parent (frontend) envía postMessage;
3
+ * este script pinta hover/select y devuelve metadata del nodo.
4
+ */
5
+ (function () {
6
+ if (typeof window === 'undefined' || window.parent === window) return;
7
+
8
+ var enabled = false;
9
+ var selectedEl = null;
10
+ var selectionLocked = false;
11
+ var previewStyles = {};
12
+ var hoverBox = null;
13
+ var hoverLabel = null;
14
+ var selectBox = null;
15
+ var selectLabel = null;
16
+
17
+ function ensureOverlay() {
18
+ if (hoverBox) return;
19
+ var style = document.createElement('style');
20
+ style.textContent =
21
+ '#__design_hover__,#__design_select__{pointer-events:none;position:fixed;z-index:2147483646;box-sizing:border-box;transition:top .05s,left .05s,width .05s,height .05s}' +
22
+ '#__design_hover__{border:2px solid #6366f1;background:rgba(99,102,241,.1)}' +
23
+ '#__design_select__{border:2px solid #c8a96e;background:rgba(200,169,110,.12)}' +
24
+ '.__design_lbl{position:fixed;z-index:2147483647;font:11px/1.3 ui-monospace,monospace;padding:2px 6px;border-radius:4px;white-space:nowrap;pointer-events:none}' +
25
+ '.__design_lbl_hover{background:#6366f1;color:#fff}' +
26
+ '.__design_lbl_select{background:#c8a96e;color:#1a1714}';
27
+ document.head.appendChild(style);
28
+
29
+ hoverBox = document.createElement('div');
30
+ hoverBox.id = '__design_hover__';
31
+ hoverBox.hidden = true;
32
+ hoverLabel = document.createElement('div');
33
+ hoverLabel.className = '__design_lbl __design_lbl_hover';
34
+ hoverLabel.hidden = true;
35
+
36
+ selectBox = document.createElement('div');
37
+ selectBox.id = '__design_select__';
38
+ selectBox.hidden = true;
39
+ selectLabel = document.createElement('div');
40
+ selectLabel.className = '__design_lbl __design_lbl_select';
41
+ selectLabel.hidden = true;
42
+
43
+ document.body.appendChild(hoverBox);
44
+ document.body.appendChild(hoverLabel);
45
+ document.body.appendChild(selectBox);
46
+ document.body.appendChild(selectLabel);
47
+ }
48
+
49
+ function escapeCss(value) {
50
+ if (typeof CSS !== 'undefined' && CSS.escape) return CSS.escape(value);
51
+ return value.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
52
+ }
53
+
54
+ function pickElement(raw) {
55
+ if (!raw || raw === document.body || raw === document.documentElement) return null;
56
+ if (raw.id === '__design_hover__' || raw.id === '__design_select__') return null;
57
+ if (raw.classList && raw.classList.contains('__design_lbl')) return null;
58
+ return raw;
59
+ }
60
+
61
+ function getLabel(el) {
62
+ var tag = el.tagName.toLowerCase();
63
+ var classes = Array.from(el.classList || []).filter(function (c) {
64
+ return !c.startsWith('__design');
65
+ });
66
+ if (classes.length) return tag + '.' + classes.slice(0, 2).join('.');
67
+ return tag;
68
+ }
69
+
70
+ function getSelector(el) {
71
+ if (el.id) return '#' + escapeCss(el.id);
72
+ var parts = [];
73
+ var current = el;
74
+ while (current && current !== document.body && current !== document.documentElement) {
75
+ var part = current.tagName.toLowerCase();
76
+ if (current.id) {
77
+ parts.unshift('#' + escapeCss(current.id));
78
+ break;
79
+ }
80
+ var parent = current.parentElement;
81
+ if (parent) {
82
+ var siblings = Array.from(parent.children).filter(function (c) {
83
+ return c.tagName === current.tagName;
84
+ });
85
+ if (siblings.length > 1) {
86
+ var idx = siblings.indexOf(current) + 1;
87
+ part += ':nth-of-type(' + idx + ')';
88
+ }
89
+ }
90
+ var cls = Array.from(current.classList || []).find(function (c) {
91
+ return !c.startsWith('__design') && !c.startsWith('design-');
92
+ });
93
+ if (cls) part += '.' + escapeCss(cls);
94
+ parts.unshift(part);
95
+ current = parent;
96
+ }
97
+ return parts.join(' > ');
98
+ }
99
+
100
+ function textSnippet(el) {
101
+ var text = (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim();
102
+ if (!text) return undefined;
103
+ return text.length > 80 ? text.slice(0, 80) + '…' : text;
104
+ }
105
+
106
+ function camelToKebab(key) {
107
+ return key.replace(/[A-Z]/g, function (m) {
108
+ return '-' + m.toLowerCase();
109
+ });
110
+ }
111
+
112
+ function computedSnapshot(el) {
113
+ var cs = window.getComputedStyle(el);
114
+ return {
115
+ display: cs.display,
116
+ paddingTop: cs.paddingTop,
117
+ paddingRight: cs.paddingRight,
118
+ paddingBottom: cs.paddingBottom,
119
+ paddingLeft: cs.paddingLeft,
120
+ marginTop: cs.marginTop,
121
+ marginRight: cs.marginRight,
122
+ marginBottom: cs.marginBottom,
123
+ marginLeft: cs.marginLeft,
124
+ fontSize: cs.fontSize,
125
+ fontWeight: cs.fontWeight,
126
+ lineHeight: cs.lineHeight,
127
+ letterSpacing: cs.letterSpacing,
128
+ color: cs.color,
129
+ backgroundColor: cs.backgroundColor,
130
+ borderWidth: cs.borderWidth,
131
+ borderStyle: cs.borderStyle,
132
+ borderColor: cs.borderColor,
133
+ borderRadius: cs.borderRadius,
134
+ opacity: cs.opacity,
135
+ boxShadow: cs.boxShadow,
136
+ textAlign: cs.textAlign,
137
+ };
138
+ }
139
+
140
+ function elementRect(el) {
141
+ var r = el.getBoundingClientRect();
142
+ return {
143
+ top: r.top,
144
+ left: r.left,
145
+ width: r.width,
146
+ height: r.height,
147
+ };
148
+ }
149
+
150
+ function elementPayload(el) {
151
+ return {
152
+ selector: getSelector(el),
153
+ tag: el.tagName.toLowerCase(),
154
+ label: getLabel(el),
155
+ classes: Array.from(el.classList || []).filter(function (c) {
156
+ return !c.startsWith('__design');
157
+ }),
158
+ textSnippet: textSnippet(el),
159
+ computedStyles: computedSnapshot(el),
160
+ rect: elementRect(el),
161
+ };
162
+ }
163
+
164
+ var rectRaf = null;
165
+ function postSelectionRect() {
166
+ if (!selectionLocked || !selectedEl) return;
167
+ if (rectRaf) return;
168
+ rectRaf = requestAnimationFrame(function () {
169
+ rectRaf = null;
170
+ post('DESIGN_RECT', { rect: elementRect(selectedEl) });
171
+ });
172
+ }
173
+
174
+ function clearPreviewStyles() {
175
+ if (!selectedEl) return;
176
+ Object.keys(previewStyles).forEach(function (key) {
177
+ selectedEl.style.removeProperty(camelToKebab(key));
178
+ });
179
+ previewStyles = {};
180
+ updateSelectOverlay();
181
+ }
182
+
183
+ function applyPreviewStyles(styles) {
184
+ if (!selectedEl || !styles) return;
185
+ Object.keys(styles).forEach(function (key) {
186
+ var val = styles[key];
187
+ if (val === '' || val == null) {
188
+ selectedEl.style.removeProperty(camelToKebab(key));
189
+ delete previewStyles[key];
190
+ } else {
191
+ selectedEl.style.setProperty(camelToKebab(key), String(val));
192
+ previewStyles[key] = String(val);
193
+ }
194
+ });
195
+ updateSelectOverlay();
196
+ }
197
+
198
+ function placeBox(box, label, el) {
199
+ var rect = el.getBoundingClientRect();
200
+ box.style.top = rect.top + 'px';
201
+ box.style.left = rect.left + 'px';
202
+ box.style.width = rect.width + 'px';
203
+ box.style.height = rect.height + 'px';
204
+ box.hidden = false;
205
+
206
+ label.textContent = getLabel(el);
207
+ label.style.top = Math.max(4, rect.top - 22) + 'px';
208
+ label.style.left = rect.left + 'px';
209
+ label.hidden = false;
210
+ }
211
+
212
+ function hideHover() {
213
+ if (hoverBox) hoverBox.hidden = true;
214
+ if (hoverLabel) hoverLabel.hidden = true;
215
+ }
216
+
217
+ function updateSelectOverlay() {
218
+ if (selectionLocked && selectedEl) {
219
+ placeBox(selectBox, selectLabel, selectedEl);
220
+ postSelectionRect();
221
+ }
222
+ }
223
+
224
+ function post(type, payload) {
225
+ window.parent.postMessage(
226
+ { source: 'design-bridge', type: type, payload: payload || {} },
227
+ '*',
228
+ );
229
+ }
230
+
231
+ function clearSelection(notifyParent) {
232
+ clearPreviewStyles();
233
+ selectedEl = null;
234
+ selectionLocked = false;
235
+ if (selectBox) selectBox.hidden = true;
236
+ if (selectLabel) selectLabel.hidden = true;
237
+ if (notifyParent) post('DESIGN_DESELECT');
238
+ }
239
+
240
+ function onMouseMove(e) {
241
+ if (!enabled) return;
242
+
243
+ if (selectionLocked && selectedEl) {
244
+ updateSelectOverlay();
245
+ hideHover();
246
+ return;
247
+ }
248
+
249
+ var el = pickElement(document.elementFromPoint(e.clientX, e.clientY));
250
+ if (!el) {
251
+ hideHover();
252
+ return;
253
+ }
254
+ placeBox(hoverBox, hoverLabel, el);
255
+ post('DESIGN_HOVER', elementPayload(el));
256
+ }
257
+
258
+ function onClick(e) {
259
+ if (!enabled) return;
260
+ e.preventDefault();
261
+ e.stopPropagation();
262
+
263
+ var el = pickElement(document.elementFromPoint(e.clientX, e.clientY));
264
+ if (!el) return;
265
+
266
+ if (selectionLocked && selectedEl) {
267
+ if (el === selectedEl) {
268
+ clearSelection(true);
269
+ return;
270
+ }
271
+ // Click fuera del popover / en otro nodo ya no deselecciona.
272
+ // clearSelection(true);
273
+ return;
274
+ }
275
+
276
+ selectedEl = el;
277
+ selectionLocked = true;
278
+ placeBox(selectBox, selectLabel, el);
279
+ hideHover();
280
+ post('DESIGN_SELECT', elementPayload(el));
281
+ }
282
+
283
+ function enable() {
284
+ if (enabled) return;
285
+ enabled = true;
286
+ ensureOverlay();
287
+ document.addEventListener('mousemove', onMouseMove, true);
288
+ document.addEventListener('click', onClick, true);
289
+ window.addEventListener('scroll', updateSelectOverlay, true);
290
+ window.addEventListener('resize', updateSelectOverlay);
291
+ document.body.style.cursor = 'crosshair';
292
+ post('DESIGN_READY');
293
+ }
294
+
295
+ function disable() {
296
+ if (!enabled) return;
297
+ enabled = false;
298
+ clearSelection(false);
299
+ document.removeEventListener('mousemove', onMouseMove, true);
300
+ document.removeEventListener('click', onClick, true);
301
+ window.removeEventListener('scroll', updateSelectOverlay, true);
302
+ window.removeEventListener('resize', updateSelectOverlay);
303
+ document.body.style.cursor = '';
304
+ hideHover();
305
+ }
306
+
307
+ window.addEventListener('message', function (event) {
308
+ var data = event.data;
309
+ if (!data || data.target !== 'design-bridge') return;
310
+ if (data.type === 'DESIGN_ENABLE') enable();
311
+ if (data.type === 'DESIGN_DISABLE') disable();
312
+ if (data.type === 'DESIGN_CLEAR_SELECTION') clearSelection(false);
313
+ if (data.type === 'DESIGN_APPLY_STYLES') applyPreviewStyles(data.payload && data.payload.styles);
314
+ if (data.type === 'DESIGN_CLEAR_PREVIEW') clearPreviewStyles();
315
+ });
316
+
317
+ post('DESIGN_READY');
318
+ })();
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\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(...args) {\n` +
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
- ` return startServer.fetch(...args)\n` +
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
+ }
@@ -21,7 +21,28 @@ const postcssBrowserStub = fileURLToPath(
21
21
  )
22
22
 
23
23
  const frameworkRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
24
+ const designBridgePath = resolve(frameworkRoot, 'public', 'design-bridge.js')
24
25
 
26
+ /** Sirve /design-bridge.js desde el package (no desde public/ del usuario). */
27
+ function designBridgePlugin() {
28
+ return {
29
+ name: 'ecomiq:design-bridge',
30
+ configureServer(server) {
31
+ server.middlewares.use((req, res, next) => {
32
+ const url = req.url?.split('?')[0]
33
+ if (url !== '/design-bridge.js') return next()
34
+ try {
35
+ const body = readFileSync(designBridgePath)
36
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8')
37
+ res.setHeader('Cache-Control', 'no-store')
38
+ res.end(body)
39
+ } catch (error) {
40
+ next(error)
41
+ }
42
+ })
43
+ },
44
+ }
45
+ }
25
46
  function resolveSanitizeHtmlPostcss(source, importer) {
26
47
  if (source !== 'postcss') return null
27
48
  if (!importer?.includes('/sanitize-html/')) return null
@@ -160,7 +181,7 @@ export function createViteConfig({ appDir, config, projectRoot, runtime }) {
160
181
  const resolvedRuntime = resolveDevRuntime({ config, runtime })
161
182
  const resolvedHttps = resolveDevHttps({ config, projectRoot: root, appDir })
162
183
 
163
- const plugins = [tailwindcss()]
184
+ const plugins = [tailwindcss(), designBridgePlugin()]
164
185
 
165
186
  if (resolvedHttps.plugin) {
166
187
  plugins.push(resolvedHttps.plugin)
@@ -50,6 +50,10 @@ function RootDocument({ children, language }: { children: ReactNode; language: s
50
50
  </head>
51
51
  <body className="bg-background text-foreground antialiased">
52
52
  {children}
53
+ {/* Puente de design mode (solo dev): el parent (admin) pinta hover/select. */}
54
+ {import.meta.env.DEV ? (
55
+ <script src="/design-bridge.js?v=1.0.0" defer />
56
+ ) : null}
53
57
  <Scripts />
54
58
  </body>
55
59
  </html>