@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.
- package/.turbo/turbo-build.log +5 -7
- package/dist/index.mjs +265 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/commands/dev.ts +1 -1
- package/src/commands/push.ts +8 -1
- package/src/theme/dev-server/index.ts +22 -2
- package/src/theme/file.ts +28 -0
- package/src/theme/syncer.ts +20 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @fluid-app/fluid-cli-theme-dev@0.1.
|
|
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
|
[34mℹ[39m tsdown [2mv0.21.0[22m powered by rolldown [2mv1.0.0-rc.7[22m
|
|
@@ -8,11 +8,9 @@
|
|
|
8
8
|
[34mℹ[39m target: [34mnode24[39m
|
|
9
9
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
10
10
|
[34mℹ[39m Build start
|
|
11
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m
|
|
12
|
-
[34mℹ[39m [2mdist/[22mindex.mjs.map [
|
|
11
|
+
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m 62.63 kB[22m [2m│ gzip: 17.02 kB[22m
|
|
12
|
+
[34mℹ[39m [2mdist/[22mindex.mjs.map [2m161.08 kB[22m [2m│ gzip: 36.15 kB[22m
|
|
13
13
|
[34mℹ[39m [2mdist/[22mindex.d.mts.map [2m 0.11 kB[22m [2m│ gzip: 0.12 kB[22m
|
|
14
14
|
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m 0.19 kB[22m [2m│ gzip: 0.16 kB[22m
|
|
15
|
-
[34mℹ[39m 4 files, total:
|
|
16
|
-
[
|
|
17
|
-
[32m✔[39m Build complete in [32m3560ms[39m
|
|
18
|
-
|
|
15
|
+
[34mℹ[39m 4 files, total: 224.01 kB
|
|
16
|
+
[32m✔[39m Build complete in [32m1278ms[39m
|
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.
|
|
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).`);
|