@ecomiq/storefront 2.0.18 → 2.0.20
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 +5 -2
- package/public/design-bridge.js +318 -0
- package/src/auth/ecomiq-oauth-password-types.ts +0 -1
- package/src/auth/storefront-auth.ts +7 -0
- package/src/codegen.mjs +44 -0
- package/src/index.ts +4 -0
- package/src/pages/api.$.ts +20 -0
- package/src/pages/api.auth.$.ts +16 -0
- package/src/providers/auth.tsx +20 -7
- package/src/providers/cart.tsx +2 -2
- package/src/runtime/context.ts +1 -0
- package/src/server.ts +4 -2
- package/src/vite-inline.mjs +22 -1
- package/templates/__root.tsx +10 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecomiq/storefront",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.20",
|
|
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",
|
|
@@ -44,12 +44,15 @@
|
|
|
44
44
|
"./internal/thank-you": "./src/pages/thank-you.tsx",
|
|
45
45
|
"./internal/cms-page": "./src/pages/pages.$pageSlug.tsx",
|
|
46
46
|
"./internal/account": "./src/pages/account.tsx",
|
|
47
|
+
"./internal/api-proxy": "./src/pages/api.$.ts",
|
|
48
|
+
"./internal/auth-api": "./src/pages/api.auth.$.ts",
|
|
47
49
|
"./package.json": "./package.json"
|
|
48
50
|
},
|
|
49
51
|
"files": [
|
|
50
52
|
"bin",
|
|
51
53
|
"src",
|
|
52
54
|
"templates",
|
|
55
|
+
"public",
|
|
53
56
|
"README.md"
|
|
54
57
|
],
|
|
55
58
|
"sideEffects": [
|
|
@@ -79,7 +82,7 @@
|
|
|
79
82
|
"vite": "^8.1.0",
|
|
80
83
|
"wrangler": "^4.100.0",
|
|
81
84
|
"zod": "^3.25.0",
|
|
82
|
-
"@ecomiq/core": "^0.1.
|
|
85
|
+
"@ecomiq/core": "^0.1.7"
|
|
83
86
|
},
|
|
84
87
|
"peerDependencies": {
|
|
85
88
|
"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
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* /checkout/$checkoutId ← flujo de checkout
|
|
18
18
|
* /thank-you ← post-compra
|
|
19
19
|
* /account/* ← área de cuenta (login/register en template: /login, /register)
|
|
20
|
+
* /api/$ ← proxy BFF same-origin
|
|
21
|
+
* /api/auth/$ ← Better Auth (handlers del framework)
|
|
20
22
|
*/
|
|
21
23
|
import {
|
|
22
24
|
mkdirSync,
|
|
@@ -90,6 +92,20 @@ const FRAMEWORK_LOCKED_PAGES = [
|
|
|
90
92
|
},
|
|
91
93
|
]
|
|
92
94
|
|
|
95
|
+
/** Server routes cerradas del framework (proxy BFF, Better Auth, etc.). */
|
|
96
|
+
const FRAMEWORK_LOCKED_API_ROUTES = [
|
|
97
|
+
{
|
|
98
|
+
rel: 'api/$.ts',
|
|
99
|
+
routeId: '/api/$',
|
|
100
|
+
importFrom: '@ecomiq/storefront/internal/api-proxy',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
rel: 'api/auth.$.ts',
|
|
104
|
+
routeId: '/api/auth/$',
|
|
105
|
+
importFrom: '@ecomiq/storefront/internal/auth-api',
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
|
|
93
109
|
export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
|
|
94
110
|
const pagesDir = resolve(cwd, 'pages')
|
|
95
111
|
if (!existsSync(pagesDir)) {
|
|
@@ -209,11 +225,13 @@ export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
|
|
|
209
225
|
}
|
|
210
226
|
|
|
211
227
|
assertNoFrameworkPageOverrides(pageEntries)
|
|
228
|
+
assertNoFrameworkApiOverrides(apiEntries)
|
|
212
229
|
|
|
213
230
|
const desiredRoutes = [
|
|
214
231
|
...pageEntries.map((entry) => entry.rel.replace(/\.jsx$/, '.tsx')),
|
|
215
232
|
...apiEntries.map((entry) => entry.rel),
|
|
216
233
|
...FRAMEWORK_LOCKED_PAGES.map((page) => page.rel),
|
|
234
|
+
...FRAMEWORK_LOCKED_API_ROUTES.map((route) => route.rel),
|
|
217
235
|
].sort()
|
|
218
236
|
|
|
219
237
|
for (const entry of pageEntries) {
|
|
@@ -244,6 +262,15 @@ export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
|
|
|
244
262
|
)
|
|
245
263
|
}
|
|
246
264
|
|
|
265
|
+
for (const locked of FRAMEWORK_LOCKED_API_ROUTES) {
|
|
266
|
+
const outFile = join(routesDir, locked.rel)
|
|
267
|
+
mkdirSync(dirname(outFile), { recursive: true })
|
|
268
|
+
writeGeneratedFile(
|
|
269
|
+
outFile,
|
|
270
|
+
apiRouteFileSource(locked.routeId, locked.importFrom),
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
247
274
|
reconcileGeneratedRoutes(appDir, routesDir, desiredRoutes)
|
|
248
275
|
|
|
249
276
|
// ── 9. wrangler.jsonc para deploy en Cloudflare Workers ──
|
|
@@ -521,6 +548,23 @@ function assertNoFrameworkPageOverrides(pageEntries) {
|
|
|
521
548
|
}
|
|
522
549
|
}
|
|
523
550
|
|
|
551
|
+
function assertNoFrameworkApiOverrides(apiEntries) {
|
|
552
|
+
const reserved = new Map(
|
|
553
|
+
FRAMEWORK_LOCKED_API_ROUTES.map((route) => [route.routeId, route.rel]),
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
for (const entry of apiEntries) {
|
|
557
|
+
const routeId = fileToRouteId(entry.rel)
|
|
558
|
+
if (!reserved.has(routeId)) continue
|
|
559
|
+
throw new Error(
|
|
560
|
+
`La ruta ${routeId} es reservada por Ecomiq. ` +
|
|
561
|
+
`Elimina pages/${entry.rel}; ya la genera @ecomiq/storefront ` +
|
|
562
|
+
`(proxy BFF /api/$ y Better Auth /api/auth/$). ` +
|
|
563
|
+
`Puedes añadir otras rutas en pages/api/.`,
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
524
568
|
/** Server route: reexporta GET/POST/... del módulo del usuario. */
|
|
525
569
|
function apiRouteFileSource(routeId, importPath) {
|
|
526
570
|
return (
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,9 @@ export {
|
|
|
31
31
|
DepartmentsSchema,
|
|
32
32
|
DistrictSchema,
|
|
33
33
|
DistrictsSchema,
|
|
34
|
+
EntityIdSchema,
|
|
35
|
+
NullableEntityIdSchema,
|
|
36
|
+
OptionalEntityIdSchema,
|
|
34
37
|
GeoDivisionSchema,
|
|
35
38
|
GeoDivisionTypeSchema,
|
|
36
39
|
GetDepartmentsParamsSchema,
|
|
@@ -43,6 +46,7 @@ export {
|
|
|
43
46
|
StorefrontMenuItemSchema,
|
|
44
47
|
getMenuItems,
|
|
45
48
|
} from '@ecomiq/core'
|
|
49
|
+
export type { EntityId } from '@ecomiq/core'
|
|
46
50
|
export { getCountries, getDepartments, getDistricts, getProvinces } from '@ecomiq/core'
|
|
47
51
|
export {
|
|
48
52
|
AddCartItemParamsSchema,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy catch-all same-origin: `/api/*` → storefront `/v1/*`.
|
|
3
|
+
* Ruta cerrada del framework (no hace falta en pages/ del usuario).
|
|
4
|
+
*/
|
|
5
|
+
import { proxyStorefrontRequest } from '../api-proxy'
|
|
6
|
+
|
|
7
|
+
type ApiHandlerArgs = {
|
|
8
|
+
request: Request
|
|
9
|
+
params: { _splat?: string }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function handle({ request, params }: ApiHandlerArgs) {
|
|
13
|
+
return proxyStorefrontRequest(request, params._splat)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const GET = handle
|
|
17
|
+
export const POST = handle
|
|
18
|
+
export const PUT = handle
|
|
19
|
+
export const PATCH = handle
|
|
20
|
+
export const DELETE = handle
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth catch-all: `/api/auth/*`.
|
|
3
|
+
* Ruta cerrada del framework (no hace falta en pages/ del usuario).
|
|
4
|
+
*/
|
|
5
|
+
import { storefrontAuth } from '../auth/storefront-auth'
|
|
6
|
+
|
|
7
|
+
type ApiHandlerArgs = {
|
|
8
|
+
request: Request
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function handle({ request }: ApiHandlerArgs) {
|
|
12
|
+
return storefrontAuth.handler(request)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const GET = handle
|
|
16
|
+
export const POST = handle
|
package/src/providers/auth.tsx
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auth storefront: Better Auth client + sesión via context.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* <AuthProvider client={client}>
|
|
7
|
-
* <App />
|
|
8
|
-
* </AuthProvider>
|
|
4
|
+
* El framework monta `<AuthProvider>` en `__root` (cliente por defecto).
|
|
5
|
+
* Puedes pasar un `client` propio si necesitas opciones custom.
|
|
9
6
|
*
|
|
7
|
+
* ```tsx
|
|
10
8
|
* const { session, signIn, signOut } = useAuth()
|
|
11
9
|
* ```
|
|
12
10
|
*/
|
|
@@ -39,14 +37,29 @@ export type StorefrontSession = StorefrontAuthClientInstance['$Infer']['Session'
|
|
|
39
37
|
|
|
40
38
|
const AuthContext = createContext<StorefrontAuthClientInstance | null>(null)
|
|
41
39
|
|
|
40
|
+
/** Cliente por defecto del framework (una instancia por bundle). */
|
|
41
|
+
let defaultAuthClient: StorefrontAuthClientInstance | null = null
|
|
42
|
+
|
|
43
|
+
function getDefaultAuthClient(): StorefrontAuthClientInstance {
|
|
44
|
+
if (!defaultAuthClient) {
|
|
45
|
+
defaultAuthClient = createStorefrontAuthClient()
|
|
46
|
+
}
|
|
47
|
+
return defaultAuthClient
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
export function AuthProvider({
|
|
43
51
|
client,
|
|
44
52
|
children,
|
|
45
53
|
}: {
|
|
46
|
-
|
|
54
|
+
/** Si se omite, usa el cliente por defecto del storefront. */
|
|
55
|
+
client?: StorefrontAuthClientInstance
|
|
47
56
|
children: ReactNode
|
|
48
57
|
}) {
|
|
49
|
-
return
|
|
58
|
+
return (
|
|
59
|
+
<AuthContext.Provider value={client ?? getDefaultAuthClient()}>
|
|
60
|
+
{children}
|
|
61
|
+
</AuthContext.Provider>
|
|
62
|
+
)
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
/** Acceso a sesión y acciones de auth. Requiere `<AuthProvider>` + `<AppEcomiqProvider>`. */
|
package/src/providers/cart.tsx
CHANGED
|
@@ -17,14 +17,14 @@ import {
|
|
|
17
17
|
useState,
|
|
18
18
|
type ReactNode,
|
|
19
19
|
} from 'react'
|
|
20
|
-
import { type StorefrontCart } from '@ecomiq/core'
|
|
20
|
+
import { type StorefrontCart, EntityIdSchema } from '@ecomiq/core'
|
|
21
21
|
import { z } from 'zod'
|
|
22
22
|
import { serverApi } from '../server'
|
|
23
23
|
|
|
24
24
|
const CartSessionSchema = z.object({
|
|
25
25
|
version: z.literal(1),
|
|
26
26
|
storeId: z.string().min(1),
|
|
27
|
-
cartId:
|
|
27
|
+
cartId: EntityIdSchema,
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
export type CartLoadStatus = 'loading' | 'ready' | 'error'
|
package/src/runtime/context.ts
CHANGED
package/src/server.ts
CHANGED
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
type RemoveCartItemParams,
|
|
59
59
|
type UpdateCartItemQuantityParams,
|
|
60
60
|
type UploadPaymentReceiptParams,
|
|
61
|
+
EntityIdSchema,
|
|
62
|
+
OptionalEntityIdSchema,
|
|
61
63
|
} from '@ecomiq/core'
|
|
62
64
|
import { z } from 'zod'
|
|
63
65
|
import { getStorefrontClient } from './client'
|
|
@@ -133,7 +135,7 @@ const removeCartItemFn = createServerFn({ method: 'POST' })
|
|
|
133
135
|
.handler(({ data }) => submitCartItemRemoval(getStorefrontClient(), data))
|
|
134
136
|
|
|
135
137
|
const AddProductToCartServerSchema = z.object({
|
|
136
|
-
cartId:
|
|
138
|
+
cartId: OptionalEntityIdSchema,
|
|
137
139
|
currencyCode: z.string().length(3),
|
|
138
140
|
variantId: z.string().regex(/^[1-9]\d*$/),
|
|
139
141
|
quantity: z.number().int().positive(),
|
|
@@ -172,7 +174,7 @@ const checkoutCartFn = createServerFn({ method: 'POST' })
|
|
|
172
174
|
.handler(({ data }) => getStorefrontClient().checkoutCart(data))
|
|
173
175
|
|
|
174
176
|
const CartPaymentReceiptServerSchema = z.object({
|
|
175
|
-
cartId:
|
|
177
|
+
cartId: EntityIdSchema,
|
|
176
178
|
fileBase64: z.string().min(1),
|
|
177
179
|
fileName: z.string().min(1),
|
|
178
180
|
mimeType: z.string().min(1).optional(),
|
package/src/vite-inline.mjs
CHANGED
|
@@ -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)
|
package/templates/__root.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useState, type ReactNode } from 'react'
|
|
3
3
|
import { createRootRoute, HeadContent, Scripts, Outlet, Link } from '@tanstack/react-router'
|
|
4
4
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
-
import { AppEcomiqProvider } from '@ecomiq/storefront/internal/context'
|
|
5
|
+
import { AppEcomiqProvider, AuthProvider } from '@ecomiq/storefront/internal/context'
|
|
6
6
|
import { buildRootHead } from '@ecomiq/storefront/internal/head'
|
|
7
7
|
import { serverApi } from '@ecomiq/storefront'
|
|
8
8
|
import { StorefrontError } from '@ecomiq/storefront/ui'
|
|
@@ -33,9 +33,11 @@ function RootComponent() {
|
|
|
33
33
|
<RootDocument language={settings.store.language}>
|
|
34
34
|
<QueryClientProvider client={queryClient}>
|
|
35
35
|
<AppEcomiqProvider config={publicConfig} settings={settings}>
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
|
|
36
|
+
<AuthProvider>
|
|
37
|
+
<Layout config={publicConfig} settings={settings}>
|
|
38
|
+
<Outlet />
|
|
39
|
+
</Layout>
|
|
40
|
+
</AuthProvider>
|
|
39
41
|
</AppEcomiqProvider>
|
|
40
42
|
</QueryClientProvider>
|
|
41
43
|
</RootDocument>
|
|
@@ -50,6 +52,10 @@ function RootDocument({ children, language }: { children: ReactNode; language: s
|
|
|
50
52
|
</head>
|
|
51
53
|
<body className="bg-background text-foreground antialiased">
|
|
52
54
|
{children}
|
|
55
|
+
{/* Puente de design mode (solo dev): el parent (admin) pinta hover/select. */}
|
|
56
|
+
{import.meta.env.DEV ? (
|
|
57
|
+
<script src="/design-bridge.js?v=1.0.0" defer />
|
|
58
|
+
) : null}
|
|
53
59
|
<Scripts />
|
|
54
60
|
</body>
|
|
55
61
|
</html>
|