@ecomiq/storefront 2.0.0 → 2.0.2

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/README.md CHANGED
@@ -47,4 +47,11 @@ npx ecomiq build
47
47
  npx ecomiq deploy
48
48
  ```
49
49
 
50
+ Si actualizas el package y `.ecomiq` quedó de una versión anterior:
51
+
52
+ ```bash
53
+ npx ecomiq clean
54
+ npx ecomiq dev
55
+ ```
56
+
50
57
  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 CHANGED
@@ -82,7 +82,18 @@ export async function runCli(command = process.argv[2] ?? 'help') {
82
82
  const port = config.dev?.port ?? 3000
83
83
  banner(`dev · ${config.name ?? 'tienda'} ${c.dim('http://localhost:' + port)}`)
84
84
  const { createServer } = await import('vite')
85
- const server = await createServer({ ...createViteConfig({ appDir, config }), server: { port, fs: { allow: ['..'] } } })
85
+ const viteConfig = createViteConfig({ appDir, config, projectRoot: cwd })
86
+ const server = await createServer({
87
+ ...viteConfig,
88
+ server: {
89
+ ...viteConfig.server,
90
+ port,
91
+ fs: {
92
+ ...viteConfig.server?.fs,
93
+ allow: [...new Set([...(viteConfig.server?.fs?.allow ?? []), '..', cwd, frameworkRoot])],
94
+ },
95
+ },
96
+ })
86
97
  await server.listen()
87
98
  const pageWatcher = watchPages({
88
99
  cwd,
@@ -104,7 +115,7 @@ export async function runCli(command = process.argv[2] ?? 'help') {
104
115
  const config = await prepare()
105
116
  banner(`build · Cloudflare Workers`)
106
117
  const { createBuilder } = await import('vite')
107
- const builder = await createBuilder(createViteConfig({ appDir, config }))
118
+ const builder = await createBuilder(createViteConfig({ appDir, config, projectRoot: cwd }))
108
119
  await builder.buildApp()
109
120
  console.log(c.green('\n ✔ Build listo en .ecomiq/dist\n'))
110
121
  break
@@ -113,7 +124,7 @@ export async function runCli(command = process.argv[2] ?? 'help') {
113
124
  const config = await prepare()
114
125
  banner(`deploy · Cloudflare Workers`)
115
126
  const { createBuilder } = await import('vite')
116
- const builder = await createBuilder(createViteConfig({ appDir, config }))
127
+ const builder = await createBuilder(createViteConfig({ appDir, config, projectRoot: cwd }))
117
128
  await builder.buildApp()
118
129
  // @cloudflare/vite-plugin emite la config final del Worker junto al build
119
130
  const built = resolve(appDir, 'dist', 'server', 'wrangler.json')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomiq/storefront",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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",
package/src/codegen.mjs CHANGED
@@ -30,7 +30,9 @@ import {
30
30
  readlinkSync,
31
31
  unlinkSync,
32
32
  statSync,
33
+ realpathSync,
33
34
  } from 'node:fs'
35
+ import { createRequire } from 'node:module'
34
36
  import { resolve, join, relative, dirname, isAbsolute, sep } from 'node:path'
35
37
 
36
38
  const HEADER = '/* Generado por Ecomiq — no editar. */\n'
@@ -68,7 +70,7 @@ export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
68
70
  mkdirSync(routesDir, { recursive: true })
69
71
 
70
72
  // ── 1. dependencias internas resolubles desde .ecomiq (soporta npm, pnpm y monorepos) ──
71
- linkFrameworkDependencies(appDir, frameworkRoot)
73
+ linkFrameworkDependencies(appDir, frameworkRoot, cwd)
72
74
 
73
75
  // ── 2. package.json interno (type: module para que Vite resuelva bien) ──
74
76
  writeGeneratedFile(
@@ -539,18 +541,18 @@ function writeGeneratedFile(path, contents) {
539
541
  }
540
542
 
541
543
  /**
542
- * Crea .ecomiq/node_modules con symlinks a las dependencias del framework
543
- * y al propio framework. Así el código generado resuelve sus imports sin
544
- * importar el package manager ni el layout (hoisted, pnpm, monorepo).
544
+ * Crea `.ecomiq/node_modules` con symlinks a las dependencias del framework.
545
+ *
546
+ * Workerd (Cloudflare Vite plugin) resuelve desde la raíz `.ecomiq`, así que
547
+ * no basta con el hoisting del proyecto: hay que materializar los packages
548
+ * ahí. En installs npm/pnpm/yarn las deps suelen vivir en el node_modules del
549
+ * proyecto (o en el store de pnpm), no bajo `@ecomiq/storefront/node_modules`.
545
550
  */
546
- function linkFrameworkDependencies(appDir, frameworkRoot) {
547
- const source = join(frameworkRoot, 'node_modules')
551
+ function linkFrameworkDependencies(appDir, frameworkRoot, projectRoot = dirname(appDir)) {
548
552
  const target = join(appDir, 'node_modules')
549
553
  mkdirSync(target, { recursive: true })
550
554
 
551
- // el framework mismo, para imports '@ecomiq/storefront/...'
552
555
  linkInto(join(target, '@ecomiq'), 'storefront', frameworkRoot)
553
- // limpia el nombre legacy del paquete renombrado
554
556
  const legacyFramework = join(target, '@ecomiq', 'framework')
555
557
  try {
556
558
  if (lstatSync(legacyFramework).isSymbolicLink()) unlinkSync(legacyFramework)
@@ -558,12 +560,60 @@ function linkFrameworkDependencies(appDir, frameworkRoot) {
558
560
  /* no existía */
559
561
  }
560
562
 
561
- if (!existsSync(source)) return // instalación hoisted: se resuelve subiendo directorios
563
+ const resolveFrom = [
564
+ join(frameworkRoot, 'package.json'),
565
+ join(projectRoot, 'package.json'),
566
+ join(frameworkRoot, 'src', 'codegen.mjs'),
567
+ ]
568
+
569
+ const manifestPath = join(frameworkRoot, 'package.json')
570
+ let depNames = []
571
+ try {
572
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
573
+ depNames = [
574
+ ...Object.keys(manifest.dependencies ?? {}),
575
+ ...Object.keys(manifest.peerDependencies ?? {}),
576
+ ]
577
+ } catch {
578
+ depNames = []
579
+ }
580
+
581
+ // Transitive crítico que Vite/TanStack piden explícitamente en SSR.
582
+ for (const extra of ['@tanstack/react-store', '@tanstack/history', '@tanstack/router-core']) {
583
+ if (!depNames.includes(extra)) depNames.push(extra)
584
+ }
585
+
586
+ for (const name of depNames) {
587
+ const pkgDir = resolvePackageDir(name, resolveFrom)
588
+ if (!pkgDir) continue
589
+ linkPackage(target, name, pkgDir)
590
+ }
591
+
592
+ // Fallback: mirror de node_modules locales (monorepo / nested installs).
593
+ for (const source of [join(frameworkRoot, 'node_modules'), join(projectRoot, 'node_modules')]) {
594
+ linkNodeModulesTree(source, target)
595
+ }
596
+ }
597
+
598
+ function linkPackage(targetRoot, packageName, packageDir) {
599
+ if (packageName.startsWith('@')) {
600
+ const [scope, name] = packageName.split('/')
601
+ if (!scope || !name) return
602
+ linkInto(join(targetRoot, scope), name, packageDir)
603
+ return
604
+ }
605
+ linkInto(targetRoot, packageName, packageDir)
606
+ }
607
+
608
+ function linkNodeModulesTree(source, target) {
609
+ if (!existsSync(source)) return
562
610
  for (const entry of readdirSync(source)) {
563
611
  if (entry === '.bin' || entry.startsWith('.')) continue
564
612
  if (entry.startsWith('@')) {
565
- for (const scoped of readdirSync(join(source, entry))) {
566
- linkInto(join(target, entry), scoped, join(source, entry, scoped))
613
+ const scopeDir = join(source, entry)
614
+ if (!statSync(scopeDir).isDirectory()) continue
615
+ for (const scoped of readdirSync(scopeDir)) {
616
+ linkInto(join(target, entry), scoped, join(scopeDir, scoped))
567
617
  }
568
618
  } else {
569
619
  linkInto(target, entry, join(source, entry))
@@ -571,14 +621,69 @@ function linkFrameworkDependencies(appDir, frameworkRoot) {
571
621
  }
572
622
  }
573
623
 
624
+ /**
625
+ * Resuelve el directorio raíz de un package usando la resolución de Node
626
+ * (funciona con hoisting npm/yarn y el store de pnpm).
627
+ */
628
+ function resolvePackageDir(packageName, fromFiles) {
629
+ for (const fromFile of fromFiles) {
630
+ if (!existsSync(fromFile)) continue
631
+ try {
632
+ const require = createRequire(fromFile)
633
+ try {
634
+ return dirname(require.resolve(`${packageName}/package.json`))
635
+ } catch {
636
+ const entry = require.resolve(packageName)
637
+ return findPackageRoot(entry, packageName)
638
+ }
639
+ } catch {
640
+ /* probar siguiente origen */
641
+ }
642
+ }
643
+ return null
644
+ }
645
+
646
+ function findPackageRoot(startPath, packageName) {
647
+ let dir = dirname(startPath)
648
+ while (true) {
649
+ const pkgPath = join(dir, 'package.json')
650
+ if (existsSync(pkgPath)) {
651
+ try {
652
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
653
+ if (pkg.name === packageName) return dir
654
+ } catch {
655
+ /* ignore */
656
+ }
657
+ }
658
+ const parent = dirname(dir)
659
+ if (parent === dir) return null
660
+ dir = parent
661
+ }
662
+ }
663
+
574
664
  function linkInto(dir, name, targetPath) {
575
665
  mkdirSync(dir, { recursive: true })
576
666
  const linkPath = join(dir, name)
667
+
668
+ // Target absoluto y ya resuelto: los symlinks relativos se rompen cuando un
669
+ // directorio padre es a su vez un symlink (p. ej. /var → /private/var en macOS).
670
+ let destination
671
+ try {
672
+ destination = realpathSync(targetPath)
673
+ } catch {
674
+ destination = resolve(targetPath)
675
+ }
676
+
577
677
  try {
578
678
  const current = lstatSync(linkPath)
579
679
  if (current.isSymbolicLink()) {
580
- const resolved = resolve(dir, readlinkSync(linkPath))
581
- if (resolved === resolve(targetPath)) return
680
+ let resolved
681
+ try {
682
+ resolved = realpathSync(linkPath)
683
+ } catch {
684
+ resolved = resolve(dir, readlinkSync(linkPath))
685
+ }
686
+ if (resolved === destination) return
582
687
  unlinkSync(linkPath)
583
688
  } else if (current.isDirectory() || current.isFile()) {
584
689
  return
@@ -587,7 +692,7 @@ function linkInto(dir, name, targetPath) {
587
692
  /* no existe aún */
588
693
  }
589
694
  try {
590
- symlinkSync(targetPath, linkPath, 'junction')
695
+ symlinkSync(destination, linkPath, 'junction')
591
696
  } catch {
592
697
  /* FS sin symlinks: la resolución cae al hoisting */
593
698
  }
@@ -4,7 +4,7 @@
4
4
  * los plugins se importan aquí (contexto del framework) y se pasan como
5
5
  * instancias, así la resolución de módulos nunca depende del proyecto.
6
6
  */
7
- import { resolve } from 'node:path'
7
+ import { dirname, resolve } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
9
9
  import { tanstackStart } from '@tanstack/react-start/plugin/vite'
10
10
  import viteReact from '@vitejs/plugin-react'
@@ -15,6 +15,8 @@ const postcssBrowserStub = fileURLToPath(
15
15
  new URL('./runtime/postcss-browser-stub.mjs', import.meta.url),
16
16
  )
17
17
 
18
+ const frameworkRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
19
+
18
20
  function resolveSanitizeHtmlPostcss(source, importer) {
19
21
  if (source !== 'postcss') return null
20
22
  if (!importer?.includes('/sanitize-html/')) return null
@@ -50,11 +52,49 @@ function sanitizeHtmlClientBoundary() {
50
52
  }
51
53
  }
52
54
 
53
- export function createViteConfig({ appDir, config }) {
55
+ export function createViteConfig({ appDir, config, projectRoot }) {
56
+ const root = projectRoot ?? resolve(appDir, '..')
57
+ const allow = [
58
+ '..',
59
+ root,
60
+ frameworkRoot,
61
+ resolve(appDir, 'node_modules'),
62
+ resolve(root, 'node_modules'),
63
+ ]
64
+
54
65
  return {
55
66
  root: appDir,
56
67
  configFile: false,
57
68
  publicDir: resolve(appDir, '../public'),
69
+ resolve: {
70
+ dedupe: [
71
+ 'react',
72
+ 'react-dom',
73
+ '@tanstack/react-router',
74
+ '@tanstack/react-start',
75
+ '@tanstack/react-query',
76
+ ],
77
+ },
78
+ // Evita que Vite externalice TanStack como file:// absoluto (rompe workerd/pnpm).
79
+ ssr: {
80
+ noExternal: [
81
+ '@tanstack/react-start',
82
+ '@tanstack/react-router',
83
+ '@tanstack/react-query',
84
+ '@tanstack/router-core',
85
+ '@tanstack/history',
86
+ '@tanstack/react-store',
87
+ '@ecomiq/storefront',
88
+ '@ecomiq/core',
89
+ ],
90
+ },
91
+ optimizeDeps: {
92
+ include: [
93
+ '@tanstack/react-start',
94
+ '@tanstack/react-router',
95
+ '@tanstack/react-query',
96
+ ],
97
+ },
58
98
  plugins: [
59
99
  tailwindcss(),
60
100
  // Emula el runtime de Workers en dev y empaqueta para Cloudflare en build.
@@ -88,7 +128,7 @@ export function createViteConfig({ appDir, config }) {
88
128
  server: {
89
129
  fs: {
90
130
  // pages/, lib/ y public/ del usuario viven un nivel arriba de .ecomiq
91
- allow: ['..'],
131
+ allow,
92
132
  },
93
133
  },
94
134
  }