openprompt-lang 0.3.0 → 0.4.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 +483 -32
- package/bin/cli.js +232 -41
- package/bin/create.js +135 -0
- package/bin/lint.js +20 -21
- package/docs/COMMANDS.md +200 -127
- package/docs/COMMITS/INDEX.md +1 -0
- package/docs/PROMPT_AI_CONTEXT.md +99 -0
- package/docs/langs/dotnet.md +36 -0
- package/docs/langs/java-spring.md +45 -0
- package/docs/langs/python-fastapi.md +35 -0
- package/docs/langs/unity.md +30 -0
- package/docs/langs/vue-nuxt.md +36 -0
- package/package.json +31 -3
- package/scaffolds/.cursorrules +6 -0
- package/scaffolds/AGENTS.md +27 -0
- package/scaffolds/Dockerfile +11 -0
- package/scaffolds/capacitor.config.ts +17 -0
- package/scaffolds/netlify.toml +8 -0
- package/scaffolds/prompt-lang.json +15 -0
- package/scaffolds/railway.json +12 -0
- package/scaffolds/tailwind.config.js +8 -0
- package/scaffolds/tauri.conf.json +26 -0
- package/schemas/language-module.json +116 -0
- package/schemas/prompt-lang.json +38 -3
- package/schemas/structures.json +145 -0
- package/src/ai/prompt-builder.js +184 -0
- package/src/ai/providers.js +247 -0
- package/src/annotations/registry.js +39 -0
- package/src/annotations/tags.json +24 -0
- package/src/commands/ai-gen.js +161 -0
- package/src/commands/component.js +242 -212
- package/src/commands/context.js +184 -109
- package/src/commands/extract.js +242 -0
- package/src/commands/figma.js +15 -15
- package/src/commands/init.js +197 -93
- package/src/commands/integrate.js +406 -0
- package/src/commands/lang.js +148 -0
- package/src/commands/qa-gen.js +139 -0
- package/src/commands/scaffold.js +127 -0
- package/src/commands/suggest.js +24 -14
- package/src/commands/teach.js +110 -0
- package/src/commands/validate.js +143 -83
- package/src/commands/wizard.js +456 -0
- package/src/generators/figma-prompt.js +20 -12
- package/src/language-service/plugin.cjs +94 -0
- package/src/language-service/plugin.d.ts +6 -0
- package/src/mcp-server.js +605 -0
- package/src/templates/langs/react/INDEX.json +262 -0
- package/src/templates/langs/react/MODULE.json +166 -0
- package/src/templates/langs/react/templates/hooks/useAuth.template.ts +134 -0
- package/src/templates/langs/react/templates/hooks/useDebounce.template.ts +45 -0
- package/src/templates/langs/react/templates/hooks/useForm.template.ts +146 -0
- package/src/templates/langs/react/templates/hooks/usePagination.template.ts +108 -0
- package/src/templates/langs/react/templates/services/apiService.template.ts +123 -0
- package/src/templates/langs/react/templates/ui/Button.template.tsx +87 -0
- package/src/templates/langs/react/templates/ui/Card.template.tsx +85 -0
- package/src/templates/langs/react/templates/ui/DataTable.template.tsx +163 -0
- package/src/templates/langs/react/templates/ui/Input.template.tsx +96 -0
- package/src/templates/langs/react/templates/ui/Modal.template.tsx +133 -0
- package/src/templates/langs/react/templates/ui/Select.template.tsx +99 -0
- package/src/templates/langs/vue/INDEX.json +246 -0
- package/src/templates/langs/vue/MODULE.json +105 -0
- package/src/templates/langs/vue/templates/composables/useAuth.template.ts +106 -0
- package/src/templates/langs/vue/templates/composables/useDebounce.template.ts +47 -0
- package/src/templates/langs/vue/templates/composables/useFetch.template.ts +54 -0
- package/src/templates/langs/vue/templates/composables/useForm.template.ts +127 -0
- package/src/templates/langs/vue/templates/composables/usePagination.template.ts +98 -0
- package/src/templates/langs/vue/templates/services/apiService.template.ts +116 -0
- package/src/templates/langs/vue/templates/ui/Button.template.vue +79 -0
- package/src/templates/langs/vue/templates/ui/Card.template.vue +73 -0
- package/src/templates/langs/vue/templates/ui/DataTable.template.vue +115 -0
- package/src/templates/langs/vue/templates/ui/Input.template.vue +70 -0
- package/src/templates/langs/vue/templates/ui/Modal.template.vue +112 -0
- package/src/templates/langs/vue/templates/ui/Select.template.vue +77 -0
- package/src/templates/scripts/log-actividad.sh +32 -0
- package/src/templates/scripts/log-commit.sh +35 -0
- package/src/templates/scripts/log-error.sh +45 -0
- package/src/templates/scripts/validate.sh +23 -0
- package/src/ts-transformer/index.cjs +86 -0
- package/src/utils/ai.js +35 -53
- package/src/utils/annotations.js +260 -214
- package/src/utils/config.js +61 -13
- package/src/utils/error-learner.js +203 -0
- package/src/utils/file-utils.js +119 -0
- package/src/utils/language-loader.js +167 -0
- package/src/utils/template-utils.js +45 -0
- package/src/vite-plugin/index.js +54 -0
- package/vscode-extension/package.json +23 -2
- package/vscode-extension/snippets/promptlang.json +1 -3
- package/vscode-extension/syntaxes/annotations-code.tmGrammar.json +15 -0
package/src/utils/annotations.js
CHANGED
|
@@ -1,339 +1,370 @@
|
|
|
1
|
-
import chalk from "chalk"
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import { getBuiltinTags, getAnnotationTags } from "../annotations/registry.js"
|
|
2
3
|
|
|
3
4
|
// ─── Parser ────────────────────────────────────────────────────────────────
|
|
4
5
|
|
|
5
|
-
export function parseAnnotations(code) {
|
|
6
|
-
const annotations = []
|
|
7
|
-
const lines = code.split("\n")
|
|
8
|
-
const errors = []
|
|
6
|
+
export function parseAnnotations(code, options = {}) {
|
|
7
|
+
const annotations = []
|
|
8
|
+
const lines = code.split("\n")
|
|
9
|
+
const errors = []
|
|
10
|
+
const { langId, projectConfig } = options
|
|
11
|
+
const knownTags = projectConfig ? getAnnotationTags(langId, projectConfig) : getBuiltinTags()
|
|
9
12
|
|
|
10
|
-
let currentBlock = null
|
|
11
|
-
let inBlock = false
|
|
13
|
+
let currentBlock = null
|
|
14
|
+
let inBlock = false
|
|
12
15
|
|
|
13
16
|
for (let i = 0; i < lines.length; i++) {
|
|
14
|
-
const line = lines[i]
|
|
15
|
-
const trimmed = line.trim()
|
|
17
|
+
const line = lines[i]
|
|
18
|
+
const trimmed = line.trim()
|
|
16
19
|
|
|
17
20
|
// Detecta bloque de anotaciones /* ... */
|
|
18
21
|
if (trimmed.startsWith("/*")) {
|
|
19
|
-
inBlock = true
|
|
20
|
-
currentBlock = ""
|
|
21
|
-
const cleaned = trimmed
|
|
22
|
-
|
|
22
|
+
inBlock = true
|
|
23
|
+
currentBlock = ""
|
|
24
|
+
const cleaned = trimmed
|
|
25
|
+
.replace(/^\/\*+\s*/, "")
|
|
26
|
+
.replace(/\s*\*+\/$/, "")
|
|
27
|
+
.trim()
|
|
23
28
|
if (cleaned) {
|
|
24
|
-
currentBlock += cleaned + " "
|
|
29
|
+
currentBlock += cleaned + " "
|
|
25
30
|
}
|
|
26
31
|
if (trimmed.includes("*/")) {
|
|
27
|
-
inBlock = false
|
|
32
|
+
inBlock = false
|
|
28
33
|
if (currentBlock.trim()) {
|
|
29
|
-
annotations.push(...parseTags(currentBlock))
|
|
34
|
+
annotations.push(...parseTags(currentBlock))
|
|
30
35
|
}
|
|
31
|
-
currentBlock = null
|
|
36
|
+
currentBlock = null
|
|
32
37
|
}
|
|
33
|
-
continue
|
|
38
|
+
continue
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
if (inBlock) {
|
|
37
42
|
if (trimmed.includes("*/")) {
|
|
38
|
-
const content = trimmed.replace(/\s*\*+\/$/, "").trim()
|
|
39
|
-
currentBlock += content + " "
|
|
40
|
-
inBlock = false
|
|
41
|
-
annotations.push(...parseTags(currentBlock))
|
|
42
|
-
currentBlock = null
|
|
43
|
+
const content = trimmed.replace(/\s*\*+\/$/, "").trim()
|
|
44
|
+
currentBlock += content + " "
|
|
45
|
+
inBlock = false
|
|
46
|
+
annotations.push(...parseTags(currentBlock))
|
|
47
|
+
currentBlock = null
|
|
43
48
|
} else {
|
|
44
|
-
const content = trimmed.replace(/^\s*\*\s*/, "").trim()
|
|
45
|
-
currentBlock += content + " "
|
|
49
|
+
const content = trimmed.replace(/^\s*\*\s*/, "").trim()
|
|
50
|
+
currentBlock += content + " "
|
|
46
51
|
}
|
|
47
|
-
continue
|
|
52
|
+
continue
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
// Detecta
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
// Detecta // comment inline (incluso después de código)
|
|
56
|
+
const commentIdx = trimmed.lastIndexOf("//")
|
|
57
|
+
const hasInlineComment = commentIdx !== -1 && trimmed.includes("@", commentIdx)
|
|
58
|
+
if (hasInlineComment) {
|
|
59
|
+
const afterComment = trimmed.slice(commentIdx + 2).trim()
|
|
60
|
+
if (afterComment.startsWith("@") || afterComment.includes("@")) {
|
|
61
|
+
annotations.push(...parseTags(afterComment))
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Detecta anotación como código (sin //)
|
|
67
|
+
const codeMatch = trimmed.match(/^@(\w[\w-]*)\(/)
|
|
68
|
+
if (codeMatch && knownTags.includes(codeMatch[1])) {
|
|
69
|
+
annotations.push(...parseTags(trimmed))
|
|
54
70
|
}
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
// Si quedó un bloque abierto sin cerrar, lo forzamos
|
|
58
74
|
if (inBlock && currentBlock) {
|
|
59
|
-
annotations.push(...parseTags(currentBlock))
|
|
60
|
-
errors.push("Comentario multinea sin cerrar. Las anotaciones se procesaron igual.")
|
|
75
|
+
annotations.push(...parseTags(currentBlock))
|
|
76
|
+
errors.push("Comentario multinea sin cerrar. Las anotaciones se procesaron igual.")
|
|
61
77
|
}
|
|
62
78
|
|
|
63
79
|
// Extraer @use primero (determina qué tags son válidos)
|
|
64
|
-
const useTag = annotations.find((a) => a.name === "use")
|
|
80
|
+
const useTag = annotations.find((a) => a.name === "use")
|
|
65
81
|
|
|
66
82
|
// Edge case: @use(*) → todos los tags son válidos
|
|
67
|
-
let validTags = []
|
|
83
|
+
let validTags = []
|
|
68
84
|
if (useTag) {
|
|
69
|
-
const starArg = useTag.args.find((a) => a.value === "*")
|
|
85
|
+
const starArg = useTag.args.find((a) => a.value === "*")
|
|
70
86
|
if (starArg) {
|
|
71
|
-
validTags = []
|
|
87
|
+
validTags = [] // empty = todos permitidos
|
|
72
88
|
} else {
|
|
73
|
-
validTags = useTag.args.map((a) => a.value)
|
|
89
|
+
validTags = useTag.args.map((a) => a.value)
|
|
74
90
|
}
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
// Edge case: @use presente pero sin tags reales
|
|
78
94
|
if (useTag && useTag.args.length === 0) {
|
|
79
|
-
errors.push("@use() está vacío — no se importaron tags")
|
|
95
|
+
errors.push("@use() está vacío — no se importaron tags")
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
// Edge case: @use sin anotaciones de tags abajo
|
|
83
99
|
if (useTag && annotations.filter((a) => a.name !== "use").length === 0) {
|
|
84
|
-
errors.push("@use presente pero no hay anotaciones de tags en el archivo")
|
|
100
|
+
errors.push("@use presente pero no hay anotaciones de tags en el archivo")
|
|
85
101
|
}
|
|
86
102
|
|
|
87
|
-
return { annotations, validTags, parseErrors: errors }
|
|
103
|
+
return { annotations, validTags, parseErrors: errors }
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
function parseTags(text) {
|
|
91
|
-
const tags = []
|
|
107
|
+
const tags = []
|
|
92
108
|
|
|
93
|
-
if (!text || text.trim().length === 0) return tags
|
|
109
|
+
if (!text || text.trim().length === 0) return tags
|
|
94
110
|
|
|
95
111
|
// Parseador manual con conteo de profundidad de paréntesis
|
|
96
|
-
let i = 0
|
|
112
|
+
let i = 0
|
|
97
113
|
while (i < text.length) {
|
|
98
114
|
// Buscar @ seguido de nombre de tag
|
|
99
|
-
const atIndex = text.indexOf("@", i)
|
|
100
|
-
if (atIndex === -1) break
|
|
115
|
+
const atIndex = text.indexOf("@", i)
|
|
116
|
+
if (atIndex === -1) break
|
|
101
117
|
|
|
102
118
|
// Extraer nombre del tag (letras, números, guiones)
|
|
103
|
-
const nameMatch = text.slice(atIndex + 1).match(
|
|
104
|
-
if (!nameMatch) {
|
|
119
|
+
const nameMatch = text.slice(atIndex + 1).match(/^[\w-]+/)
|
|
120
|
+
if (!nameMatch) {
|
|
121
|
+
i = atIndex + 1
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
105
124
|
|
|
106
|
-
const name = nameMatch[0]
|
|
107
|
-
let argsRaw = ""
|
|
108
|
-
let parenDepth = 0
|
|
109
|
-
let pos = atIndex + 1 + name.length
|
|
125
|
+
const name = nameMatch[0]
|
|
126
|
+
let argsRaw = ""
|
|
127
|
+
let parenDepth = 0
|
|
128
|
+
let pos = atIndex + 1 + name.length
|
|
110
129
|
|
|
111
130
|
// Si hay paréntesis, extraemos con depth tracking
|
|
112
131
|
if (pos < text.length && text[pos] === "(") {
|
|
113
|
-
parenDepth = 1
|
|
114
|
-
pos
|
|
115
|
-
const start = pos
|
|
132
|
+
parenDepth = 1
|
|
133
|
+
pos++
|
|
134
|
+
const start = pos
|
|
116
135
|
while (pos < text.length && parenDepth > 0) {
|
|
117
|
-
if (text[pos] === "(") parenDepth
|
|
118
|
-
else if (text[pos] === ")") parenDepth
|
|
119
|
-
pos
|
|
136
|
+
if (text[pos] === "(") parenDepth++
|
|
137
|
+
else if (text[pos] === ")") parenDepth--
|
|
138
|
+
pos++
|
|
120
139
|
}
|
|
121
|
-
argsRaw = text.slice(start, pos - 1).trim()
|
|
140
|
+
argsRaw = text.slice(start, pos - 1).trim()
|
|
122
141
|
}
|
|
123
142
|
|
|
124
|
-
tags.push({ name, args: parseArgs(argsRaw), raw: `@${name}(${argsRaw})` })
|
|
125
|
-
i = pos
|
|
143
|
+
tags.push({ name, args: parseArgs(argsRaw), raw: `@${name}(${argsRaw})` })
|
|
144
|
+
i = pos
|
|
126
145
|
}
|
|
127
146
|
|
|
128
|
-
return tags
|
|
147
|
+
return tags
|
|
129
148
|
}
|
|
130
149
|
|
|
131
150
|
function parseArgs(raw) {
|
|
132
|
-
const args = []
|
|
151
|
+
const args = []
|
|
133
152
|
|
|
134
|
-
if (!raw || raw.trim().length === 0) return args
|
|
153
|
+
if (!raw || raw.trim().length === 0) return args
|
|
135
154
|
|
|
136
|
-
const trimmed = raw.trim()
|
|
137
|
-
const lenient = trimmed.replace(/\)$/, "");
|
|
155
|
+
const trimmed = raw.trim()
|
|
138
156
|
|
|
139
157
|
// Intenta parsear como JSON object primero: { key: value, ... }
|
|
140
158
|
if (trimmed.startsWith("{")) {
|
|
141
159
|
try {
|
|
142
|
-
const obj = JSON.parse(trimmed)
|
|
160
|
+
const obj = JSON.parse(trimmed)
|
|
143
161
|
for (const [key, value] of Object.entries(obj)) {
|
|
144
|
-
args.push({ key, value: String(value), type: "kv" })
|
|
162
|
+
args.push({ key, value: String(value), type: "kv" })
|
|
145
163
|
}
|
|
146
|
-
return args
|
|
164
|
+
return args
|
|
147
165
|
} catch {
|
|
148
166
|
// Fallback: parsear como TypeScript object { key: type, key?: type }
|
|
149
|
-
const tsProps = parseTsObject(trimmed)
|
|
167
|
+
const tsProps = parseTsObject(trimmed)
|
|
150
168
|
if (tsProps.length > 0) {
|
|
151
|
-
args.push(...tsProps)
|
|
152
|
-
return args
|
|
169
|
+
args.push(...tsProps)
|
|
170
|
+
return args
|
|
153
171
|
}
|
|
154
172
|
// Edge case: JSON inválido → error reportado en validación
|
|
155
|
-
args.push({ key: null, value: trimmed, type: "invalid-json" })
|
|
156
|
-
return args
|
|
173
|
+
args.push({ key: null, value: trimmed, type: "invalid-json" })
|
|
174
|
+
return args
|
|
157
175
|
}
|
|
158
176
|
}
|
|
159
177
|
|
|
160
178
|
// Detectar mezcla de subtags, flags y key:values
|
|
161
179
|
// Dividir por coma respetando corchetes
|
|
162
|
-
const parts = splitRespectingBrackets(
|
|
180
|
+
const parts = splitRespectingBrackets(trimmed)
|
|
163
181
|
|
|
164
182
|
for (const part of parts) {
|
|
165
|
-
const p = part.trim()
|
|
166
|
-
if (!p) continue
|
|
183
|
+
const p = part.trim()
|
|
184
|
+
if (!p) continue
|
|
167
185
|
|
|
168
186
|
// Subtag: @algo: [lista]
|
|
169
|
-
const subMatch = p.match(/^@(\w+)\s*:\s*\[([^\]]*)\]/)
|
|
187
|
+
const subMatch = p.match(/^@(\w+)\s*:\s*\[([^\]]*)\]/)
|
|
170
188
|
if (subMatch) {
|
|
171
189
|
args.push({
|
|
172
190
|
key: subMatch[1],
|
|
173
191
|
value: subMatch[2].split(",").map((s) => s.trim().replace(/["']/g, "")),
|
|
174
192
|
type: "subtag",
|
|
175
|
-
})
|
|
176
|
-
continue
|
|
193
|
+
})
|
|
194
|
+
continue
|
|
177
195
|
}
|
|
178
196
|
|
|
179
197
|
// Flag: @algo
|
|
180
|
-
const flagMatch = p.match(/^@(\w+)/)
|
|
198
|
+
const flagMatch = p.match(/^@(\w+)/)
|
|
181
199
|
if (flagMatch) {
|
|
182
|
-
args.push({ key: flagMatch[1], value: true, type: "flag" })
|
|
183
|
-
continue
|
|
200
|
+
args.push({ key: flagMatch[1], value: true, type: "flag" })
|
|
201
|
+
continue
|
|
184
202
|
}
|
|
185
203
|
|
|
186
204
|
// Key: value
|
|
187
|
-
const kvMatch = p.match(/^(\w+)\s*:\s*(.+)/)
|
|
205
|
+
const kvMatch = p.match(/^(\w+)\s*:\s*(.+)/)
|
|
188
206
|
if (kvMatch) {
|
|
189
|
-
args.push({
|
|
190
|
-
|
|
207
|
+
args.push({
|
|
208
|
+
key: kvMatch[1].trim(),
|
|
209
|
+
value: kvMatch[2].trim().replace(/["']/g, ""),
|
|
210
|
+
type: "kv",
|
|
211
|
+
})
|
|
212
|
+
continue
|
|
191
213
|
}
|
|
192
214
|
|
|
193
215
|
// Valor plano
|
|
194
|
-
args.push({ key: null, value: p.replace(/["']/g, ""), type: "plain" })
|
|
216
|
+
args.push({ key: null, value: p.replace(/["']/g, ""), type: "plain" })
|
|
195
217
|
}
|
|
196
218
|
|
|
197
|
-
return args
|
|
219
|
+
return args
|
|
198
220
|
}
|
|
199
221
|
|
|
200
222
|
function parseTsObject(raw) {
|
|
201
223
|
// Parsear { key: type, key?: type, "key": "type" }
|
|
202
|
-
const inner = raw.trim().replace(/^{/, "").replace(/}$/, "").trim()
|
|
203
|
-
if (!inner) return []
|
|
204
|
-
const parts = splitRespectingBrackets(inner)
|
|
205
|
-
const result = []
|
|
224
|
+
const inner = raw.trim().replace(/^{/, "").replace(/}$/, "").trim()
|
|
225
|
+
if (!inner) return []
|
|
226
|
+
const parts = splitRespectingBrackets(inner)
|
|
227
|
+
const result = []
|
|
206
228
|
for (const part of parts) {
|
|
207
|
-
const p = part.trim()
|
|
208
|
-
if (!p) continue
|
|
229
|
+
const p = part.trim()
|
|
230
|
+
if (!p) continue
|
|
209
231
|
// key: type o key?: type o "key": "type"
|
|
210
|
-
const m = p.match(/^"([^"]+)"\s*:\s*(.+)/) || p.match(/^(\w+\??)\s*:\s*(.+)/)
|
|
232
|
+
const m = p.match(/^"([^"]+)"\s*:\s*(.+)/) || p.match(/^(\w+\??)\s*:\s*(.+)/)
|
|
211
233
|
if (m) {
|
|
212
|
-
const key = m[1].replace(/\?$/, "?")
|
|
213
|
-
let value = m[2].trim().replace(/["']/g, "")
|
|
234
|
+
const key = m[1].replace(/\?$/, "?")
|
|
235
|
+
let value = m[2].trim().replace(/["']/g, "")
|
|
214
236
|
// Quitar default values: = "Default" → ignorar
|
|
215
|
-
value = value.replace(/\s*=\s*.+$/, "").trim()
|
|
216
|
-
result.push({ key, value, type: "kv" })
|
|
237
|
+
value = value.replace(/\s*=\s*.+$/, "").trim()
|
|
238
|
+
result.push({ key, value, type: "kv" })
|
|
217
239
|
}
|
|
218
240
|
}
|
|
219
|
-
return result
|
|
241
|
+
return result
|
|
220
242
|
}
|
|
221
243
|
|
|
222
244
|
function splitRespectingBrackets(str) {
|
|
223
|
-
const result = []
|
|
224
|
-
let current = ""
|
|
225
|
-
let depth = 0
|
|
245
|
+
const result = []
|
|
246
|
+
let current = ""
|
|
247
|
+
let depth = 0
|
|
226
248
|
|
|
227
249
|
for (const ch of str) {
|
|
228
250
|
if (ch === "[" || ch === "{" || ch === "(") {
|
|
229
|
-
depth
|
|
230
|
-
current += ch
|
|
251
|
+
depth++
|
|
252
|
+
current += ch
|
|
231
253
|
} else if (ch === "]" || ch === "}" || ch === ")") {
|
|
232
|
-
depth
|
|
233
|
-
current += ch
|
|
254
|
+
depth--
|
|
255
|
+
current += ch
|
|
234
256
|
} else if (ch === "," && depth === 0) {
|
|
235
|
-
result.push(current.trim())
|
|
236
|
-
current = ""
|
|
257
|
+
result.push(current.trim())
|
|
258
|
+
current = ""
|
|
237
259
|
} else {
|
|
238
|
-
current += ch
|
|
260
|
+
current += ch
|
|
239
261
|
}
|
|
240
262
|
}
|
|
241
|
-
if (current.trim()) result.push(current.trim())
|
|
242
|
-
return result
|
|
263
|
+
if (current.trim()) result.push(current.trim())
|
|
264
|
+
return result
|
|
243
265
|
}
|
|
244
266
|
|
|
245
267
|
// ─── Validador ──────────────────────────────────────────────────────────────
|
|
246
268
|
|
|
247
269
|
export function validateAnnotations(annotations, validTags, options = {}) {
|
|
248
|
-
const errors = []
|
|
249
|
-
const warnings = []
|
|
250
|
-
const tags = annotations.filter((a) => a.name !== "use")
|
|
270
|
+
const errors = []
|
|
271
|
+
const warnings = []
|
|
272
|
+
const tags = annotations.filter((a) => a.name !== "use")
|
|
251
273
|
|
|
252
274
|
// Validar tags contra @use
|
|
253
|
-
const useAll =
|
|
254
|
-
|
|
255
|
-
|
|
275
|
+
const useAll =
|
|
276
|
+
validTags.length === 0 &&
|
|
277
|
+
annotations.some((a) => a.name === "use" && a.args.some((x) => x.value === "*"))
|
|
256
278
|
|
|
257
279
|
if (validTags.length > 0 && !useAll) {
|
|
258
280
|
for (const tag of tags) {
|
|
259
281
|
if (!validTags.includes(tag.name)) {
|
|
260
|
-
errors.push(`Tag @${tag.name} no está importado. Agrega @use(${tag.name})`)
|
|
282
|
+
errors.push(`Tag @${tag.name} no está importado. Agrega @use(${tag.name})`)
|
|
261
283
|
}
|
|
262
284
|
}
|
|
263
285
|
}
|
|
264
286
|
|
|
265
287
|
// Validar @kind
|
|
266
|
-
const kindTag = tags.find((t) => t.name === "kind")
|
|
288
|
+
const kindTag = tags.find((t) => t.name === "kind")
|
|
267
289
|
const validKinds = [
|
|
268
|
-
"hook",
|
|
269
|
-
"
|
|
270
|
-
|
|
290
|
+
"hook",
|
|
291
|
+
"component",
|
|
292
|
+
"page",
|
|
293
|
+
"service",
|
|
294
|
+
"store",
|
|
295
|
+
"util",
|
|
296
|
+
"type",
|
|
297
|
+
"layout",
|
|
298
|
+
"feature",
|
|
299
|
+
]
|
|
271
300
|
|
|
272
301
|
if (kindTag) {
|
|
273
|
-
const kindValue = kindTag.args[0]?.value
|
|
302
|
+
const kindValue = kindTag.args[0]?.value
|
|
274
303
|
|
|
275
304
|
// Edge case: @kind sin valor
|
|
276
305
|
if (!kindValue || kindValue.trim() === "") {
|
|
277
|
-
errors.push("@kind() necesita un valor. Válidos: " + validKinds.join(", "))
|
|
306
|
+
errors.push("@kind() necesita un valor. Válidos: " + validKinds.join(", "))
|
|
278
307
|
} else if (!validKinds.includes(kindValue)) {
|
|
279
|
-
errors.push(
|
|
280
|
-
`@kind("${kindValue}") inválido. Valores válidos: ${validKinds.join(", ")}`
|
|
281
|
-
);
|
|
308
|
+
errors.push(`@kind("${kindValue}") inválido. Valores válidos: ${validKinds.join(", ")}`)
|
|
282
309
|
}
|
|
283
310
|
|
|
284
311
|
if (kindValue && validKinds.includes(kindValue)) {
|
|
285
|
-
const hasContract = tags.some((t) => t.name === "contract")
|
|
286
|
-
const hasProps = tags.some((t) => t.name === "props")
|
|
287
|
-
const hasLimit = tags.some((t) => t.name === "limit")
|
|
288
|
-
const hasCompose = tags.some((t) => t.name === "compose")
|
|
312
|
+
const hasContract = tags.some((t) => t.name === "contract")
|
|
313
|
+
const hasProps = tags.some((t) => t.name === "props")
|
|
314
|
+
const hasLimit = tags.some((t) => t.name === "limit")
|
|
315
|
+
const hasCompose = tags.some((t) => t.name === "compose")
|
|
289
316
|
|
|
290
317
|
if (kindValue === "hook" && !hasContract) {
|
|
291
|
-
warnings.push("@kind(hook) debería tener @contract")
|
|
318
|
+
warnings.push("@kind(hook) debería tener @contract")
|
|
292
319
|
}
|
|
293
320
|
if (kindValue === "component" && hasContract) {
|
|
294
|
-
errors.push("@kind(component) no puede tener @contract. Usa @props en su lugar.")
|
|
321
|
+
errors.push("@kind(component) no puede tener @contract. Usa @props en su lugar.")
|
|
295
322
|
}
|
|
296
323
|
if (kindValue === "component" && !hasProps) {
|
|
297
|
-
warnings.push("@kind(component) debería tener @props")
|
|
324
|
+
warnings.push("@kind(component) debería tener @props")
|
|
298
325
|
}
|
|
299
326
|
if (kindValue === "page" && !hasCompose) {
|
|
300
|
-
warnings.push("@kind(page) debería tener @compose (debe componer subcomponentes)")
|
|
327
|
+
warnings.push("@kind(page) debería tener @compose (debe componer subcomponentes)")
|
|
301
328
|
}
|
|
302
329
|
if (kindValue === "service" && !hasContract) {
|
|
303
|
-
errors.push("@kind(service) requiere @contract")
|
|
330
|
+
errors.push("@kind(service) requiere @contract")
|
|
304
331
|
}
|
|
305
332
|
if (kindValue === "hook" && hasProps) {
|
|
306
|
-
errors.push("@kind(hook) no puede tener @props")
|
|
333
|
+
errors.push("@kind(hook) no puede tener @props")
|
|
307
334
|
}
|
|
308
335
|
if (kindValue === "type" && hasLimit) {
|
|
309
|
-
warnings.push("@kind(type) no necesita @limit")
|
|
336
|
+
warnings.push("@kind(type) no necesita @limit")
|
|
310
337
|
}
|
|
311
338
|
if (kindValue === "store") {
|
|
312
|
-
const hasDeps = tags.some((t) => t.name === "deps")
|
|
339
|
+
const hasDeps = tags.some((t) => t.name === "deps")
|
|
313
340
|
if (!hasDeps) {
|
|
314
|
-
warnings.push("@kind(store) debería tener @deps (zustand, context, etc.)")
|
|
341
|
+
warnings.push("@kind(store) debería tener @deps (zustand, context, etc.)")
|
|
315
342
|
}
|
|
316
343
|
}
|
|
317
344
|
|
|
318
345
|
// Límites por defecto según kind
|
|
319
|
-
const limitTag = tags.find((t) => t.name === "limit")
|
|
346
|
+
const limitTag = tags.find((t) => t.name === "limit")
|
|
320
347
|
if (limitTag) {
|
|
321
|
-
const linesArg = limitTag.args.find((a) => a.key === "lines")
|
|
348
|
+
const linesArg = limitTag.args.find((a) => a.key === "lines")
|
|
322
349
|
if (linesArg) {
|
|
323
350
|
const maxLines = {
|
|
324
|
-
hook: 80,
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
351
|
+
hook: 80,
|
|
352
|
+
component: 120,
|
|
353
|
+
page: 200,
|
|
354
|
+
service: 150,
|
|
355
|
+
store: 100,
|
|
356
|
+
layout: 150,
|
|
357
|
+
}
|
|
358
|
+
const expected = maxLines[kindValue]
|
|
359
|
+
const numVal = parseInt(linesArg.value)
|
|
329
360
|
if (expected && !isNaN(numVal) && numVal > expected) {
|
|
330
361
|
warnings.push(
|
|
331
362
|
`@kind(${kindValue}) @limit(lines) debería ser ≤ ${expected} (actual: ${linesArg.value})`
|
|
332
|
-
)
|
|
363
|
+
)
|
|
333
364
|
}
|
|
334
365
|
// Edge case: límite 0 o negativo
|
|
335
366
|
if (!isNaN(numVal) && numVal <= 0) {
|
|
336
|
-
errors.push(`@limit(lines: ${numVal}) debe ser mayor a 0`)
|
|
367
|
+
errors.push(`@limit(lines: ${numVal}) debe ser mayor a 0`)
|
|
337
368
|
}
|
|
338
369
|
}
|
|
339
370
|
}
|
|
@@ -341,16 +372,14 @@ export function validateAnnotations(annotations, validTags, options = {}) {
|
|
|
341
372
|
}
|
|
342
373
|
|
|
343
374
|
// Validar @contract — edge cases
|
|
344
|
-
const contractTag = tags.find((t) => t.name === "contract")
|
|
375
|
+
const contractTag = tags.find((t) => t.name === "contract")
|
|
345
376
|
if (contractTag) {
|
|
346
|
-
const args = contractTag.args
|
|
347
|
-
const hasInput = args.some((a) => a.key === "in" || a.type === "plain")
|
|
348
|
-
const
|
|
349
|
-
const contractText = contractTag.raw;
|
|
377
|
+
const args = contractTag.args
|
|
378
|
+
const hasInput = args.some((a) => a.key === "in" || a.type === "plain")
|
|
379
|
+
const contractText = contractTag.raw
|
|
350
380
|
|
|
351
|
-
// Edge case: contrato vacío
|
|
352
381
|
if (args.length === 0 && contractTag.raw === "@contract()") {
|
|
353
|
-
errors.push("@contract() está vacío. Formato: @contract(in: tipo -> out: tipo @error: tipo)")
|
|
382
|
+
errors.push("@contract() está vacío. Formato: @contract(in: tipo -> out: tipo @error: tipo)")
|
|
354
383
|
}
|
|
355
384
|
|
|
356
385
|
// Edge case: sin input claro
|
|
@@ -358,153 +387,170 @@ export function validateAnnotations(annotations, validTags, options = {}) {
|
|
|
358
387
|
// Puede ser formato @contract(in: -> out:) sin inputs
|
|
359
388
|
if (contractText.includes("->")) {
|
|
360
389
|
// tiene flecha pero sin "in:" → intentamos extraer
|
|
361
|
-
const parts = contractText.split("->")
|
|
362
|
-
const inputPart = parts[0].replace("@contract(", "").replace("@contract", "").trim()
|
|
390
|
+
const parts = contractText.split("->")
|
|
391
|
+
const inputPart = parts[0].replace("@contract(", "").replace("@contract", "").trim()
|
|
363
392
|
if (!inputPart || inputPart === "in: " || inputPart === "in:") {
|
|
364
|
-
errors.push("@contract: falta definir tipos de entrada (in:)")
|
|
393
|
+
errors.push("@contract: falta definir tipos de entrada (in:)")
|
|
365
394
|
}
|
|
366
395
|
}
|
|
367
396
|
}
|
|
368
397
|
|
|
369
398
|
// Edge case: @contract(in: any -> out: any)
|
|
370
399
|
if (contractTag.raw.includes("any")) {
|
|
371
|
-
warnings.push("@contract usa 'any' — especifica tipos concretos")
|
|
400
|
+
warnings.push("@contract usa 'any' — especifica tipos concretos")
|
|
372
401
|
}
|
|
373
402
|
}
|
|
374
403
|
|
|
375
404
|
// Validar @props — edge case JSON inválido
|
|
376
|
-
const propsTag = tags.find((t) => t.name === "props")
|
|
405
|
+
const propsTag = tags.find((t) => t.name === "props")
|
|
377
406
|
if (propsTag) {
|
|
378
|
-
const hasInvalidJson = propsTag.args.some((a) => a.type === "invalid-json")
|
|
407
|
+
const hasInvalidJson = propsTag.args.some((a) => a.type === "invalid-json")
|
|
379
408
|
if (hasInvalidJson) {
|
|
380
|
-
errors.push(
|
|
409
|
+
errors.push(
|
|
410
|
+
`@props(${propsTag.args[0]?.value}) no es un JSON válido. Formato: @props({ key: type })`
|
|
411
|
+
)
|
|
381
412
|
}
|
|
382
413
|
}
|
|
383
414
|
|
|
384
415
|
// Validar @platform — edge case web + capacitor
|
|
385
|
-
const platformTag = tags.find((t) => t.name === "platform")
|
|
386
|
-
const depsTag = tags.find((t) => t.name === "deps")
|
|
416
|
+
const platformTag = tags.find((t) => t.name === "platform")
|
|
417
|
+
const depsTag = tags.find((t) => t.name === "deps")
|
|
387
418
|
if (platformTag && depsTag) {
|
|
388
|
-
const platforms = platformTag.args.map((a) => a.value)
|
|
389
|
-
const deps = depsTag.args
|
|
419
|
+
const platforms = platformTag.args.map((a) => a.value)
|
|
420
|
+
const deps = depsTag.args
|
|
390
421
|
|
|
391
422
|
if (platforms.includes("web")) {
|
|
392
|
-
const hasCapacitor = deps.some(
|
|
393
|
-
(d) => d.value && (String(d.value).includes("@capacitor"))
|
|
394
|
-
);
|
|
423
|
+
const hasCapacitor = deps.some((d) => d.value && String(d.value).includes("@capacitor"))
|
|
395
424
|
if (hasCapacitor) {
|
|
396
|
-
errors.push("@platform(web) no es compatible con @capacitor/*")
|
|
425
|
+
errors.push("@platform(web) no es compatible con @capacitor/*")
|
|
397
426
|
}
|
|
398
|
-
const hasTauri = deps.some(
|
|
399
|
-
(d) => d.value && String(d.value).includes("@tauri-apps")
|
|
400
|
-
);
|
|
427
|
+
const hasTauri = deps.some((d) => d.value && String(d.value).includes("@tauri-apps"))
|
|
401
428
|
if (hasTauri) {
|
|
402
|
-
errors.push("@platform(web) no es compatible con @tauri-apps/*")
|
|
429
|
+
errors.push("@platform(web) no es compatible con @tauri-apps/*")
|
|
403
430
|
}
|
|
404
431
|
}
|
|
405
432
|
|
|
406
433
|
if (platforms.includes("mobile")) {
|
|
407
|
-
const hasTauri = deps.some(
|
|
408
|
-
(d) => d.value && String(d.value).includes("@tauri-apps")
|
|
409
|
-
);
|
|
434
|
+
const hasTauri = deps.some((d) => d.value && String(d.value).includes("@tauri-apps"))
|
|
410
435
|
if (hasTauri) {
|
|
411
|
-
errors.push("@platform(mobile) no es compatible con @tauri-apps/*")
|
|
436
|
+
errors.push("@platform(mobile) no es compatible con @tauri-apps/*")
|
|
412
437
|
}
|
|
413
438
|
}
|
|
414
439
|
if (platforms.includes("desktop")) {
|
|
415
|
-
const hasCapacitor = deps.some(
|
|
416
|
-
(d) => d.value && String(d.value).includes("@capacitor")
|
|
417
|
-
);
|
|
440
|
+
const hasCapacitor = deps.some((d) => d.value && String(d.value).includes("@capacitor"))
|
|
418
441
|
if (hasCapacitor) {
|
|
419
|
-
errors.push("@platform(desktop) no es compatible con @capacitor/*")
|
|
442
|
+
errors.push("@platform(desktop) no es compatible con @capacitor/*")
|
|
420
443
|
}
|
|
421
444
|
}
|
|
422
445
|
}
|
|
423
446
|
|
|
424
447
|
// Validar @limit — edge cases de valores
|
|
425
|
-
const limitTag = tags.find((t) => t.name === "limit")
|
|
448
|
+
const limitTag = tags.find((t) => t.name === "limit")
|
|
426
449
|
if (limitTag) {
|
|
427
450
|
for (const arg of limitTag.args) {
|
|
428
451
|
if (arg.type === "kv" && arg.value !== undefined) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
errors.push(`@limit(${arg.key}: "${arg.value}") esperaba un número, recibió texto`);
|
|
452
|
+
if (!/^\d+(\.\d+)?$/.test(arg.value)) {
|
|
453
|
+
errors.push(`@limit(${arg.key}: "${arg.value}") esperaba un número, recibió texto`)
|
|
432
454
|
}
|
|
433
455
|
}
|
|
434
456
|
}
|
|
435
457
|
// Edge case: @limit() vacío
|
|
436
458
|
if (limitTag.args.length === 0) {
|
|
437
|
-
errors.push("@limit() está vacío. Especifica al menos un límite: @limit(lines: 120)")
|
|
459
|
+
errors.push("@limit() está vacío. Especifica al menos un límite: @limit(lines: 120)")
|
|
438
460
|
}
|
|
439
461
|
}
|
|
440
462
|
|
|
441
463
|
// Validar @forbidden
|
|
442
|
-
const forbiddenTag = tags.find((t) => t.name === "forbidden")
|
|
464
|
+
const forbiddenTag = tags.find((t) => t.name === "forbidden")
|
|
443
465
|
if (forbiddenTag) {
|
|
444
466
|
if (forbiddenTag.args.length === 0) {
|
|
445
|
-
warnings.push("@forbidden() está vacío — no prohíbe nada")
|
|
467
|
+
warnings.push("@forbidden() está vacío — no prohíbe nada")
|
|
446
468
|
}
|
|
447
469
|
}
|
|
448
470
|
|
|
449
471
|
// Validar @scope breaking
|
|
450
|
-
const scopeTag = tags.find((t) => t.name === "scope")
|
|
472
|
+
const scopeTag = tags.find((t) => t.name === "scope")
|
|
451
473
|
if (scopeTag) {
|
|
452
|
-
const breaking = scopeTag.args.find((a) => a.key === "breaking")
|
|
474
|
+
const breaking = scopeTag.args.find((a) => a.key === "breaking")
|
|
453
475
|
if (breaking && breaking.value === "true") {
|
|
454
|
-
warnings.push("@scope(breaking: true) — el commit debe indicar breaking change (!)")
|
|
476
|
+
warnings.push("@scope(breaking: true) — el commit debe indicar breaking change (!)")
|
|
455
477
|
}
|
|
456
478
|
}
|
|
457
479
|
|
|
458
480
|
// Validar @test
|
|
459
|
-
const testTag = tags.find((t) => t.name === "test")
|
|
481
|
+
const testTag = tags.find((t) => t.name === "test")
|
|
460
482
|
if (testTag) {
|
|
461
|
-
const coverage = testTag.args.find((a) => a.key === "coverage")
|
|
483
|
+
const coverage = testTag.args.find((a) => a.key === "coverage")
|
|
462
484
|
if (coverage) {
|
|
463
|
-
const val = parseInt(coverage.value)
|
|
485
|
+
const val = parseInt(coverage.value)
|
|
464
486
|
if (!isNaN(val) && val > 100) {
|
|
465
|
-
errors.push(`@test(@coverage: ${val}) no puede exceder 100`)
|
|
487
|
+
errors.push(`@test(@coverage: ${val}) no puede exceder 100`)
|
|
466
488
|
}
|
|
467
489
|
}
|
|
468
490
|
}
|
|
469
491
|
|
|
492
|
+
// Validar @pattern
|
|
493
|
+
const patternTag = tags.find((t) => t.name === "pattern")
|
|
494
|
+
if (patternTag) {
|
|
495
|
+
const validPatterns = [
|
|
496
|
+
"compound",
|
|
497
|
+
"composition",
|
|
498
|
+
"generic",
|
|
499
|
+
"render-prop",
|
|
500
|
+
"provider",
|
|
501
|
+
"singleton",
|
|
502
|
+
]
|
|
503
|
+
const patternValue = patternTag.args[0]?.value
|
|
504
|
+
|
|
505
|
+
if (!patternValue || patternValue.trim() === "") {
|
|
506
|
+
errors.push("@pattern() necesita un valor. Válidos: " + validPatterns.join(", "))
|
|
507
|
+
} else if (patternValue === "inheritance") {
|
|
508
|
+
warnings.push("@pattern(inheritance) — usa @pattern(composition) en su lugar")
|
|
509
|
+
} else if (!validPatterns.includes(patternValue)) {
|
|
510
|
+
warnings.push(
|
|
511
|
+
`@pattern("${patternValue}") no es un valor estándar. Válidos: ${validPatterns.join(", ")}`
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
470
516
|
// Validar @state error → debe tener @contract (solo en kinds que requieren contract)
|
|
471
|
-
const stateTag = tags.find((t) => t.name === "state")
|
|
517
|
+
const stateTag = tags.find((t) => t.name === "state")
|
|
472
518
|
if (stateTag) {
|
|
473
|
-
const hasError = stateTag.args.some((a) => a.value === "error")
|
|
474
|
-
const hasContract = tags.some((t) => t.name === "contract")
|
|
475
|
-
const kindValue = kindTag?.args[0]?.value
|
|
476
|
-
const contractKinds = ["hook", "service"]
|
|
519
|
+
const hasError = stateTag.args.some((a) => a.value === "error")
|
|
520
|
+
const hasContract = tags.some((t) => t.name === "contract")
|
|
521
|
+
const kindValue = kindTag?.args[0]?.value
|
|
522
|
+
const contractKinds = ["hook", "service"]
|
|
477
523
|
if (hasError && !hasContract && contractKinds.includes(kindValue)) {
|
|
478
|
-
warnings.push("@state(error) presente pero no hay @contract — define el tipo de error")
|
|
524
|
+
warnings.push("@state(error) presente pero no hay @contract — define el tipo de error")
|
|
479
525
|
}
|
|
480
526
|
}
|
|
481
527
|
|
|
482
|
-
return { errors, warnings }
|
|
528
|
+
return { errors, warnings }
|
|
483
529
|
}
|
|
484
530
|
|
|
485
531
|
// ─── API pública ────────────────────────────────────────────────────────────
|
|
486
532
|
|
|
487
|
-
export function lintFile(code, strict = false) {
|
|
488
|
-
const { annotations, validTags, parseErrors } = parseAnnotations(code)
|
|
533
|
+
export function lintFile(code, strict = false, options = {}) {
|
|
534
|
+
const { annotations, validTags, parseErrors } = parseAnnotations(code, options)
|
|
489
535
|
|
|
490
536
|
if (annotations.length === 0) {
|
|
491
|
-
return { annotations: [], errors: parseErrors, warnings: [] }
|
|
537
|
+
return { annotations: [], errors: parseErrors, warnings: [] }
|
|
492
538
|
}
|
|
493
539
|
|
|
494
|
-
const { errors, warnings } = validateAnnotations(annotations, validTags)
|
|
495
|
-
const allErrors = [...parseErrors, ...errors]
|
|
496
|
-
const allWarnings = [...warnings]
|
|
540
|
+
const { errors, warnings } = validateAnnotations(annotations, validTags)
|
|
541
|
+
const allErrors = [...parseErrors, ...errors]
|
|
542
|
+
const allWarnings = [...warnings]
|
|
497
543
|
|
|
498
544
|
if (strict && allWarnings.length > 0) {
|
|
499
|
-
allErrors.push(...allWarnings.map(w => `[strict] ${w}`))
|
|
500
|
-
return { annotations, errors: allErrors, warnings:
|
|
545
|
+
allErrors.push(...allWarnings.map((w) => `[strict] ${w}`))
|
|
546
|
+
return { annotations, errors: allErrors, warnings: allWarnings }
|
|
501
547
|
}
|
|
502
548
|
|
|
503
549
|
return {
|
|
504
550
|
annotations,
|
|
505
551
|
errors: allErrors,
|
|
506
552
|
warnings: allWarnings,
|
|
507
|
-
}
|
|
553
|
+
}
|
|
508
554
|
}
|
|
509
555
|
|
|
510
|
-
export { parseArgs }
|
|
556
|
+
export { parseArgs }
|