@fluid-app/fluid-cli-theme-dev 0.1.11 → 0.1.13

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @fluid-app/fluid-cli-theme-dev@0.1.11 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
2
+ > @fluid-app/fluid-cli-theme-dev@0.1.13 build /home/runner/_work/fluid-mono/fluid-mono/packages/cli/theme-dev
3
3
  > tsdown
4
4
 
5
5
  ℹ tsdown v0.21.0 powered by rolldown v1.0.0-rc.7
@@ -8,11 +8,9 @@
8
8
  ℹ target: node24
9
9
  ℹ tsconfig: tsconfig.json
10
10
  ℹ Build start
11
- ℹ dist/index.mjs  54.76 kB │ gzip: 15.09 kB
12
- ℹ dist/index.mjs.map 140.30 kB │ gzip: 31.59 kB
11
+ ℹ dist/index.mjs  62.63 kB │ gzip: 17.02 kB
12
+ ℹ dist/index.mjs.map 161.08 kB │ gzip: 36.15 kB
13
13
  ℹ dist/index.d.mts.map  0.11 kB │ gzip: 0.12 kB
14
14
  ℹ dist/index.d.mts  0.19 kB │ gzip: 0.16 kB
15
- ℹ 4 files, total: 195.36 kB
16
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugin `rolldown-plugin-dts:generate`. See https://rolldown.rs/options/checks#plugintimings for more details.
17
- ✔ Build complete in 3560ms
18
-
15
+ ℹ 4 files, total: 224.01 kB
16
+ ✔ Build complete in 1278ms
package/dist/index.mjs CHANGED
@@ -295,6 +295,225 @@ function mimeTypeFor(ext) {
295
295
  };
296
296
  }
297
297
  //#endregion
