primo-cli 0.1.2 → 0.1.4
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 +113 -41
- package/dist/commands/build.js +488 -272
- package/dist/commands/deploy.d.ts +1 -1
- package/dist/commands/deploy.js +293 -141
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +2007 -150
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.js +65 -43
- package/dist/commands/login.d.ts +1 -2
- package/dist/commands/login.js +24 -6
- package/dist/commands/new.js +161 -274
- package/dist/commands/pull-library.d.ts +7 -0
- package/dist/commands/pull-library.js +92 -0
- package/dist/commands/pull.d.ts +0 -1
- package/dist/commands/pull.js +160 -165
- package/dist/commands/push-library.d.ts +7 -0
- package/dist/commands/push-library.js +88 -0
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +358 -51
- package/dist/commands/validate.d.ts +1 -1
- package/dist/commands/validate.js +379 -161
- package/dist/index.js +110 -20
- package/dist/utils/binary.js +1 -1
- package/dist/utils/format.d.ts +12 -0
- package/dist/utils/format.js +98 -0
- package/dist/utils/head-svelte.d.ts +2 -0
- package/dist/utils/head-svelte.js +53 -0
- package/dist/utils/server-config.d.ts +19 -0
- package/dist/utils/server-config.js +49 -0
- package/dist/utils/site-config.d.ts +11 -0
- package/dist/utils/site-config.js +14 -0
- package/package.json +9 -5
- package/scripts/postinstall.js +1 -1
- package/dist/commands/export.d.ts +0 -8
- package/dist/commands/export.js +0 -163
- package/dist/commands/import.d.ts +0 -9
- package/dist/commands/import.js +0 -118
- package/dist/commands/publish.d.ts +0 -6
- package/dist/commands/publish.js +0 -239
|
@@ -1,22 +1,8 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const pages_dir = path.join(site_dir, 'pages');
|
|
7
|
-
const index_path = path.join(pages_dir, 'index.yaml');
|
|
8
|
-
try {
|
|
9
|
-
let content = await fs.readFile(index_path, 'utf-8');
|
|
10
|
-
const slug_index_pattern = /^slug:\s*index\s*$/m;
|
|
11
|
-
if (slug_index_pattern.test(content)) {
|
|
12
|
-
content = content.replace(slug_index_pattern, "slug: ''");
|
|
13
|
-
await fs.writeFile(index_path, content, 'utf-8');
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
// File doesn't exist, skip
|
|
18
|
-
}
|
|
19
|
-
}
|
|
4
|
+
import { load as load_yaml } from 'js-yaml';
|
|
5
|
+
import { get_head_svelte_validation_error } from '../utils/head-svelte.js';
|
|
20
6
|
const VALID_FIELD_TYPES = [
|
|
21
7
|
'text',
|
|
22
8
|
'rich-text',
|
|
@@ -38,6 +24,184 @@ const VALID_FIELD_TYPES = [
|
|
|
38
24
|
'date',
|
|
39
25
|
'info'
|
|
40
26
|
];
|
|
27
|
+
const MAX_FIELDS_FILE_BYTES = 256 * 1024;
|
|
28
|
+
const MAX_TOTAL_FIELDS = 5000;
|
|
29
|
+
// Warn-only normalization - logs issues but doesn't modify files
|
|
30
|
+
export async function normalize_site(site_dir) {
|
|
31
|
+
const warnings = [];
|
|
32
|
+
const pages_dir = path.join(site_dir, 'pages');
|
|
33
|
+
const homepage_path = path.join(pages_dir, 'index.yaml');
|
|
34
|
+
try {
|
|
35
|
+
await fs.access(homepage_path);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`Missing required homepage file: pages/index.yaml`);
|
|
39
|
+
}
|
|
40
|
+
// Note: Missing IDs are normal for new entities - they will be assigned during import.
|
|
41
|
+
// We only log the warnings array if other checks added warnings.
|
|
42
|
+
if (warnings.length > 0) {
|
|
43
|
+
for (const warning of warnings) {
|
|
44
|
+
console.log(chalk.yellow(` ⚠ ${warning}`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function parse_fields_file(file_path) {
|
|
49
|
+
const stat = await fs.stat(file_path);
|
|
50
|
+
if (stat.size > MAX_FIELDS_FILE_BYTES) {
|
|
51
|
+
throw new Error(`fields file is too large (${stat.size} bytes). This usually indicates corrupted duplicated schema data.`);
|
|
52
|
+
}
|
|
53
|
+
const content = await fs.readFile(file_path, 'utf-8');
|
|
54
|
+
const parsed = load_yaml(content);
|
|
55
|
+
const total_fields = count_fields_recursive(get_fields_array(parsed));
|
|
56
|
+
if (total_fields > MAX_TOTAL_FIELDS) {
|
|
57
|
+
throw new Error(`fields file defines too many fields (${total_fields}). This usually indicates corrupted duplicated schema data.`);
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
function get_field_id(field) {
|
|
62
|
+
return field?._id || field?.id;
|
|
63
|
+
}
|
|
64
|
+
function get_fields_array(data) {
|
|
65
|
+
if (Array.isArray(data)) {
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
if (data && typeof data === 'object' && Array.isArray(data.fields)) {
|
|
69
|
+
return data.fields;
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
function count_fields_recursive(fields) {
|
|
74
|
+
let count = 0;
|
|
75
|
+
for (const field of fields) {
|
|
76
|
+
count += 1;
|
|
77
|
+
if (Array.isArray(field?.subfields)) {
|
|
78
|
+
count += count_fields_recursive(field.subfields);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return count;
|
|
82
|
+
}
|
|
83
|
+
function is_plain_object(value) {
|
|
84
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
function validate_field_config(field, field_name, file_path) {
|
|
87
|
+
const errors = [];
|
|
88
|
+
if (!Object.prototype.hasOwnProperty.call(field, 'config')) {
|
|
89
|
+
return errors;
|
|
90
|
+
}
|
|
91
|
+
const config = field.config;
|
|
92
|
+
if (config === null || config === undefined) {
|
|
93
|
+
return errors;
|
|
94
|
+
}
|
|
95
|
+
if (typeof config === 'string') {
|
|
96
|
+
if (config.trim() === '') {
|
|
97
|
+
errors.push({
|
|
98
|
+
file: file_path,
|
|
99
|
+
field: field_name,
|
|
100
|
+
message: 'Deprecated empty string "config". Remove it or replace it with a config object.',
|
|
101
|
+
severity: 'warning'
|
|
102
|
+
});
|
|
103
|
+
return errors;
|
|
104
|
+
}
|
|
105
|
+
errors.push({
|
|
106
|
+
file: file_path,
|
|
107
|
+
field: field_name,
|
|
108
|
+
message: '"config" must be an object, null, or omitted',
|
|
109
|
+
severity: 'error'
|
|
110
|
+
});
|
|
111
|
+
return errors;
|
|
112
|
+
}
|
|
113
|
+
if (!is_plain_object(config)) {
|
|
114
|
+
errors.push({
|
|
115
|
+
file: file_path,
|
|
116
|
+
field: field_name,
|
|
117
|
+
message: '"config" must be an object, null, or omitted',
|
|
118
|
+
severity: 'error'
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return errors;
|
|
122
|
+
}
|
|
123
|
+
function collect_field_names(fields, names = new Set()) {
|
|
124
|
+
for (const field of fields) {
|
|
125
|
+
if (field?.name) {
|
|
126
|
+
names.add(field.name);
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(field?.subfields)) {
|
|
129
|
+
collect_field_names(field.subfields, names);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return names;
|
|
133
|
+
}
|
|
134
|
+
function validate_field_recursive(field, field_path, file_path, field_ids, field_names) {
|
|
135
|
+
const errors = [];
|
|
136
|
+
const field_id = get_field_id(field);
|
|
137
|
+
const field_name = field.name || field_id || '(unnamed)';
|
|
138
|
+
const display_name = field_path || field_name;
|
|
139
|
+
// IDs are optional - they're assigned by the system during import.
|
|
140
|
+
// Only check for duplicates if IDs are present.
|
|
141
|
+
if (field_id) {
|
|
142
|
+
if (field_ids.has(field_id)) {
|
|
143
|
+
errors.push({
|
|
144
|
+
file: file_path,
|
|
145
|
+
field: display_name,
|
|
146
|
+
message: `Duplicate field ID: "${field_id}"`,
|
|
147
|
+
severity: 'error'
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
field_ids.add(field_id);
|
|
151
|
+
}
|
|
152
|
+
if (!field.name) {
|
|
153
|
+
errors.push({
|
|
154
|
+
file: file_path,
|
|
155
|
+
field: display_name,
|
|
156
|
+
message: 'Missing required "name" field',
|
|
157
|
+
severity: 'error'
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (!field.label && field.type !== 'info') {
|
|
161
|
+
errors.push({
|
|
162
|
+
file: file_path,
|
|
163
|
+
field: display_name,
|
|
164
|
+
message: 'Missing "label" field',
|
|
165
|
+
severity: 'warning'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
if (!field.type) {
|
|
169
|
+
errors.push({
|
|
170
|
+
file: file_path,
|
|
171
|
+
field: display_name,
|
|
172
|
+
message: 'Missing required "type" field',
|
|
173
|
+
severity: 'error'
|
|
174
|
+
});
|
|
175
|
+
return errors;
|
|
176
|
+
}
|
|
177
|
+
if (!VALID_FIELD_TYPES.includes(field.type)) {
|
|
178
|
+
errors.push({
|
|
179
|
+
file: file_path,
|
|
180
|
+
field: display_name,
|
|
181
|
+
message: `Invalid field type: "${field.type}". Valid types: ${VALID_FIELD_TYPES.join(', ')}`,
|
|
182
|
+
severity: 'error'
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
errors.push(...validate_field_config(field, display_name, file_path));
|
|
186
|
+
if (field.type === 'select') {
|
|
187
|
+
errors.push(...validate_select_field(field, display_name, file_path));
|
|
188
|
+
}
|
|
189
|
+
if (field.parent && !field_names.has(field.parent)) {
|
|
190
|
+
errors.push({
|
|
191
|
+
file: file_path,
|
|
192
|
+
field: display_name,
|
|
193
|
+
message: `Parent field "${field.parent}" not found`,
|
|
194
|
+
severity: 'error'
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (Array.isArray(field.subfields)) {
|
|
198
|
+
for (const subfield of field.subfields) {
|
|
199
|
+
const subfield_name = subfield?.name || get_field_id(subfield) || '(unnamed subfield)';
|
|
200
|
+
errors.push(...validate_field_recursive(subfield, `${display_name}.${subfield_name}`, file_path, field_ids, field_names));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return errors;
|
|
204
|
+
}
|
|
41
205
|
export async function validate_site(options) {
|
|
42
206
|
const errors = [];
|
|
43
207
|
const warnings = [];
|
|
@@ -51,6 +215,8 @@ export async function validate_site(options) {
|
|
|
51
215
|
console.log(chalk.red(`✖ Directory not found: ${site_dir}`));
|
|
52
216
|
process.exit(1);
|
|
53
217
|
}
|
|
218
|
+
// Check for issues (warn-only, no modifications)
|
|
219
|
+
await normalize_site(site_dir);
|
|
54
220
|
// Validate blocks
|
|
55
221
|
const blocks_errors = await validate_blocks(site_dir);
|
|
56
222
|
errors.push(...blocks_errors.filter(e => e.severity === 'error'));
|
|
@@ -63,6 +229,10 @@ export async function validate_site(options) {
|
|
|
63
229
|
const site_errors = await validate_site_fields(site_dir);
|
|
64
230
|
errors.push(...site_errors.filter(e => e.severity === 'error'));
|
|
65
231
|
warnings.push(...site_errors.filter(e => e.severity === 'warning'));
|
|
232
|
+
// Validate site head
|
|
233
|
+
const site_head_errors = await validate_site_head(site_dir);
|
|
234
|
+
errors.push(...site_head_errors.filter(e => e.severity === 'error'));
|
|
235
|
+
warnings.push(...site_head_errors.filter(e => e.severity === 'warning'));
|
|
66
236
|
// Validate pages
|
|
67
237
|
const pages_errors = await validate_pages(site_dir);
|
|
68
238
|
errors.push(...pages_errors.filter(e => e.severity === 'error'));
|
|
@@ -110,8 +280,10 @@ async function validate_blocks(site_dir) {
|
|
|
110
280
|
if (!stat.isDirectory())
|
|
111
281
|
continue;
|
|
112
282
|
// Check for required files - read actual directory listing to handle case-insensitive filesystems
|
|
113
|
-
const fields_path = path.join(block_dir, 'fields.json');
|
|
114
283
|
const block_files = await fs.readdir(block_dir);
|
|
284
|
+
const fields_path = path.join(block_dir, 'fields.yaml');
|
|
285
|
+
const config_path = path.join(block_dir, 'config.yaml');
|
|
286
|
+
const content_path = path.join(block_dir, 'content.yaml');
|
|
115
287
|
// Check for component.svelte with correct casing
|
|
116
288
|
const component_file = block_files.find(f => f.toLowerCase() === 'component.svelte');
|
|
117
289
|
if (!component_file) {
|
|
@@ -128,28 +300,62 @@ async function validate_blocks(site_dir) {
|
|
|
128
300
|
severity: 'error'
|
|
129
301
|
});
|
|
130
302
|
}
|
|
303
|
+
// config.yaml is required (holds _id and display name).
|
|
131
304
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
305
|
+
const raw = await fs.readFile(config_path, 'utf-8');
|
|
306
|
+
const config = load_yaml(raw);
|
|
307
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
308
|
+
errors.push({
|
|
309
|
+
file: `blocks/${block_name}/config.yaml`,
|
|
310
|
+
message: 'Must be a YAML object with at least a `name` key.',
|
|
311
|
+
severity: 'error'
|
|
312
|
+
});
|
|
136
313
|
}
|
|
137
|
-
|
|
314
|
+
else if (!config.name) {
|
|
138
315
|
errors.push({
|
|
139
|
-
file: `blocks/${block_name}/
|
|
140
|
-
message: '
|
|
316
|
+
file: `blocks/${block_name}/config.yaml`,
|
|
317
|
+
message: 'Missing required "name" field.',
|
|
141
318
|
severity: 'error'
|
|
142
319
|
});
|
|
143
|
-
continue;
|
|
144
320
|
}
|
|
145
|
-
// Validate fields structure
|
|
146
|
-
const field_errors = validate_fields(fields_json, `blocks/${block_name}/fields.json`);
|
|
147
|
-
errors.push(...field_errors);
|
|
148
321
|
}
|
|
149
322
|
catch {
|
|
150
323
|
errors.push({
|
|
151
|
-
file: `blocks/${block_name}
|
|
152
|
-
message: 'Missing
|
|
324
|
+
file: `blocks/${block_name}/config.yaml`,
|
|
325
|
+
message: 'Missing config.yaml or invalid YAML syntax. Each block needs a config.yaml with `_id` (system-stamped) and `name`.',
|
|
326
|
+
severity: 'error'
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
// fields.yaml is a bare top-level list of field definitions.
|
|
330
|
+
try {
|
|
331
|
+
const relative_fields_path = `blocks/${block_name}/fields.yaml`;
|
|
332
|
+
const fields_json = await parse_fields_file(fields_path);
|
|
333
|
+
if (!Array.isArray(fields_json)) {
|
|
334
|
+
errors.push({
|
|
335
|
+
file: relative_fields_path,
|
|
336
|
+
message: 'Must be a bare list of field definitions (no `fields:` wrapper). Start the file with `- name: ...`.',
|
|
337
|
+
severity: 'error'
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
errors.push(...validate_fields(fields_json, relative_fields_path));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
errors.push({
|
|
346
|
+
file: `blocks/${block_name}/fields.yaml`,
|
|
347
|
+
message: 'Missing fields.yaml file or invalid YAML syntax',
|
|
348
|
+
severity: 'error'
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
// content.yaml is required for blocks — it seeds the editor sidebar.
|
|
352
|
+
try {
|
|
353
|
+
await fs.access(content_path);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
errors.push({
|
|
357
|
+
file: `blocks/${block_name}/content.yaml`,
|
|
358
|
+
message: 'Missing content.yaml. Every block needs default values (use `{}` if there are no defaults yet).',
|
|
153
359
|
severity: 'error'
|
|
154
360
|
});
|
|
155
361
|
}
|
|
@@ -177,207 +383,209 @@ async function validate_page_types(site_dir) {
|
|
|
177
383
|
const stat = await fs.stat(page_type_dir);
|
|
178
384
|
if (!stat.isDirectory())
|
|
179
385
|
continue;
|
|
180
|
-
const config_path = path.join(page_type_dir, 'config.
|
|
386
|
+
const config_path = path.join(page_type_dir, 'config.yaml');
|
|
387
|
+
const fields_path = path.join(page_type_dir, 'fields.yaml');
|
|
388
|
+
const layout_path = path.join(page_type_dir, 'layout.yaml');
|
|
181
389
|
try {
|
|
182
390
|
const config_data = await fs.readFile(config_path, 'utf-8');
|
|
183
391
|
let config;
|
|
184
392
|
try {
|
|
185
|
-
config =
|
|
393
|
+
config = load_yaml(config_data);
|
|
186
394
|
}
|
|
187
395
|
catch {
|
|
188
396
|
errors.push({
|
|
189
|
-
file: `page-types/${page_type_name}/config.
|
|
190
|
-
message: 'Invalid
|
|
397
|
+
file: `page-types/${page_type_name}/config.yaml`,
|
|
398
|
+
message: 'Invalid YAML syntax',
|
|
191
399
|
severity: 'error'
|
|
192
400
|
});
|
|
193
401
|
continue;
|
|
194
402
|
}
|
|
195
403
|
// Validate config has required fields
|
|
404
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
405
|
+
errors.push({
|
|
406
|
+
file: `page-types/${page_type_name}/config.yaml`,
|
|
407
|
+
message: 'Must be a YAML object',
|
|
408
|
+
severity: 'error'
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
196
412
|
if (!config.name) {
|
|
197
413
|
errors.push({
|
|
198
|
-
file: `page-types/${page_type_name}/config.
|
|
414
|
+
file: `page-types/${page_type_name}/config.yaml`,
|
|
199
415
|
message: 'Missing "name" field',
|
|
200
416
|
severity: 'error'
|
|
201
417
|
});
|
|
202
418
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
419
|
+
if ('fields' in config) {
|
|
420
|
+
errors.push({
|
|
421
|
+
file: `page-types/${page_type_name}/config.yaml`,
|
|
422
|
+
message: 'Page-type fields no longer live in config.yaml — move them to a sibling fields.yaml as a bare list.',
|
|
423
|
+
severity: 'error'
|
|
424
|
+
});
|
|
207
425
|
}
|
|
208
426
|
}
|
|
209
427
|
catch {
|
|
210
428
|
errors.push({
|
|
211
429
|
file: `page-types/${page_type_name}/`,
|
|
212
|
-
message: 'Missing config.
|
|
430
|
+
message: 'Missing config.yaml file',
|
|
213
431
|
severity: 'error'
|
|
214
432
|
});
|
|
215
433
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
434
|
+
// fields.yaml is a sibling bare list (may be `[]`); absent is allowed.
|
|
435
|
+
try {
|
|
436
|
+
const relative_fields_path = `page-types/${page_type_name}/fields.yaml`;
|
|
437
|
+
const fields_json = await parse_fields_file(fields_path);
|
|
438
|
+
if (!Array.isArray(fields_json)) {
|
|
439
|
+
errors.push({
|
|
440
|
+
file: relative_fields_path,
|
|
441
|
+
message: 'Must be a bare list of field definitions (no `fields:` wrapper). Use `[]` if there are no fields.',
|
|
442
|
+
severity: 'error'
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
else if (fields_json.length > 0) {
|
|
446
|
+
errors.push(...validate_fields(fields_json, relative_fields_path));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
if (err?.code !== 'ENOENT') {
|
|
451
|
+
errors.push({
|
|
452
|
+
file: `page-types/${page_type_name}/fields.yaml`,
|
|
453
|
+
message: 'Invalid YAML syntax in fields.yaml',
|
|
454
|
+
severity: 'error'
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// layout.yaml is required — comment-only stub is fine, but the file
|
|
459
|
+
// must exist so the page type's shared header/footer slots are
|
|
460
|
+
// discoverable.
|
|
225
461
|
try {
|
|
226
|
-
|
|
462
|
+
await fs.access(layout_path);
|
|
227
463
|
}
|
|
228
464
|
catch {
|
|
229
465
|
errors.push({
|
|
230
|
-
file:
|
|
231
|
-
message: '
|
|
466
|
+
file: `page-types/${page_type_name}/layout.yaml`,
|
|
467
|
+
message: 'Missing layout.yaml. Each page type needs one (use the comment-only stub if there are no shared header/footer sections yet).',
|
|
232
468
|
severity: 'error'
|
|
233
469
|
});
|
|
234
|
-
return errors;
|
|
235
470
|
}
|
|
236
|
-
|
|
471
|
+
}
|
|
472
|
+
return errors;
|
|
473
|
+
}
|
|
474
|
+
async function validate_site_fields(site_dir) {
|
|
475
|
+
const errors = [];
|
|
476
|
+
const site_fields_path = path.join(site_dir, 'site', 'fields.yaml');
|
|
477
|
+
try {
|
|
478
|
+
const relative_path = 'site/fields.yaml';
|
|
479
|
+
const fields_json = await parse_fields_file(site_fields_path);
|
|
480
|
+
// site fields must be a plain array, not wrapped in an object
|
|
237
481
|
if (!Array.isArray(fields_json)) {
|
|
238
482
|
errors.push({
|
|
239
|
-
file:
|
|
483
|
+
file: relative_path,
|
|
240
484
|
message: 'Must be a plain array (e.g., []) not wrapped in an object. The import will fail.',
|
|
241
485
|
severity: 'error'
|
|
242
486
|
});
|
|
243
487
|
return errors;
|
|
244
488
|
}
|
|
245
|
-
// Validate as array of fields
|
|
489
|
+
// Validate as bare array of fields
|
|
246
490
|
if (fields_json.length > 0) {
|
|
247
|
-
const field_errors = validate_fields(
|
|
491
|
+
const field_errors = validate_fields(fields_json, relative_path);
|
|
248
492
|
errors.push(...field_errors);
|
|
249
493
|
}
|
|
250
494
|
}
|
|
251
495
|
catch {
|
|
252
496
|
errors.push({
|
|
253
|
-
file: 'site/',
|
|
254
|
-
message: 'Missing fields.
|
|
497
|
+
file: 'site/fields.yaml',
|
|
498
|
+
message: 'Missing fields.yaml file or invalid YAML syntax',
|
|
255
499
|
severity: 'warning'
|
|
256
500
|
});
|
|
257
501
|
}
|
|
258
502
|
return errors;
|
|
259
503
|
}
|
|
504
|
+
async function validate_head_svelte_file(site_dir, relative_path) {
|
|
505
|
+
const head_path = path.join(site_dir, relative_path);
|
|
506
|
+
try {
|
|
507
|
+
const head_content = await fs.readFile(head_path, 'utf-8');
|
|
508
|
+
const message = get_head_svelte_validation_error(head_content, relative_path);
|
|
509
|
+
if (message) {
|
|
510
|
+
return [{
|
|
511
|
+
file: relative_path,
|
|
512
|
+
message,
|
|
513
|
+
severity: 'error'
|
|
514
|
+
}];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
if (error?.code === 'ENOENT') {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
return [{
|
|
522
|
+
file: relative_path,
|
|
523
|
+
message: `Failed to read file: ${error}`,
|
|
524
|
+
severity: 'error'
|
|
525
|
+
}];
|
|
526
|
+
}
|
|
527
|
+
return [];
|
|
528
|
+
}
|
|
529
|
+
async function validate_site_head(site_dir) {
|
|
530
|
+
const errors = [];
|
|
531
|
+
errors.push(...await validate_head_svelte_file(site_dir, 'site/head.svelte'));
|
|
532
|
+
// Per-page-type head fragments use the same rules as the site head.
|
|
533
|
+
const page_types_dir = path.join(site_dir, 'page-types');
|
|
534
|
+
let entries = [];
|
|
535
|
+
try {
|
|
536
|
+
entries = await fs.readdir(page_types_dir);
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
if (error?.code !== 'ENOENT')
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
for (const name of entries) {
|
|
543
|
+
errors.push(...await validate_head_svelte_file(site_dir, `page-types/${name}/head.svelte`));
|
|
544
|
+
}
|
|
545
|
+
return errors;
|
|
546
|
+
}
|
|
260
547
|
function validate_fields(fields_json, file_path) {
|
|
261
548
|
const errors = [];
|
|
262
|
-
|
|
549
|
+
// Accept a bare list (canonical fields.yaml shape) or an object with a
|
|
550
|
+
// `fields:` array (page/site doc passing its already-parsed fields list).
|
|
551
|
+
const fields_array = get_fields_array(fields_json);
|
|
552
|
+
if (fields_array.length === 0 && !Array.isArray(fields_json) && !is_plain_object(fields_json)) {
|
|
263
553
|
errors.push({
|
|
264
554
|
file: file_path,
|
|
265
|
-
message: '
|
|
555
|
+
message: 'Expected a list of field definitions',
|
|
266
556
|
severity: 'error'
|
|
267
557
|
});
|
|
268
558
|
return errors;
|
|
269
559
|
}
|
|
270
560
|
const field_ids = new Set();
|
|
271
|
-
const field_names =
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
for (const field of fields_json.fields) {
|
|
275
|
-
if (field.name) {
|
|
276
|
-
field_names.add(field.name);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
for (const field of fields_json.fields) {
|
|
280
|
-
const field_name = field.name || field.id || '(unnamed)';
|
|
281
|
-
// Check required properties
|
|
282
|
-
if (!field.id) {
|
|
283
|
-
errors.push({
|
|
284
|
-
file: file_path,
|
|
285
|
-
field: field_name,
|
|
286
|
-
message: 'Missing required "id" field',
|
|
287
|
-
severity: 'error'
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
else {
|
|
291
|
-
// Check for duplicate IDs
|
|
292
|
-
if (field_ids.has(field.id)) {
|
|
293
|
-
errors.push({
|
|
294
|
-
file: file_path,
|
|
295
|
-
field: field_name,
|
|
296
|
-
message: `Duplicate field ID: "${field.id}"`,
|
|
297
|
-
severity: 'error'
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
field_ids.add(field.id);
|
|
301
|
-
}
|
|
302
|
-
if (!field.name) {
|
|
303
|
-
errors.push({
|
|
304
|
-
file: file_path,
|
|
305
|
-
field: field_name,
|
|
306
|
-
message: 'Missing required "name" field',
|
|
307
|
-
severity: 'error'
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
if (!field.label && field.type !== 'info') {
|
|
311
|
-
errors.push({
|
|
312
|
-
file: file_path,
|
|
313
|
-
field: field_name,
|
|
314
|
-
message: 'Missing "label" field',
|
|
315
|
-
severity: 'warning'
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
if (!field.type) {
|
|
319
|
-
errors.push({
|
|
320
|
-
file: file_path,
|
|
321
|
-
field: field_name,
|
|
322
|
-
message: 'Missing required "type" field',
|
|
323
|
-
severity: 'error'
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
// Check valid field type
|
|
328
|
-
if (!VALID_FIELD_TYPES.includes(field.type)) {
|
|
329
|
-
errors.push({
|
|
330
|
-
file: file_path,
|
|
331
|
-
field: field_name,
|
|
332
|
-
message: `Invalid field type: "${field.type}". Valid types: ${VALID_FIELD_TYPES.join(', ')}`,
|
|
333
|
-
severity: 'error'
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
// Type-specific validation
|
|
337
|
-
if (field.type === 'select') {
|
|
338
|
-
const select_errors = validate_select_field(field, field_name, file_path);
|
|
339
|
-
errors.push(...select_errors);
|
|
340
|
-
}
|
|
341
|
-
if (field.type === 'repeater' || field.type === 'group') {
|
|
342
|
-
parent_names.add(field.name);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
// Validate parent reference
|
|
346
|
-
if (field.parent) {
|
|
347
|
-
if (!field_names.has(field.parent)) {
|
|
348
|
-
errors.push({
|
|
349
|
-
file: file_path,
|
|
350
|
-
field: field_name,
|
|
351
|
-
message: `Parent field "${field.parent}" not found`,
|
|
352
|
-
severity: 'error'
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
}
|
|
561
|
+
const field_names = collect_field_names(fields_array);
|
|
562
|
+
for (const field of fields_array) {
|
|
563
|
+
errors.push(...validate_field_recursive(field, field.name || get_field_id(field) || '(unnamed)', file_path, field_ids, field_names));
|
|
356
564
|
}
|
|
357
565
|
return errors;
|
|
358
566
|
}
|
|
359
567
|
function validate_select_field(field, field_name, file_path) {
|
|
360
568
|
const errors = [];
|
|
361
|
-
if (!field.
|
|
569
|
+
if (!is_plain_object(field.config) || !field.config.options) {
|
|
362
570
|
errors.push({
|
|
363
571
|
file: file_path,
|
|
364
572
|
field: field_name,
|
|
365
|
-
message: 'Select field missing "
|
|
573
|
+
message: 'Select field missing "config.options" array',
|
|
366
574
|
severity: 'error'
|
|
367
575
|
});
|
|
368
576
|
return errors;
|
|
369
577
|
}
|
|
370
|
-
if (!Array.isArray(field.
|
|
578
|
+
if (!Array.isArray(field.config.options)) {
|
|
371
579
|
errors.push({
|
|
372
580
|
file: file_path,
|
|
373
581
|
field: field_name,
|
|
374
|
-
message: 'Select field "
|
|
582
|
+
message: 'Select field "config.options" must be an array',
|
|
375
583
|
severity: 'error'
|
|
376
584
|
});
|
|
377
585
|
return errors;
|
|
378
586
|
}
|
|
379
|
-
for (let i = 0; i < field.
|
|
380
|
-
const option = field.
|
|
587
|
+
for (let i = 0; i < field.config.options.length; i++) {
|
|
588
|
+
const option = field.config.options[i];
|
|
381
589
|
if (!option.label) {
|
|
382
590
|
errors.push({
|
|
383
591
|
file: file_path,
|
|
@@ -408,6 +616,7 @@ function validate_select_field(field, field_name, file_path) {
|
|
|
408
616
|
async function validate_pages(site_dir) {
|
|
409
617
|
const errors = [];
|
|
410
618
|
const pages_dir = path.join(site_dir, 'pages');
|
|
619
|
+
const homepage_path = path.join(pages_dir, 'index.yaml');
|
|
411
620
|
try {
|
|
412
621
|
await fs.access(pages_dir);
|
|
413
622
|
}
|
|
@@ -418,19 +627,28 @@ async function validate_pages(site_dir) {
|
|
|
418
627
|
severity: 'warning'
|
|
419
628
|
}];
|
|
420
629
|
}
|
|
630
|
+
try {
|
|
631
|
+
await fs.access(homepage_path);
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
errors.push({
|
|
635
|
+
file: 'pages/index.yaml',
|
|
636
|
+
message: 'Missing required homepage file. Primo requires pages/index.yaml for the site root.',
|
|
637
|
+
severity: 'error'
|
|
638
|
+
});
|
|
639
|
+
}
|
|
421
640
|
// Recursively find all YAML files in pages directory
|
|
422
641
|
const yaml_files = await find_yaml_files(pages_dir, 'pages');
|
|
423
642
|
for (const yaml_file of yaml_files) {
|
|
424
643
|
const file_path = path.join(site_dir, yaml_file);
|
|
425
644
|
try {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
645
|
+
const content = await fs.readFile(file_path, 'utf-8');
|
|
646
|
+
if (/^slug:/m.test(content)) {
|
|
647
|
+
errors.push({
|
|
648
|
+
file: yaml_file,
|
|
649
|
+
message: 'Page slug is now derived from the file path. Remove the "slug:" field; it is ignored.',
|
|
650
|
+
severity: 'warning'
|
|
651
|
+
});
|
|
434
652
|
}
|
|
435
653
|
// Check for common mistakes
|
|
436
654
|
if (content.includes('\nblocks:') || content.match(/^blocks:/m)) {
|
|
@@ -494,7 +712,7 @@ async function find_yaml_files(dir, relative_path) {
|
|
|
494
712
|
const nested = await find_yaml_files(full_path, rel_path);
|
|
495
713
|
files.push(...nested);
|
|
496
714
|
}
|
|
497
|
-
else if (entry.name.endsWith('.yaml')
|
|
715
|
+
else if (entry.name.endsWith('.yaml')) {
|
|
498
716
|
files.push(rel_path);
|
|
499
717
|
}
|
|
500
718
|
}
|