rook-cli 1.0.3 → 1.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/package.json +2 -2
- package/src/app.js +21 -0
- package/src/commands/GenerateCommand.js +114 -0
- package/src/services/ScaffoldService.js +131 -0
- package/src/templates/block.liquid.txt +52 -0
- package/src/templates/controller.js.txt +27 -0
- package/src/templates/section.liquid.txt +92 -0
- package/src/templates/snippet.liquid.txt +21 -0
- package/src/ui/PromptUI.js +54 -1
- package/src/utils/stringUtils.js +86 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rook-cli",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "CLI para instalar componentes Shopify
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "CLI para instalar componentes Shopify",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/chesslabdev/rook-cli.git"
|
package/src/app.js
CHANGED
|
@@ -20,6 +20,8 @@ import { FileMapper } from './filesystem/FileMapper.js';
|
|
|
20
20
|
import { ConflictResolver } from './filesystem/ConflictResolver.js';
|
|
21
21
|
import { AddCommand } from './commands/AddCommand.js';
|
|
22
22
|
import { ConfigCommand } from './commands/ConfigCommand.js';
|
|
23
|
+
import { GenerateCommand } from './commands/GenerateCommand.js';
|
|
24
|
+
import { ScaffoldService } from './services/ScaffoldService.js';
|
|
23
25
|
import { CLI_NAME } from './config/constants.js';
|
|
24
26
|
|
|
25
27
|
export class App {
|
|
@@ -50,6 +52,15 @@ export class App {
|
|
|
50
52
|
this.logger,
|
|
51
53
|
this.tokenManager
|
|
52
54
|
);
|
|
55
|
+
|
|
56
|
+
this.scaffoldService = new ScaffoldService(this.logger);
|
|
57
|
+
|
|
58
|
+
this.generateCommand = new GenerateCommand(
|
|
59
|
+
this.logger,
|
|
60
|
+
this.promptUI,
|
|
61
|
+
this.scaffoldService,
|
|
62
|
+
this.conflictResolver
|
|
63
|
+
);
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
/**
|
|
@@ -80,6 +91,16 @@ export class App {
|
|
|
80
91
|
await this.configCommand.executar();
|
|
81
92
|
});
|
|
82
93
|
|
|
94
|
+
// Comando: rook generate [nome-do-componente]
|
|
95
|
+
this.programa
|
|
96
|
+
.command('generate [nome]')
|
|
97
|
+
.alias('g')
|
|
98
|
+
.description('Gera scaffold de componentes Shopify (sections, blocks, snippets, assets)')
|
|
99
|
+
.action(async (nome) => {
|
|
100
|
+
this.logger.banner();
|
|
101
|
+
await this.generateCommand.executar(nome);
|
|
102
|
+
});
|
|
103
|
+
|
|
83
104
|
// Comando padrão (sem argumentos) — abre o menu interativo
|
|
84
105
|
this.programa
|
|
85
106
|
.action(async () => {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenerateCommand — Comando "generate" do CLI.
|
|
3
|
+
*
|
|
4
|
+
* Orquestra o fluxo de scaffold de componentes:
|
|
5
|
+
* 1. Recebe o nome do componente (argumento ou prompt)
|
|
6
|
+
* 2. Exibe menu multi-select para tipos de arquivo
|
|
7
|
+
* 3. Normaliza o nome para os formatos corretos (kebab, Pascal, snake)
|
|
8
|
+
* 4. Delega a geração ao ScaffoldService
|
|
9
|
+
* 5. Grava os arquivos via FileSystemManager (fs-extra)
|
|
10
|
+
* 6. Trata conflitos de sobrescrita
|
|
11
|
+
*
|
|
12
|
+
* Princípio: Inversão de Dependência (DIP) — todas as dependências são injetadas
|
|
13
|
+
* Princípio: Responsabilidade Única (SRP) — orquestra apenas o fluxo do comando "generate"
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import fs from 'fs-extra';
|
|
18
|
+
import { generateNames } from '../utils/stringUtils.js';
|
|
19
|
+
|
|
20
|
+
export class GenerateCommand {
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import('../utils/logger.js').Logger} logger
|
|
24
|
+
* @param {import('../ui/PromptUI.js').PromptUI} promptUI
|
|
25
|
+
* @param {import('../services/ScaffoldService.js').ScaffoldService} scaffoldService
|
|
26
|
+
* @param {import('../filesystem/ConflictResolver.js').ConflictResolver} conflictResolver
|
|
27
|
+
*/
|
|
28
|
+
constructor(logger, promptUI, scaffoldService, conflictResolver) {
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
this.promptUI = promptUI;
|
|
31
|
+
this.scaffoldService = scaffoldService;
|
|
32
|
+
this.conflictResolver = conflictResolver;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Executa o fluxo principal do comando "generate".
|
|
37
|
+
*
|
|
38
|
+
* @param {string} [nomeComponente] - Nome passado como argumento CLI (opcional)
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
*/
|
|
41
|
+
async executar(nomeComponente) {
|
|
42
|
+
try {
|
|
43
|
+
// 1. Se o nome não veio como argumento, pergunta ao usuário
|
|
44
|
+
const nome = nomeComponente || await this.promptUI.perguntarNomeComponente();
|
|
45
|
+
|
|
46
|
+
if (!nome || nome.trim() === '') {
|
|
47
|
+
this.logger.erro('Nome do componente é obrigatório.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Normaliza os nomes
|
|
52
|
+
const names = generateNames(nome);
|
|
53
|
+
|
|
54
|
+
this.logger.destaque(`\n🔧 Gerando componente: ${names.PascalName} (${names.kebabName})\n`);
|
|
55
|
+
|
|
56
|
+
// 3. Pergunta quais tipos gerar
|
|
57
|
+
const tiposSelecionados = await this.promptUI.selecionarTiposGerador();
|
|
58
|
+
|
|
59
|
+
if (tiposSelecionados.length === 0) {
|
|
60
|
+
this.logger.aviso('Nenhum tipo selecionado. Operação cancelada.');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 4. Gera os arquivos via ScaffoldService
|
|
65
|
+
const arquivosGerados = await this.scaffoldService.generateMultiple(tiposSelecionados, names);
|
|
66
|
+
|
|
67
|
+
// 5. Grava os arquivos no disco
|
|
68
|
+
const diretorioAtual = process.cwd();
|
|
69
|
+
let totalGravados = 0;
|
|
70
|
+
|
|
71
|
+
// Reseta decisões anteriores do ConflictResolver
|
|
72
|
+
this.conflictResolver.resetar();
|
|
73
|
+
|
|
74
|
+
for (const arquivo of arquivosGerados) {
|
|
75
|
+
const destDir = path.join(diretorioAtual, arquivo.outputDir);
|
|
76
|
+
const destPath = path.join(destDir, arquivo.fileName);
|
|
77
|
+
const caminhoRelativo = path.join(arquivo.outputDir, arquivo.fileName);
|
|
78
|
+
|
|
79
|
+
// RF03: Verifica se já existe
|
|
80
|
+
if (await fs.pathExists(destPath)) {
|
|
81
|
+
const sobrescrever = await this.conflictResolver.resolver(caminhoRelativo);
|
|
82
|
+
|
|
83
|
+
if (!sobrescrever) {
|
|
84
|
+
this.logger.aviso(` ⏩ Pulando: ${caminhoRelativo}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Cria o diretório se não existe e grava o arquivo
|
|
90
|
+
await fs.ensureDir(destDir);
|
|
91
|
+
await fs.writeFile(destPath, arquivo.content, 'utf-8');
|
|
92
|
+
|
|
93
|
+
this.logger.sucesso(` ✅ ${caminhoRelativo}`);
|
|
94
|
+
totalGravados++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 6. Resumo final
|
|
98
|
+
console.log('');
|
|
99
|
+
this.logger.destaque('═══════════════════════════════════════════');
|
|
100
|
+
this.logger.sucesso(`🎉 ${totalGravados}/${arquivosGerados.length} arquivo(s) gerado(s) com sucesso!`);
|
|
101
|
+
this.logger.sutil(`Componente: ${names.PascalName} (${names.kebabName})`);
|
|
102
|
+
this.logger.destaque('═══════════════════════════════════════════');
|
|
103
|
+
console.log('');
|
|
104
|
+
|
|
105
|
+
} catch (erro) {
|
|
106
|
+
// Trata cancelamento do usuário (Ctrl+C)
|
|
107
|
+
if (erro.name === 'ExitPromptError') {
|
|
108
|
+
this.logger.aviso('Operação cancelada pelo usuário.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.logger.erro(`Erro durante a geração: ${erro.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScaffoldService — Motor de geração de scaffold via templates.
|
|
3
|
+
*
|
|
4
|
+
* Responsável por:
|
|
5
|
+
* - Ler os templates base da pasta src/templates/
|
|
6
|
+
* - Substituir placeholders ({{kebabName}}, {{PascalName}}, etc.)
|
|
7
|
+
* - Retornar o conteúdo final já renderizado
|
|
8
|
+
*
|
|
9
|
+
* Princípio: Responsabilidade Única (SRP) — lida apenas com renderização de templates
|
|
10
|
+
* Princípio: Aberto/Fechado (OCP) — novos templates são adicionados sem alterar lógica existente
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs-extra';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
|
|
20
|
+
/** Diretório onde os templates estão armazenados */
|
|
21
|
+
const TEMPLATES_DIR = path.resolve(__dirname, '..', 'templates');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Mapa de tipos para seus respectivos arquivos de template
|
|
25
|
+
* e destinos de saída.
|
|
26
|
+
*
|
|
27
|
+
* @type {Object.<string, { templateFile: string, outputDir: string, outputFileName: (kebabName: string) => string }>}
|
|
28
|
+
*/
|
|
29
|
+
const TEMPLATE_MAP = {
|
|
30
|
+
section: {
|
|
31
|
+
templateFile: 'section.liquid.txt',
|
|
32
|
+
outputDir: 'sections',
|
|
33
|
+
outputFileName: (kebabName) => `${kebabName}.liquid`,
|
|
34
|
+
},
|
|
35
|
+
block: {
|
|
36
|
+
templateFile: 'block.liquid.txt',
|
|
37
|
+
outputDir: 'blocks',
|
|
38
|
+
outputFileName: (kebabName) => `${kebabName}-block.liquid`,
|
|
39
|
+
},
|
|
40
|
+
snippet: {
|
|
41
|
+
templateFile: 'snippet.liquid.txt',
|
|
42
|
+
outputDir: 'snippets',
|
|
43
|
+
outputFileName: (kebabName) => `${kebabName}.liquid`,
|
|
44
|
+
},
|
|
45
|
+
controller: {
|
|
46
|
+
templateFile: 'controller.js.txt',
|
|
47
|
+
outputDir: 'assets',
|
|
48
|
+
outputFileName: (kebabName) => `${kebabName}-controller.js`,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export class ScaffoldService {
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {import('../utils/logger.js').Logger} logger
|
|
56
|
+
*/
|
|
57
|
+
constructor(logger) {
|
|
58
|
+
this.logger = logger;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Retorna os tipos de componente disponíveis para geração.
|
|
63
|
+
* @returns {string[]} Lista de tipos disponíveis
|
|
64
|
+
*/
|
|
65
|
+
getAvailableTypes() {
|
|
66
|
+
return Object.keys(TEMPLATE_MAP);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Renderiza um template substituindo todos os placeholders.
|
|
71
|
+
* Utiliza replace via regex (RF02 do PRD).
|
|
72
|
+
*
|
|
73
|
+
* @param {string} templateContent - Conteúdo bruto do template
|
|
74
|
+
* @param {{ kebabName: string, PascalName: string, snake_name: string }} names - Nomes formatados
|
|
75
|
+
* @returns {string} Conteúdo final renderizado
|
|
76
|
+
*/
|
|
77
|
+
render(templateContent, names) {
|
|
78
|
+
return templateContent
|
|
79
|
+
.replace(/\{\{kebabName\}\}/g, names.kebabName)
|
|
80
|
+
.replace(/\{\{PascalName\}\}/g, names.PascalName)
|
|
81
|
+
.replace(/\{\{snake_name\}\}/g, names.snake_name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gera um único componente: lê o template, renderiza e retorna
|
|
86
|
+
* o conteúdo com as informações de destino.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} type - Tipo do componente ('section', 'block', 'snippet', 'controller')
|
|
89
|
+
* @param {{ kebabName: string, PascalName: string, snake_name: string }} names - Nomes formatados
|
|
90
|
+
* @returns {Promise<{ content: string, outputDir: string, fileName: string }>}
|
|
91
|
+
* @throws {Error} Se o tipo não for suportado
|
|
92
|
+
*/
|
|
93
|
+
async generate(type, names) {
|
|
94
|
+
const config = TEMPLATE_MAP[type];
|
|
95
|
+
|
|
96
|
+
if (!config) {
|
|
97
|
+
throw new Error(`Tipo de componente não suportado: "${type}". Tipos válidos: ${this.getAvailableTypes().join(', ')}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Lê o template do disco
|
|
101
|
+
const templatePath = path.join(TEMPLATES_DIR, config.templateFile);
|
|
102
|
+
const templateContent = await fs.readFile(templatePath, 'utf-8');
|
|
103
|
+
|
|
104
|
+
// Renderiza com as variáveis
|
|
105
|
+
const content = this.render(templateContent, names);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
content,
|
|
109
|
+
outputDir: config.outputDir,
|
|
110
|
+
fileName: config.outputFileName(names.kebabName),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Gera múltiplos componentes de uma vez.
|
|
116
|
+
*
|
|
117
|
+
* @param {string[]} types - Lista de tipos a serem gerados
|
|
118
|
+
* @param {{ kebabName: string, PascalName: string, snake_name: string }} names - Nomes formatados
|
|
119
|
+
* @returns {Promise<Array<{ content: string, outputDir: string, fileName: string }>>}
|
|
120
|
+
*/
|
|
121
|
+
async generateMultiple(types, names) {
|
|
122
|
+
const results = [];
|
|
123
|
+
|
|
124
|
+
for (const type of types) {
|
|
125
|
+
const result = await this.generate(type, names);
|
|
126
|
+
results.push(result);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{% doc %}
|
|
2
|
+
Block: {{PascalName}} Block
|
|
3
|
+
Usage: Use inside sections that support blocks.
|
|
4
|
+
{% enddoc %}
|
|
5
|
+
|
|
6
|
+
{% liquid
|
|
7
|
+
assign block_id = block.id
|
|
8
|
+
%}
|
|
9
|
+
|
|
10
|
+
<{{kebabName}}-block
|
|
11
|
+
id="block-{{ block_id }}"
|
|
12
|
+
class="{{kebabName}}-block"
|
|
13
|
+
data-block-id="{{ block_id }}"
|
|
14
|
+
{{ block.shopify_attributes }}
|
|
15
|
+
>
|
|
16
|
+
<div class="{{kebabName}}-block__inner">
|
|
17
|
+
{% if block.settings.title != blank %}
|
|
18
|
+
<h3 class="{{kebabName}}-block__title">{{ block.settings.title | escape }}</h3>
|
|
19
|
+
{% endif %}
|
|
20
|
+
</div>
|
|
21
|
+
</{{kebabName}}-block>
|
|
22
|
+
|
|
23
|
+
{% schema %}
|
|
24
|
+
{
|
|
25
|
+
"name": "{{PascalName}} Block",
|
|
26
|
+
"target": "section",
|
|
27
|
+
"settings": [
|
|
28
|
+
{
|
|
29
|
+
"type": "text",
|
|
30
|
+
"id": "title",
|
|
31
|
+
"label": "Block Title",
|
|
32
|
+
"default": "Feature"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
{% endschema %}
|
|
37
|
+
|
|
38
|
+
{% javascript %}
|
|
39
|
+
class {{PascalName}}Block extends HTMLElement {
|
|
40
|
+
connectedCallback() {
|
|
41
|
+
this.addEventListener('click', this.handleClick.bind(this));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
handleClick() {
|
|
45
|
+
console.log('Block clicked:', this.dataset.blockId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!customElements.get('{{kebabName}}-block')) {
|
|
50
|
+
customElements.define('{{kebabName}}-block', {{PascalName}}Block);
|
|
51
|
+
}
|
|
52
|
+
{% endjavascript %}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Controller: {{PascalName}}Controller
|
|
3
|
+
* Path: assets/{{kebabName}}-controller.js
|
|
4
|
+
* Description: Logic separated for reuse across multiple sections/blocks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class {{PascalName}}Controller extends HTMLElement {
|
|
8
|
+
connectedCallback() {
|
|
9
|
+
const sectionId = this.dataset.sectionId;
|
|
10
|
+
if (!sectionId) return;
|
|
11
|
+
|
|
12
|
+
this.root = document.getElementById(`section-${sectionId}`);
|
|
13
|
+
this.bindEvents();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
bindEvents() {
|
|
17
|
+
// Add event listeners
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
disconnectedCallback() {
|
|
21
|
+
// Remove event listeners
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!customElements.get('{{kebabName}}-controller')) {
|
|
26
|
+
customElements.define('{{kebabName}}-controller', {{PascalName}}Controller);
|
|
27
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{% doc %}
|
|
2
|
+
Section: {{PascalName}}
|
|
3
|
+
Description: Componente principal controlado via Web Component.
|
|
4
|
+
Architecture:
|
|
5
|
+
- HTML: Semântico com IDs dinâmicos
|
|
6
|
+
- JS: Web Component desacoplado
|
|
7
|
+
- CSS: Scoped ou via Assets
|
|
8
|
+
{% enddoc %}
|
|
9
|
+
|
|
10
|
+
{% liquid
|
|
11
|
+
assign section_id = section.id
|
|
12
|
+
assign container_id = 'section-' | append: section_id
|
|
13
|
+
%}
|
|
14
|
+
|
|
15
|
+
<{{kebabName}}-section
|
|
16
|
+
id="{{ container_id }}"
|
|
17
|
+
class="{{kebabName}} section-container"
|
|
18
|
+
data-section-id="{{ section_id }}"
|
|
19
|
+
>
|
|
20
|
+
<div class="{{kebabName}}__wrapper page-width">
|
|
21
|
+
{%- if section.settings.heading != blank -%}
|
|
22
|
+
<h2 class="{{kebabName}}__heading">{{ section.settings.heading | escape }}</h2>
|
|
23
|
+
{%- endif -%}
|
|
24
|
+
|
|
25
|
+
<div class="{{kebabName}}__content">
|
|
26
|
+
{% content_for 'blocks' %}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</{{kebabName}}-section>
|
|
30
|
+
|
|
31
|
+
{% schema %}
|
|
32
|
+
{
|
|
33
|
+
"name": "{{PascalName}}",
|
|
34
|
+
"tag": "section",
|
|
35
|
+
"class": "section-{{kebabName}}",
|
|
36
|
+
"settings": [
|
|
37
|
+
{
|
|
38
|
+
"type": "text",
|
|
39
|
+
"id": "heading",
|
|
40
|
+
"label": "Heading",
|
|
41
|
+
"default": "{{PascalName}}"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"blocks": [
|
|
45
|
+
{
|
|
46
|
+
"type": "@theme"
|
|
47
|
+
}
|
|
48
|
+
],
|
|
49
|
+
"presets": [
|
|
50
|
+
{
|
|
51
|
+
"name": "{{PascalName}}"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
{% endschema %}
|
|
56
|
+
|
|
57
|
+
{% stylesheet %}
|
|
58
|
+
/* BEM Styling */
|
|
59
|
+
.{{kebabName}} {
|
|
60
|
+
display: block;
|
|
61
|
+
position: relative;
|
|
62
|
+
padding: 2rem 0;
|
|
63
|
+
}
|
|
64
|
+
.{{kebabName}}__heading {
|
|
65
|
+
margin-bottom: 1.5rem;
|
|
66
|
+
}
|
|
67
|
+
{% endstylesheet %}
|
|
68
|
+
|
|
69
|
+
{% javascript %}
|
|
70
|
+
class {{PascalName}}Section extends HTMLElement {
|
|
71
|
+
constructor() {
|
|
72
|
+
super();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
connectedCallback() {
|
|
76
|
+
this.init();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
init() {
|
|
80
|
+
console.log('{{PascalName}} initialized', this.dataset.sectionId);
|
|
81
|
+
// Logic goes here
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
disconnectedCallback() {
|
|
85
|
+
// Cleanup events
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!customElements.get('{{kebabName}}-section')) {
|
|
90
|
+
customElements.define('{{kebabName}}-section', {{PascalName}}Section);
|
|
91
|
+
}
|
|
92
|
+
{% endjavascript %}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{% doc %}
|
|
2
|
+
Snippet: {{PascalName}}
|
|
3
|
+
Renders a reusable UI component.
|
|
4
|
+
|
|
5
|
+
@param {string} id - Unique DOM id
|
|
6
|
+
@param {string} [modifier_class] - Optional CSS class
|
|
7
|
+
@param {string} [content] - Optional inner HTML
|
|
8
|
+
|
|
9
|
+
@example
|
|
10
|
+
{% render '{{kebabName}}', id: 'my-id', content: 'Hello' %}
|
|
11
|
+
{% enddoc %}
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
id="{{ id | escape }}"
|
|
15
|
+
class="{{kebabName}} {{ modifier_class }}"
|
|
16
|
+
data-ui-component="{{kebabName}}"
|
|
17
|
+
>
|
|
18
|
+
{% if content %}
|
|
19
|
+
{{ content }}
|
|
20
|
+
{% endif %}
|
|
21
|
+
</div>
|
package/src/ui/PromptUI.js
CHANGED
|
@@ -8,10 +8,63 @@
|
|
|
8
8
|
* Princípio: Aberto/Fechado (OCP) — fácil adicionar novos menus
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { select, checkbox } from '@inquirer/prompts';
|
|
11
|
+
import { select, checkbox, input } from '@inquirer/prompts';
|
|
12
12
|
|
|
13
13
|
export class PromptUI {
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Pergunta o nome do componente ao usuário.
|
|
17
|
+
* Usado quando o nome não é fornecido via argumento CLI.
|
|
18
|
+
*
|
|
19
|
+
* @returns {Promise<string>} Nome digitado pelo usuário
|
|
20
|
+
*/
|
|
21
|
+
async perguntarNomeComponente() {
|
|
22
|
+
const nome = await input({
|
|
23
|
+
message: '📝 Qual o nome do componente?',
|
|
24
|
+
validate: (value) => {
|
|
25
|
+
if (!value || value.trim() === '') {
|
|
26
|
+
return 'O nome do componente é obrigatório.';
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return nome.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Exibe menu multi-select para escolher os tipos de arquivo a serem gerados.
|
|
37
|
+
* Implementa o RF do PRD: "O que você deseja criar?"
|
|
38
|
+
*
|
|
39
|
+
* @returns {Promise<string[]>} Tipos selecionados ('section', 'block', 'snippet', 'controller')
|
|
40
|
+
*/
|
|
41
|
+
async selecionarTiposGerador() {
|
|
42
|
+
const tipos = await checkbox({
|
|
43
|
+
message: '🧱 O que você deseja criar?',
|
|
44
|
+
choices: [
|
|
45
|
+
{
|
|
46
|
+
name: '📄 Section — Cria sections/[nome].liquid',
|
|
47
|
+
value: 'section',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: '🧩 Block — Cria blocks/[nome]-block.liquid',
|
|
51
|
+
value: 'block',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: '🔗 Snippet — Cria snippets/[nome].liquid',
|
|
55
|
+
value: 'snippet',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: '⚡ Asset JS Controller — Cria assets/[nome]-controller.js',
|
|
59
|
+
value: 'controller',
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
required: true,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return tipos;
|
|
66
|
+
}
|
|
67
|
+
|
|
15
68
|
/**
|
|
16
69
|
* Exibe o menu principal com as opções de instalação.
|
|
17
70
|
*
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stringUtils — Utilitários puros de manipulação de strings.
|
|
3
|
+
*
|
|
4
|
+
* Responsável pelas conversões de casing necessárias
|
|
5
|
+
* no fluxo de geração de componentes (scaffold).
|
|
6
|
+
*
|
|
7
|
+
* Princípio: Responsabilidade Única (SRP) — funções puras sem side-effects
|
|
8
|
+
* Princípio: Aberto/Fechado (OCP) — novos formatadores são adicionados sem alterar existentes
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Converte qualquer input para kebab-case.
|
|
13
|
+
* Usado em: nomes de arquivo, classes CSS (BEM), tags HTML.
|
|
14
|
+
*
|
|
15
|
+
* Exemplos:
|
|
16
|
+
* "hero banner" → "hero-banner"
|
|
17
|
+
* "HeroBanner" → "hero-banner"
|
|
18
|
+
* "hero_banner" → "hero-banner"
|
|
19
|
+
* "HERO-BANNER" → "hero-banner"
|
|
20
|
+
*
|
|
21
|
+
* @param {string} input - String para converter
|
|
22
|
+
* @returns {string} String em kebab-case
|
|
23
|
+
*/
|
|
24
|
+
export function toKebabCase(input) {
|
|
25
|
+
return input
|
|
26
|
+
// Insere hífen antes de letras maiúsculas em camelCase/PascalCase
|
|
27
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
28
|
+
// Substitui espaços, underscores e múltiplos hífens por hífen único
|
|
29
|
+
.replace(/[\s_]+/g, '-')
|
|
30
|
+
// Remove múltiplos hífens consecutivos
|
|
31
|
+
.replace(/-+/g, '-')
|
|
32
|
+
// Remove hífens no início e final
|
|
33
|
+
.replace(/^-|-$/g, '')
|
|
34
|
+
.toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Converte qualquer input para PascalCase.
|
|
39
|
+
* Usado em: classes JS (Web Components), labels do Schema Shopify.
|
|
40
|
+
*
|
|
41
|
+
* Exemplos:
|
|
42
|
+
* "hero banner" → "HeroBanner"
|
|
43
|
+
* "hero-banner" → "HeroBanner"
|
|
44
|
+
* "hero_banner" → "HeroBanner"
|
|
45
|
+
* "HeroBanner" → "HeroBanner"
|
|
46
|
+
*
|
|
47
|
+
* @param {string} input - String para converter
|
|
48
|
+
* @returns {string} String em PascalCase
|
|
49
|
+
*/
|
|
50
|
+
export function toPascalCase(input) {
|
|
51
|
+
return toKebabCase(input)
|
|
52
|
+
.split('-')
|
|
53
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
54
|
+
.join('');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Converte qualquer input para snake_case.
|
|
59
|
+
* Usado em: variáveis Liquid, IDs de settings.
|
|
60
|
+
*
|
|
61
|
+
* Exemplos:
|
|
62
|
+
* "hero banner" → "hero_banner"
|
|
63
|
+
* "hero-banner" → "hero_banner"
|
|
64
|
+
* "HeroBanner" → "hero_banner"
|
|
65
|
+
*
|
|
66
|
+
* @param {string} input - String para converter
|
|
67
|
+
* @returns {string} String em snake_case
|
|
68
|
+
*/
|
|
69
|
+
export function toSnakeCase(input) {
|
|
70
|
+
return toKebabCase(input).replace(/-/g, '_');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Gera todos os formatos de nome a partir de um input qualquer.
|
|
75
|
+
* Retorna um objeto padronizado usado nos templates.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} input - Nome original do componente
|
|
78
|
+
* @returns {{ kebabName: string, PascalName: string, snake_name: string }}
|
|
79
|
+
*/
|
|
80
|
+
export function generateNames(input) {
|
|
81
|
+
return {
|
|
82
|
+
kebabName: toKebabCase(input),
|
|
83
|
+
PascalName: toPascalCase(input),
|
|
84
|
+
snake_name: toSnakeCase(input),
|
|
85
|
+
};
|
|
86
|
+
}
|