298
+ //#region ../../platform/theme-schema/src/types.ts
299
+ const VALID_SETTING_TYPES = Object.values({
300
+ "input": [
301
+ "text",
302
+ "rich_text",
303
+ "richtext",
304
+ "textarea",
305
+ "html",
306
+ "html_textarea",
307
+ "url"
308
+ ],
309
+ "number_and_selection": [
310
+ "range",
311
+ "select",
312
+ "radio",
313
+ "checkbox"
314
+ ],
315
+ "visual_and_media": [
316
+ "color",
317
+ "color_background",
318
+ "font_picker",
319
+ "image",
320
+ "image_picker",
321
+ "video_picker",
322
+ "media_picker",
323
+ "text_alignment"
324
+ ],
325
+ "layout": [
326
+ "media_fit",
327
+ "corner_radius",
328
+ "padding",
329
+ "border",
330
+ "gradient_overlay"
331
+ ],
332
+ "organization": ["header"],
333
+ "resource_single": [
334
+ "product",
335
+ "products",
336
+ "collection",
337
+ "collections",
338
+ "category",
339
+ "categories",
340
+ "blog",
341
+ "posts",
342
+ "enrollment",
343
+ "enrollments",
344
+ "enrollment_pack",
345
+ "forms",
346
+ "media",
347
+ "link_list"
348
+ ],
349
+ "resource_list": [
350
+ "product_list",
351
+ "products_list",
352
+ "collection_list",
353
+ "collections_list",
354
+ "category_list",
355
+ "categories_list",
356
+ "posts_list",
357
+ "enrollment_list",
358
+ "enrollments_list"
359
+ ]
360
+ }).flat();
361
+ //#endregion
362
+ //#region ../../platform/theme-schema/src/validate-settings.ts
363
+ function validateSettings(settings) {
364
+ const diagnostics = [];
365
+ const ids = /* @__PURE__ */ new Set();
366
+ for (let index = 0; index < settings.length; index++) {
367
+ const raw = settings[index];
368
+ const setting = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
369
+ const id = setting.id;
370
+ const type = setting.type;
371
+ if (typeof id === "string" && id.trim() === "") diagnostics.push({
372
+ severity: "error",
373
+ message: "Error in settings: id cannot be empty"
374
+ });
375
+ else if (id && ids.has(id)) diagnostics.push({
376
+ severity: "error",
377
+ message: `Error in settings: duplicate id '${id}' found`
378
+ });
379
+ else if (id) ids.add(id);
380
+ if (!type) diagnostics.push({
381
+ severity: "error",
382
+ message: `Error in setting '${id ?? index}': missing required field 'type'`
383
+ });
384
+ else if (!VALID_SETTING_TYPES.includes(type)) diagnostics.push({
385
+ severity: "error",
386
+ message: `Invalid settings type: '${type}'`
387
+ });
388
+ }
389
+ return diagnostics;
390
+ }
391
+ //#endregion
392
+ //#region ../../platform/theme-schema/src/validate-blocks.ts
393
+ function validateBlocks(blocks) {
394
+ const diagnostics = [];
395
+ const types = /* @__PURE__ */ new Set();
396
+ for (let index = 0; index < blocks.length; index++) {
397
+ const raw = blocks[index];
398
+ const block = raw !== null && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
399
+ const type = block.type;
400
+ const name = block.name;
401
+ const settings = block.settings;
402
+ if (!type) diagnostics.push({
403
+ severity: "error",
404
+ message: `Error in blocks at index ${index}: missing required field 'type'`
405
+ });
406
+ else if (types.has(type)) diagnostics.push({
407
+ severity: "warning",
408
+ message: `Warning in blocks: duplicate type '${type}' found`
409
+ });
410
+ else types.add(type);
411
+ if (!name && type !== "@app" && type !== "@theme" && !(!name && !settings)) diagnostics.push({
412
+ severity: "error",
413
+ message: `Error in block '${type ?? index}': missing required field 'name'`
414
+ });
415
+ if (Array.isArray(settings)) diagnostics.push(...validateSettings(settings));
416
+ if (Array.isArray(block.blocks)) diagnostics.push(...validateBlocks(block.blocks));
417
+ }
418
+ return diagnostics;
419
+ }
420
+ //#endregion
421
+ //#region ../../platform/theme-schema/src/validate.ts
422
+ function stripLiquidComments(text) {
423
+ return text.replace(/\{%-?\s*comment\s*-?%\}[\s\S]*?\{%-?\s*endcomment\s*-?%\}/g, "");
424
+ }
425
+ function findDuplicateBlocksKeys(jsonText) {
426
+ let count = 0;
427
+ const stack = [];
428
+ let i = 0;
429
+ while (i < jsonText.length) {
430
+ const ch = jsonText.charCodeAt(i);
431
+ if (ch === 32 || ch === 10 || ch === 13 || ch === 9) {
432
+ i++;
433
+ continue;
434
+ }
435
+ if (ch === 123) {
436
+ stack.push({
437
+ type: "object",
438
+ keys: /* @__PURE__ */ new Set(),
439
+ expectingKey: true
440
+ });
441
+ i++;
442
+ } else if (ch === 125) {
443
+ stack.pop();
444
+ i++;
445
+ } else if (ch === 91) {
446
+ stack.push({
447
+ type: "array",
448
+ keys: /* @__PURE__ */ new Set(),
449
+ expectingKey: false
450
+ });
451
+ i++;
452
+ } else if (ch === 93) {
453
+ stack.pop();
454
+ i++;
455
+ } else if (ch === 58) i++;
456
+ else if (ch === 44) {
457
+ const top = stack[stack.length - 1];
458
+ if (top?.type === "object") top.expectingKey = true;
459
+ i++;
460
+ } else if (ch === 34) {
461
+ let j = i + 1;
462
+ while (j < jsonText.length) {
463
+ if (jsonText.charCodeAt(j) === 34 && jsonText.charCodeAt(j - 1) !== 92) break;
464
+ j++;
465
+ }
466
+ const str = jsonText.slice(i + 1, j);
467
+ i = j + 1;
468
+ const top = stack[stack.length - 1];
469
+ if (top?.type === "object" && top.expectingKey) {
470
+ if (str === "blocks" && top.keys.has(str)) count++;
471
+ top.keys.add(str);
472
+ top.expectingKey = false;
473
+ }
474
+ } else i++;
475
+ }
476
+ return count;
477
+ }
478
+ function validateSchemaText(text, options) {
479
+ const blocksSchemaType = options?.blocksSchemaType ?? "unknown";
480
+ const diagnostics = [];
481
+ const match = stripLiquidComments(text).match(/\{%-?\s*schema\s*-?%\}([\s\S]*?)\{%-?\s*endschema\s*-?%\}/);
482
+ if (!match) return diagnostics;
483
+ const jsonText = match[1] ?? "";
484
+ let schema;
485
+ try {
486
+ schema = JSON.parse(jsonText);
487
+ } catch (e) {
488
+ diagnostics.push({
489
+ severity: "error",
490
+ message: `Invalid JSON:\n ${e.message}`
491
+ });
492
+ return diagnostics;
493
+ }
494
+ const dupes = findDuplicateBlocksKeys(jsonText);
495
+ for (let d = 0; d < dupes; d++) diagnostics.push({
496
+ severity: "error",
497
+ message: "Error in blocks: duplicate 'blocks' key in the same object"
498
+ });
499
+ if (schema !== null && typeof schema === "object" && Array.isArray(schema.settings)) diagnostics.push(...validateSettings(schema.settings));
500
+ if (schema !== null && typeof schema === "object" && "blocks" in schema) {
501
+ const blocks = schema.blocks;
502
+ if (blocksSchemaType === "array") if (!Array.isArray(blocks)) diagnostics.push({
503
+ severity: "error",
504
+ message: "Error in blocks: expected an array ([])"
505
+ });
506
+ else diagnostics.push(...validateBlocks(blocks));
507
+ else if (blocksSchemaType === "object") {
508
+ if (Array.isArray(blocks) || typeof blocks !== "object" || blocks === null) diagnostics.push({
509
+ severity: "error",
510
+ message: "Error in blocks: expected an object ({})"
511
+ });
512
+ } else if (Array.isArray(blocks)) diagnostics.push(...validateBlocks(blocks));
513
+ }
514
+ return diagnostics;
515
+ }
516
+ //#endregion
298
517
  //#region src/theme/file.ts
