create-ecomiq 0.1.1 → 1.0.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/LICENSE +21 -0
- package/README.md +9 -40
- package/dist/index.js +42 -0
- package/package.json +18 -26
- package/bin/create-ecomiq.mjs +0 -10
- package/src/index.mjs +0 -243
- package/src/scaffold.mjs +0 -251
- package/src/templates.mjs +0 -84
- package/src/verify-domain.mjs +0 -65
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ecomiq
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,48 +1,17 @@
|
|
|
1
1
|
# create-ecomiq
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Usa siempre el template **`scaffold`** (home, catálogo, PDP, carrito, login y registro).
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm create ecomiq@latest
|
|
9
|
-
# o
|
|
10
|
-
npx create-ecomiq@latest my-store
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Flujo
|
|
14
|
-
|
|
15
|
-
1. Nombre del proyecto
|
|
16
|
-
2. **Domain** — se valida con `GET /v1/settings` + `x-Store-Domain`
|
|
17
|
-
3. Package manager + install
|
|
18
|
-
|
|
19
|
-
## Flags
|
|
3
|
+
Crea el proyecto de tu tienda online de Ecomiq en un solo paso.
|
|
20
4
|
|
|
21
5
|
```bash
|
|
22
|
-
create
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
--yes
|
|
6
|
+
npm create ecomiq@latest mi-tienda
|
|
7
|
+
cd mi-tienda && npm install
|
|
8
|
+
npx ecomiq dev
|
|
26
9
|
```
|
|
27
10
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
| `--pm` | `npm` \| `pnpm` \| `yarn` \| `bun` |
|
|
32
|
-
| `--yes` / `-y` | Sin prompts |
|
|
33
|
-
| `--skip-install` | Solo archivos |
|
|
34
|
-
|
|
35
|
-
## Template
|
|
36
|
-
|
|
37
|
-
El zip se descarga desde R2:
|
|
11
|
+
Inicia sesión con el CLI [`ecomiq`](https://www.npmjs.com/package/ecomiq), eliges
|
|
12
|
+
la tienda en el navegador y baja su código a la carpeta. Sin carpeta, usa el
|
|
13
|
+
slug de la tienda. La carpeta debe estar vacía o no existir.
|
|
38
14
|
|
|
39
|
-
|
|
15
|
+
Necesita Node.js 22.22 o superior.
|
|
40
16
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
cd my-store
|
|
45
|
-
npm run dev # ecomiq dev
|
|
46
|
-
npm run build
|
|
47
|
-
npm run deploy
|
|
48
|
-
```
|
|
17
|
+
Guía completa: https://docs.ecomiq.pe/es/docs/guides/storefront/themes
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
// src/create.ts
|
|
6
|
+
const HELP = `Uso: npm create ecomiq@latest [carpeta]
|
|
7
|
+
|
|
8
|
+
Inicia sesión, eliges tu tienda en el navegador y baja su código a una
|
|
9
|
+
carpeta nueva. Sin carpeta, usa el slug de la tienda.
|
|
10
|
+
|
|
11
|
+
--local Usa el backend local de desarrollo
|
|
12
|
+
`;
|
|
13
|
+
function cliCommands(argv) {
|
|
14
|
+
const flags = argv.filter((arg) => arg === "--local");
|
|
15
|
+
const [folder, ...extra] = argv.filter((arg) => arg !== "--local");
|
|
16
|
+
if (folder?.startsWith("-") || extra.length > 0) throw new Error(`Argumentos no válidos: ${argv.join(" ")}`);
|
|
17
|
+
const init = ["init"];
|
|
18
|
+
if (folder !== void 0) init.push(folder);
|
|
19
|
+
return [["login", ...flags], [...init, ...flags]];
|
|
20
|
+
}
|
|
21
|
+
function create(argv, run) {
|
|
22
|
+
for (const args of cliCommands(argv)) {
|
|
23
|
+
const status = run(args);
|
|
24
|
+
if (status !== 0) return status;
|
|
25
|
+
}
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
// src/index.ts
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const manifest = require.resolve("ecomiq/package.json");
|
|
31
|
+
const { bin } = require(manifest);
|
|
32
|
+
const cli = join(dirname(manifest), bin.ecomiq);
|
|
33
|
+
const argv = process.argv.slice(2);
|
|
34
|
+
if (argv.includes("--help") || argv.includes("-h")) console.log(HELP);
|
|
35
|
+
else try {
|
|
36
|
+
process.exitCode = create(argv, (args) => spawnSync(process.execPath, [cli, ...args], { stdio: "inherit" }).status ?? 1);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(`✗ ${error instanceof Error ? error.message : String(error)}`);
|
|
39
|
+
console.log(HELP);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
42
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,47 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ecomiq",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Crea el proyecto de tu tienda online de Ecomiq con npm create ecomiq",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Ecomiq",
|
|
8
|
+
"homepage": "https://docs.ecomiq.pe/es/docs/guides/storefront/themes",
|
|
8
9
|
"repository": {
|
|
9
10
|
"type": "git",
|
|
10
11
|
"url": "git+https://github.com/ecomiq/ecomiq-storefront.git",
|
|
11
12
|
"directory": "packages/create-ecomiq"
|
|
12
13
|
},
|
|
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
14
|
"bin": {
|
|
25
|
-
"create-ecomiq": "./
|
|
15
|
+
"create-ecomiq": "./dist/index.js"
|
|
26
16
|
},
|
|
27
17
|
"files": [
|
|
28
|
-
"
|
|
29
|
-
"src",
|
|
30
|
-
"README.md"
|
|
18
|
+
"dist"
|
|
31
19
|
],
|
|
32
20
|
"engines": {
|
|
33
|
-
"node": ">=
|
|
34
|
-
},
|
|
35
|
-
"publishConfig": {
|
|
36
|
-
"access": "public",
|
|
37
|
-
"registry": "https://registry.npmjs.org/"
|
|
21
|
+
"node": ">=22.22.0"
|
|
38
22
|
},
|
|
39
23
|
"dependencies": {
|
|
40
|
-
"
|
|
41
|
-
"adm-zip": "^0.6.0"
|
|
24
|
+
"ecomiq": "^0.1.0"
|
|
42
25
|
},
|
|
43
26
|
"devDependencies": {
|
|
44
|
-
"@types/
|
|
27
|
+
"@types/node": "^26.1.1",
|
|
28
|
+
"tsdown": "^0.23.0",
|
|
29
|
+
"typescript": "^7.0.2"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"registry": "https://registry.npmjs.org"
|
|
45
34
|
},
|
|
46
|
-
"scripts": {
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsdown",
|
|
37
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
38
|
+
}
|
|
47
39
|
}
|
package/bin/create-ecomiq.mjs
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
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/src/index.mjs
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
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 } from './templates.mjs'
|
|
11
|
-
import {
|
|
12
|
-
detectPackageManager,
|
|
13
|
-
extractTemplateZip,
|
|
14
|
-
installDependencies,
|
|
15
|
-
resolveTargetDirectory,
|
|
16
|
-
slugifyDeployName,
|
|
17
|
-
writeProjectConfig,
|
|
18
|
-
} from './scaffold.mjs'
|
|
19
|
-
|
|
20
|
-
/** Único template que descarga create-ecomiq (CI/CD puede publicar varios). */
|
|
21
|
-
export const CREATE_TEMPLATE_ID = 'scaffold'
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {string[]} argv
|
|
25
|
-
*/
|
|
26
|
-
export function parseArgs(argv) {
|
|
27
|
-
/** @type {{
|
|
28
|
-
* dir?: string
|
|
29
|
-
* domain?: string
|
|
30
|
-
* pm?: 'npm' | 'pnpm' | 'yarn' | 'bun'
|
|
31
|
-
* yes: boolean
|
|
32
|
-
* skipInstall: boolean
|
|
33
|
-
* help: boolean
|
|
34
|
-
* }} */
|
|
35
|
-
const out = {
|
|
36
|
-
yes: false,
|
|
37
|
-
skipInstall: false,
|
|
38
|
-
help: false,
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
42
|
-
const arg = argv[i]
|
|
43
|
-
if (arg === '--help' || arg === '-h') {
|
|
44
|
-
out.help = true
|
|
45
|
-
continue
|
|
46
|
-
}
|
|
47
|
-
if (arg === '--yes' || arg === '-y') {
|
|
48
|
-
out.yes = true
|
|
49
|
-
continue
|
|
50
|
-
}
|
|
51
|
-
if (arg === '--skip-install') {
|
|
52
|
-
out.skipInstall = true
|
|
53
|
-
continue
|
|
54
|
-
}
|
|
55
|
-
if (arg === '--domain') {
|
|
56
|
-
out.domain = argv[++i]
|
|
57
|
-
continue
|
|
58
|
-
}
|
|
59
|
-
if (arg === '--pm') {
|
|
60
|
-
const value = argv[++i]
|
|
61
|
-
if (value === 'npm' || value === 'pnpm' || value === 'yarn' || value === 'bun') {
|
|
62
|
-
out.pm = value
|
|
63
|
-
} else {
|
|
64
|
-
throw new Error(`Package manager no soportado: ${value}`)
|
|
65
|
-
}
|
|
66
|
-
continue
|
|
67
|
-
}
|
|
68
|
-
if (arg.startsWith('-')) {
|
|
69
|
-
throw new Error(`Flag desconocida: ${arg}`)
|
|
70
|
-
}
|
|
71
|
-
if (!out.dir) out.dir = arg
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return out
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function printHelp() {
|
|
78
|
-
console.log(`
|
|
79
|
-
Uso
|
|
80
|
-
create-ecomiq [dir] [opciones]
|
|
81
|
-
|
|
82
|
-
Opciones
|
|
83
|
-
--domain <slug> Domain de la tienda (x-Store-Domain)
|
|
84
|
-
--pm <npm|pnpm|yarn|bun>
|
|
85
|
-
--yes, -y Sin prompts (requiere --domain)
|
|
86
|
-
--skip-install No ejecutar install
|
|
87
|
-
--help, -h
|
|
88
|
-
|
|
89
|
-
Ejemplos
|
|
90
|
-
npx create-ecomiq@latest my-store
|
|
91
|
-
npm create ecomiq@latest
|
|
92
|
-
create-ecomiq . --domain marketplace --yes
|
|
93
|
-
`)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* @param {string[]} argv
|
|
98
|
-
*/
|
|
99
|
-
export async function runCreate(argv) {
|
|
100
|
-
const args = parseArgs(argv)
|
|
101
|
-
if (args.help) {
|
|
102
|
-
printHelp()
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
p.intro('◈ create-ecomiq')
|
|
107
|
-
|
|
108
|
-
if (args.yes && !args.domain) {
|
|
109
|
-
throw new Error('Con --yes debes pasar --domain')
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
let projectName = args.dir
|
|
113
|
-
if (!projectName && !args.yes) {
|
|
114
|
-
const answer = await p.text({
|
|
115
|
-
message: '¿Nombre del proyecto?',
|
|
116
|
-
placeholder: 'my-store',
|
|
117
|
-
defaultValue: 'my-store',
|
|
118
|
-
validate: (value) => {
|
|
119
|
-
if (!value.trim()) return 'El nombre es obligatorio'
|
|
120
|
-
},
|
|
121
|
-
})
|
|
122
|
-
if (p.isCancel(answer)) {
|
|
123
|
-
p.cancel('Cancelado')
|
|
124
|
-
process.exitCode = 1
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
projectName = answer.trim()
|
|
128
|
-
}
|
|
129
|
-
projectName = (projectName ?? 'my-store').trim() || 'my-store'
|
|
130
|
-
|
|
131
|
-
const targetDir = resolveTargetDirectory(
|
|
132
|
-
projectName === '.' ? process.cwd() : resolve(process.cwd(), projectName),
|
|
133
|
-
)
|
|
134
|
-
const folderName = basename(targetDir)
|
|
135
|
-
|
|
136
|
-
let domain = args.domain?.trim()
|
|
137
|
-
let storeName = domain ?? ''
|
|
138
|
-
|
|
139
|
-
if (!args.yes) {
|
|
140
|
-
while (true) {
|
|
141
|
-
const answer = await p.text({
|
|
142
|
-
message: 'Domain de la tienda (x-Store-Domain)',
|
|
143
|
-
placeholder: 'marketplace',
|
|
144
|
-
initialValue: domain ?? '',
|
|
145
|
-
validate: (value) => {
|
|
146
|
-
if (!value.trim()) return 'El domain es obligatorio'
|
|
147
|
-
},
|
|
148
|
-
})
|
|
149
|
-
if (p.isCancel(answer)) {
|
|
150
|
-
p.cancel('Cancelado')
|
|
151
|
-
process.exitCode = 1
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const spinner = p.spinner()
|
|
156
|
-
spinner.start('Verificando domain en la API…')
|
|
157
|
-
const result = await verifyDomain(answer.trim())
|
|
158
|
-
if (!result.ok) {
|
|
159
|
-
spinner.stop(result.message)
|
|
160
|
-
p.log.error(result.message)
|
|
161
|
-
domain = answer.trim()
|
|
162
|
-
continue
|
|
163
|
-
}
|
|
164
|
-
spinner.stop(`Tienda: ${result.storeName}`)
|
|
165
|
-
domain = result.domain
|
|
166
|
-
storeName = result.storeName
|
|
167
|
-
break
|
|
168
|
-
}
|
|
169
|
-
} else {
|
|
170
|
-
const spinner = p.spinner()
|
|
171
|
-
spinner.start('Verificando domain en la API…')
|
|
172
|
-
const result = await verifyDomain(domain)
|
|
173
|
-
if (!result.ok) {
|
|
174
|
-
spinner.stop(result.message)
|
|
175
|
-
throw new Error(result.message)
|
|
176
|
-
}
|
|
177
|
-
spinner.stop(`Tienda: ${result.storeName}`)
|
|
178
|
-
domain = result.domain
|
|
179
|
-
storeName = result.storeName
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const templateId = CREATE_TEMPLATE_ID
|
|
183
|
-
|
|
184
|
-
let pm = args.pm ?? detectPackageManager()
|
|
185
|
-
if (!args.yes && !args.pm) {
|
|
186
|
-
const selected = await p.select({
|
|
187
|
-
message: 'Package manager',
|
|
188
|
-
options: [
|
|
189
|
-
{ value: 'npm', label: 'npm' },
|
|
190
|
-
{ value: 'pnpm', label: 'pnpm' },
|
|
191
|
-
{ value: 'yarn', label: 'yarn' },
|
|
192
|
-
{ value: 'bun', label: 'bun' },
|
|
193
|
-
],
|
|
194
|
-
initialValue: pm,
|
|
195
|
-
})
|
|
196
|
-
if (p.isCancel(selected)) {
|
|
197
|
-
p.cancel('Cancelado')
|
|
198
|
-
process.exitCode = 1
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
pm = selected
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const deployName = slugifyDeployName(folderName)
|
|
205
|
-
const downloadSpinner = p.spinner()
|
|
206
|
-
downloadSpinner.start('Descargando template…')
|
|
207
|
-
const zipBytes = await downloadTemplateZip(templateId)
|
|
208
|
-
downloadSpinner.stop('Template descargado')
|
|
209
|
-
|
|
210
|
-
const extractSpinner = p.spinner()
|
|
211
|
-
extractSpinner.start('Extrayendo archivos…')
|
|
212
|
-
extractTemplateZip(zipBytes, targetDir, templateId)
|
|
213
|
-
writeProjectConfig(targetDir, { domain, deployName })
|
|
214
|
-
extractSpinner.stop('Proyecto listo')
|
|
215
|
-
|
|
216
|
-
if (!args.skipInstall) {
|
|
217
|
-
const installSpinner = p.spinner()
|
|
218
|
-
installSpinner.start(`Instalando con ${pm}…`)
|
|
219
|
-
try {
|
|
220
|
-
await installDependencies(targetDir, pm)
|
|
221
|
-
installSpinner.stop('Dependencias instaladas')
|
|
222
|
-
} catch (error) {
|
|
223
|
-
installSpinner.stop('Install falló')
|
|
224
|
-
throw error
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const rel =
|
|
229
|
-
targetDir === process.cwd() ? '.' : projectName === '.' ? '.' : projectName
|
|
230
|
-
|
|
231
|
-
p.outro(
|
|
232
|
-
[
|
|
233
|
-
`Storefront para ${storeName} (${domain})`,
|
|
234
|
-
'',
|
|
235
|
-
` cd ${rel}`,
|
|
236
|
-
args.skipInstall ? ` ${pm} install` : null,
|
|
237
|
-
` ${pm === 'npm' ? 'npm run' : pm} dev`,
|
|
238
|
-
'',
|
|
239
|
-
]
|
|
240
|
-
.filter(Boolean)
|
|
241
|
-
.join('\n'),
|
|
242
|
-
)
|
|
243
|
-
}
|
package/src/scaffold.mjs
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
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
|
-
* Dependencias nativas / postinstall del stack @ecomiq/storefront (Vite + workerd).
|
|
143
|
-
* pnpm 10+ bloquea sus scripts hasta aprobarlos (`ERR_PNPM_IGNORED_BUILDS`).
|
|
144
|
-
*/
|
|
145
|
-
export const PNPM_ALLOW_BUILDS = Object.freeze(['esbuild', 'workerd'])
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Escribe/actualiza `pnpm-workspace.yaml` para permitir builds de deps críticas.
|
|
149
|
-
* Compatible con pnpm 10 (`onlyBuiltDependencies`) y 11 (`allowBuilds`).
|
|
150
|
-
* @param {string} destDir
|
|
151
|
-
* @param {readonly string[]} [packages]
|
|
152
|
-
*/
|
|
153
|
-
export function ensurePnpmAllowBuilds(destDir, packages = PNPM_ALLOW_BUILDS) {
|
|
154
|
-
const path = join(destDir, 'pnpm-workspace.yaml')
|
|
155
|
-
const existing = existsSync(path) ? readFileSync(path, 'utf8') : ''
|
|
156
|
-
const lines = []
|
|
157
|
-
|
|
158
|
-
if (existing.trim()) {
|
|
159
|
-
// Conserva contenido previo (packages:, etc.) y reescribe bloques de builds.
|
|
160
|
-
const withoutBuildBlocks = existing
|
|
161
|
-
.replace(/\n?allowBuilds:\n(?:[ \t]+.+\n)*/g, '\n')
|
|
162
|
-
.replace(/\n?onlyBuiltDependencies:\n(?:[ \t]+-.+\n)*/g, '\n')
|
|
163
|
-
.trimEnd()
|
|
164
|
-
if (withoutBuildBlocks.trim()) lines.push(withoutBuildBlocks, '')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
lines.push('allowBuilds:')
|
|
168
|
-
for (const name of packages) {
|
|
169
|
-
lines.push(` ${name}: true`)
|
|
170
|
-
}
|
|
171
|
-
lines.push('')
|
|
172
|
-
lines.push('onlyBuiltDependencies:')
|
|
173
|
-
for (const name of packages) {
|
|
174
|
-
lines.push(` - ${name}`)
|
|
175
|
-
}
|
|
176
|
-
lines.push('')
|
|
177
|
-
|
|
178
|
-
writeFileSync(path, `${lines.join('\n')}\n`)
|
|
179
|
-
return path
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* @param {string} command
|
|
184
|
-
* @param {string[]} args
|
|
185
|
-
* @param {string} cwd
|
|
186
|
-
* @param {NodeJS.ProcessEnv} [env]
|
|
187
|
-
*/
|
|
188
|
-
function runCommand(command, args, cwd, env = process.env) {
|
|
189
|
-
return new Promise((resolvePromise, reject) => {
|
|
190
|
-
const child = spawn(command, args, {
|
|
191
|
-
cwd,
|
|
192
|
-
stdio: 'inherit',
|
|
193
|
-
shell: process.platform === 'win32',
|
|
194
|
-
env,
|
|
195
|
-
})
|
|
196
|
-
child.on('exit', (code) => {
|
|
197
|
-
if (code === 0) resolvePromise()
|
|
198
|
-
else reject(new Error(`${command} ${args.join(' ')} salió con código ${code}`))
|
|
199
|
-
})
|
|
200
|
-
child.on('error', reject)
|
|
201
|
-
})
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* @param {string} destDir
|
|
206
|
-
* @param {'npm' | 'pnpm' | 'yarn' | 'bun'} pm
|
|
207
|
-
*/
|
|
208
|
-
export async function installDependencies(destDir, pm) {
|
|
209
|
-
// No forzar CI=true: en pnpm activa frozen-lockfile / strict builds y rompe scaffolds locales.
|
|
210
|
-
const env = { ...process.env }
|
|
211
|
-
|
|
212
|
-
if (pm === 'pnpm') {
|
|
213
|
-
ensurePnpmAllowBuilds(destDir)
|
|
214
|
-
await runCommand('pnpm', ['install'], destDir, env)
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
await runCommand(pm, ['install'], destDir, env)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Detecta package manager desde user-agent de npm_config o locks.
|
|
223
|
-
* @param {string} [cwd]
|
|
224
|
-
* @returns {'npm' | 'pnpm' | 'yarn' | 'bun'}
|
|
225
|
-
*/
|
|
226
|
-
export function detectPackageManager(cwd = process.cwd()) {
|
|
227
|
-
const ua = process.env.npm_config_user_agent ?? ''
|
|
228
|
-
if (ua.includes('pnpm')) return 'pnpm'
|
|
229
|
-
if (ua.includes('yarn')) return 'yarn'
|
|
230
|
-
if (ua.includes('bun')) return 'bun'
|
|
231
|
-
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
232
|
-
if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'
|
|
233
|
-
if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun'
|
|
234
|
-
return 'npm'
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* @param {string} targetPath
|
|
239
|
-
*/
|
|
240
|
-
export function resolveTargetDirectory(targetPath) {
|
|
241
|
-
const resolved = resolve(targetPath)
|
|
242
|
-
if (existsSync(resolved)) {
|
|
243
|
-
const entries = readdirSync(resolved)
|
|
244
|
-
if (entries.length > 0) {
|
|
245
|
-
throw new Error(`El directorio ya existe y no está vacío: ${resolved}`)
|
|
246
|
-
}
|
|
247
|
-
} else {
|
|
248
|
-
mkdirSync(resolved, { recursive: true })
|
|
249
|
-
}
|
|
250
|
-
return resolved
|
|
251
|
-
}
|
package/src/templates.mjs
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
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: 'scaffold',
|
|
10
|
-
name: 'Scaffold',
|
|
11
|
-
description: 'Mínimo: home, catálogo, PDP, carrito, login y registro',
|
|
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
|
-
}
|
package/src/verify-domain.mjs
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
}
|