@jxsuite/schema 0.27.0 → 0.28.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/class-schema.json CHANGED
@@ -60,6 +60,12 @@
60
60
  "./lib/calculator.js"
61
61
  ]
62
62
  },
63
+ "format": {
64
+ "$ref": "#/$defs/FormatDef"
65
+ },
66
+ "$studio": {
67
+ "$ref": "#/$defs/StudioHints"
68
+ },
63
69
  "$defs": {
64
70
  "description": "Class members: parameters, returnTypes, fields, constructor, methods.",
65
71
  "type": "object",
@@ -229,14 +235,18 @@
229
235
  }
230
236
  },
231
237
  "ClassMethodDef": {
232
- "description": "A class method or accessor definition.",
238
+ "description": "A class method or accessor definition. Capability roles (parse, serialize, discover, load) mark static methods that hosts (compiler, server, studio) invoke for format dispatch.",
233
239
  "type": "object",
234
240
  "properties": {
235
241
  "role": {
236
242
  "type": "string",
237
243
  "enum": [
238
244
  "method",
239
- "accessor"
245
+ "accessor",
246
+ "parse",
247
+ "serialize",
248
+ "discover",
249
+ "load"
240
250
  ]
241
251
  },
242
252
  "$prototype": {
@@ -261,6 +271,18 @@
261
271
  "identifier": {
262
272
  "type": "string"
263
273
  },
274
+ "timing": {
275
+ "description": "Execution environments allowed to call this capability directly. Hosts outside the list round-trip through the dev server.",
276
+ "type": "array",
277
+ "items": {
278
+ "type": "string",
279
+ "enum": [
280
+ "compiler",
281
+ "server",
282
+ "client"
283
+ ]
284
+ }
285
+ },
264
286
  "parameters": {
265
287
  "type": "array",
266
288
  "items": {
@@ -320,6 +342,146 @@
320
342
  "type": "string"
321
343
  }
322
344
  }
345
+ },
346
+ "FormatDef": {
347
+ "description": "Format participation marker. A class carrying this block is auto-discovered from the project imports map and used for file-extension dispatch by the compiler, server, and studio.",
348
+ "type": "object",
349
+ "required": [
350
+ "extensions"
351
+ ],
352
+ "properties": {
353
+ "extensions": {
354
+ "description": "File extensions this format claims, with leading dot (e.g. [\".md\"]).",
355
+ "type": "array",
356
+ "items": {
357
+ "type": "string",
358
+ "pattern": "^\\."
359
+ },
360
+ "minItems": 1
361
+ },
362
+ "mediaType": {
363
+ "description": "MIME type for the format (icons, labels, HTTP).",
364
+ "type": "string"
365
+ },
366
+ "documentKinds": {
367
+ "description": "\"page\"/\"component\" allow the extension in pages/components discovery; \"content\" allows it as a content-type source.",
368
+ "type": "array",
369
+ "items": {
370
+ "type": "string",
371
+ "enum": [
372
+ "page",
373
+ "component",
374
+ "content"
375
+ ]
376
+ }
377
+ },
378
+ "exportTarget": {
379
+ "description": "When true, site builds emit a serialized sidecar per page in this format.",
380
+ "type": "boolean"
381
+ },
382
+ "remote": {
383
+ "description": "When true, the load capability accepts http(s) URLs as sources.",
384
+ "type": "boolean"
385
+ }
386
+ }
387
+ },
388
+ "StudioHints": {
389
+ "description": "Studio control-surface hints for a format class: editor modes, document-mode rules, templates, and the element/nesting constraints that gate structural editing.",
390
+ "type": "object",
391
+ "properties": {
392
+ "icon": {
393
+ "type": "string"
394
+ },
395
+ "modes": {
396
+ "description": "Editor modes the studio offers for documents of this format.",
397
+ "type": "array",
398
+ "items": {
399
+ "type": "string"
400
+ }
401
+ },
402
+ "documentMode": {
403
+ "type": "object",
404
+ "properties": {
405
+ "default": {
406
+ "type": "string",
407
+ "enum": [
408
+ "content",
409
+ "component"
410
+ ]
411
+ },
412
+ "componentWhen": {
413
+ "description": "Treat the document as a component when this top-level/frontmatter key matches the regex.",
414
+ "type": "object",
415
+ "properties": {
416
+ "frontmatterKey": {
417
+ "type": "string"
418
+ },
419
+ "matches": {
420
+ "type": "string"
421
+ }
422
+ }
423
+ }
424
+ }
425
+ },
426
+ "newFileTemplate": {
427
+ "description": "Initial source text for newly created files of this format.",
428
+ "type": "string"
429
+ },
430
+ "elements": {
431
+ "description": "Element allowlist and nesting constraints for structural editing.",
432
+ "type": "object",
433
+ "properties": {
434
+ "block": {
435
+ "type": "array",
436
+ "items": {
437
+ "type": "string"
438
+ }
439
+ },
440
+ "inline": {
441
+ "type": "array",
442
+ "items": {
443
+ "type": "string"
444
+ }
445
+ },
446
+ "void": {
447
+ "type": "array",
448
+ "items": {
449
+ "type": "string"
450
+ }
451
+ },
452
+ "textOnly": {
453
+ "type": "array",
454
+ "items": {
455
+ "type": "string"
456
+ }
457
+ },
458
+ "nesting": {
459
+ "description": "Per-parent child rules keyed by tag (or \"_root\"): { block, inline, directive } booleans or { only: [tags] }.",
460
+ "type": "object",
461
+ "additionalProperties": {
462
+ "type": "object",
463
+ "properties": {
464
+ "block": {
465
+ "type": "boolean"
466
+ },
467
+ "inline": {
468
+ "type": "boolean"
469
+ },
470
+ "directive": {
471
+ "type": "boolean"
472
+ },
473
+ "only": {
474
+ "type": "array",
475
+ "items": {
476
+ "type": "string"
477
+ }
478
+ }
479
+ }
480
+ }
481
+ }
482
+ }
483
+ }
484
+ }
323
485
  }
324
486
  }
325
487
  }
