fastmode-mcp 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/README.md +561 -0
- package/bin/run.js +50 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +802 -0
- package/dist/lib/api-client.d.ts +81 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +237 -0
- package/dist/lib/auth-state.d.ts +13 -0
- package/dist/lib/auth-state.d.ts.map +1 -0
- package/dist/lib/auth-state.js +24 -0
- package/dist/lib/context-fetcher.d.ts +67 -0
- package/dist/lib/context-fetcher.d.ts.map +1 -0
- package/dist/lib/context-fetcher.js +190 -0
- package/dist/lib/credentials.d.ts +52 -0
- package/dist/lib/credentials.d.ts.map +1 -0
- package/dist/lib/credentials.js +196 -0
- package/dist/lib/device-flow.d.ts +14 -0
- package/dist/lib/device-flow.d.ts.map +1 -0
- package/dist/lib/device-flow.js +244 -0
- package/dist/tools/cms-items.d.ts +56 -0
- package/dist/tools/cms-items.d.ts.map +1 -0
- package/dist/tools/cms-items.js +376 -0
- package/dist/tools/create-site.d.ts +9 -0
- package/dist/tools/create-site.d.ts.map +1 -0
- package/dist/tools/create-site.js +202 -0
- package/dist/tools/deploy-package.d.ts +9 -0
- package/dist/tools/deploy-package.d.ts.map +1 -0
- package/dist/tools/deploy-package.js +434 -0
- package/dist/tools/generate-samples.d.ts +19 -0
- package/dist/tools/generate-samples.d.ts.map +1 -0
- package/dist/tools/generate-samples.js +272 -0
- package/dist/tools/get-conversion-guide.d.ts +7 -0
- package/dist/tools/get-conversion-guide.d.ts.map +1 -0
- package/dist/tools/get-conversion-guide.js +1323 -0
- package/dist/tools/get-example.d.ts +7 -0
- package/dist/tools/get-example.d.ts.map +1 -0
- package/dist/tools/get-example.js +1568 -0
- package/dist/tools/get-field-types.d.ts +30 -0
- package/dist/tools/get-field-types.d.ts.map +1 -0
- package/dist/tools/get-field-types.js +154 -0
- package/dist/tools/get-schema.d.ts +5 -0
- package/dist/tools/get-schema.d.ts.map +1 -0
- package/dist/tools/get-schema.js +320 -0
- package/dist/tools/get-started.d.ts +21 -0
- package/dist/tools/get-started.d.ts.map +1 -0
- package/dist/tools/get-started.js +624 -0
- package/dist/tools/get-tenant-schema.d.ts +18 -0
- package/dist/tools/get-tenant-schema.d.ts.map +1 -0
- package/dist/tools/get-tenant-schema.js +158 -0
- package/dist/tools/list-projects.d.ts +5 -0
- package/dist/tools/list-projects.d.ts.map +1 -0
- package/dist/tools/list-projects.js +101 -0
- package/dist/tools/sync-schema.d.ts +41 -0
- package/dist/tools/sync-schema.d.ts.map +1 -0
- package/dist/tools/sync-schema.js +483 -0
- package/dist/tools/validate-manifest.d.ts +5 -0
- package/dist/tools/validate-manifest.d.ts.map +1 -0
- package/dist/tools/validate-manifest.js +311 -0
- package/dist/tools/validate-package.d.ts +5 -0
- package/dist/tools/validate-package.d.ts.map +1 -0
- package/dist/tools/validate-package.js +337 -0
- package/dist/tools/validate-template.d.ts +12 -0
- package/dist/tools/validate-template.d.ts.map +1 -0
- package/dist/tools/validate-template.js +790 -0
- package/package.json +54 -0
- package/scripts/postinstall.js +129 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateTemplate = validateTemplate;
|
|
4
|
+
const api_client_1 = require("../lib/api-client");
|
|
5
|
+
// Common wrong field names and suggestions (generic)
|
|
6
|
+
const COMMON_SUGGESTIONS = {
|
|
7
|
+
'body': 'Consider using a descriptive name like "content" or "description"',
|
|
8
|
+
'content': 'Good field name - make sure it exists in your collection',
|
|
9
|
+
'image': 'Consider using a descriptive name like "heroImage", "thumbnail", etc.',
|
|
10
|
+
'link': 'Use "url" as the field name for links',
|
|
11
|
+
'href': 'Use "url" as the field name for links',
|
|
12
|
+
'date': 'Use "publishedAt" (auto-tracked) or define a custom date field',
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Resolve project identifier to tenant ID
|
|
16
|
+
*/
|
|
17
|
+
async function resolveProjectId(projectIdentifier) {
|
|
18
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
19
|
+
if (uuidPattern.test(projectIdentifier)) {
|
|
20
|
+
return { tenantId: projectIdentifier };
|
|
21
|
+
}
|
|
22
|
+
const response = await (0, api_client_1.apiRequest)('/api/tenants');
|
|
23
|
+
if ((0, api_client_1.isApiError)(response)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const match = response.data.find(p => p.name.toLowerCase() === projectIdentifier.toLowerCase() ||
|
|
27
|
+
p.name.toLowerCase().includes(projectIdentifier.toLowerCase()));
|
|
28
|
+
return match ? { tenantId: match.id } : null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fetch tenant schema for validation
|
|
32
|
+
*/
|
|
33
|
+
async function fetchTenantSchema(tenantId) {
|
|
34
|
+
// Fetch custom collections
|
|
35
|
+
const collectionsRes = await (0, api_client_1.apiRequest)('/api/collections', { tenantId });
|
|
36
|
+
if ((0, api_client_1.isApiError)(collectionsRes)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
collections: collectionsRes.data,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Extract field names used in template tokens
|
|
45
|
+
*/
|
|
46
|
+
function extractTokenFieldNames(html) {
|
|
47
|
+
const fields = new Set();
|
|
48
|
+
// Match {{fieldName}} and {{{fieldName}}}
|
|
49
|
+
const doubleTokens = html.match(/\{\{([^{}#/][^{}]*)\}\}/g) || [];
|
|
50
|
+
const tripleTokens = html.match(/\{\{\{([^{}]+)\}\}\}/g) || [];
|
|
51
|
+
for (const token of [...doubleTokens, ...tripleTokens]) {
|
|
52
|
+
const fieldName = token.replace(/\{|\}/g, '').trim();
|
|
53
|
+
// Skip special tokens
|
|
54
|
+
if (fieldName.startsWith('#') || fieldName.startsWith('/') ||
|
|
55
|
+
fieldName === 'else' || fieldName.startsWith('@') ||
|
|
56
|
+
fieldName.startsWith('this') || fieldName.startsWith('../')) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// Get the base field name (before any dots)
|
|
60
|
+
const baseName = fieldName.split('.')[0];
|
|
61
|
+
if (baseName && baseName !== 'site') {
|
|
62
|
+
fields.add(baseName);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return Array.from(fields);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check which fields are missing from schema
|
|
69
|
+
*/
|
|
70
|
+
function findMissingFields(usedFields, collectionType, schema) {
|
|
71
|
+
const missing = [];
|
|
72
|
+
// Check against custom collection fields
|
|
73
|
+
const collection = schema.collections.find(c => c.slug === collectionType);
|
|
74
|
+
if (collection) {
|
|
75
|
+
// Built-in fields available on all items
|
|
76
|
+
const builtInFields = ['name', 'slug', 'url', 'publishedAt', 'createdAt', 'updatedAt'];
|
|
77
|
+
const collectionFields = [...builtInFields, ...collection.fields.map(f => f.slug)];
|
|
78
|
+
for (const field of usedFields) {
|
|
79
|
+
if (!collectionFields.includes(field)) {
|
|
80
|
+
missing.push(field);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Collection doesn't exist - all fields except built-ins are "missing"
|
|
86
|
+
const builtInFields = ['name', 'slug', 'url', 'publishedAt', 'createdAt', 'updatedAt'];
|
|
87
|
+
return usedFields.filter(f => !builtInFields.includes(f));
|
|
88
|
+
}
|
|
89
|
+
return missing;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get richText fields from collection
|
|
93
|
+
*/
|
|
94
|
+
function getRichTextFields(collectionSlug, schema) {
|
|
95
|
+
const collection = schema.collections.find(c => c.slug === collectionSlug);
|
|
96
|
+
if (!collection)
|
|
97
|
+
return [];
|
|
98
|
+
return collection.fields.filter(f => f.type === 'richText').map(f => f.slug);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Validate forms in HTML templates
|
|
102
|
+
* Checks for proper data-form attribute, input names, and submit buttons
|
|
103
|
+
*/
|
|
104
|
+
function validateForms(html) {
|
|
105
|
+
const errors = [];
|
|
106
|
+
const warnings = [];
|
|
107
|
+
const suggestions = [];
|
|
108
|
+
// Find all form elements
|
|
109
|
+
const formPattern = /<form[^>]*>/gi;
|
|
110
|
+
const forms = html.match(formPattern) || [];
|
|
111
|
+
if (forms.length === 0) {
|
|
112
|
+
return { errors, warnings, suggestions };
|
|
113
|
+
}
|
|
114
|
+
// Track forms for validation
|
|
115
|
+
let formsWithDataForm = 0;
|
|
116
|
+
let formsWithLegacyAttr = 0;
|
|
117
|
+
for (const formTag of forms) {
|
|
118
|
+
// Check for data-form attribute (correct format)
|
|
119
|
+
const hasDataForm = /data-form=["'][^"']+["']/i.test(formTag);
|
|
120
|
+
// Check for legacy data-form-name attribute
|
|
121
|
+
const hasLegacyDataFormName = /data-form-name=["'][^"']+["']/i.test(formTag);
|
|
122
|
+
if (hasDataForm) {
|
|
123
|
+
formsWithDataForm++;
|
|
124
|
+
// Check if form has proper action attribute pointing to /_forms/
|
|
125
|
+
if (!formTag.includes('action=')) {
|
|
126
|
+
errors.push(`- Form has data-form but no action attribute. Add action="/_forms/formname" for the form to work.`);
|
|
127
|
+
}
|
|
128
|
+
else if (!formTag.includes('/_forms/')) {
|
|
129
|
+
warnings.push(`- Form has action but it doesn't point to /_forms/. Fast Mode forms must submit to /_forms/{formName}.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (hasLegacyDataFormName) {
|
|
133
|
+
formsWithLegacyAttr++;
|
|
134
|
+
warnings.push(`- Form uses deprecated data-form-name attribute. Migrate to data-form="formname" for consistency.`);
|
|
135
|
+
// Also check action for legacy forms
|
|
136
|
+
if (!formTag.includes('action=')) {
|
|
137
|
+
errors.push(`- Form has data-form-name but no action attribute. Add action="/_forms/formname" for the form to work.`);
|
|
138
|
+
}
|
|
139
|
+
else if (!formTag.includes('/_forms/')) {
|
|
140
|
+
warnings.push(`- Form has action but it doesn't point to /_forms/. Fast Mode forms must submit to /_forms/{formName}.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
// Form without any data-form attribute - check if it looks like a CMS form
|
|
145
|
+
// Don't error on forms that might be external (like search forms, login forms)
|
|
146
|
+
if (!formTag.includes('action=')) {
|
|
147
|
+
errors.push(`- Form is missing data-form attribute. Add data-form="formname" and action="/_forms/formname" to identify the form for CMS submission.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Extract form content for deeper validation
|
|
152
|
+
// Find each form block
|
|
153
|
+
const formBlocks = html.match(/<form[^>]*data-form[^>]*>[\s\S]*?<\/form>/gi) || [];
|
|
154
|
+
for (const formBlock of formBlocks) {
|
|
155
|
+
// Check for inputs with name attributes
|
|
156
|
+
const inputs = formBlock.match(/<input[^>]*>/gi) || [];
|
|
157
|
+
const textareas = formBlock.match(/<textarea[^>]*>/gi) || [];
|
|
158
|
+
const selects = formBlock.match(/<select[^>]*>/gi) || [];
|
|
159
|
+
const allInputElements = [...inputs, ...textareas, ...selects];
|
|
160
|
+
let inputsWithName = 0;
|
|
161
|
+
let inputsWithoutName = 0;
|
|
162
|
+
for (const input of allInputElements) {
|
|
163
|
+
// Skip hidden inputs and submit buttons
|
|
164
|
+
if (/type=["'](?:submit|button|hidden)["']/i.test(input)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (/name=["'][^"']+["']/i.test(input)) {
|
|
168
|
+
inputsWithName++;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
inputsWithoutName++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (inputsWithoutName > 0) {
|
|
175
|
+
errors.push(`- Found ${inputsWithoutName} form input(s) without name attribute. All inputs must have name="fieldname" to be captured.`);
|
|
176
|
+
}
|
|
177
|
+
if (inputsWithName === 0 && allInputElements.length > 0) {
|
|
178
|
+
errors.push(`- Form has no inputs with name attributes - no data will be captured.`);
|
|
179
|
+
}
|
|
180
|
+
// Check for submit button
|
|
181
|
+
const hasSubmitButton = /<button[^>]*type=["']submit["'][^>]*>/i.test(formBlock) ||
|
|
182
|
+
/<input[^>]*type=["']submit["'][^>]*>/i.test(formBlock) ||
|
|
183
|
+
/<button[^>]*>(?!.*type=["']button["'])/i.test(formBlock); // button without type defaults to submit
|
|
184
|
+
if (!hasSubmitButton) {
|
|
185
|
+
warnings.push(`- Form may be missing a submit button. Add <button type="submit">Submit</button> for the form to work.`);
|
|
186
|
+
}
|
|
187
|
+
// Check for form handler script
|
|
188
|
+
const hasFormHandlerScript = html.includes("form[data-form]") || html.includes("form[data-form-name]");
|
|
189
|
+
if (!hasFormHandlerScript) {
|
|
190
|
+
suggestions.push(`- No form handler script detected. Make sure to include JavaScript that handles form submission to /_forms/{formName}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Summary suggestion
|
|
194
|
+
if (formsWithDataForm > 0) {
|
|
195
|
+
suggestions.push(`- Found ${formsWithDataForm} form(s) with data-form attribute - forms will submit to /_forms/{formName}`);
|
|
196
|
+
}
|
|
197
|
+
// Check for thank-you page redirect pattern
|
|
198
|
+
const hasThankYouRedirect = /window\.location\.href\s*=\s*['"]\/thank-you['"]/i.test(html) ||
|
|
199
|
+
/window\.location\s*=\s*['"]\/thank-you['"]/i.test(html);
|
|
200
|
+
if (formsWithDataForm > 0 && !hasThankYouRedirect) {
|
|
201
|
+
suggestions.push(`- Consider adding a /thank-you page and redirecting there on successful submission for better UX`);
|
|
202
|
+
}
|
|
203
|
+
return { errors, warnings, suggestions };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Extract collection slugs referenced in {{#each}} loops on static pages
|
|
207
|
+
* Supports both {{#each collection}} and {{#each @root.collection}} syntax
|
|
208
|
+
*/
|
|
209
|
+
function extractCollectionReferences(html) {
|
|
210
|
+
const collections = [];
|
|
211
|
+
// Match both {{#each collection}} and {{#each @root.collection}}
|
|
212
|
+
const eachLoops = html.match(/\{\{#each\s+(?:@root\.)?([\w]+)[^}]*\}\}/g) || [];
|
|
213
|
+
for (const loop of eachLoops) {
|
|
214
|
+
// Extract collection name, handling @root. prefix
|
|
215
|
+
const match = loop.match(/\{\{#each\s+(?:@root\.)?([\w]+)/);
|
|
216
|
+
if (match) {
|
|
217
|
+
collections.push(match[1]);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return [...new Set(collections)]; // Unique collections
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Validate that loop variables (@first, @last, @index) are only used inside {{#each}} blocks
|
|
224
|
+
* Returns warnings for any usage found outside loops
|
|
225
|
+
*/
|
|
226
|
+
function validateLoopVariables(html) {
|
|
227
|
+
const warnings = [];
|
|
228
|
+
// Find all @first, @last, @index, @length usage (standalone or in conditionals)
|
|
229
|
+
const loopVarPattern = /\{\{[#/]?(?:if|unless)?\s*@(first|last|index|length)[^}]*\}\}/g;
|
|
230
|
+
// Find all {{#each}}...{{/each}} blocks
|
|
231
|
+
// Use a simple approach: remove all loop content and check what's left
|
|
232
|
+
let outsideLoops = html;
|
|
233
|
+
// Repeatedly remove innermost loops until none left
|
|
234
|
+
// This handles nested loops correctly
|
|
235
|
+
let previousLength = 0;
|
|
236
|
+
while (outsideLoops.length !== previousLength) {
|
|
237
|
+
previousLength = outsideLoops.length;
|
|
238
|
+
// Match non-greedy: find {{#each...}} followed by content without nested {{#each}}, then {{/each}}
|
|
239
|
+
outsideLoops = outsideLoops.replace(/\{\{#each[^}]*\}\}(?:(?!\{\{#each)[\s\S])*?\{\{\/each\}\}/g, '');
|
|
240
|
+
}
|
|
241
|
+
// Now check if any loop variables remain in the content outside loops
|
|
242
|
+
const matches = outsideLoops.match(loopVarPattern);
|
|
243
|
+
if (matches && matches.length > 0) {
|
|
244
|
+
const uniqueVars = [...new Set(matches.map(m => {
|
|
245
|
+
const varMatch = m.match(/@(first|last|index|length)/);
|
|
246
|
+
return varMatch ? `@${varMatch[1]}` : m;
|
|
247
|
+
}))];
|
|
248
|
+
warnings.push(`- Loop variables (${uniqueVars.join(', ')}) found outside {{#each}} blocks. These only work inside loops.`);
|
|
249
|
+
}
|
|
250
|
+
return warnings;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Validates an HTML template for correct CMS token usage
|
|
254
|
+
*
|
|
255
|
+
* @param html - The HTML template content
|
|
256
|
+
* @param templateType - The type of template (custom_index, custom_detail, static_page)
|
|
257
|
+
* @param collectionSlug - For custom collections, the collection slug
|
|
258
|
+
* @param projectId - Optional: Project ID or name to validate against actual schema
|
|
259
|
+
*/
|
|
260
|
+
async function validateTemplate(html, templateType, collectionSlug, projectId) {
|
|
261
|
+
const errors = [];
|
|
262
|
+
const warnings = [];
|
|
263
|
+
const suggestions = [];
|
|
264
|
+
let richTextFields = [];
|
|
265
|
+
// Extract all tokens from the HTML
|
|
266
|
+
const doubleTokens = html.match(/\{\{([^{}#/]+)\}\}/g) || [];
|
|
267
|
+
const tripleTokens = html.match(/\{\{\{([^{}]+)\}\}\}/g) || [];
|
|
268
|
+
// Match both {{#each collection}} and {{#each @root.collection}}
|
|
269
|
+
const eachLoops = html.match(/\{\{#each\s+(?:@root\.)?([\w]+)[^}]*\}\}/g) || [];
|
|
270
|
+
const conditionals = html.match(/\{\{#if\s+([^}]+)\}\}/g) || [];
|
|
271
|
+
// Check for tokens with spaces (like {{ name }} instead of {{name}})
|
|
272
|
+
const tokensWithSpaces = html.match(/\{\{\s+[^{}]+\s*\}\}/g) || [];
|
|
273
|
+
const tokensWithSpaces2 = html.match(/\{\{[^{}]+\s+\}\}/g) || [];
|
|
274
|
+
if (tokensWithSpaces.length > 0 || tokensWithSpaces2.length > 0) {
|
|
275
|
+
warnings.push('- Some tokens have spaces inside braces (e.g., {{ name }} instead of {{name}}). While supported, {{name}} without spaces is preferred.');
|
|
276
|
+
}
|
|
277
|
+
// Check for common suggestions
|
|
278
|
+
for (const token of doubleTokens) {
|
|
279
|
+
const fieldName = token.replace(/\{\{|\}\}/g, '').trim();
|
|
280
|
+
// Skip control structures
|
|
281
|
+
if (fieldName.startsWith('#') || fieldName.startsWith('/') || fieldName === 'else' || fieldName.startsWith('@')) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
// Check against common suggestions
|
|
285
|
+
const baseName = fieldName.split('.')[0];
|
|
286
|
+
if (COMMON_SUGGESTIONS[baseName]) {
|
|
287
|
+
suggestions.push(`- Token {{${baseName}}}: ${COMMON_SUGGESTIONS[baseName]}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Check loop syntax for index templates
|
|
291
|
+
if (templateType === 'custom_index') {
|
|
292
|
+
if (eachLoops.length === 0) {
|
|
293
|
+
errors.push(`MISSING LOOP: Index templates MUST have an {{#each}} loop.
|
|
294
|
+
|
|
295
|
+
An index template without a loop will show an empty page.
|
|
296
|
+
|
|
297
|
+
REQUIRED pattern:
|
|
298
|
+
{{#each ${collectionSlug || 'collection'}}}
|
|
299
|
+
<div>
|
|
300
|
+
<h2>{{name}}</h2>
|
|
301
|
+
<a href="{{url}}">Read more</a>
|
|
302
|
+
</div>
|
|
303
|
+
{{/each}}
|
|
304
|
+
|
|
305
|
+
Add an {{#each}} loop to iterate over collection items.`);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// Check if loop uses correct collection name (handles @root. prefix)
|
|
309
|
+
for (const loop of eachLoops) {
|
|
310
|
+
const match = loop.match(/\{\{#each\s+(?:@root\.)?([\w]+)/);
|
|
311
|
+
if (match) {
|
|
312
|
+
const usedCollection = match[1];
|
|
313
|
+
if (collectionSlug && usedCollection !== collectionSlug) {
|
|
314
|
+
errors.push(`WRONG COLLECTION: Loop uses '${usedCollection}' but template is for '${collectionSlug}'.
|
|
315
|
+
|
|
316
|
+
Change {{#each ${usedCollection}}} to {{#each ${collectionSlug}}}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Check detail templates have CMS tokens (otherwise all item pages will be identical)
|
|
323
|
+
if (templateType === 'custom_detail') {
|
|
324
|
+
const hasItemTokens = /\{\{(name|slug|title|content|body|description)\}\}/i.test(html) ||
|
|
325
|
+
/\{\{\{[^}]+\}\}\}/i.test(html) ||
|
|
326
|
+
doubleTokens.length > 0;
|
|
327
|
+
if (!hasItemTokens) {
|
|
328
|
+
errors.push(`MISSING ITEM DATA: Detail template has no CMS tokens.
|
|
329
|
+
|
|
330
|
+
A detail template should display item data like:
|
|
331
|
+
- {{name}} - Item name
|
|
332
|
+
- {{{content}}} - Rich text content (triple braces!)
|
|
333
|
+
- {{image}} - Image URL
|
|
334
|
+
|
|
335
|
+
Without tokens, every item page will show the same static content.`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Check for {{#each}} without closing {{/each}}
|
|
339
|
+
const eachOpens = (html.match(/\{\{#each/g) || []).length;
|
|
340
|
+
const eachCloses = (html.match(/\{\{\/each\}\}/g) || []).length;
|
|
341
|
+
if (eachOpens !== eachCloses) {
|
|
342
|
+
errors.push(`- Unbalanced {{#each}} loops: ${eachOpens} opens, ${eachCloses} closes`);
|
|
343
|
+
}
|
|
344
|
+
// Check for {{#if}} without closing {{/if}}
|
|
345
|
+
const ifOpens = (html.match(/\{\{#if/g) || []).length;
|
|
346
|
+
const ifCloses = (html.match(/\{\{\/if\}\}/g) || []).length;
|
|
347
|
+
if (ifOpens !== ifCloses) {
|
|
348
|
+
errors.push(`- Unbalanced {{#if}} conditionals: ${ifOpens} opens, ${ifCloses} closes`);
|
|
349
|
+
}
|
|
350
|
+
// Check for {{#unless}} without closing
|
|
351
|
+
const unlessOpens = (html.match(/\{\{#unless/g) || []).length;
|
|
352
|
+
const unlessCloses = (html.match(/\{\{\/unless\}\}/g) || []).length;
|
|
353
|
+
if (unlessOpens !== unlessCloses) {
|
|
354
|
+
errors.push(`- Unbalanced {{#unless}}: ${unlessOpens} opens, ${unlessCloses} closes`);
|
|
355
|
+
}
|
|
356
|
+
// Check for loop variables (@first, @last, @index) used outside {{#each}} blocks
|
|
357
|
+
const loopVarWarnings = validateLoopVariables(html);
|
|
358
|
+
for (const warning of loopVarWarnings) {
|
|
359
|
+
warnings.push(warning);
|
|
360
|
+
}
|
|
361
|
+
// Check for data-edit-key in static pages (IMPORTANT for inline editing)
|
|
362
|
+
if (templateType === 'static_page') {
|
|
363
|
+
const editKeys = html.match(/data-edit-key="[^"]+"/g) || [];
|
|
364
|
+
// Check if page has meaningful text content but no edit keys
|
|
365
|
+
const hasHeadings = /<h[1-6][^>]*>[^<]+<\/h[1-6]>/gi.test(html);
|
|
366
|
+
const hasParagraphs = /<p[^>]*>[^<]+<\/p>/gi.test(html);
|
|
367
|
+
if (editKeys.length === 0 && (hasHeadings || hasParagraphs)) {
|
|
368
|
+
errors.push(`MISSING INLINE EDITING: Static page has no data-edit-key attributes.
|
|
369
|
+
|
|
370
|
+
Without data-edit-key, users CANNOT edit text content in the visual editor!
|
|
371
|
+
|
|
372
|
+
WRONG (no inline editing):
|
|
373
|
+
<h1>Welcome</h1>
|
|
374
|
+
<p>Introduction text here.</p>
|
|
375
|
+
|
|
376
|
+
CORRECT (enables inline editing):
|
|
377
|
+
<h1 data-edit-key="hero-title">Welcome</h1>
|
|
378
|
+
<p data-edit-key="hero-subtitle">Introduction text here.</p>
|
|
379
|
+
|
|
380
|
+
REQUIRED: Add data-edit-key="unique-name" to each text element that should be editable.
|
|
381
|
+
Every heading, paragraph, button label, etc. needs its own unique key.`);
|
|
382
|
+
}
|
|
383
|
+
else if (editKeys.length > 0) {
|
|
384
|
+
suggestions.push(`- Found ${editKeys.length} inline editing point(s) with data-edit-key`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// Also check CMS templates for data-edit-key on hardcoded content
|
|
388
|
+
if (templateType === 'custom_index' || templateType === 'custom_detail') {
|
|
389
|
+
const editKeys = html.match(/data-edit-key="[^"]+"/g) || [];
|
|
390
|
+
// Check for non-CMS content that might be editable (like page headers outside loops)
|
|
391
|
+
const outsideLoopContent = html.replace(/\{\{#each[\s\S]*?\{\{\/each\}\}/g, '');
|
|
392
|
+
const hasHardcodedHeadings = /<h[1-6][^>]*>(?!.*\{\{)[^<]+<\/h[1-6]>/gi.test(outsideLoopContent);
|
|
393
|
+
if (hasHardcodedHeadings && editKeys.length === 0) {
|
|
394
|
+
suggestions.push(`- Consider adding data-edit-key to static text in templates (like page headers) to make them editable`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Check asset paths
|
|
398
|
+
const wrongAssetPaths = html.match(/(href|src)=["'](?!\/public\/|https?:\/\/|#|\/\?|mailto:|tel:)[^"']+["']/g) || [];
|
|
399
|
+
for (const path of wrongAssetPaths.slice(0, 5)) { // Limit to 5 examples
|
|
400
|
+
if (!path.includes('{{') && !path.includes('data:')) {
|
|
401
|
+
warnings.push(`- Asset path may need /public/ prefix: ${path}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (wrongAssetPaths.length > 5) {
|
|
405
|
+
warnings.push(`- ...and ${wrongAssetPaths.length - 5} more asset paths that may need /public/ prefix`);
|
|
406
|
+
}
|
|
407
|
+
// ============ Smart Field Detection (Suggestions) ============
|
|
408
|
+
// Detect YouTube/Vimeo URLs that could benefit from videoEmbed field type
|
|
409
|
+
const videoPatterns = [/youtube\.com\/watch/i, /youtu\.be\//i, /vimeo\.com\//i, /wistia\.com\//i, /loom\.com\//i];
|
|
410
|
+
let videoPatternFound = false;
|
|
411
|
+
for (const pattern of videoPatterns) {
|
|
412
|
+
if (pattern.test(html)) {
|
|
413
|
+
videoPatternFound = true;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (videoPatternFound) {
|
|
418
|
+
suggestions.push(`TIP: Video content detected. For CMS-managed videos, use the "videoEmbed" field type with:
|
|
419
|
+
{{#videoEmbed videoFieldName}}{{/videoEmbed}}
|
|
420
|
+
This creates responsive iframes with correct YouTube settings.`);
|
|
421
|
+
}
|
|
422
|
+
// Check for tokens that look like video fields but might be using wrong helper
|
|
423
|
+
const videoTokensWithoutHelper = html.match(/\{\{(?!#videoEmbed)([^}]*video[^}]*)\}\}/gi) || [];
|
|
424
|
+
if (videoTokensWithoutHelper.length > 0 && !html.includes('{{#videoEmbed')) {
|
|
425
|
+
const tokens = videoTokensWithoutHelper.slice(0, 3).map(t => t.replace(/\{|\}/g, '')).join(', ');
|
|
426
|
+
suggestions.push(`TIP: Found video-related token(s): ${tokens}. If these are video URLs, consider using:
|
|
427
|
+
{{#videoEmbed fieldName}}{{/videoEmbed}}
|
|
428
|
+
This outputs a responsive iframe instead of just the URL.`);
|
|
429
|
+
}
|
|
430
|
+
// Detect dot notation that suggests relation fields
|
|
431
|
+
const dotNotationTokens = html.match(/\{\{(\w+)\.(\w+)\}\}/g) || [];
|
|
432
|
+
const relationHints = new Set();
|
|
433
|
+
for (const token of dotNotationTokens) {
|
|
434
|
+
const match = token.match(/\{\{(\w+)\.(\w+)\}\}/);
|
|
435
|
+
if (match && !['site', 'this', 'root'].includes(match[1])) {
|
|
436
|
+
relationHints.add(match[1]);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (relationHints.size > 0) {
|
|
440
|
+
const fields = Array.from(relationHints).slice(0, 3).join(', ');
|
|
441
|
+
suggestions.push(`RELATION FIELDS: Using ${fields} with dot notation (e.g., {{author.name}}).
|
|
442
|
+
Make sure these are created as "relation" type fields with sync_schema, not "text".
|
|
443
|
+
Relation fields link to items in another collection.`);
|
|
444
|
+
}
|
|
445
|
+
// Check YouTube iframes for required attributes
|
|
446
|
+
const allIframes = html.match(/<iframe[^>]*>/gi) || [];
|
|
447
|
+
for (const iframe of allIframes) {
|
|
448
|
+
const isYouTubeEmbed = /youtube\.com|youtu\.be/i.test(iframe);
|
|
449
|
+
const hasTemplateSrc = /src=["'][^"']*\{\{[^}]+\}\}[^"']*["']/i.test(iframe);
|
|
450
|
+
if (isYouTubeEmbed || hasTemplateSrc) {
|
|
451
|
+
if (!/referrerpolicy/i.test(iframe)) {
|
|
452
|
+
errors.push(`- YouTube iframe missing referrerpolicy attribute. Add: referrerpolicy="strict-origin-when-cross-origin" (without this, YouTube will show Error 153)`);
|
|
453
|
+
}
|
|
454
|
+
if (!/allowfullscreen/i.test(iframe)) {
|
|
455
|
+
warnings.push(`- iframe missing allowfullscreen attribute - fullscreen button won't work`);
|
|
456
|
+
}
|
|
457
|
+
if (!/title=/i.test(iframe)) {
|
|
458
|
+
suggestions.push(`- Consider adding a title attribute to iframe for accessibility`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// ============ Form Validation ============
|
|
463
|
+
const formValidation = validateForms(html);
|
|
464
|
+
errors.push(...formValidation.errors);
|
|
465
|
+
warnings.push(...formValidation.warnings);
|
|
466
|
+
suggestions.push(...formValidation.suggestions);
|
|
467
|
+
// Validate site tokens
|
|
468
|
+
const siteTokens = html.match(/\{\{site\.(\w+)\}\}/g) || [];
|
|
469
|
+
for (const token of siteTokens) {
|
|
470
|
+
const fieldMatch = token.match(/\{\{site\.(\w+)\}\}/);
|
|
471
|
+
if (fieldMatch) {
|
|
472
|
+
const field = fieldMatch[1];
|
|
473
|
+
const validSiteFields = ['site_name', 'siteName', 'name'];
|
|
474
|
+
if (!validSiteFields.includes(field)) {
|
|
475
|
+
warnings.push(`- Unknown site token: ${token} - valid fields are: site_name, siteName, name`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (siteTokens.length > 0) {
|
|
480
|
+
suggestions.push(`- Found ${siteTokens.length} site token(s) like {{site.site_name}} - these come from manifest.json`);
|
|
481
|
+
}
|
|
482
|
+
// Validate parent context references (../)
|
|
483
|
+
const parentRefs = html.match(/\{\{\.\.\/([\w.]+)\}\}/g) || [];
|
|
484
|
+
if (parentRefs.length > 0) {
|
|
485
|
+
if (eachLoops.length === 0) {
|
|
486
|
+
warnings.push(`- Found ${parentRefs.length} parent reference(s) like {{../name}} but no {{#each}} loop - these only work inside loops`);
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
suggestions.push(`- Found ${parentRefs.length} parent context reference(s) ({{../fieldName}}) - accesses parent scope in loops`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Validate nested loops with parent context in where clause
|
|
493
|
+
// Supports both {{#each collection}} and {{#each @root.collection}}
|
|
494
|
+
const nestedLoopPattern = /\{\{#each\s+(?:@root\.)?(\w+)\s+[^}]*where="[^:]+:\{\{(\w+)\}\}"[^}]*\}\}/g;
|
|
495
|
+
const nestedLoops = html.match(nestedLoopPattern) || [];
|
|
496
|
+
if (nestedLoops.length > 0) {
|
|
497
|
+
suggestions.push(`- Found ${nestedLoops.length} nested loop(s) with parent context filtering (e.g., where="field:{{parentField}}")`);
|
|
498
|
+
// Check if there's an outer loop to provide context
|
|
499
|
+
// Match both {{#each collection}} and {{#each @root.collection}}
|
|
500
|
+
const allLoopMatches = html.match(/\{\{#each\s+(?:@root\.)?\w+/g) || [];
|
|
501
|
+
if (allLoopMatches.length < 2 && nestedLoops.length > 0) {
|
|
502
|
+
warnings.push(`- Nested loop with where="...{{field}}" found but no outer loop detected. The {{field}} needs to come from an outer loop's item.`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// Detect common nested loop patterns
|
|
506
|
+
const hierarchicalPatterns = [
|
|
507
|
+
{ outer: 'categories', inner: 'posts', relation: 'category' },
|
|
508
|
+
{ outer: 'doc_categories', inner: 'doc_pages', relation: 'category' },
|
|
509
|
+
{ outer: 'authors', inner: 'posts', relation: 'author' },
|
|
510
|
+
{ outer: 'tags', inner: 'posts', relation: 'tags' },
|
|
511
|
+
];
|
|
512
|
+
for (const pattern of hierarchicalPatterns) {
|
|
513
|
+
const outerMatch = new RegExp(`\\{\\{#each\\s+${pattern.outer}`, 'g').test(html);
|
|
514
|
+
const innerMatch = new RegExp(`\\{\\{#each\\s+${pattern.inner}`, 'g').test(html);
|
|
515
|
+
if (outerMatch && innerMatch) {
|
|
516
|
+
// Check if inner loop has proper where clause
|
|
517
|
+
const innerWithWhere = new RegExp(`\\{\\{#each\\s+${pattern.inner}\\s+[^}]*where=`, 'g').test(html);
|
|
518
|
+
if (!innerWithWhere) {
|
|
519
|
+
warnings.push(`- Nested loops: ${pattern.outer} → ${pattern.inner} detected. Consider adding where="${pattern.relation}.slug:{{slug}}" to filter ${pattern.inner} by parent ${pattern.outer}.`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// Validate equality helper syntax
|
|
524
|
+
const eqHelpers = html.match(/\{\{#if\s+\(eq\s+[^)]+\)\s*\}\}/g) || [];
|
|
525
|
+
if (eqHelpers.length > 0) {
|
|
526
|
+
suggestions.push(`- Found ${eqHelpers.length} equality comparison(s) like {{#if (eq field1 field2)}} - compares two values`);
|
|
527
|
+
for (const helper of eqHelpers) {
|
|
528
|
+
if (!html.includes('{{/if}}')) {
|
|
529
|
+
errors.push(`- Missing {{/if}} to close: ${helper}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Validate {{#eq}} blocks
|
|
534
|
+
const eqBlocks = html.match(/\{\{#eq\s+[\w.]+\s+"[^"]+"\s*\}\}/g) || [];
|
|
535
|
+
const eqCloses = (html.match(/\{\{\/eq\}\}/g) || []).length;
|
|
536
|
+
if (eqBlocks.length !== eqCloses) {
|
|
537
|
+
errors.push(`- Unbalanced {{#eq}}: ${eqBlocks.length} opens, ${eqCloses} closes`);
|
|
538
|
+
}
|
|
539
|
+
// ============ Schema Validation (if authenticated with projectId) ============
|
|
540
|
+
let schemaValidation = '';
|
|
541
|
+
if (projectId && !(await (0, api_client_1.needsAuthentication)())) {
|
|
542
|
+
// Resolve project
|
|
543
|
+
const resolved = await resolveProjectId(projectId);
|
|
544
|
+
if (resolved) {
|
|
545
|
+
const schema = await fetchTenantSchema(resolved.tenantId);
|
|
546
|
+
if (schema) {
|
|
547
|
+
// For static pages, validate any collection references in {{#each}} loops
|
|
548
|
+
if (templateType === 'static_page') {
|
|
549
|
+
const referencedCollections = extractCollectionReferences(html);
|
|
550
|
+
if (referencedCollections.length > 0) {
|
|
551
|
+
suggestions.push(`- Found ${referencedCollections.length} collection reference(s) in static page: ${referencedCollections.join(', ')}`);
|
|
552
|
+
// Check each referenced collection exists
|
|
553
|
+
const missingCollections = [];
|
|
554
|
+
const existingCollections = [];
|
|
555
|
+
for (const collSlug of referencedCollections) {
|
|
556
|
+
const exists = schema.collections.some(c => c.slug === collSlug);
|
|
557
|
+
if (exists) {
|
|
558
|
+
existingCollections.push(collSlug);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
missingCollections.push(collSlug);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (missingCollections.length > 0) {
|
|
565
|
+
schemaValidation += `
|
|
566
|
+
|
|
567
|
+
## ACTION REQUIRED: Missing Collections
|
|
568
|
+
|
|
569
|
+
The following collections are referenced in this static page but **do not exist**:
|
|
570
|
+
|
|
571
|
+
${missingCollections.map(c => `- \`${c}\``).join('\n')}
|
|
572
|
+
|
|
573
|
+
You must create these collections using \`sync_schema\` before deployment.
|
|
574
|
+
|
|
575
|
+
**Example:**
|
|
576
|
+
\`\`\`json
|
|
577
|
+
{
|
|
578
|
+
"projectId": "${resolved.tenantId}",
|
|
579
|
+
"collections": [
|
|
580
|
+
${missingCollections.map(c => ` {
|
|
581
|
+
"slug": "${c}",
|
|
582
|
+
"name": "${c.charAt(0).toUpperCase() + c.slice(1)}",
|
|
583
|
+
"nameSingular": "${c.endsWith('s') ? c.slice(0, -1).charAt(0).toUpperCase() + c.slice(0, -1).slice(1) : c}",
|
|
584
|
+
"fields": []
|
|
585
|
+
}`).join(',\n')}
|
|
586
|
+
]
|
|
587
|
+
}
|
|
588
|
+
\`\`\`
|
|
589
|
+
`;
|
|
590
|
+
}
|
|
591
|
+
if (existingCollections.length > 0) {
|
|
592
|
+
schemaValidation += `
|
|
593
|
+
|
|
594
|
+
## Static Page Collection References Validated
|
|
595
|
+
|
|
596
|
+
The following collections exist and can be used: ${existingCollections.map(c => `\`${c}\``).join(', ')}
|
|
597
|
+
`;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const targetCollection = collectionSlug || '';
|
|
602
|
+
if (targetCollection && templateType !== 'static_page') {
|
|
603
|
+
// Get richText fields for triple brace validation
|
|
604
|
+
richTextFields = getRichTextFields(targetCollection, schema);
|
|
605
|
+
// Check richText fields are using triple braces
|
|
606
|
+
for (const fieldSlug of richTextFields) {
|
|
607
|
+
const doublePattern = new RegExp(`\\{\\{${fieldSlug}\\}\\}`, 'g');
|
|
608
|
+
const triplePattern = new RegExp(`\\{\\{\\{${fieldSlug}\\}\\}\\}`, 'g');
|
|
609
|
+
const hasDouble = doublePattern.test(html);
|
|
610
|
+
const hasTriple = triplePattern.test(html);
|
|
611
|
+
if (hasDouble && !hasTriple) {
|
|
612
|
+
errors.push(`- {{${fieldSlug}}} must use triple braces {{{${fieldSlug}}}} because it contains HTML (richText field)`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// Extract fields used in template
|
|
616
|
+
const usedFields = extractTokenFieldNames(html);
|
|
617
|
+
// Check if collection exists
|
|
618
|
+
const collectionExists = schema.collections.some(c => c.slug === targetCollection);
|
|
619
|
+
if (!collectionExists) {
|
|
620
|
+
// Collection doesn't exist - need to create it
|
|
621
|
+
schemaValidation = `
|
|
622
|
+
|
|
623
|
+
## ACTION REQUIRED: Collection Does Not Exist
|
|
624
|
+
|
|
625
|
+
The collection "${targetCollection}" does **not exist** in this project.
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
### YOU MUST CREATE THIS COLLECTION
|
|
630
|
+
|
|
631
|
+
Use the \`sync_schema\` tool to create the collection and its fields before deploying.
|
|
632
|
+
|
|
633
|
+
**Step 1:** First, call \`get_field_types\` to see available field types.
|
|
634
|
+
|
|
635
|
+
**Step 2:** Then call \`sync_schema\` to create the collection with its fields:
|
|
636
|
+
|
|
637
|
+
\`\`\`json
|
|
638
|
+
{
|
|
639
|
+
"projectId": "${resolved.tenantId}",
|
|
640
|
+
"collections": [
|
|
641
|
+
{
|
|
642
|
+
"slug": "${targetCollection}",
|
|
643
|
+
"name": "${targetCollection.charAt(0).toUpperCase() + targetCollection.slice(1)}",
|
|
644
|
+
"nameSingular": "${targetCollection.endsWith('s') ? targetCollection.slice(0, -1).charAt(0).toUpperCase() + targetCollection.slice(0, -1).slice(1) : targetCollection.charAt(0).toUpperCase() + targetCollection.slice(1)}",
|
|
645
|
+
"fields": [
|
|
646
|
+
${usedFields.filter(f => !['name', 'slug', 'url', 'publishedAt', 'createdAt', 'updatedAt'].includes(f)).map(f => ` { "slug": "${f}", "name": "${f.charAt(0).toUpperCase() + f.slice(1).replace(/([A-Z])/g, ' $1').trim()}", "type": "YOUR_TYPE" }`).join(',\n')}
|
|
647
|
+
]
|
|
648
|
+
}
|
|
649
|
+
]
|
|
650
|
+
}
|
|
651
|
+
\`\`\`
|
|
652
|
+
|
|
653
|
+
**Common field types:**
|
|
654
|
+
- \`text\` - Short text (titles, names)
|
|
655
|
+
- \`richText\` - Long formatted content with HTML
|
|
656
|
+
- \`image\` - Image upload
|
|
657
|
+
- \`url\` - Links
|
|
658
|
+
- \`boolean\` - True/false toggles
|
|
659
|
+
- \`date\` - Date picker
|
|
660
|
+
- \`number\` - Numeric values
|
|
661
|
+
- \`select\` - Dropdown (requires \`options\` parameter)
|
|
662
|
+
- \`relation\` - Link to another collection (requires \`referenceCollection\` parameter)
|
|
663
|
+
|
|
664
|
+
**DO NOT SKIP THIS STEP** - Templates will not render without the collection.
|
|
665
|
+
`;
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
// Find missing fields
|
|
669
|
+
const missingFields = findMissingFields(usedFields, targetCollection, schema);
|
|
670
|
+
if (missingFields.length > 0) {
|
|
671
|
+
schemaValidation = `
|
|
672
|
+
|
|
673
|
+
## ACTION REQUIRED: Missing Fields
|
|
674
|
+
|
|
675
|
+
The following fields are used in the template but **do not exist** in the "${targetCollection}" collection:
|
|
676
|
+
|
|
677
|
+
${missingFields.map(f => `- \`${f}\``).join('\n')}
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
### YOU MUST CREATE THESE FIELDS
|
|
682
|
+
|
|
683
|
+
Use the \`sync_schema\` tool to create the missing fields before deploying. This is required for your template to work correctly.
|
|
684
|
+
|
|
685
|
+
**Step 1:** First, call \`get_field_types\` to see available field types.
|
|
686
|
+
|
|
687
|
+
**Step 2:** Then call \`sync_schema\` with the following structure (replace YOUR_TYPE with actual field types):
|
|
688
|
+
|
|
689
|
+
\`\`\`json
|
|
690
|
+
{
|
|
691
|
+
"projectId": "${resolved.tenantId}",
|
|
692
|
+
"fieldsToAdd": [
|
|
693
|
+
{
|
|
694
|
+
"collectionSlug": "${targetCollection}",
|
|
695
|
+
"fields": [
|
|
696
|
+
${missingFields.map(f => ` { "slug": "${f}", "name": "${f.charAt(0).toUpperCase() + f.slice(1).replace(/([A-Z])/g, ' $1').trim()}", "type": "YOUR_TYPE" }`).join(',\n')}
|
|
697
|
+
]
|
|
698
|
+
}
|
|
699
|
+
]
|
|
700
|
+
}
|
|
701
|
+
\`\`\`
|
|
702
|
+
|
|
703
|
+
**Common field types:**
|
|
704
|
+
- \`text\` - Short text (titles, names)
|
|
705
|
+
- \`richText\` - Long formatted content with HTML
|
|
706
|
+
- \`image\` - Image upload
|
|
707
|
+
- \`url\` - Links
|
|
708
|
+
- \`boolean\` - True/false toggles
|
|
709
|
+
- \`date\` - Date picker
|
|
710
|
+
- \`number\` - Numeric values
|
|
711
|
+
- \`select\` - Dropdown (requires \`options\` parameter)
|
|
712
|
+
- \`relation\` - Link to another collection (requires \`referenceCollection\` parameter)
|
|
713
|
+
|
|
714
|
+
**DO NOT SKIP THIS STEP** - Templates will not render correctly without these fields.
|
|
715
|
+
`;
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
schemaValidation = `
|
|
719
|
+
|
|
720
|
+
## Schema Validation Passed
|
|
721
|
+
|
|
722
|
+
All fields used in this template exist in the "${targetCollection}" collection.
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
else if (projectId) {
|
|
731
|
+
// projectId provided but not authenticated
|
|
732
|
+
schemaValidation = `
|
|
733
|
+
|
|
734
|
+
## Schema Validation Skipped
|
|
735
|
+
|
|
736
|
+
A projectId was provided but you're not authenticated.
|
|
737
|
+
To validate against your project's schema, authenticate first using the device flow or FASTMODE_AUTH_TOKEN.
|
|
738
|
+
|
|
739
|
+
Without authentication, only syntax validation is performed.
|
|
740
|
+
`;
|
|
741
|
+
}
|
|
742
|
+
// Build result
|
|
743
|
+
let output = '';
|
|
744
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
745
|
+
output = `TEMPLATE VALID
|
|
746
|
+
|
|
747
|
+
The ${templateType} template structure looks correct.
|
|
748
|
+
|
|
749
|
+
Found:
|
|
750
|
+
- ${doubleTokens.length} double-brace tokens
|
|
751
|
+
- ${tripleTokens.length} triple-brace tokens
|
|
752
|
+
- ${eachLoops.length} {{#each}} loop(s)
|
|
753
|
+
- ${conditionals.length} {{#if}} conditional(s)`;
|
|
754
|
+
if (suggestions.length > 0) {
|
|
755
|
+
output += `
|
|
756
|
+
|
|
757
|
+
Suggestions:
|
|
758
|
+
${suggestions.join('\n')}`;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
else if (errors.length === 0) {
|
|
762
|
+
output = `TEMPLATE VALID WITH WARNINGS
|
|
763
|
+
|
|
764
|
+
Warnings:
|
|
765
|
+
${warnings.join('\n')}`;
|
|
766
|
+
if (suggestions.length > 0) {
|
|
767
|
+
output += `
|
|
768
|
+
|
|
769
|
+
Suggestions:
|
|
770
|
+
${suggestions.join('\n')}`;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
output = `TEMPLATE HAS ERRORS
|
|
775
|
+
|
|
776
|
+
Errors (must fix):
|
|
777
|
+
${errors.join('\n')}`;
|
|
778
|
+
if (warnings.length > 0) {
|
|
779
|
+
output += `
|
|
780
|
+
|
|
781
|
+
Warnings:
|
|
782
|
+
${warnings.join('\n')}`;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// Add schema validation results if available
|
|
786
|
+
if (schemaValidation) {
|
|
787
|
+
output += schemaValidation;
|
|
788
|
+
}
|
|
789
|
+
return output;
|
|
790
|
+
}
|