@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli-theme-dev",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Fluid CLI plugin for theme developer workflows — dev server, push, pull, init",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -30,9 +30,10 @@
30
30
  "@types/prompts": "^2.4.9",
31
31
  "tsdown": "^0.21.0",
32
32
  "typescript": "^5",
33
- "@fluid-app/typescript-config": "0.0.0",
34
33
  "@fluid-app/api-client-core": "0.1.0",
35
- "@fluid-app/themes-api-client": "0.1.0"
34
+ "@fluid-app/theme-schema": "0.1.0",
35
+ "@fluid-app/themes-api-client": "0.1.0",
36
+ "@fluid-app/typescript-config": "0.0.0"
36
37
  },
37
38
  "engines": {
38
39
  "node": ">=18.0.0"
@@ -147,7 +147,7 @@ export function createDevCommand(): Command {
147
147
  editorUrl,
148
148
  },
149
149
  themeRoot,
150
- { host: opts.host, port, reloadMode },
150
+ { host: opts.host, port, reloadMode, validate: !opts.force },
151
151
  (address) => {
152
152
  console.log(`\n Dev server: ${address}`);
153
153
  console.log(` Web editor: ${editorUrl}`);
@@ -177,12 +177,19 @@ export function createPushCommand(): Command {
177
177
 
178
178
  const result = await syncer.uploadTheme({
179
179
  delete: !opts.nodelete,
180
+ validate: !opts.force,
180
181
  onProgress: (d, total) => {
181
182
  spinner.text = `Pushing ${d}/${total} files…`;
182
183
  },
183
184
  });
184
185
 
185
- if (result.errors.length) {
186
+ if (result.validationFailed) {
187
+ spinner.fail(
188
+ `Schema validation failed (${result.errors.length} error(s)). Use --force to skip.`,
189
+ );
190
+ for (const e of result.errors) console.error(` ${e}`);
191
+ process.exit(1);
192
+ } else if (result.errors.length) {
186
193
  spinner.warn(`Pushed with ${result.errors.length} error(s).`);
187
194
  for (const e of result.errors) console.error(` ${e}`);
188
195
  } else {
@@ -23,7 +23,7 @@ export async function startDevServer(
23
23
  api: ApiClient,
24
24
  theme: DevServerTheme,
25
25
  themeRoot: ThemeRoot,
26
- opts: DevServerOptions,
26
+ opts: DevServerOptions & { validate?: boolean },
27
27
  onReady?: (address: string) => void,
28
28
  ): Promise<() => void> {
29
29
  const sse = new SSEStream();
@@ -33,13 +33,23 @@ export async function startDevServer(
33
33
 
34
34
  // ── Initial sync ─────────────────────────────────────────────────────────
35
35
  console.log(`\nSyncing theme ${theme.name} (#${theme.id})…`);
36
- await syncer.uploadTheme({
36
+ const syncResult = await syncer.uploadTheme({
37
37
  delete: true,
38
+ validate: opts.validate,
38
39
  onProgress: (done, total) => {
39
40
  process.stdout.write(`\r Uploading ${done}/${total} files…`);
40
41
  },
41
42
  });
42
43
  process.stdout.write("\n");
44
+ if (syncResult.validationFailed) {
45
+ console.error(
46
+ `\nSchema validation failed (${syncResult.errors.length} error(s)). Use --force to skip.\n`,
47
+ );
48
+ for (const e of syncResult.errors) console.error(` ${e}`);
49
+ process.exit(1);
50
+ } else if (syncResult.errors.length > 0) {
51
+ for (const e of syncResult.errors) console.error(` ${e}`);
52
+ }
43
53
 
44
54
  // ── File watcher ─────────────────────────────────────────────────────────
45
55
  const stopWatcher = watchTheme(
@@ -48,6 +58,16 @@ export async function startDevServer(
48
58
  const changed = [...modified, ...added];
49
59
 
50
60
  for (const file of changed) {
61
+ // Validate schema on liquid files during dev (warn, don't block)
62
+ if (opts.validate && file.isLiquid) {
63
+ const diagnostics = file.validateSchema();
64
+ for (const d of diagnostics) {
65
+ const prefix =
66
+ d.severity === "error" ? "Schema error" : "Schema warning";
67
+ console.warn(`\n[${prefix}] ${file.relativePath}: ${d.message}`);
68
+ }
69
+ }
70
+
51
71
  pendingUpdates.add(file.relativePath);
52
72
  try {
53
73
  await syncer.uploadFile(file);
package/src/theme/file.ts CHANGED
@@ -8,6 +8,11 @@ import {
8
8
  import { extname, basename, relative, dirname } from "node:path";
9
9
  import { createHash } from "node:crypto";
10
10
  import { mimeTypeFor, type MimeType } from "./mime-type.js";
11
+ import {
12
+ validateSchemaText,
13
+ type Diagnostic,
14
+ type BlocksSchemaType,
15
+ } from "@fluid-app/theme-schema";
11
16
 
12
17
  export class ThemeFile {
13
18
  readonly absolutePath: string;
@@ -65,4 +70,27 @@ export class ThemeFile {
65
70
  size(): number {
66
71
  return statSync(this.absolutePath).size;
67
72
  }
73
+
74
+ get isTemplate(): boolean {
75
+ // Template files (home_page, product, etc.) expect blocks as objects.
76
+ // Section files expect blocks as arrays.
77
+ const parts = this.relativePath.split(/[/\\]/);
78
+ return (
79
+ parts[0] === "templates" &&
80
+ parts.length >= 3 &&
81
+ parts[1] !== "sections" &&
82
+ parts[1] !== "blocks" &&
83
+ parts[1] !== "components"
84
+ );
85
+ }
86
+
87
+ validateSchema(): Diagnostic[] {
88
+ if (!this.isLiquid) return [];
89
+
90
+ const blocksSchemaType: BlocksSchemaType = this.isTemplate
91
+ ? "object"
92
+ : "array";
93
+
94
+ return validateSchemaText(this.read(), { blocksSchemaType });
95
+ }
68
96
  }
@@ -11,6 +11,7 @@ export interface SyncResult {
11
11
  downloaded: number;
12
12
  deleted: number;
13
13
  errors: string[];
14
+ validationFailed: boolean;
14
15
  }
15
16
 
16
17
  export class Syncer {
@@ -201,6 +202,7 @@ export class Syncer {
201
202
  async uploadTheme(
202
203
  opts: {
203
204
  delete?: boolean;
205
+ validate?: boolean;
204
206
  onProgress?: (done: number, total: number) => void;
205
207
  } = {},
206
208
  ): Promise<SyncResult> {
@@ -212,8 +214,25 @@ export class Syncer {
212
214
  deleted: 0,
213
215
  downloaded: 0,
214
216
  errors: [],
217
+ validationFailed: false,
215
218
  };
216
219
 
220
+ // Schema validation pass
221
+ if (opts.validate) {
222
+ for (const file of localFiles) {
223
+ if (!file.isLiquid) continue;
224
+ const diagnostics = file.validateSchema();
225
+ const errors = diagnostics.filter((d) => d.severity === "error");
226
+ for (const d of errors) {
227
+ result.errors.push(`${file.relativePath}: ${d.message}`);
228
+ }
229
+ }
230
+ if (result.errors.length > 0) {
231
+ result.validationFailed = true;
232
+ return result;
233
+ }
234
+ }
235
+
217
236
  const toUpload = localFiles.filter((f) => f.exists && this.hasChanged(f));
218
237
  let done = 0;
219
238
  for (const file of toUpload) {
@@ -258,6 +277,7 @@ export class Syncer {
258
277
  downloaded: 0,
259
278
  skipped: 0,
260
279
  errors: [],
280
+ validationFailed: false,
261
281
  };
262
282
 
263
283
  let done = 0;