@@ -69,14 +69,26 @@ export const classConstructorDefSchema = {
69
69
  } as const;
70
70
 
71
71
  export const classMethodDefSchema = {
72
- description: "A class method or accessor definition.",
72
+ description:
73
+ "A class method or accessor definition. Capability roles (parse, serialize, discover, load) " +
74
+ "mark static methods that hosts (compiler, server, studio) invoke for format dispatch.",
73
75
  type: "object",
74
76
  properties: {
75
- role: { type: "string", enum: ["method", "accessor"] },
77
+ role: {
78
+ type: "string",
79
+ enum: ["method", "accessor", "parse", "serialize", "discover", "load"],
80
+ },
76
81
  $prototype: { type: "string", const: "Function" },
77
82
  access: { type: "string", enum: ["public", "private", "protected"] },
78
83
  scope: { type: "string", enum: ["instance", "static"] },
79
84
  identifier: { type: "string" },
85
+ timing: {
86
+ description:
87
+ "Execution environments allowed to call this capability directly. " +
88
+ "Hosts outside the list round-trip through the dev server.",
89
+ type: "array",
90
+ items: { type: "string", enum: ["compiler", "server", "client"] },
91
+ },
80
92
  parameters: {
81
93
  type: "array",
82
94
  items: {
@@ -110,6 +122,100 @@ export const classMethodDefSchema = {
110
122
  },
111
123
  } as const;
112
124
 
125
+ export const formatDefSchema = {
126
+ description:
127
+ "Format participation marker. A class carrying this block is auto-discovered from the " +
128
+ "project imports map and used for file-extension dispatch by the compiler, server, and studio.",
129
+ type: "object",
130
+ required: ["extensions"],
131
+ properties: {
132
+ extensions: {
133
+ description: 'File extensions this format claims, with leading dot (e.g. [".md"]).',
134
+ type: "array",
135
+ items: { type: "string", pattern: "^\\." },
136
+ minItems: 1,
137
+ },
138
+ mediaType: {
139
+ description: "MIME type for the format (icons, labels, HTTP).",
140
+ type: "string",
141
+ },
142
+ documentKinds: {
143
+ description:
144
+ '"page"/"component" allow the extension in pages/components discovery; ' +
145
+ '"content" allows it as a content-type source.',
146
+ type: "array",
147
+ items: { type: "string", enum: ["page", "component", "content"] },
148
+ },
149
+ exportTarget: {
150
+ description: "When true, site builds emit a serialized sidecar per page in this format.",
151
+ type: "boolean",
152
+ },
153
+ remote: {
154
+ description: "When true, the load capability accepts http(s) URLs as sources.",
155
+ type: "boolean",
156
+ },
157
+ },
158
+ } as const;
159
+
160
+ export const studioHintsSchema = {
161
+ description:
162
+ "Studio control-surface hints for a format class: editor modes, document-mode rules, " +
163
+ "templates, and the element/nesting constraints that gate structural editing.",
164
+ type: "object",
165
+ properties: {
166
+ icon: { type: "string" },
167
+ modes: {
168
+ description: "Editor modes the studio offers for documents of this format.",
169
+ type: "array",
170
+ items: { type: "string" },
171
+ },
172
+ documentMode: {
173
+ type: "object",
174
+ properties: {
175
+ default: { type: "string", enum: ["content", "component"] },
176
+ componentWhen: {
177
+ description:
178
+ "Treat the document as a component when this top-level/frontmatter key matches the regex.",
179
+ type: "object",
180
+ properties: {
181
+ frontmatterKey: { type: "string" },
182
+ matches: { type: "string" },
183
+ },
184
+ },
185
+ },
186
+ },
187
+ newFileTemplate: {
188
+ description: "Initial source text for newly created files of this format.",
189
+ type: "string",
190
+ },
191
+ elements: {
192
+ description: "Element allowlist and nesting constraints for structural editing.",
193
+ type: "object",
194
+ properties: {
195
+ block: { type: "array", items: { type: "string" } },
196
+ inline: { type: "array", items: { type: "string" } },
197
+ void: { type: "array", items: { type: "string" } },
198
+ textOnly: { type: "array", items: { type: "string" } },
199
+ nesting: {
200
+ description:
201
+ 'Per-parent child rules keyed by tag (or "_root"): ' +
202
+ "{ block, inline, directive } booleans or { only: [tags] }.",
203
+ type: "object",
204
+ additionalProperties: {
205
+ type: "object",
206
+ properties: {
207
+ block: { type: "boolean" },
208
+ inline: { type: "boolean" },
209
+ directive: { type: "boolean" },
210
+ only: { type: "array", items: { type: "string" } },
211
+ },
212
+ },
213
+ },
214
+ },
215
+ },
216
+ },
217
+ } as const;
218
+
113
219
  export const classDefSchema = {
114
220
  description:
115
221
  'A .class.json schema-defined class. $prototype must be "Class". ' +
@@ -126,6 +232,8 @@ export const classDefSchema = {
126
232
  type: "string",
127
233
  },
128
234
  description: { type: "string" },
235
+ format: { $ref: "#/$defs/FormatDef" },
236
+ $studio: { $ref: "#/$defs/StudioHints" },
129
237
  extends: {
130
238
  description: "Base class — string name or $ref to another .class.json.",
131
239
  oneOf: [
@@ -9,8 +9,11 @@ export const contentTypeDefSchema = {
9
9
  examples: ["./content/blog/", "./content/docs/"],
10
10
  },
11
11
  format: {
12
+ description:
13
+ "Import name of a format class declared in the project imports map " +
14
+ '(e.g. "Markdown", "Csv"). When omitted, the format is derived from the ' +
15
+ 'source file extension via the format registry. "json" is the only built-in.',
12
16
  type: "string",
13
- enum: ["md", "json", "csv", "yaml"],
14
17
  },
15
18
  schema: {
16
19
  description: "JSON Schema for validating frontmatter of content type entries.",
@@ -16,5 +16,15 @@ export const imageConfigSchema = {
16
16
  },
17
17
  sizes: { type: "string" },
18
18
  lazyLoad: { type: "boolean" },
19
+ service: { type: "string", enum: ["build", "cloudflare"] },
20
+ binding: { type: "string" },
21
+ remoteDomains: {
22
+ description:
23
+ "Hostnames whose remote (https) images are optimized through the /_jx/image endpoint " +
24
+ '(cloudflare service only), e.g. ["drive.usercontent.google.com"]. Remote sources from ' +
25
+ "other hosts are left untouched.",
26
+ type: "array",
27
+ items: { type: "string" },
28
+ },
19
29
  },
20
30
  } as const;
package/defs/index.ts CHANGED
@@ -69,6 +69,8 @@ export {
69
69
  classConstructorDefSchema,
70
70
  classMethodDefSchema,
71
71
  classDefSchema,
72
+ formatDefSchema,
73
+ studioHintsSchema,
72
74
  } from "./class-def.schema";
73
75
 
74
76
  // Content & project
@@ -88,7 +88,7 @@ export const projectConfigSchema = {
88
88
  additionalProperties: { type: "string" },
89
89
  examples: [
90
90
  {
91
- MarkdownFile: "@jxsuite/parser/MarkdownFile.class.json",
91
+ Markdown: "@jxsuite/parser/Markdown.class.json",
92
92
  MarkdownCollection: "@jxsuite/parser/MarkdownCollection.class.json",
93
93
  },
94
94
  ],
package/package.json CHANGED
@@ -7,6 +7,7 @@
7
7
  "description": "JSON Schema 2020-12 meta-schema generator for Jx documents",
8
8
  "exports": {
9
9
  ".": "./src/schema.ts",
10
+ "./format-registry": "./src/format-registry.ts",
10
11
  "./types": "./types.ts",
11
12
  "./defs": "./defs/index.ts",
12
13
  "./schema.json": "./schema.json",
@@ -36,7 +37,7 @@
36
37
  "upgrade": "bunx npm-check-updates -u && bun install"
37
38
  },
38
39
  "type": "module",
39
- "version": "0.27.0",
40
+ "version": "0.28.0",
40
41
  "devDependencies": {
41
42
  "json-schema-to-ts": "^3.1.1"
42
43
  }
@@ -120,7 +120,7 @@
120
120
  },
121
121
  "examples": [
122
122
  {
123
- "MarkdownFile": "@jxsuite/parser/MarkdownFile.class.json",
123
+ "Markdown": "@jxsuite/parser/Markdown.class.json",
124
124
  "MarkdownCollection": "@jxsuite/parser/MarkdownCollection.class.json"
125
125
  }
126
126
  ]
@@ -311,6 +311,23 @@
311
311
  },
312
312
  "lazyLoad": {
313
313
  "type": "boolean"
314
+ },
315
+ "service": {
316
+ "type": "string",
317
+ "enum": [
318
+ "build",
319
+ "cloudflare"
320
+ ]
321
+ },
322
+ "binding": {
323
+ "type": "string"
324
+ },
325
+ "remoteDomains": {
326
+ "description": "Hostnames whose remote (https) images are optimized through the /_jx/image endpoint (cloudflare service only), e.g. [\"drive.usercontent.google.com\"]. Remote sources from other hosts are left untouched.",
327
+ "type": "array",
328
+ "items": {
329
+ "type": "string"
330
+ }
314
331
  }
315
332
  }
316
333
  },
@@ -327,13 +344,8 @@
327
344
  ]
328
345
  },
