create-ecomiq 0.1.0

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 ADDED
@@ -0,0 +1,50 @@
1
+ # create-ecomiq
2
+
3
+ Scaffold interactivo de storefronts Ecomiq (estilo `create-vite` / `create-next-app`).
4
+
5
+ ```bash
6
+ npm create ecomiq@latest
7
+ # o
8
+ npx create-ecomiq@latest my-store
9
+ ```
10
+
11
+ ## Flujo
12
+
13
+ 1. Nombre del proyecto
14
+ 2. **Domain** — se valida con `GET /v1/settings` + `x-Store-Domain`
15
+ 3. **Template** — catálogo en R2 (`templates.json`); hoy solo `default`
16
+ 4. Package manager + install
17
+
18
+ ## Flags
19
+
20
+ ```bash
21
+ create-ecomiq my-store \
22
+ --domain marketplace \
23
+ --template default \
24
+ --pm pnpm \
25
+ --yes
26
+ ```
27
+
28
+ | Flag | Descripción |
29
+ | --- | --- |
30
+ | `--domain` | Domain de la tienda (obligatorio con `--yes`) |
31
+ | `--template` | Id del template (`default`) |
32
+ | `--pm` | `npm` \| `pnpm` \| `yarn` \| `bun` |
33
+ | `--yes` / `-y` | Sin prompts |
34
+ | `--skip-install` | Solo archivos |
35
+
36
+ ## Templates
37
+
38
+ Los zips viven en R2:
39
+
40
+ - `https://pub-2b5396b516d940dbb1088d750701749c.r2.dev/templates.json`
41
+ - `https://pub-2b5396b516d940dbb1088d750701749c.r2.dev/template-{id}.zip`
42
+
43
+ Tras el scaffold usa la CLI del framework:
44
+
45
+ ```bash
46
+ cd my-store
47
+ npm run dev # ecomiq dev
48
+ npm run build
49
+ npm run deploy
50
+ ```
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { runCreate } from '../src/index.mjs'
3
+
4
+ try {
5
+ await runCreate(process.argv.slice(2))
6
+ } catch (error) {
7
+ const message = error instanceof Error ? error.message : 'Error desconocido'
8
+ console.error(`\n ✖ ${message}\n`)
9
+ process.exitCode = 1
10
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "create-ecomiq",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold interactivo de storefronts Ecomiq (estilo create-vite)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Ecomiq",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ecomiq/ecomiq-storefront.git",
11
+ "directory": "packages/create-ecomiq"
12
+ },
13
+ "homepage": "https://github.com/ecomiq/ecomiq-storefront/tree/main/packages/create-ecomiq",
14
+ "bugs": {
15
+ "url": "https://github.com/ecomiq/ecomiq-storefront/issues"
16
+ },
17
+ "keywords": [
18
+ "ecomiq",
19
+ "create-ecomiq",
20
+ "scaffold",
21
+ "storefront",
22
+ "ecommerce"
23
+ ],
24
+ "bin": {
25
+ "create-ecomiq": "./bin/create-ecomiq.mjs"
26
+ },
27
+ "files": [
28
+ "bin",
29
+ "src",
30
+ "README.md"
31
+ ],
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "registry": "https://registry.npmjs.org/"
38
+ },
39
+ "dependencies": {
40
+ "@clack/prompts": "^0.11.0",
41
+ "adm-zip": "^0.6.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/adm-zip": "^0.5.8"
45
+ },
46
+ "scripts": {}
47
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,268 @@
1
+ /**
2
+ * CLI create-ecomiq — scaffold interactivo de storefronts.
3
+ *
4
+ * npx create-ecomiq@latest [dir]
5
+ * npm create ecomiq@latest
6
+ */
7
+ import * as p from '@clack/prompts'
8
+ import { basename, resolve } from 'node:path'
9
+ import { verifyDomain } from './verify-domain.mjs'
10
+ import { downloadTemplateZip, listTemplates } from './templates.mjs'
11
+ import {
12
+ detectPackageManager,
13
+ extractTemplateZip,
14
+ installDependencies,
15
+ resolveTargetDirectory,
16
+ slugifyDeployName,
17
+ writeProjectConfig,
18
+ } from './scaffold.mjs'
19
+
20
+ /**
21
+ * @param {string[]} argv
22
+ */
23
+ export function parseArgs(argv) {
24
+ /** @type {{
25
+ * dir?: string
26
+ * domain?: string
27
+ * template?: string
28
+ * pm?: 'npm' | 'pnpm' | 'yarn' | 'bun'
29
+ * yes: boolean
30
+ * skipInstall: boolean
31
+ * help: boolean
32
+ * }} */
33
+ const out = {
34
+ yes: false,
35
+ skipInstall: false,
36
+ help: false,
37
+ }
38
+
39
+ for (let i = 0; i < argv.length; i += 1) {
40
+ const arg = argv[i]
41
+ if (arg === '--help' || arg === '-h') {
42
+ out.help = true
43
+ continue
44
+ }
45
+ if (arg === '--yes' || arg === '-y') {
46
+ out.yes = true
47
+ continue
48
+ }
49
+ if (arg === '--skip-install') {
50
+ out.skipInstall = true
51
+ continue
52
+ }
53
+ if (arg === '--domain') {
54
+ out.domain = argv[++i]
55
+ continue
56
+ }
57
+ if (arg === '--template') {
58
+ out.template = argv[++i]
59
+ continue
60
+ }
61
+ if (arg === '--pm') {
62
+ const value = argv[++i]
63
+ if (value === 'npm' || value === 'pnpm' || value === 'yarn' || value === 'bun') {
64
+ out.pm = value
65
+ } else {
66
+ throw new Error(`Package manager no soportado: ${value}`)
67
+ }
68
+ continue
69
+ }
70
+ if (arg.startsWith('-')) {
71
+ throw new Error(`Flag desconocida: ${arg}`)
72
+ }
73
+ if (!out.dir) out.dir = arg
74
+ }
75
+
76
+ return out
77
+ }
78
+
79
+ function printHelp() {
80
+ console.log(`
81
+ Uso
82
+ create-ecomiq [dir] [opciones]
83
+
84
+ Opciones
85
+ --domain <slug> Domain de la tienda (x-Store-Domain)
86
+ --template <id> Template (default: default)
87
+ --pm <npm|pnpm|yarn|bun>
88
+ --yes, -y Sin prompts (requiere --domain)
89
+ --skip-install No ejecutar install
90
+ --help, -h
91
+
92
+ Ejemplos
93
+ npx create-ecomiq@latest my-store
94
+ npm create ecomiq@latest
95
+ create-ecomiq . --domain marketplace --template default --yes
96
+ `)
97
+ }
98
+
99
+ /**
100
+ * @param {string[]} argv
101
+ */
102
+ export async function runCreate(argv) {
103
+ const args = parseArgs(argv)
104
+ if (args.help) {
105
+ printHelp()
106
+ return
107
+ }
108
+
109
+ p.intro('◈ create-ecomiq')
110
+
111
+ if (args.yes && !args.domain) {
112
+ throw new Error('Con --yes debes pasar --domain')
113
+ }
114
+
115
+ let projectName = args.dir
116
+ if (!projectName && !args.yes) {
117
+ const answer = await p.text({
118
+ message: '¿Nombre del proyecto?',
119
+ placeholder: 'my-store',
120
+ defaultValue: 'my-store',
121
+ validate: (value) => {
122
+ if (!value.trim()) return 'El nombre es obligatorio'
123
+ },
124
+ })
125
+ if (p.isCancel(answer)) {
126
+ p.cancel('Cancelado')
127
+ process.exitCode = 1
128
+ return
129
+ }
130
+ projectName = answer.trim()
131
+ }
132
+ projectName = (projectName ?? 'my-store').trim() || 'my-store'
133
+
134
+ const targetDir = resolveTargetDirectory(
135
+ projectName === '.' ? process.cwd() : resolve(process.cwd(), projectName),
136
+ )
137
+ const folderName = basename(targetDir)
138
+
139
+ let domain = args.domain?.trim()
140
+ let storeName = domain ?? ''
141
+
142
+ if (!args.yes) {
143
+ while (true) {
144
+ const answer = await p.text({
145
+ message: 'Domain de la tienda (x-Store-Domain)',
146
+ placeholder: 'marketplace',
147
+ initialValue: domain ?? '',
148
+ validate: (value) => {
149
+ if (!value.trim()) return 'El domain es obligatorio'
150
+ },
151
+ })
152
+ if (p.isCancel(answer)) {
153
+ p.cancel('Cancelado')
154
+ process.exitCode = 1
155
+ return
156
+ }
157
+
158
+ const spinner = p.spinner()
159
+ spinner.start('Verificando domain en la API…')
160
+ const result = await verifyDomain(answer.trim())
161
+ if (!result.ok) {
162
+ spinner.stop(result.message)
163
+ p.log.error(result.message)
164
+ domain = answer.trim()
165
+ continue
166
+ }
167
+ spinner.stop(`Tienda: ${result.storeName}`)
168
+ domain = result.domain
169
+ storeName = result.storeName
170
+ break
171
+ }
172
+ } else {
173
+ const spinner = p.spinner()
174
+ spinner.start('Verificando domain en la API…')
175
+ const result = await verifyDomain(domain)
176
+ if (!result.ok) {
177
+ spinner.stop(result.message)
178
+ throw new Error(result.message)
179
+ }
180
+ spinner.stop(`Tienda: ${result.storeName}`)
181
+ domain = result.domain
182
+ storeName = result.storeName
183
+ }
184
+
185
+ const templates = await listTemplates()
186
+ let templateId = args.template?.trim() || 'default'
187
+
188
+ if (!args.yes) {
189
+ const options = templates.map((tpl) => ({
190
+ value: tpl.id,
191
+ label: tpl.name,
192
+ hint: tpl.description,
193
+ }))
194
+ const selected = await p.select({
195
+ message: 'Elige un template',
196
+ options: options.length ? options : [{ value: 'default', label: 'Default' }],
197
+ initialValue: templateId,
198
+ })
199
+ if (p.isCancel(selected)) {
200
+ p.cancel('Cancelado')
201
+ process.exitCode = 1
202
+ return
203
+ }
204
+ templateId = selected
205
+ } else if (!templates.some((tpl) => tpl.id === templateId) && templateId !== 'default') {
206
+ throw new Error(`Template desconocido: ${templateId}`)
207
+ }
208
+
209
+ let pm = args.pm ?? detectPackageManager()
210
+ if (!args.yes && !args.pm) {
211
+ const selected = await p.select({
212
+ message: 'Package manager',
213
+ options: [
214
+ { value: 'npm', label: 'npm' },
215
+ { value: 'pnpm', label: 'pnpm' },
216
+ { value: 'yarn', label: 'yarn' },
217
+ { value: 'bun', label: 'bun' },
218
+ ],
219
+ initialValue: pm,
220
+ })
221
+ if (p.isCancel(selected)) {
222
+ p.cancel('Cancelado')
223
+ process.exitCode = 1
224
+ return
225
+ }
226
+ pm = selected
227
+ }
228
+
229
+ const deployName = slugifyDeployName(folderName)
230
+ const downloadSpinner = p.spinner()
231
+ downloadSpinner.start(`Descargando template "${templateId}"…`)
232
+ const zipBytes = await downloadTemplateZip(templateId)
233
+ downloadSpinner.stop('Template descargado')
234
+
235
+ const extractSpinner = p.spinner()
236
+ extractSpinner.start('Extrayendo archivos…')
237
+ extractTemplateZip(zipBytes, targetDir, templateId)
238
+ writeProjectConfig(targetDir, { domain, deployName })
239
+ extractSpinner.stop('Proyecto listo')
240
+
241
+ if (!args.skipInstall) {
242
+ const installSpinner = p.spinner()
243
+ installSpinner.start(`Instalando con ${pm}…`)
244
+ try {
245
+ await installDependencies(targetDir, pm)
246
+ installSpinner.stop('Dependencias instaladas')
247
+ } catch (error) {
248
+ installSpinner.stop('Install falló')
249
+ throw error
250
+ }
251
+ }
252
+
253
+ const rel =
254
+ targetDir === process.cwd() ? '.' : projectName === '.' ? '.' : projectName
255
+
256
+ p.outro(
257
+ [
258
+ `Storefront para ${storeName} (${domain})`,
259
+ '',
260
+ ` cd ${rel}`,
261
+ args.skipInstall ? ` ${pm} install` : null,
262
+ ` ${pm === 'npm' ? 'npm run' : pm} dev`,
263
+ '',
264
+ ]
265
+ .filter(Boolean)
266
+ .join('\n'),
267
+ )
268
+ }
@@ -0,0 +1,194 @@
1
+ import { spawn } from 'node:child_process'
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from 'node:fs'
11
+ import { tmpdir } from 'node:os'
12
+ import { dirname, join, resolve } from 'node:path'
13
+ import AdmZip from 'adm-zip'
14
+
15
+ /**
16
+ * @param {string} name
17
+ */
18
+ export function slugifyDeployName(name) {
19
+ const slug = name
20
+ .trim()
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9]+/g, '-')
23
+ .replace(/^-+|-+$/g, '')
24
+ return slug || 'my-storefront'
25
+ }
26
+
27
+ /**
28
+ * @param {{ domain: string, deployName: string }} options
29
+ */
30
+ export function buildEcomiqConfig({ domain, deployName }) {
31
+ return `import { defineConfig } from '@ecomiq/storefront/config'
32
+
33
+ export default defineConfig({
34
+ // Identidad para x-Store-Domain: slug corto o dominio completo
35
+ // (marketplace | marketplace.ecomiq.app | marketplace.pe)
36
+ domain: ${JSON.stringify(domain)},
37
+
38
+ theme: {
39
+ primary: 'oklch(0.45 0.15 165)',
40
+ radius: '0.75rem',
41
+ },
42
+
43
+ // Portable: Node por defecto. Opt-in a workerd con runtime: 'workerd'.
44
+ dev: { port: 3000 },
45
+
46
+ deploy: {
47
+ target: 'cloudflare',
48
+ name: ${JSON.stringify(deployName)},
49
+ // routes: [{ pattern: 'tienda.example.com', custom_domain: true }],
50
+ },
51
+ })
52
+ `
53
+ }
54
+
55
+ /**
56
+ * @param {string} configSource
57
+ * @param {{ domain: string, deployName: string }} patch
58
+ */
59
+ export function patchEcomiqConfigSource(configSource, patch) {
60
+ let next = configSource
61
+ if (/domain:\s*['"][^'"]*['"]/.test(next)) {
62
+ next = next.replace(/domain:\s*['"][^'"]*['"]/, `domain: ${JSON.stringify(patch.domain)}`)
63
+ }
64
+ if (/name:\s*['"][^'"]*['"]/.test(next)) {
65
+ // Solo el name dentro de deploy (primera aparición tras deploy suele bastar;
66
+ // fallback: reescribir archivo completo si el patrón es ambiguo).
67
+ next = next.replace(
68
+ /(deploy:\s*\{[\s\S]*?name:\s*)['"][^'"]*['"]/,
69
+ `$1${JSON.stringify(patch.deployName)}`,
70
+ )
71
+ }
72
+ return next
73
+ }
74
+
75
+ /**
76
+ * Extrae un zip a destino (JS puro vía adm-zip; Windows/macOS/Linux sin CLI).
77
+ * Espera raíz `template-{id}/` o archivos sueltos.
78
+ * @param {Uint8Array | Buffer} zipBytes
79
+ * @param {string} destDir
80
+ * @param {string} templateId
81
+ */
82
+ export function extractTemplateZip(zipBytes, destDir, templateId) {
83
+ mkdirSync(destDir, { recursive: true })
84
+ const staging = mkdtempSync(join(tmpdir(), 'create-ecomiq-'))
85
+
86
+ try {
87
+ const zip = new AdmZip(Buffer.from(zipBytes))
88
+ zip.extractAllTo(staging, /* overwrite */ true)
89
+
90
+ const nested = join(staging, `template-${templateId}`)
91
+ const source = existsSync(nested) ? nested : findExtractedRoot(staging)
92
+ if (!source) {
93
+ throw new Error(`No se encontró el contenido del template "${templateId}" en el zip`)
94
+ }
95
+
96
+ copyTree(source, destDir)
97
+ } finally {
98
+ rmSync(staging, { recursive: true, force: true })
99
+ }
100
+ }
101
+
102
+ function findExtractedRoot(staging) {
103
+ const entries = readdirSync(staging, { withFileTypes: true })
104
+ const dirs = entries.filter((entry) => entry.isDirectory())
105
+ if (dirs.length === 1) return join(staging, dirs[0].name)
106
+ if (entries.some((entry) => entry.name === 'ecomiq.config.js' || entry.name === 'package.json')) {
107
+ return staging
108
+ }
109
+ return null
110
+ }
111
+
112
+ function copyTree(from, to) {
113
+ mkdirSync(to, { recursive: true })
114
+ for (const entry of readdirSync(from, { withFileTypes: true })) {
115
+ const src = join(from, entry.name)
116
+ const dest = join(to, entry.name)
117
+ if (entry.isDirectory()) {
118
+ copyTree(src, dest)
119
+ } else {
120
+ mkdirSync(dirname(dest), { recursive: true })
121
+ // rename across devices can fail; read/write is safer for tmp → project
122
+ writeFileSync(dest, readFileSync(src))
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * @param {string} destDir
129
+ * @param {{ domain: string, deployName: string }} options
130
+ */
131
+ export function writeProjectConfig(destDir, { domain, deployName }) {
132
+ const configPath = join(destDir, 'ecomiq.config.js')
133
+ if (existsSync(configPath)) {
134
+ const current = readFileSync(configPath, 'utf8')
135
+ writeFileSync(configPath, patchEcomiqConfigSource(current, { domain, deployName }))
136
+ return
137
+ }
138
+ writeFileSync(configPath, buildEcomiqConfig({ domain, deployName }))
139
+ }
140
+
141
+ /**
142
+ * @param {string} destDir
143
+ * @param {'npm' | 'pnpm' | 'yarn' | 'bun'} pm
144
+ */
145
+ export function installDependencies(destDir, pm) {
146
+ const args =
147
+ pm === 'yarn' ? ['install'] : pm === 'pnpm' ? ['install'] : pm === 'bun' ? ['install'] : ['install']
148
+
149
+ return new Promise((resolvePromise, reject) => {
150
+ const child = spawn(pm, args, {
151
+ cwd: destDir,
152
+ stdio: 'inherit',
153
+ shell: process.platform === 'win32',
154
+ env: { ...process.env, CI: process.env.CI ?? 'true' },
155
+ })
156
+ child.on('exit', (code) => {
157
+ if (code === 0) resolvePromise()
158
+ else reject(new Error(`${pm} install salió con código ${code}`))
159
+ })
160
+ child.on('error', reject)
161
+ })
162
+ }
163
+
164
+ /**
165
+ * Detecta package manager desde user-agent de npm_config o locks.
166
+ * @param {string} [cwd]
167
+ * @returns {'npm' | 'pnpm' | 'yarn' | 'bun'}
168
+ */
169
+ export function detectPackageManager(cwd = process.cwd()) {
170
+ const ua = process.env.npm_config_user_agent ?? ''
171
+ if (ua.includes('pnpm')) return 'pnpm'
172
+ if (ua.includes('yarn')) return 'yarn'
173
+ if (ua.includes('bun')) return 'bun'
174
+ if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
175
+ if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'
176
+ if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun'
177
+ return 'npm'
178
+ }
179
+
180
+ /**
181
+ * @param {string} targetPath
182
+ */
183
+ export function resolveTargetDirectory(targetPath) {
184
+ const resolved = resolve(targetPath)
185
+ if (existsSync(resolved)) {
186
+ const entries = readdirSync(resolved)
187
+ if (entries.length > 0) {
188
+ throw new Error(`El directorio ya existe y no está vacío: ${resolved}`)
189
+ }
190
+ } else {
191
+ mkdirSync(resolved, { recursive: true })
192
+ }
193
+ return resolved
194
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Catálogo y descarga de templates desde R2 público.
3
+ */
4
+ export const DEFAULT_TEMPLATES_BASE_URL =
5
+ 'https://pub-2b5396b516d940dbb1088d750701749c.r2.dev'
6
+
7
+ export const FALLBACK_TEMPLATES = [
8
+ {
9
+ id: 'default',
10
+ name: 'Default',
11
+ description: 'Storefront base: catálogo, carrito, checkout y cuenta',
12
+ },
13
+ ]
14
+
15
+ /**
16
+ * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }} [options]
17
+ * @returns {Promise<Array<{ id: string, name: string, description?: string }>>}
18
+ */
19
+ export async function listTemplates(options = {}) {
20
+ const baseUrl = (options.baseUrl ?? DEFAULT_TEMPLATES_BASE_URL).replace(/\/$/, '')
21
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch
22
+
23
+ if (typeof fetchImpl !== 'function') return [...FALLBACK_TEMPLATES]
24
+
25
+ try {
26
+ const response = await fetchImpl(`${baseUrl}/templates.json`, {
27
+ headers: { Accept: 'application/json' },
28
+ })
29
+ if (!response.ok) return [...FALLBACK_TEMPLATES]
30
+ const json = await response.json()
31
+ const items = Array.isArray(json)
32
+ ? json
33
+ : Array.isArray(json?.templates)
34
+ ? json.templates
35
+ : null
36
+ if (!items?.length) return [...FALLBACK_TEMPLATES]
37
+
38
+ return items
39
+ .filter((item) => item && typeof item.id === 'string' && item.id.trim())
40
+ .map((item) => ({
41
+ id: item.id.trim(),
42
+ name: typeof item.name === 'string' && item.name.trim() ? item.name.trim() : item.id,
43
+ description:
44
+ typeof item.description === 'string' ? item.description : undefined,
45
+ }))
46
+ } catch {
47
+ return [...FALLBACK_TEMPLATES]
48
+ }
49
+ }
50
+
51
+ /**
52
+ * @param {string} templateId
53
+ * @param {{ baseUrl?: string }} [options]
54
+ */
55
+ export function templateZipUrl(templateId, options = {}) {
56
+ const baseUrl = (options.baseUrl ?? DEFAULT_TEMPLATES_BASE_URL).replace(/\/$/, '')
57
+ return `${baseUrl}/template-${templateId}.zip`
58
+ }
59
+
60
+ /**
61
+ * @param {string} templateId
62
+ * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }} [options]
63
+ * @returns {Promise<Uint8Array>}
64
+ */
65
+ export async function downloadTemplateZip(templateId, options = {}) {
66
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch
67
+ if (typeof fetchImpl !== 'function') {
68
+ throw new Error('fetch no está disponible en este runtime')
69
+ }
70
+
71
+ const url = templateZipUrl(templateId, options)
72
+ const response = await fetchImpl(url)
73
+ if (!response.ok) {
74
+ throw new Error(
75
+ `No se pudo descargar el template "${templateId}" (${response.status}): ${url}`,
76
+ )
77
+ }
78
+
79
+ const buffer = new Uint8Array(await response.arrayBuffer())
80
+ if (buffer.byteLength === 0) {
81
+ throw new Error(`El zip del template "${templateId}" está vacío`)
82
+ }
83
+ return buffer
84
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Verifica que un domain exista en la API storefront (GET /v1/settings).
3
+ */
4
+ export const DEFAULT_SETTINGS_URL = 'https://storefront.ecomiq.pe/v1/settings'
5
+
6
+ /**
7
+ * @param {string} domain
8
+ * @param {{ settingsUrl?: string, fetchImpl?: typeof fetch }} [options]
9
+ * @returns {Promise<{ ok: true, storeName: string, domain: string, settings: unknown } | { ok: false, status: number, message: string }>}
10
+ */
11
+ export async function verifyDomain(domain, options = {}) {
12
+ const trimmed = domain.trim()
13
+ if (!trimmed) {
14
+ return { ok: false, status: 0, message: 'El domain es obligatorio' }
15
+ }
16
+
17
+ const settingsUrl = options.settingsUrl ?? DEFAULT_SETTINGS_URL
18
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch
19
+ if (typeof fetchImpl !== 'function') {
20
+ return { ok: false, status: 0, message: 'fetch no está disponible en este runtime' }
21
+ }
22
+
23
+ let response
24
+ try {
25
+ response = await fetchImpl(settingsUrl, {
26
+ method: 'GET',
27
+ headers: {
28
+ Accept: 'application/json',
29
+ 'x-Store-Domain': trimmed,
30
+ },
31
+ })
32
+ } catch (error) {
33
+ const detail = error instanceof Error ? error.message : 'Error de red'
34
+ return { ok: false, status: 0, message: `No se pudo contactar la API: ${detail}` }
35
+ }
36
+
37
+ if (!response.ok) {
38
+ return {
39
+ ok: false,
40
+ status: response.status,
41
+ message:
42
+ response.status === 404
43
+ ? `No existe una tienda con domain "${trimmed}"`
44
+ : `La API respondió ${response.status} para domain "${trimmed}"`,
45
+ }
46
+ }
47
+
48
+ let settings
49
+ try {
50
+ settings = await response.json()
51
+ } catch {
52
+ return { ok: false, status: response.status, message: 'La API no devolvió JSON válido' }
53
+ }
54
+
55
+ const storeName =
56
+ settings &&
57
+ typeof settings === 'object' &&
58
+ settings.store &&
59
+ typeof settings.store === 'object' &&
60
+ typeof settings.store.name === 'string'
61
+ ? settings.store.name
62
+ : trimmed
63
+
64
+ return { ok: true, storeName, domain: trimmed, settings }
65
+ }