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,311 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateManifest = validateManifest;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const pageSchema = zod_1.z.object({
|
|
6
|
+
path: zod_1.z.string().startsWith('/'),
|
|
7
|
+
file: zod_1.z.string(),
|
|
8
|
+
title: zod_1.z.string(),
|
|
9
|
+
editable: zod_1.z.boolean().optional(),
|
|
10
|
+
});
|
|
11
|
+
// CMS templates use flat format: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath
|
|
12
|
+
// This schema allows any string keys for collection templates
|
|
13
|
+
const cmsTemplatesSchema = zod_1.z.record(zod_1.z.string()).optional();
|
|
14
|
+
const manifestSchema = zod_1.z.object({
|
|
15
|
+
pages: zod_1.z.array(pageSchema),
|
|
16
|
+
cmsTemplates: cmsTemplatesSchema,
|
|
17
|
+
defaultHeadHtml: zod_1.z.string().optional(),
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Validates a manifest.json file and returns errors or success message
|
|
21
|
+
*/
|
|
22
|
+
async function validateManifest(manifestJson) {
|
|
23
|
+
const errors = [];
|
|
24
|
+
const warnings = [];
|
|
25
|
+
// Try to parse JSON
|
|
26
|
+
let manifest;
|
|
27
|
+
try {
|
|
28
|
+
manifest = JSON.parse(manifestJson);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
const errorMessage = e instanceof Error ? e.message : 'Failed to parse JSON';
|
|
32
|
+
// Provide more specific diagnostics for common JSON errors
|
|
33
|
+
let diagnostics = '';
|
|
34
|
+
const trimmed = manifestJson.trim();
|
|
35
|
+
// Check for missing closing brace
|
|
36
|
+
if (!trimmed.endsWith('}')) {
|
|
37
|
+
diagnostics = `
|
|
38
|
+
Detected Issue: File does not end with a closing brace '}'
|
|
39
|
+
The manifest.json appears to be truncated or missing the final '}'.
|
|
40
|
+
|
|
41
|
+
Fix: Add a closing '}' at the end of the file.`;
|
|
42
|
+
}
|
|
43
|
+
// Check for missing opening brace
|
|
44
|
+
else if (!trimmed.startsWith('{')) {
|
|
45
|
+
diagnostics = `
|
|
46
|
+
Detected Issue: File does not start with an opening brace '{'
|
|
47
|
+
The manifest.json must be a JSON object starting with '{'.`;
|
|
48
|
+
}
|
|
49
|
+
// Check bracket balance
|
|
50
|
+
else {
|
|
51
|
+
const openBraces = (manifestJson.match(/{/g) || []).length;
|
|
52
|
+
const closeBraces = (manifestJson.match(/}/g) || []).length;
|
|
53
|
+
const openBrackets = (manifestJson.match(/\[/g) || []).length;
|
|
54
|
+
const closeBrackets = (manifestJson.match(/]/g) || []).length;
|
|
55
|
+
if (openBraces !== closeBraces) {
|
|
56
|
+
diagnostics = `
|
|
57
|
+
Detected Issue: Mismatched braces
|
|
58
|
+
Found ${openBraces} opening '{' but ${closeBraces} closing '}'
|
|
59
|
+
${openBraces > closeBraces ? `Missing ${openBraces - closeBraces} closing brace(s) '}'` : `Extra ${closeBraces - openBraces} closing brace(s) '}'`}`;
|
|
60
|
+
}
|
|
61
|
+
else if (openBrackets !== closeBrackets) {
|
|
62
|
+
diagnostics = `
|
|
63
|
+
Detected Issue: Mismatched brackets
|
|
64
|
+
Found ${openBrackets} opening '[' but ${closeBrackets} closing ']'
|
|
65
|
+
${openBrackets > closeBrackets ? `Missing ${openBrackets - closeBrackets} closing bracket(s) ']'` : `Extra ${closeBrackets - openBrackets} closing bracket(s) ']'`}`;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Generic guidance
|
|
69
|
+
diagnostics = `
|
|
70
|
+
Common JSON issues to check:
|
|
71
|
+
- Missing or extra commas between items
|
|
72
|
+
- Trailing comma after last item in arrays/objects
|
|
73
|
+
- Unquoted property names (must use "key" not key)
|
|
74
|
+
- Single quotes instead of double quotes
|
|
75
|
+
- Unescaped special characters in strings`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return `INVALID JSON
|
|
79
|
+
|
|
80
|
+
Error: ${errorMessage}
|
|
81
|
+
${diagnostics}
|
|
82
|
+
|
|
83
|
+
Tip: Use a JSON validator (like jsonlint.com) to find the exact error location.`;
|
|
84
|
+
}
|
|
85
|
+
// Validate against schema
|
|
86
|
+
const result = manifestSchema.safeParse(manifest);
|
|
87
|
+
if (!result.success) {
|
|
88
|
+
for (const issue of result.error.issues) {
|
|
89
|
+
errors.push(`- ${issue.path.join('.')}: ${issue.message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Additional validation
|
|
93
|
+
const m = manifest;
|
|
94
|
+
// CRITICAL: Detect wrong "collections:" format (common AI mistake)
|
|
95
|
+
// AIs often use the wrong nested format instead of the flat cmsTemplates format
|
|
96
|
+
if (m.collections && !m.cmsTemplates) {
|
|
97
|
+
errors.push(`WRONG FORMAT: Use "cmsTemplates", not "collections"
|
|
98
|
+
|
|
99
|
+
This is a common mistake. The correct format is:
|
|
100
|
+
|
|
101
|
+
WRONG (what you have):
|
|
102
|
+
"collections": {
|
|
103
|
+
"posts": { "indexPath": "/blog", "indexFile": "...", "detailPath": "...", "detailFile": "..." }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
CORRECT (what you need):
|
|
107
|
+
"cmsTemplates": {
|
|
108
|
+
"postsIndex": "templates/posts_index.html",
|
|
109
|
+
"postsDetail": "templates/posts_detail.html",
|
|
110
|
+
"postsIndexPath": "/blog",
|
|
111
|
+
"postsDetailPath": "/blog"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
Note the flat structure with {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath keys.`);
|
|
115
|
+
}
|
|
116
|
+
// Detect if cmsTemplates uses wrong nested format
|
|
117
|
+
if (m.cmsTemplates && typeof m.cmsTemplates === 'object') {
|
|
118
|
+
const templates = m.cmsTemplates;
|
|
119
|
+
for (const [key, value] of Object.entries(templates)) {
|
|
120
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
121
|
+
const nested = value;
|
|
122
|
+
if (nested.indexPath || nested.detailPath || nested.indexFile || nested.detailFile) {
|
|
123
|
+
errors.push(`WRONG FORMAT in cmsTemplates.${key}: Using nested object format
|
|
124
|
+
|
|
125
|
+
WRONG (what you have):
|
|
126
|
+
"cmsTemplates": {
|
|
127
|
+
"${key}": { "indexPath": "...", "indexFile": "...", "detailPath": "...", "detailFile": "..." }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
CORRECT (what you need):
|
|
131
|
+
"cmsTemplates": {
|
|
132
|
+
"${key}Index": "templates/${key}_index.html",
|
|
133
|
+
"${key}Detail": "templates/${key}_detail.html",
|
|
134
|
+
"${key}IndexPath": "${nested.indexPath || '/' + key}",
|
|
135
|
+
"${key}DetailPath": "${nested.detailPath || '/' + key}"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Use the FLAT format with separate keys for each property.`);
|
|
139
|
+
break; // Only show this error once
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Check pages array
|
|
145
|
+
if (!Array.isArray(m.pages)) {
|
|
146
|
+
errors.push('- pages: Must be an array');
|
|
147
|
+
}
|
|
148
|
+
else if (m.pages.length === 0) {
|
|
149
|
+
errors.push(`NO PAGES DEFINED: The pages array is empty.
|
|
150
|
+
|
|
151
|
+
You must define at least one page, typically the homepage:
|
|
152
|
+
|
|
153
|
+
"pages": [
|
|
154
|
+
{ "path": "/", "file": "pages/index.html", "title": "Home" }
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
A site without pages will have no accessible content.`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Check for homepage
|
|
161
|
+
const hasHomepage = m.pages.some((p) => p.path === '/');
|
|
162
|
+
if (!hasHomepage) {
|
|
163
|
+
errors.push(`NO HOMEPAGE: Missing path "/" in pages array.
|
|
164
|
+
|
|
165
|
+
Every site needs a homepage. Add a page with path "/":
|
|
166
|
+
|
|
167
|
+
"pages": [
|
|
168
|
+
{ "path": "/", "file": "pages/index.html", "title": "Home" },
|
|
169
|
+
...your other pages...
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
Without a homepage, visitors will see a 404 error.`);
|
|
173
|
+
}
|
|
174
|
+
// Check file paths
|
|
175
|
+
for (const page of m.pages) {
|
|
176
|
+
if (typeof page.file === 'string') {
|
|
177
|
+
if (!page.file.startsWith('pages/')) {
|
|
178
|
+
warnings.push(`- Page "${page.path}": file should be in pages/ folder (got: ${page.file})`);
|
|
179
|
+
}
|
|
180
|
+
if (!page.file.endsWith('.html')) {
|
|
181
|
+
warnings.push(`- Page "${page.path}": file should end with .html (got: ${page.file})`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Check cmsTemplates - enforce strict pattern-based validation
|
|
187
|
+
const templates = m.cmsTemplates;
|
|
188
|
+
const customCollections = new Set();
|
|
189
|
+
if (templates) {
|
|
190
|
+
// All keys must match the unified format: {slug}Index, {slug}Detail, {slug}IndexPath, {slug}DetailPath
|
|
191
|
+
const VALID_KEY_PATTERN = /^[a-z][a-zA-Z0-9_]*(Index|Detail|IndexPath|DetailPath)$/;
|
|
192
|
+
const KEY_PARTS_PATTERN = /^([a-z][a-zA-Z0-9_]*)(Index|Detail|IndexPath|DetailPath)$/;
|
|
193
|
+
for (const key of Object.keys(templates)) {
|
|
194
|
+
if (!VALID_KEY_PATTERN.test(key)) {
|
|
195
|
+
// Provide helpful error message with suggestion
|
|
196
|
+
let suggestion = '';
|
|
197
|
+
if (key.toLowerCase().includes('post')) {
|
|
198
|
+
suggestion = ` Did you mean "${key.replace(/[Pp]ost/g, 'Detail')}"?`;
|
|
199
|
+
}
|
|
200
|
+
else if (!key.includes('Index') && !key.includes('Detail')) {
|
|
201
|
+
suggestion = ` Did you mean "${key}Index" or "${key}Detail"?`;
|
|
202
|
+
}
|
|
203
|
+
errors.push(`- Invalid cmsTemplates key "${key}". ` +
|
|
204
|
+
`All keys must use format: {slug}Index, {slug}Detail, {slug}IndexPath, or {slug}DetailPath.${suggestion}`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
// Extract collection slug from valid keys
|
|
208
|
+
const match = key.match(KEY_PARTS_PATTERN);
|
|
209
|
+
if (match) {
|
|
210
|
+
customCollections.add(match[1]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Validate each detected collection
|
|
214
|
+
for (const slug of customCollections) {
|
|
215
|
+
const indexTemplate = templates[`${slug}Index`];
|
|
216
|
+
const detailTemplate = templates[`${slug}Detail`];
|
|
217
|
+
const indexPath = templates[`${slug}IndexPath`];
|
|
218
|
+
const detailPath = templates[`${slug}DetailPath`];
|
|
219
|
+
// Check template file paths
|
|
220
|
+
if (typeof indexTemplate === 'string') {
|
|
221
|
+
if (!indexTemplate.endsWith('.html')) {
|
|
222
|
+
warnings.push(`- cmsTemplates.${slug}Index: should end with .html (got: ${indexTemplate})`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (typeof detailTemplate === 'string') {
|
|
226
|
+
if (!detailTemplate.endsWith('.html')) {
|
|
227
|
+
warnings.push(`- cmsTemplates.${slug}Detail: should end with .html (got: ${detailTemplate})`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Check path format
|
|
231
|
+
if (typeof indexPath === 'string' && !indexPath.startsWith('/')) {
|
|
232
|
+
errors.push(`- cmsTemplates.${slug}IndexPath: must start with "/" (got: ${indexPath})`);
|
|
233
|
+
}
|
|
234
|
+
if (typeof detailPath === 'string' && !detailPath.startsWith('/')) {
|
|
235
|
+
errors.push(`- cmsTemplates.${slug}DetailPath: must start with "/" (got: ${detailPath})`);
|
|
236
|
+
}
|
|
237
|
+
// Check consistency - incomplete CMS config will cause broken routes
|
|
238
|
+
if (indexTemplate && !detailTemplate) {
|
|
239
|
+
errors.push(`INCOMPLETE CMS CONFIG: Collection "${slug}" has ${slug}Index but no ${slug}Detail.
|
|
240
|
+
|
|
241
|
+
Both templates are required for a collection to work:
|
|
242
|
+
"cmsTemplates": {
|
|
243
|
+
"${slug}Index": "templates/${slug}_index.html",
|
|
244
|
+
"${slug}Detail": "templates/${slug}_detail.html", // <- MISSING
|
|
245
|
+
"${slug}IndexPath": "${indexPath || '/' + slug}",
|
|
246
|
+
"${slug}DetailPath": "${detailPath || '/' + slug}"
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
Without both templates, clicking on list items will lead to 404 errors.`);
|
|
250
|
+
}
|
|
251
|
+
if (detailTemplate && !indexTemplate) {
|
|
252
|
+
errors.push(`INCOMPLETE CMS CONFIG: Collection "${slug}" has ${slug}Detail but no ${slug}Index.
|
|
253
|
+
|
|
254
|
+
Both templates are required for a collection to work:
|
|
255
|
+
"cmsTemplates": {
|
|
256
|
+
"${slug}Index": "templates/${slug}_index.html", // <- MISSING
|
|
257
|
+
"${slug}Detail": "templates/${slug}_detail.html",
|
|
258
|
+
"${slug}IndexPath": "${indexPath || '/' + slug}",
|
|
259
|
+
"${slug}DetailPath": "${detailPath || '/' + slug}"
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
Without an index template, there's no way to list and navigate to items.`);
|
|
263
|
+
}
|
|
264
|
+
if (indexTemplate && !indexPath) {
|
|
265
|
+
warnings.push(`- Collection "${slug}": has ${slug}Index but no ${slug}IndexPath - using default path`);
|
|
266
|
+
}
|
|
267
|
+
if (detailTemplate && !detailPath) {
|
|
268
|
+
warnings.push(`- Collection "${slug}": has ${slug}Detail but no ${slug}DetailPath - using default path`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Build result
|
|
273
|
+
let output = '';
|
|
274
|
+
// Build collections summary
|
|
275
|
+
const collectionsSummary = customCollections.size > 0
|
|
276
|
+
? `\n- Collections: ${Array.from(customCollections).join(', ')}`
|
|
277
|
+
: '';
|
|
278
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
279
|
+
output = `MANIFEST VALID
|
|
280
|
+
|
|
281
|
+
The manifest.json structure is correct.
|
|
282
|
+
|
|
283
|
+
Summary:
|
|
284
|
+
- ${m.pages?.length || 0} static page(s) defined
|
|
285
|
+
- CMS templates: ${templates ? 'configured in manifest' : 'not configured (can be set via Settings → CMS Templates after upload)'}${collectionsSummary}
|
|
286
|
+
- Head HTML: ${m.defaultHeadHtml ? 'configured' : 'not configured'}`;
|
|
287
|
+
}
|
|
288
|
+
else if (errors.length === 0) {
|
|
289
|
+
output = `MANIFEST VALID WITH WARNINGS
|
|
290
|
+
|
|
291
|
+
The manifest structure is valid but has potential issues:
|
|
292
|
+
|
|
293
|
+
Warnings:
|
|
294
|
+
${warnings.join('\n')}
|
|
295
|
+
|
|
296
|
+
Tip: CMS templates can also be configured after upload via Dashboard → Settings → CMS Templates`;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
output = `MANIFEST INVALID
|
|
300
|
+
|
|
301
|
+
Errors (must fix):
|
|
302
|
+
${errors.join('\n')}`;
|
|
303
|
+
if (warnings.length > 0) {
|
|
304
|
+
output += `
|
|
305
|
+
|
|
306
|
+
Warnings:
|
|
307
|
+
${warnings.join('\n')}`;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return output;
|
|
311
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-package.d.ts","sourceRoot":"","sources":["../../src/tools/validate-package.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAAE,EAClB,eAAe,EAAE,MAAM,EACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACxC,OAAO,CAAC,MAAM,CAAC,CA4VjB"}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validatePackage = validatePackage;
|
|
4
|
+
const validate_manifest_1 = require("./validate-manifest");
|
|
5
|
+
const validate_template_1 = require("./validate-template");
|
|
6
|
+
/**
|
|
7
|
+
* Validates the complete structure of a CMS website package
|
|
8
|
+
*/
|
|
9
|
+
async function validatePackage(fileList, manifestContent, templateContents) {
|
|
10
|
+
const errors = [];
|
|
11
|
+
const warnings = [];
|
|
12
|
+
const validations = [];
|
|
13
|
+
// 1. Check required structure
|
|
14
|
+
const hasManifest = fileList.some(f => f === 'manifest.json' || f.endsWith('/manifest.json'));
|
|
15
|
+
const hasPagesFolder = fileList.some(f => f.includes('pages/'));
|
|
16
|
+
const hasPublicFolder = fileList.some(f => f.includes('public/'));
|
|
17
|
+
if (!hasManifest) {
|
|
18
|
+
errors.push('- Missing manifest.json at package root');
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
validations.push('manifest.json found');
|
|
22
|
+
}
|
|
23
|
+
if (!hasPagesFolder) {
|
|
24
|
+
warnings.push('- No pages/ folder found - static pages should be in pages/');
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
validations.push('pages/ folder found');
|
|
28
|
+
}
|
|
29
|
+
if (!hasPublicFolder) {
|
|
30
|
+
warnings.push('- No public/ folder found - assets (CSS, JS, images) should be in public/');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
validations.push('public/ folder found');
|
|
34
|
+
}
|
|
35
|
+
// 1b. Check for WRONG folder structures (common AI mistakes)
|
|
36
|
+
const hasAssetsFolder = fileList.some(f => f.includes('assets/') && !f.includes('public/assets/'));
|
|
37
|
+
const hasCollectionsFolder = fileList.some(f => f.includes('collections/') && f.endsWith('.html'));
|
|
38
|
+
const hasTemplatesFolder = fileList.some(f => f.includes('templates/'));
|
|
39
|
+
// Error: Using /assets/ instead of /public/
|
|
40
|
+
if (hasAssetsFolder && !hasPublicFolder) {
|
|
41
|
+
errors.push(`WRONG FOLDER: Using "assets/" instead of "public/"
|
|
42
|
+
|
|
43
|
+
Fast Mode serves static assets from the /public/ folder, NOT /assets/.
|
|
44
|
+
|
|
45
|
+
WRONG structure:
|
|
46
|
+
assets/
|
|
47
|
+
├── css/style.css ← Won't be served!
|
|
48
|
+
└── js/main.js ← Won't be served!
|
|
49
|
+
|
|
50
|
+
CORRECT structure:
|
|
51
|
+
public/
|
|
52
|
+
├── css/style.css ← Served at /css/style.css
|
|
53
|
+
└── js/main.js ← Served at /js/main.js
|
|
54
|
+
|
|
55
|
+
Move all files from assets/ to public/`);
|
|
56
|
+
}
|
|
57
|
+
else if (hasAssetsFolder) {
|
|
58
|
+
warnings.push(`- Found assets/ folder - assets should be in public/, not assets/`);
|
|
59
|
+
}
|
|
60
|
+
// Error: Using /collections/ instead of /templates/
|
|
61
|
+
if (hasCollectionsFolder) {
|
|
62
|
+
errors.push(`WRONG FOLDER: Using "collections/" instead of "templates/"
|
|
63
|
+
|
|
64
|
+
CMS templates should be in the templates/ folder, NOT collections/.
|
|
65
|
+
|
|
66
|
+
WRONG structure:
|
|
67
|
+
collections/
|
|
68
|
+
├── posts/
|
|
69
|
+
│ ├── index.html ← Wrong location!
|
|
70
|
+
│ └── detail.html ← Wrong location!
|
|
71
|
+
|
|
72
|
+
CORRECT structure:
|
|
73
|
+
templates/
|
|
74
|
+
├── posts_index.html
|
|
75
|
+
└── posts_detail.html
|
|
76
|
+
|
|
77
|
+
Note: Template files use {collection}_index.html and {collection}_detail.html naming.`);
|
|
78
|
+
}
|
|
79
|
+
// Warning: No templates folder but has cmsTemplates in manifest (potential issue)
|
|
80
|
+
if (!hasTemplatesFolder && hasPagesFolder) {
|
|
81
|
+
// This is just a hint - might be fine if no CMS content
|
|
82
|
+
// We'll check more specifically later when validating manifest
|
|
83
|
+
}
|
|
84
|
+
// 2. Validate manifest
|
|
85
|
+
const manifestResult = await (0, validate_manifest_1.validateManifest)(manifestContent);
|
|
86
|
+
if (manifestResult.includes('INVALID')) {
|
|
87
|
+
errors.push('- Manifest validation failed (see details below)');
|
|
88
|
+
}
|
|
89
|
+
else if (manifestResult.includes('WARNINGS')) {
|
|
90
|
+
warnings.push('- Manifest has warnings (see details below)');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
validations.push('manifest.json is valid');
|
|
94
|
+
}
|
|
95
|
+
// 3. Parse manifest for template checks
|
|
96
|
+
let manifest = null;
|
|
97
|
+
try {
|
|
98
|
+
manifest = JSON.parse(manifestContent);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Already caught in manifest validation
|
|
102
|
+
}
|
|
103
|
+
// 4. Check that referenced files exist
|
|
104
|
+
if (manifest?.pages && Array.isArray(manifest.pages)) {
|
|
105
|
+
for (const page of manifest.pages) {
|
|
106
|
+
const p = page;
|
|
107
|
+
if (typeof p.file === 'string') {
|
|
108
|
+
const fileExists = fileList.some(f => f === p.file || f.endsWith('/' + p.file));
|
|
109
|
+
if (!fileExists) {
|
|
110
|
+
errors.push(`- Page file not found: ${p.file} (referenced by path ${p.path})`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// 5. Check CMS template files exist (detect custom collections from manifest)
|
|
116
|
+
if (manifest?.cmsTemplates) {
|
|
117
|
+
const templates = manifest.cmsTemplates;
|
|
118
|
+
const checkedTemplates = new Set();
|
|
119
|
+
for (const [key, value] of Object.entries(templates)) {
|
|
120
|
+
// Skip path keys and non-string values
|
|
121
|
+
if (key.endsWith('Path') || typeof value !== 'string' || !value.endsWith('.html')) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// Avoid duplicate checks
|
|
125
|
+
if (checkedTemplates.has(value))
|
|
126
|
+
continue;
|
|
127
|
+
checkedTemplates.add(value);
|
|
128
|
+
const fileExists = fileList.some(f => f === value || f.endsWith('/' + value));
|
|
129
|
+
if (!fileExists) {
|
|
130
|
+
errors.push(`- Template file not found: ${value} (cmsTemplates.${key})`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
validations.push(`${key} template exists`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// 6. Validate template contents if provided
|
|
138
|
+
const templateValidations = [];
|
|
139
|
+
if (templateContents) {
|
|
140
|
+
for (const [path, content] of Object.entries(templateContents)) {
|
|
141
|
+
// Determine template type based on path
|
|
142
|
+
let templateType = 'static_page';
|
|
143
|
+
// Check if it's an index template
|
|
144
|
+
if (path.includes('_index') || path.includes('-index')) {
|
|
145
|
+
templateType = 'custom_index';
|
|
146
|
+
}
|
|
147
|
+
// Check if it's a detail template
|
|
148
|
+
else if (path.includes('_detail') || path.includes('-detail') || path.includes('_post') || path.includes('-post')) {
|
|
149
|
+
templateType = 'custom_detail';
|
|
150
|
+
}
|
|
151
|
+
// Check if it's in templates/ folder (likely a CMS template)
|
|
152
|
+
else if (path.includes('templates/')) {
|
|
153
|
+
// If it has collection-like patterns
|
|
154
|
+
if (content.includes('{{#each')) {
|
|
155
|
+
templateType = 'custom_index';
|
|
156
|
+
}
|
|
157
|
+
else if (content.includes('{{name}}') || content.includes('{{slug}}')) {
|
|
158
|
+
templateType = 'custom_detail';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const result = await (0, validate_template_1.validateTemplate)(content, templateType);
|
|
162
|
+
if (result.includes('ERRORS')) {
|
|
163
|
+
errors.push(`- Template ${path} has errors`);
|
|
164
|
+
templateValidations.push(`\n### ${path}\n${result}`);
|
|
165
|
+
}
|
|
166
|
+
else if (result.includes('WARNINGS')) {
|
|
167
|
+
warnings.push(`- Template ${path} has warnings`);
|
|
168
|
+
templateValidations.push(`\n### ${path}\n${result}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// 7. Check template URL consistency with manifest paths
|
|
173
|
+
if (manifest?.cmsTemplates && templateContents) {
|
|
174
|
+
// Extract collection paths from manifest
|
|
175
|
+
const collectionPaths = {};
|
|
176
|
+
for (const [key, value] of Object.entries(manifest.cmsTemplates)) {
|
|
177
|
+
if (key.endsWith('IndexPath') && typeof value === 'string') {
|
|
178
|
+
const slug = key.replace('IndexPath', '');
|
|
179
|
+
if (!collectionPaths[slug])
|
|
180
|
+
collectionPaths[slug] = {};
|
|
181
|
+
collectionPaths[slug].indexPath = value;
|
|
182
|
+
}
|
|
183
|
+
if (key.endsWith('DetailPath') && typeof value === 'string') {
|
|
184
|
+
const slug = key.replace('DetailPath', '');
|
|
185
|
+
if (!collectionPaths[slug])
|
|
186
|
+
collectionPaths[slug] = {};
|
|
187
|
+
collectionPaths[slug].detailPath = value;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Check templates for URL mismatches
|
|
191
|
+
for (const [templatePath, content] of Object.entries(templateContents)) {
|
|
192
|
+
// Look for hardcoded links that might not match manifest
|
|
193
|
+
const hrefMatches = content.match(/href=["']\/([^"'{}]+)\/\{\{/g);
|
|
194
|
+
if (hrefMatches) {
|
|
195
|
+
for (const match of hrefMatches) {
|
|
196
|
+
// Extract the path like "/posts/" from href="/posts/{{slug}}"
|
|
197
|
+
const pathMatch = match.match(/href=["']\/([^"'{}]+)\//);
|
|
198
|
+
if (pathMatch) {
|
|
199
|
+
const usedPath = '/' + pathMatch[1];
|
|
200
|
+
// Check if this path exists in any of our defined collection paths
|
|
201
|
+
let pathFound = false;
|
|
202
|
+
for (const paths of Object.values(collectionPaths)) {
|
|
203
|
+
if (paths.detailPath === usedPath || paths.indexPath === usedPath) {
|
|
204
|
+
pathFound = true;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!pathFound && Object.keys(collectionPaths).length > 0) {
|
|
209
|
+
const availablePaths = Object.entries(collectionPaths)
|
|
210
|
+
.map(([slug, p]) => `${slug}: ${p.indexPath || p.detailPath}`)
|
|
211
|
+
.join(', ');
|
|
212
|
+
warnings.push(`- Template ${templatePath}: Uses "${usedPath}" which doesn't match manifest paths (${availablePaths})`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// 8. Check for common issues in file structure
|
|
220
|
+
const htmlFiles = fileList.filter(f => f.endsWith('.html'));
|
|
221
|
+
const cssFiles = fileList.filter(f => f.endsWith('.css'));
|
|
222
|
+
const jsFiles = fileList.filter(f => f.endsWith('.js'));
|
|
223
|
+
const imageFiles = fileList.filter(f => /\.(png|jpg|jpeg|gif|svg|webp|ico)$/i.test(f));
|
|
224
|
+
// Check CSS files are in public - these will 404 if not in public/
|
|
225
|
+
for (const css of cssFiles) {
|
|
226
|
+
if (!css.includes('public/')) {
|
|
227
|
+
const fileName = css.split('/').pop();
|
|
228
|
+
errors.push(`WRONG LOCATION: CSS file "${css}" must be in public/ folder.
|
|
229
|
+
|
|
230
|
+
Move to: public/css/${fileName}
|
|
231
|
+
Reference in HTML as: /css/${fileName}
|
|
232
|
+
|
|
233
|
+
Files outside public/ will not be served and will cause 404 errors.`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Check JS files are in public - these will 404 if not in public/
|
|
237
|
+
for (const js of jsFiles) {
|
|
238
|
+
if (!js.includes('public/') && !js.includes('node_modules')) {
|
|
239
|
+
const fileName = js.split('/').pop();
|
|
240
|
+
errors.push(`WRONG LOCATION: JS file "${js}" must be in public/ folder.
|
|
241
|
+
|
|
242
|
+
Move to: public/js/${fileName}
|
|
243
|
+
Reference in HTML as: /js/${fileName}
|
|
244
|
+
|
|
245
|
+
Files outside public/ will not be served and will cause 404 errors.`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Check images are in public - these will 404 if not in public/
|
|
249
|
+
for (const img of imageFiles) {
|
|
250
|
+
if (!img.includes('public/')) {
|
|
251
|
+
const fileName = img.split('/').pop();
|
|
252
|
+
errors.push(`WRONG LOCATION: Image file "${img}" must be in public/ folder.
|
|
253
|
+
|
|
254
|
+
Move to: public/images/${fileName}
|
|
255
|
+
Reference in HTML as: /images/${fileName}
|
|
256
|
+
|
|
257
|
+
Files outside public/ will not be served and will cause 404 errors.`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// 9. Check JS files for potential form handler conflicts
|
|
261
|
+
if (templateContents) {
|
|
262
|
+
for (const [path, content] of Object.entries(templateContents)) {
|
|
263
|
+
if (path.endsWith('.js')) {
|
|
264
|
+
// Check for form submit handlers that might block CMS form submission
|
|
265
|
+
const hasSubmitListener = content.includes('addEventListener') && content.includes('submit');
|
|
266
|
+
const hasPreventDefault = content.includes('preventDefault');
|
|
267
|
+
const hasFormsEndpoint = content.includes('/_forms/');
|
|
268
|
+
if (hasSubmitListener && hasPreventDefault && !hasFormsEndpoint) {
|
|
269
|
+
warnings.push(`POTENTIAL FORM ISSUE: ${path} has form handlers with preventDefault() but no /_forms/ endpoint.
|
|
270
|
+
|
|
271
|
+
This JavaScript may block form submissions from reaching Fast Mode!
|
|
272
|
+
|
|
273
|
+
LOOK FOR patterns like:
|
|
274
|
+
form.addEventListener('submit', (e) => {
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
showToast('Message sent!'); // FAKE - data not saved!
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
FIX BY either:
|
|
280
|
+
1. Remove the original handler entirely (use native form submission)
|
|
281
|
+
2. Update handler to POST to /_forms/{formName}
|
|
282
|
+
|
|
283
|
+
See get_conversion_guide(section: "forms") for proper form setup.`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// Build result
|
|
289
|
+
let output = '';
|
|
290
|
+
if (errors.length === 0) {
|
|
291
|
+
output = `PACKAGE STRUCTURE VALID
|
|
292
|
+
|
|
293
|
+
${validations.map(v => `- ${v}`).join('\n')}
|
|
294
|
+
|
|
295
|
+
Summary:
|
|
296
|
+
- ${htmlFiles.length} HTML file(s)
|
|
297
|
+
- ${cssFiles.length} CSS file(s)
|
|
298
|
+
- ${jsFiles.length} JS file(s)
|
|
299
|
+
- ${imageFiles.length} image file(s)`;
|
|
300
|
+
if (warnings.length > 0) {
|
|
301
|
+
output += `
|
|
302
|
+
|
|
303
|
+
Warnings (consider fixing):
|
|
304
|
+
${warnings.join('\n')}`;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
output = `PACKAGE HAS ERRORS
|
|
309
|
+
|
|
310
|
+
Errors (must fix):
|
|
311
|
+
${errors.join('\n')}
|
|
312
|
+
|
|
313
|
+
What passed:
|
|
314
|
+
${validations.map(v => `- ${v}`).join('\n')}`;
|
|
315
|
+
if (warnings.length > 0) {
|
|
316
|
+
output += `
|
|
317
|
+
|
|
318
|
+
Warnings:
|
|
319
|
+
${warnings.join('\n')}`;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Add manifest details
|
|
323
|
+
output += `
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
## Manifest Validation Details
|
|
327
|
+
${manifestResult}`;
|
|
328
|
+
// Add template validation details
|
|
329
|
+
if (templateValidations.length > 0) {
|
|
330
|
+
output += `
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
## Template Validation Details
|
|
334
|
+
${templateValidations.join('\n')}`;
|
|
335
|
+
}
|
|
336
|
+
return output;
|
|
337
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type TemplateType = 'custom_index' | 'custom_detail' | 'static_page';
|
|
2
|
+
/**
|
|
3
|
+
* Validates an HTML template for correct CMS token usage
|
|
4
|
+
*
|
|
5
|
+
* @param html - The HTML template content
|
|
6
|
+
* @param templateType - The type of template (custom_index, custom_detail, static_page)
|
|
7
|
+
* @param collectionSlug - For custom collections, the collection slug
|
|
8
|
+
* @param projectId - Optional: Project ID or name to validate against actual schema
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateTemplate(html: string, templateType: TemplateType, collectionSlug?: string, projectId?: string): Promise<string>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=validate-template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-template.d.ts","sourceRoot":"","sources":["../../src/tools/validate-template.ts"],"names":[],"mappings":"AAMA,KAAK,YAAY,GAAG,cAAc,GAAG,eAAe,GAAG,aAAa,CAAC;AAmUrE;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,YAAY,EAC1B,cAAc,CAAC,EAAE,MAAM,EACvB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CA8kBjB"}
|