329
346
  "format": {
330
- "type": "string",
331
- "enum": [
332
- "md",
333
- "json",
334
- "csv",
335
- "yaml"
336
- ]
347
+ "description": "Import name of a format class declared in the project imports map (e.g. \"Markdown\", \"Csv\"). When omitted, the format is derived from the source file extension via the format registry. \"json\" is the only built-in.",
348
+ "type": "string"
337
349
  },
338
350
  "schema": {
339
351
  "description": "JSON Schema for validating frontmatter of content type entries.",
package/schema.json CHANGED
@@ -458,6 +458,12 @@
458
458
  "description": {
459
459
  "type": "string"
460
460
  },
461
+ "format": {
462
+ "$ref": "#/$defs/FormatDef"
463
+ },
464
+ "$studio": {
465
+ "$ref": "#/$defs/StudioHints"
466
+ },
461
467
  "extends": {
462
468
  "description": "Base class — string name or $ref to another .class.json.",
463
469
  "oneOf": [
@@ -650,14 +656,18 @@
650
656
  }
651
657
  },
652
658
  "ClassMethodDef": {
653
- "description": "A class method or accessor definition.",
659
+ "description": "A class method or accessor definition. Capability roles (parse, serialize, discover, load) mark static methods that hosts (compiler, server, studio) invoke for format dispatch.",
654
660
  "type": "object",
655
661
  "properties": {
656
662
  "role": {
657
663
  "type": "string",
658
664
  "enum": [
659
665
  "method",
660
- "accessor"
666
+ "accessor",
667
+ "parse",
668
+ "serialize",
669
+ "discover",
670
+ "load"
661
671
  ]
662
672
  },
663
673
  "$prototype": {
@@ -682,6 +692,18 @@
682
692
  "identifier": {
683
693
  "type": "string"
684
694
  },
695
+ "timing": {
696
+ "description": "Execution environments allowed to call this capability directly. Hosts outside the list round-trip through the dev server.",
697
+ "type": "array",
698
+ "items": {
699
+ "type": "string",
700
+ "enum": [
701
+ "compiler",
702
+ "server",
703
+ "client"
704
+ ]
705
+ }
706
+ },
685
707
  "parameters": {
686
708
  "type": "array",
687
709
  "items": {
@@ -742,6 +764,146 @@
742
764
  }
743
765
  }
744
766
  },
767
+ "FormatDef": {
768
+ "description": "Format participation marker. A class carrying this block is auto-discovered from the project imports map and used for file-extension dispatch by the compiler, server, and studio.",
769
+ "type": "object",
770
+ "required": [
771
+ "extensions"
772
+ ],
773
+ "properties": {
774
+ "extensions": {
775
+ "description": "File extensions this format claims, with leading dot (e.g. [\".md\"]).",
776
+ "type": "array",
777
+ "items": {
778
+ "type": "string",
779
+ "pattern": "^\\."
780
+ },
781
+ "minItems": 1
782
+ },
783
+ "mediaType": {
784
+ "description": "MIME type for the format (icons, labels, HTTP).",
785
+ "type": "string"
786
+ },
787
+ "documentKinds": {
788
+ "description": "\"page\"/\"component\" allow the extension in pages/components discovery; \"content\" allows it as a content-type source.",
789
+ "type": "array",
790
+ "items": {
791
+ "type": "string",
792
+ "enum": [
793
+ "page",
794
+ "component",
795
+ "content"
796
+ ]
797
+ }
798
+ },
799
+ "exportTarget": {
800
+ "description": "When true, site builds emit a serialized sidecar per page in this format.",
801
+ "type": "boolean"
802
+ },
803
+ "remote": {
804
+ "description": "When true, the load capability accepts http(s) URLs as sources.",
805
+ "type": "boolean"
806
+ }
807
+ }
808
+ },
809
+ "StudioHints": {
810
+ "description": "Studio control-surface hints for a format class: editor modes, document-mode rules, templates, and the element/nesting constraints that gate structural editing.",
811
+ "type": "object",
812
+ "properties": {
813
+ "icon": {
814
+ "type": "string"
815
+ },
816
+ "modes": {
817
+ "description": "Editor modes the studio offers for documents of this format.",
818
+ "type": "array",
819
+ "items": {
820
+ "type": "string"
821
+ }
822
+ },
823
+ "documentMode": {
824
+ "type": "object",
825
+ "properties": {
826
+ "default": {
827
+ "type": "string",
828
+ "enum": [
829
+ "content",
830
+ "component"
831
+ ]
832
+ },
833
+ "componentWhen": {
834
+ "description": "Treat the document as a component when this top-level/frontmatter key matches the regex.",
835
+ "type": "object",
836
+ "properties": {
837
+ "frontmatterKey": {
838
+ "type": "string"
839
+ },
840
+ "matches": {
841
+ "type": "string"
842
+ }
843
+ }
844
+ }
845
+ }
846
+ },
847
+ "newFileTemplate": {
848
+ "description": "Initial source text for newly created files of this format.",
849
+ "type": "string"
850
+ },
851
+ "elements": {
852
+ "description": "Element allowlist and nesting constraints for structural editing.",
853
+ "type": "object",
854
+ "properties": {
855
+ "block": {
856
+ "type": "array",
857
+ "items": {
858
+ "type": "string"
859
+ }
860
+ },
861
+ "inline": {
862
+ "type": "array",
863
+ "items": {
864
+ "type": "string"
865
+ }
866
+ },
867
+ "void": {
868
+ "type": "array",
869
+ "items": {
870
+ "type": "string"
871
+ }
872
+ },
873
+ "textOnly": {
874
+ "type": "array",
875
+ "items": {
876
+ "type": "string"
877
+ }
878
+ },
879
+ "nesting": {
880
+ "description": "Per-parent child rules keyed by tag (or \"_root\"): { block, inline, directive } booleans or { only: [tags] }.",
881
+ "type": "object",
882
+ "additionalProperties": {
883
+ "type": "object",
884
+ "properties": {
885
+ "block": {
886
+ "type": "boolean"
887
+ },
888
+ "inline": {
889
+ "type": "boolean"
890
+ },
891
+ "directive": {
892
+ "type": "boolean"
893
+ },
894
+ "only": {
895
+ "type": "array",
896
+ "items": {
897
+ "type": "string"
898
+ }
899
+ }
900
+ }
901
+ }
902
+ }
903
+ }
904
+ }
905
+ }
906
+ },
745
907
  "ExternalClassDef": {
746
908
  "description": "An external class / data source. $prototype is a constructor name (not \"Function\"). When $prototype is not in the built-in registry, $src is required. All state entries are reactive by default.",
747
909
  "type": "object",
@@ -21611,6 +21773,23 @@
21611
21773
  },
21612
21774
  "lazyLoad": {
21613
21775
  "type": "boolean"
21776
+ },
21777
+ "service": {
21778
+ "type": "string",
21779
+ "enum": [
21780
+ "build",
21781
+ "cloudflare"
21782
+ ]
21783
+ },
21784
+ "binding": {
21785
+ "type": "string"
21786
+ },
21787
+ "remoteDomains": {
21788
+ "description": "Hostnames whose remote (https) images are optimized through the /_jx/image endpoint (cloudflare service only), e.g. [\"drive.usercontent.google.com\"]. Remote sources from other hosts are left untouched.",
21789
+ "type": "array",
21790
+ "items": {
21791
+ "type": "string"
21792
+ }
21614
21793
  }
21615
21794
  }
21616
21795
  },
@@ -21627,13 +21806,8 @@
21627
21806
  ]