299
518
  var ThemeFile = class {
300
519
  absolutePath;
@@ -338,6 +557,15 @@ var ThemeFile = class {
338
557
  size() {
339
558
  return statSync(this.absolutePath).size;
340
559
  }
560
+ get isTemplate() {
561
+ const parts = this.relativePath.split(/[/\\]/);
562
+ return parts[0] === "templates" && parts.length >= 3 && parts[1] !== "sections" && parts[1] !== "blocks" && parts[1] !== "components";
563
+ }
564
+ validateSchema() {
565
+ if (!this.isLiquid) return [];
566
+ const blocksSchemaType = this.isTemplate ? "object" : "array";
567
+ return validateSchemaText(this.read(), { blocksSchemaType });
568
+ }
341
569
  };
342
570
  //#endregion
343
571
  //#region src/theme/fluid-ignore.ts
@@ -836,8 +1064,20 @@ var Syncer = class {
836
1064
  uploaded: 0,
837
1065
  deleted: 0,
838
1066
  downloaded: 0,
839
- errors: []
1067
+ errors: [],
1068
+ validationFailed: false
840
1069
  };
1070
+ if (opts.validate) {
1071
+ for (const file of localFiles) {
1072
+ if (!file.isLiquid) continue;
1073
+ const errors = file.validateSchema().filter((d) => d.severity === "error");
1074
+ for (const d of errors) result.errors.push(`${file.relativePath}: ${d.message}`);
1075
+ }
1076
+ if (result.errors.length > 0) {
1077
+ result.validationFailed = true;
1078
+ return result;
1079
+ }
1080
+ }
841
1081
  const toUpload = localFiles.filter((f) => f.exists && this.hasChanged(f));
842
1082
  let done = 0;
843
1083
  for (const file of toUpload) {
@@ -868,7 +1108,8 @@ var Syncer = class {
868
1108
  deleted: 0,
869
1109
  downloaded: 0,
870
1110
  skipped: 0,
871
- errors: []
1111
+ errors: [],
1112
+ validationFailed: false
872
1113
  };
873
1114
  let done = 0;
874
1115
  for (const resource of resources) {
@@ -915,16 +1156,29 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
915
1156
  const syncer = new Syncer(api, theme.id, themeRoot);
916
1157
  const pendingUpdates = /* @__PURE__ */ new Set();
917
1158
  console.log(`\nSyncing theme ${theme.name} (#${theme.id})…`);
918
- await syncer.uploadTheme({
1159
+ const syncResult = await syncer.uploadTheme({
919
1160
  delete: true,
1161
+ validate: opts.validate,
920
1162
  onProgress: (done, total) => {
921
1163
  process.stdout.write(`\r Uploading ${done}/${total} files…`);
922
1164
  }
923
1165
  });
924
1166
  process.stdout.write("\n");
1167
+ if (syncResult.validationFailed) {
1168
+ console.error(`\nSchema validation failed (${syncResult.errors.length} error(s)). Use --force to skip.\n`);
1169
+ for (const e of syncResult.errors) console.error(` ${e}`);
1170
+ process.exit(1);
1171
+ } else if (syncResult.errors.length > 0) for (const e of syncResult.errors) console.error(` ${e}`);
925
1172
  const stopWatcher = watchTheme(themeRoot, async (modified, added, removed) => {
926
1173
  const changed = [...modified, ...added];
927
1174
  for (const file of changed) {
1175
+ if (opts.validate && file.isLiquid) {
1176
+ const diagnostics = file.validateSchema();
1177
+ for (const d of diagnostics) {
1178
+ const prefix = d.severity === "error" ? "Schema error" : "Schema warning";
1179
+ console.warn(`\n[${prefix}] ${file.relativePath}: ${d.message}`);
1180
+ }
1181
+ }
928
1182
  pendingUpdates.add(file.relativePath);
929
1183
  try {
930
1184
  await syncer.uploadFile(file);
@@ -1198,7 +1452,8 @@ function createDevCommand() {
1198
1452
  }, themeRoot, {
1199
1453
  host: opts.host,
1200
1454
  port,
1201
- reloadMode
1455
+ reloadMode,
1456
+ validate: !opts.force
1202
1457
  }, (address) => {
1203
1458
  console.log(`\n Dev server: ${address}`);
1204
1459
  console.log(` Web editor: ${editorUrl}`);
@@ -1307,11 +1562,16 @@ function createPushCommand() {
1307
1562
  const spinner = ora(`Pushing to ${theme.name} (#${theme.id})…`).start();
1308
1563
  const result = await syncer.uploadTheme({
1309
1564
  delete: !opts.nodelete,
1565
+ validate: !opts.force,
1310
1566
  onProgress: (d, total) => {
1311
1567
  spinner.text = `Pushing ${d}/${total} files…`;
1312
1568
  }
1313
1569
  });
1314
- if (result.errors.length) {
1570
+ if (result.validationFailed) {
1571
+ spinner.fail(`Schema validation failed (${result.errors.length} error(s)). Use --force to skip.`);
1572
+ for (const e of result.errors) console.error(` ${e}`);
1573
+ process.exit(1);
1574
+ } else if (result.errors.length) {
1315
1575
  spinner.warn(`Pushed with ${result.errors.length} error(s).`);
1316
1576
  for (const e of result.errors) console.error(` ${e}`);
1317
1577
  } else spinner.succeed(`Pushed ${result.uploaded} file(s), deleted ${result.deleted} remote file(s).`);