@ecomiq/storefront 2.0.5 → 2.0.8

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/bin/ecomiq.mjs CHANGED
@@ -89,8 +89,7 @@ export async function runCli(command = process.argv[2] ?? 'help') {
89
89
  runtime,
90
90
  })
91
91
  banner(
92
- `dev · ${config.name ?? 'tienda'} ${c.dim('http://localhost:' + port)}` +
93
- (runtime === 'workerd' ? c.dim(' · runtime workerd') : ''),
92
+ `dev · ${config.name ?? 'tienda'} ${c.dim(`· runtime ${runtime}`)} ${c.dim('http://localhost:' + port)}`,
94
93
  )
95
94
  const server = await createServer({
96
95
  ...viteConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomiq/storefront",
3
- "version": "2.0.5",
3
+ "version": "2.0.8",
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",
@@ -41,6 +41,7 @@
41
41
  "./internal/checkout": "./src/pages/checkout.tsx",
42
42
  "./internal/checkout-session": "./src/pages/checkout.$checkoutId.tsx",
43
43
  "./internal/thank-you": "./src/pages/thank-you.tsx",
44
+ "./internal/cms-page": "./src/pages/pages.$pageSlug.tsx",
44
45
  "./package.json": "./package.json"
45
46
  },
46
47
  "files": [
package/src/codegen.mjs CHANGED
@@ -57,6 +57,11 @@ const FRAMEWORK_LOCKED_PAGES = [
57
57
  routeId: '/thank-you',
58
58
  importFrom: '@ecomiq/storefront/internal/thank-you',
59
59
  },
60
+ {
61
+ rel: 'pages.$pageSlug.tsx',
62
+ routeId: '/pages/$pageSlug',
63
+ importFrom: '@ecomiq/storefront/internal/cms-page',
64
+ },
60
65
  ]
61
66
 
62
67
  export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
@@ -487,7 +492,7 @@ function assertNoFrameworkPageOverrides(pageEntries) {
487
492
  if (!reserved.has(routeId)) continue
488
493
  throw new Error(
489
494
  `La ruta ${routeId} es reservada por Ecomiq (página cerrada). ` +
490
- `Elimina pages/${entry.rel}; las páginas de checkout y thank-you viven en @ecomiq/storefront.`,
495
+ `Elimina pages/${entry.rel}; vive en @ecomiq/storefront (checkout, thank-you, CMS).`,
491
496
  )
492
497
  }
493
498
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Página CMS cerrada (`/pages/$pageSlug`).
3
+ * Resuelve contenido publicado vía GET /v1/pages/{slug}.
4
+ */
5
+ import type { CmsPageDetailsDto } from '@ecomiq/core'
6
+ import {
7
+ definePage,
8
+ sanitizeHtml,
9
+ serverApi,
10
+ stripHtml,
11
+ z,
12
+ } from '../index'
13
+ import { StorefrontError } from '../ui'
14
+
15
+ const ContentPageSettingsSchema = z.object({
16
+ title: z.string().optional(),
17
+ html: z.string().optional(),
18
+ seo: z.object({
19
+ title: z.string().optional(),
20
+ description: z.string().optional(),
21
+ }).optional(),
22
+ }).passthrough()
23
+
24
+ type ContentPageData = {
25
+ page: CmsPageDetailsDto
26
+ content: z.infer<typeof ContentPageSettingsSchema>
27
+ }
28
+
29
+ export default definePage<ContentPageData, { pageSlug: string }>({
30
+ loader: async ({ params }) => {
31
+ const page = await serverApi.getPageBySlug(params.pageSlug)
32
+ if (page.status !== 'published') throw new Error('Página no publicada')
33
+
34
+ return {
35
+ page,
36
+ content: ContentPageSettingsSchema.parse(page.settings ?? {}),
37
+ }
38
+ },
39
+
40
+ meta: ({ data }) => ({
41
+ title: data.content.seo?.title ?? data.content.title ?? data.page.name,
42
+ description: data.content.seo?.description ?? (
43
+ data.content.html
44
+ ? stripHtml(data.content.html).slice(0, 155)
45
+ : undefined
46
+ ),
47
+ }),
48
+
49
+ error: () => (
50
+ <StorefrontError
51
+ title="Página no disponible"
52
+ description="El contenido no existe o todavía no está publicado."
53
+ />
54
+ ),
55
+
56
+ component: ({ data }) => (
57
+ <article className="mx-auto w-full max-w-4xl px-4 py-10 sm:py-14">
58
+ <header className="mb-8 border-b pb-8">
59
+ <h1 className="text-balance text-3xl font-semibold tracking-[-0.02em] md:text-4xl">
60
+ {data.content.title ?? data.page.name}
61
+ </h1>
62
+ </header>
63
+
64
+ {data.content.html ? (
65
+ <div
66
+ className="prose prose-neutral max-w-none text-foreground prose-headings:font-semibold prose-a:text-primary"
67
+ dangerouslySetInnerHTML={{ __html: sanitizeHtml(data.content.html) }}
68
+ />
69
+ ) : (
70
+ <p className="text-muted-foreground">Contenido no disponible.</p>
71
+ )}
72
+ </article>
73
+ ),
74
+ })
package/src/server.ts CHANGED
@@ -86,6 +86,10 @@ const getCollectionBySlugFn = createServerFn({ method: 'GET' })
86
86
  .validator(z.object({ slug: z.string().min(1) }))
87
87
  .handler(({ data }) => getStorefrontClient().getCollectionBySlug(data.slug))
88
88
 
89
+ const getPageBySlugFn = createServerFn({ method: 'GET' })
90
+ .validator(z.object({ slug: z.string().min(1) }))
91
+ .handler(({ data }) => getStorefrontClient().getPageBySlug(data.slug))
92
+
89
93
  const getProductsFn = createServerFn({ method: 'GET' })
90
94
  .validator(ProductsParamsSchema)
91
95
  .handler(({ data }) => getStorefrontClient().getProducts(data))
@@ -219,6 +223,7 @@ export const serverApi = {
219
223
  getStoreId: () => getStoreIdFn(),
220
224
  getCollections: () => getCollectionsFn(),
221
225
  getCollectionBySlug: (slug: string) => getCollectionBySlugFn({ data: { slug } }),
226
+ getPageBySlug: (slug: string) => getPageBySlugFn({ data: { slug } }),
222
227
  getProducts: (
223
228
  params: ProductsParamsInput = {},
224
229
  options: { signal?: AbortSignal } = {},
@@ -59,11 +59,24 @@ function sanitizeHtmlClientBoundary() {
59
59
  * TanStack Start marca `@tanstack/react-router` como `resolve.external` fuera de
60
60
  * su monorepo. Cloudflare Vite plugin no lo tolera y workerd acaba ejecutando
61
61
  * CJS con `require` → "require is not defined".
62
+ *
63
+ * Solo aplica en runtime `workerd`. En `node`, React y otras deps CJS deben
64
+ * quedar external: `noExternal` las pasa por el module runner ESM de Vite y
65
+ * falla con "module is not defined".
62
66
  * @see https://github.com/TanStack/router/issues/5291
63
67
  */
64
- function clearSsrExternal() {
68
+ function bundleSsrDependenciesForWorkerd() {
65
69
  return {
66
- name: 'ecomiq:clear-ssr-external',
70
+ name: 'ecomiq:ssr-no-external',
71
+ configEnvironment(name) {
72
+ if (name !== 'ssr') return
73
+ return {
74
+ resolve: {
75
+ external: [],
76
+ noExternal: true,
77
+ },
78
+ }
79
+ },
67
80
  configResolved(config) {
68
81
  const ssr = config.environments?.ssr
69
82
  if (ssr?.resolve) {
@@ -99,10 +112,11 @@ export function createViteConfig({ appDir, config, projectRoot, runtime }) {
99
112
 
100
113
  const resolvedRuntime = resolveDevRuntime({ config, runtime })
101
114
 
102
- const plugins = [
103
- clearSsrExternal(),
104
- tailwindcss(),
105
- ]
115
+ const plugins = [tailwindcss()]
116
+
117
+ if (resolvedRuntime === 'workerd') {
118
+ plugins.unshift(bundleSsrDependenciesForWorkerd())
119
+ }
106
120
 
107
121
  // workerd emula Cloudflare Workers (opt-in en `ecomiq dev`).
108
122
  // Default es Node; build/deploy siempre usan workerd.
@@ -143,6 +157,9 @@ export function createViteConfig({ appDir, config, projectRoot, runtime }) {
143
157
  plugins,
144
158
  build: {
145
159
  cssCodeSplit: false,
160
+ commonjsOptions: {
161
+ transformMixedEsModules: true,
162
+ },
146
163
  rollupOptions: {
147
164
  output: {
148
165
  assetFileNames: (assetInfo) => {