21628
21807
  },
21629
21808
  "format": {
21630
- "type": "string",
21631
- "enum": [
21632
- "md",
21633
- "json",
21634
- "csv",
21635
- "yaml"
21636
- ]
21809
+ "description": "Import name of a format class declared in the project imports map (e.g. \"Markdown\", \"Csv\"). When omitted, the format is derived from the source file extension via the format registry. \"json\" is the only built-in.",
21810
+ "type": "string"
21637
21811
  },
21638
21812
  "schema": {
21639
21813
  "description": "JSON Schema for validating frontmatter of content type entries.",
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Format registry — auto-discovery of format-extension classes from a project imports map.
3
+ *
4
+ * A class participates in format dispatch iff its .class.json carries a top-level `format` block.
5
+ * Hosts (compiler, server, studio) build a registry from the project-level imports map and dispatch
6
+ * file-extension work (parse, serialize, discover, load) through it. `.json` is the single native
7
+ * built-in and never appears in a registry.
8
+ *
9
+ * All I/O is injected via FormatHostIO so the same module serves node (fs + import) and browser
10
+ * (fetch + dynamic import) hosts.
11
+ */
12
+
13
+ export type FormatCapability = "parse" | "serialize" | "discover" | "load";
14
+
15
+ export const FORMAT_CAPABILITIES: readonly FormatCapability[] = [
16
+ "parse",
17
+ "serialize",
18
+ "discover",
19
+ "load",
20
+ ];
21
+
22
+ export type FormatTiming = "compiler" | "server" | "client";
23
+
24
+ export type FormatDocumentKind = "page" | "component" | "content";
25
+
26
+ export interface FormatBlock {
27
+ extensions: string[];
28
+ mediaType?: string;
29
+ documentKinds?: FormatDocumentKind[];
30
+ exportTarget?: boolean;
31
+ remote?: boolean;
32
+ }
33
+
34
+ export interface StudioHints {
35
+ icon?: string;
36
+ modes?: string[];
37
+ documentMode?: {
38
+ default?: "content" | "component";
39
+ componentWhen?: { frontmatterKey?: string; matches?: string };
40
+ };
41
+ newFileTemplate?: string;
42
+ elements?: {
43
+ block?: string[];
44
+ inline?: string[];
45
+ void?: string[];
46
+ textOnly?: string[];
47
+ nesting?: Record<
48
+ string,
49
+ {
50
+ block?: boolean;
51
+ inline?: boolean;
52
+ directive?: boolean;
53
+ only?: string[];
54
+ }
55
+ >;
56
+ };
57
+ [key: string]: unknown;
58
+ }
59
+
60
+ export interface CapabilityInfo {
61
+ /** Static method name on the implementation class. */
62
+ identifier: string;
63
+ /** Environments allowed to call directly; others round-trip via the dev server. */
64
+ timing: FormatTiming[];
65
+ }
66
+
67
+ export interface FormatHostIO {
68
+ /** Read and parse a JSON file or URL. */
69
+ loadJson(pathOrUrl: string): Promise<Record<string, unknown>>;
70
+ /** Import a JS module by path or URL. */
71
+ importModule(pathOrUrl: string): Promise<Record<string, unknown>>;
72
+ /** Resolve a relative reference against a base (node: path.resolve; browser: new URL). */
73
+ resolvePath(base: string, ref: string): string;
74
+ }
75
+
76
+ interface ClassMethodLike {
77
+ role?: string;
78
+ identifier?: string;
79
+ scope?: string;
80
+ timing?: string[];
81
+ }
82
+
83
+ interface ClassDefLike {
84
+ title?: string;
85
+ $implementation?: string;
86
+ format?: FormatBlock;
87
+ $studio?: StudioHints;
88
+ $defs?: { methods?: Record<string, ClassMethodLike> };
89
+ [key: string]: unknown;
90
+ }
91
+
92
+ const DEFAULT_TIMING: FormatTiming[] = ["compiler", "server"];
93
+
94
+ export class FormatEntry {
95
+ readonly name: string;
96
+ readonly classPath: string;
97
+ readonly classDef: ClassDefLike;
98
+ readonly extensions: string[];
99
+ readonly mediaType: string | null;
100
+ readonly documentKinds: FormatDocumentKind[];
101
+ readonly exportTarget: boolean;
102
+ readonly remote: boolean;
103
+ readonly studio: StudioHints | null;
104
+ readonly capabilities: Partial<Record<FormatCapability, CapabilityInfo>>;
105
+
106
+ #io: FormatHostIO;
107
+ #implementation: Promise<Record<string, unknown>> | null = null;
108
+
109
+ constructor(name: string, classPath: string, classDef: ClassDefLike, io: FormatHostIO) {
110
+ this.name = name;
111
+ this.classPath = classPath;
112
+ this.classDef = classDef;
113
+ this.#io = io;
114
+
115
+ const format = classDef.format as FormatBlock;
116
+ this.extensions = (format.extensions ?? []).map((e) => e.toLowerCase());
117
+ this.mediaType = format.mediaType ?? null;
118
+ this.documentKinds = format.documentKinds ?? [];
119
+ this.exportTarget = format.exportTarget === true;
120
+ this.remote = format.remote === true;
121
+ this.studio = classDef.$studio ?? null;
122
+ this.capabilities = extractCapabilities(classDef);
123
+ }
124
+
125
+ /** Import (and cache) the class's $implementation module. */
126
+ implementation(): Promise<Record<string, unknown>> {
127
+ if (!this.#implementation) {
128
+ const impl = this.classDef.$implementation;
129
+ if (!impl) {
130
+ return Promise.reject(
131
+ new Error(`Format class "${this.name}" has no $implementation to import`),
132
+ );
133
+ }
134
+ this.#implementation = this.#io.importModule(this.#io.resolvePath(this.classPath, impl));
135
+ }
136
+ return this.#implementation;
137
+ }
138
+
139
+ /** Invoke a capability's static method on the implementation class. */
140
+ async call(capability: FormatCapability, ...args: unknown[]): Promise<unknown> {
141
+ const cap = this.capabilities[capability];
142
+ if (!cap) {
143
+ throw new Error(`Format class "${this.name}" does not declare a "${capability}" capability`);
144
+ }
145
+ const mod = await this.implementation();
146
+ const title = this.classDef.title ?? this.name;
147
+ const Cls = (mod[title] ?? (mod.default as Record<string, unknown>)?.[title] ?? mod.default) as
148
+ | Record<string, unknown>
149
+ | undefined;
150
+ const fn = Cls?.[cap.identifier];
151
+ if (typeof fn !== "function") {
152
+ throw new Error(
153
+ `Format class "${this.name}": implementation export "${title}" has no static "${cap.identifier}" method`,
154
+ );
155
+ }
156
+ return await fn.call(Cls, ...args);
157
+ }
158
+ }
159
+
160
+ export class FormatRegistry {
161
+ #entries: FormatEntry[];
162
+
163
+ constructor(entries: FormatEntry[]) {
164
+ this.#entries = entries;
165
+
166
+ // Ambiguous (extension, capability) claims are a configuration error.
167
+ for (const cap of FORMAT_CAPABILITIES) {
168
+ const seen = new Map<string, FormatEntry>();
169
+ for (const entry of entries) {
170
+ if (!entry.capabilities[cap]) continue;
171
+ for (const ext of entry.extensions) {
172
+ const prior = seen.get(ext);
173
+ if (prior) {
174
+ throw new Error(
175
+ `Format conflict: "${prior.name}" and "${entry.name}" both claim "${ext}" with the "${cap}" capability`,
176
+ );
177
+ }
178
+ seen.set(ext, entry);
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ get entries(): readonly FormatEntry[] {
185
+ return this.#entries;
186
+ }
187
+
188
+ /** Look up the format claiming an extension, optionally requiring a capability. */
189
+ byExtension(ext: string, capability?: FormatCapability): FormatEntry | undefined {
190
+ const norm = normalizeExt(ext);
191
+ return this.#entries.find(
192
+ (e) => e.extensions.includes(norm) && (!capability || e.capabilities[capability]),
193
+ );
194
+ }
195
+
196
+ byName(name: string): FormatEntry | undefined {
197
+ return this.#entries.find((e) => e.name === name);
198
+ }
199
+
200
+ withCapability(capability: FormatCapability): FormatEntry[] {
201
+ return this.#entries.filter((e) => e.capabilities[capability]);
202
+ }
203
+
204
+ /**
205
+ * All registered extensions, optionally filtered by document kind. Never includes ".json" — JSON
206
+ * is the native built-in, handled inline by hosts.
207
+ */
208
+ documentExtensions(kind?: FormatDocumentKind): string[] {
209
+ const exts = new Set<string>();
210
+ for (const entry of this.#entries) {
211
+ if (kind && !entry.documentKinds.includes(kind)) continue;
212
+ for (const ext of entry.extensions) exts.add(ext);
213
+ }
214
+ exts.delete(".json");
215
+ return [...exts];
216
+ }
217
+
218
+ has(ext: string): boolean {
219
+ const norm = normalizeExt(ext);
220
+ return this.#entries.some((e) => e.extensions.includes(norm));
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Build a registry by scanning an imports map ({name: path}) for format classes.
226
+ *
227
+ * Only values ending in ".class.json" are inspected; other imports (layouts, components) are
228
+ * skipped, as are class files that fail to load or carry no `format` block.
229
+ */
230
+ export async function buildFormatRegistry(
231
+ imports: Record<string, string> | undefined,
232
+ io: FormatHostIO,
233
+ base?: string,
234
+ ): Promise<FormatRegistry> {
235
+ const entries: FormatEntry[] = [];
236
+ for (const [name, ref] of Object.entries(imports ?? {})) {
237
+ if (typeof ref !== "string" || !ref.endsWith(".class.json")) continue;
238
+ const classPath = base ? io.resolvePath(base, ref) : ref;
239
+ let classDef: ClassDefLike;
240
+ try {
241
+ classDef = (await io.loadJson(classPath)) as ClassDefLike;
242
+ } catch {
243
+ continue; // unreadable imports are not format classes
244
+ }
245
+ if (!classDef || typeof classDef !== "object") continue;
246
+ if (!classDef.format || !Array.isArray(classDef.format.extensions)) continue;
247
+ entries.push(new FormatEntry(name, classPath, classDef, io));
248
+ }
249
+ return new FormatRegistry(entries);
250
+ }
251
+
252
+ function extractCapabilities(
253
+ classDef: ClassDefLike,
254
+ ): Partial<Record<FormatCapability, CapabilityInfo>> {
255
+ const out: Partial<Record<FormatCapability, CapabilityInfo>> = {};
256
+ const methods = classDef.$defs?.methods ?? {};
257
+ for (const [key, method] of Object.entries(methods)) {
258
+ const role = method.role as FormatCapability | undefined;
259
+ if (!role || !FORMAT_CAPABILITIES.includes(role)) continue;
260
+ out[role] = {
261
+ identifier: method.identifier ?? key,
262
+ timing: (method.timing as FormatTiming[] | undefined) ?? DEFAULT_TIMING,
263
+ };
264
+ }
265
+ return out;
266
+ }
267
+
268
+ function normalizeExt(ext: string): string {
269
+ const lower = ext.toLowerCase();
270
+ return lower.startsWith(".") ? lower : `.${lower}`;
271
+ }
package/src/schema.ts CHANGED
@@ -87,6 +87,8 @@ import {
87
87
  classConstructorDefSchema,
88
88
  classMethodDefSchema,
89
89
  classDefSchema,
90
+ formatDefSchema,
91
+ studioHintsSchema,
90
92
  } from "../defs/class-def.schema";
91
93
  import { contentTypeDefSchema } from "../defs/content-type-def.schema";
92
94
  import { imageConfigSchema } from "../defs/image-config.schema";
@@ -305,6 +307,8 @@ export async function generateSchema() {
305
307
  ClassFieldDef: classFieldDefSchema,
306
308
  ClassConstructorDef: classConstructorDefSchema,
307
309
  ClassMethodDef: classMethodDefSchema,
310
+ FormatDef: formatDefSchema,
311
+ StudioHints: studioHintsSchema,
308
312
  ExternalClassDef: externalClassDefSchema,
309
313
  ExpressionPointer: expressionPointerSchema,
310
314
  ExpressionLiteral: expressionLiteralSchema,
@@ -431,6 +435,8 @@ export function generateClassSchema() {
431
435
  type: "string",
432
436
  examples: ["./md.js", "./lib/calculator.js"],
433
437
  },
438
+ format: { $ref: "#/$defs/FormatDef" },
439
+ $studio: { $ref: "#/$defs/StudioHints" },
434
440
  $defs: {
435
441
  description: "Class members: parameters, returnTypes, fields, constructor, methods.",
436
442
  type: "object",
@@ -465,6 +471,8 @@ export function generateClassSchema() {
465
471
  ClassFieldDef: classFieldDefSchema,
466
472
  ClassConstructorDef: classConstructorDefSchema,
467
473
  ClassMethodDef: classMethodDefSchema,
474
+ FormatDef: formatDefSchema,
475
+ StudioHints: studioHintsSchema,
468
476
  },
469
477
  };
470
478
  }
package/types.ts CHANGED
@@ -71,6 +71,9 @@ export interface ImageConfig {
71
71
  quality?: { webp?: number; avif?: number; jpeg?: number; png?: number };
72
72
  sizes?: string;
73
73
  lazyLoad?: boolean;
74
+ service?: "build" | "cloudflare";
75
+ binding?: string;
76
+ remoteDomains?: string[];
74
77
  }
75
78
 
76
79
  export type CemParameter = FromSchema<typeof cemParameterSchema>;