@jxsuite/compiler 0.35.0 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/compiler",
3
- "version": "0.35.0",
3
+ "version": "1.0.0",
4
4
  "description": "Jx static HTML compiler, island detector, and site builder",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -19,15 +19,17 @@
19
19
  "type": "module",
20
20
  "exports": {
21
21
  ".": "./src/compiler.ts",
22
- "./content-loader": "./src/site/content-loader.ts",
23
22
  "./format-host": "./src/site/format-host.ts",
23
+ "./project-sections": "./src/site/project-sections.ts",
24
+ "./schema-command": "./src/site/schema-command.ts",
24
25
  "./context-injection": "./src/site/context-injection.ts",
25
26
  "./head-merger": "./src/site/head-merger.ts",
26
27
  "./layout-resolver": "./src/site/layout-resolver.ts",
27
28
  "./pages-discovery": "./src/site/pages-discovery.ts",
28
29
  "./site": "./src/site/site-build.ts",
29
30
  "./site-loader": "./src/site/site-loader.ts",
30
- "./types": "./src/types.ts"
31
+ "./types": "./src/types.ts",
32
+ "./db-push": "./src/site/db-push.ts"
31
33
  },
32
34
  "publishConfig": {
33
35
  "provenance": true
@@ -42,14 +44,16 @@
42
44
  "upgrade": "bunx npm-check-updates -u && bun install"
43
45
  },
44
46
  "dependencies": {
45
- "@apidevtools/json-schema-ref-parser": "^15.4.0",
46
- "@jxsuite/parser": "^0.35.1",
47
- "@jxsuite/runtime": "^0.34.2",
48
- "@jxsuite/schema": "^0.35.0",
47
+ "@apidevtools/json-schema-ref-parser": "^15.5.0",
48
+ "@jxsuite/create": "^1.0.0",
49
+ "@jxsuite/runtime": "^1.0.0",
50
+ "@jxsuite/schema": "^1.0.0",
49
51
  "sharp": "^0.35.3"
50
52
  },
51
53
  "devDependencies": {
52
54
  "@happy-dom/global-registrator": "^20.10.6",
55
+ "@jxsuite/connector": "^0.2.0",
56
+ "@jxsuite/parser": "^1.0.0",
53
57
  "@types/mdast": "^4.0.4"
54
58
  }
55
59
  }
package/src/cli.ts CHANGED
@@ -2,12 +2,13 @@
2
2
  /**
3
3
  * Jx — Unified CLI for the Jx platform
4
4
  *
5
- * Commands: jx build [project-root] [--verbose] [--no-clean]
5
+ * Commands: jx build [project-root] [--verbose] [--no-clean] jx schema [project-root] jx validate
6
+ * [project-root]
6
7
  *
7
8
  * @module jx-cli
8
9
  */
9
10
 
10
- import { resolve } from "node:path";
11
+ import { relative, resolve } from "node:path";
11
12
 
12
13
  // The CLI body runs inside an async function rather than via top-level await: when a test pulls this
13
14
  // Entry in with a dynamic import(), Bun's test runtime drops the continuation after a top-level await
@@ -21,20 +22,79 @@ async function main() {
21
22
  console.log(`Usage: jx <command> [options]
22
23
 
23
24
  Commands:
24
- build [root] Build a Jx site to dist/
25
+ build [root] Build a Jx site to dist/
26
+ schema [root] Generate project.schema.json + document.schema.json from project.json#/extensions
27
+ validate [root] Validate project.json against its generated project.schema.json
28
+ db push [root] Sync the data section's tables to their connections (additive-only)
25
29
 
26
30
  Options:
27
31
  --verbose Print detailed build progress
28
- --no-clean Don't clean outDir before building`);
32
+ --no-clean Don't clean outDir before building
33
+ --dry-run db push: print the statements without executing them
34
+ --connection db push: restrict to one connection name`);
29
35
  process.exit(0);
30
36
  }
31
37
 
32
- if (command === "build") {
33
- const rest = args.slice(1);
34
- const flags = new Set(rest.filter((a) => a.startsWith("--")));
35
- const positional = rest.find((a) => !a.startsWith("--"));
38
+ // `db push` is a two-word command; normalize it before the flag/positional split.
39
+ const isDb = command === "db";
40
+ const dbSubcommand = isDb ? args[1] : null;
41
+ const rest = args.slice(isDb ? 2 : 1);
42
+ const flags = new Set<string>();
43
+ const positionals: string[] = [];
44
+ let connectionArg: string | undefined;
45
+ for (let index = 0; index < rest.length; index += 1) {
46
+ const arg = rest[index]!;
47
+ if (arg === "--connection") {
48
+ index += 1;
49
+ connectionArg = rest[index];
50
+ } else if (arg.startsWith("--")) {
51
+ flags.add(arg);
52
+ } else {
53
+ positionals.push(arg);
54
+ }
55
+ }
56
+ const [positional] = positionals;
57
+ const projectRoot = resolve(positional ?? ".");
58
+
59
+ if (isDb) {
60
+ if (dbSubcommand !== "push") {
61
+ console.error(
62
+ `Unknown db subcommand: ${dbSubcommand ?? "(none)"}\nUsage: jx db push [root] [--dry-run] [--connection <name>]`,
63
+ );
64
+ process.exit(1);
65
+ }
66
+ const dryRun = flags.has("--dry-run");
67
+ try {
68
+ const { dbPush } = await import("./site/db-push.ts");
69
+ const { results, bindingsPatched, wranglerPath } = await dbPush(projectRoot, {
70
+ dryRun,
71
+ ...(connectionArg === undefined ? {} : { connection: connectionArg }),
72
+ });
73
+ for (const result of results) {
74
+ const mode = result.applied ? "applied" : "dry-run";
75
+ console.log(
76
+ `${result.connection} (${result.provider}) — ${result.tables.length} table(s), ` +
77
+ `${result.statements.length} statement(s) [${mode}]`,
78
+ );
79
+ for (const statement of result.statements) {
80
+ console.log(` ${statement}`);
81
+ }
82
+ for (const warning of result.warnings) {
83
+ console.warn(` warning: ${warning}`);
84
+ }
85
+ }
86
+ if (bindingsPatched && wranglerPath) {
87
+ console.log(`Updated bindings in ${relative(projectRoot, wranglerPath)}`);
88
+ }
89
+ } catch (error) {
90
+ const err = error as Error;
91
+ console.error(`db push failed: ${err.message}`);
92
+ process.exit(1);
93
+ }
94
+ return;
95
+ }
36
96
 
37
- const projectRoot = resolve(positional ?? ".");
97
+ if (command === "build") {
38
98
  const verbose = flags.has("--verbose");
39
99
  const clean = !flags.has("--no-clean");
40
100
 
@@ -58,6 +118,39 @@ Options:
58
118
  console.error(`Build failed: ${err.message}`);
59
119
  process.exit(1);
60
120
  }
121
+ } else if (command === "schema") {
122
+ try {
123
+ const { writeProjectSchemas } = await import("./site/schema-command.ts");
124
+ const { projectSchemaPath, documentSchemaPath } = await writeProjectSchemas(projectRoot);
125
+ console.log(`Wrote ${relative(projectRoot, projectSchemaPath)}`);
126
+ console.log(`Wrote ${relative(projectRoot, documentSchemaPath)}`);
127
+ } catch (error) {
128
+ const err = error as Error;
129
+ console.error(`Schema generation failed: ${err.message}`);
130
+ process.exit(1);
131
+ }
132
+ } else if (command === "validate") {
133
+ try {
134
+ const { validateProjectFile } = await import("@jxsuite/schema/validate-project");
135
+ const { valid, errors } = await validateProjectFile(projectRoot);
136
+ if (valid) {
137
+ console.log(`project.json is valid (${projectRoot})`);
138
+ } else {
139
+ console.error(`project.json is INVALID (${projectRoot}):`);
140
+ for (const error of errors ?? []) {
141
+ const { instancePath, message } = (error ?? {}) as {
142
+ instancePath?: string;
143
+ message?: string;
144
+ };
145
+ console.error(` - ${instancePath || "/"}: ${message ?? JSON.stringify(error)}`);
146
+ }
147
+ process.exit(1);
148
+ }
149
+ } catch (error) {
150
+ const err = error as Error;
151
+ console.error(`Validation failed: ${err.message}`);
152
+ process.exit(1);
153
+ }
61
154
  } else {
62
155
  console.error(`Unknown command: ${command}\nRun "jx --help" for usage.`);
63
156
  process.exit(1);
@@ -4,9 +4,6 @@
4
4
  * Injects project-level and page-level context variables into a page's state before compilation.
5
5
  * These are available as $site.* and $page.* in template expressions.
6
6
  *
7
- * Also resolves ContentCollection and ContentEntry $prototype entries against loaded content types
8
- * (Phase 2, spec §6.4).
9
- *
10
7
  * Per site-architecture spec §10: $site.name — from project.json name $site.url — from project.json
11
8
  * url $site.state.* — site-wide reactive state $page.url — current page URL path $page.title — page
12
9
  * title $page.params — dynamic route parameters (if any)
@@ -19,7 +16,6 @@ import type {
19
16
  JxStateDefinition,
20
17
  ProjectConfig,
21
18
  } from "@jxsuite/schema/types";
22
- import type { ContentLoaderEntry } from "@jxsuite/parser/types";
23
19
  import type { SiteRoute } from "../types.ts";
24
20
 
25
21
  /**
@@ -28,7 +24,6 @@ import type { SiteRoute } from "../types.ts";
28
24
  * @param {JxDocument} doc - The page document (mutated)
29
25
  * @param {ProjectConfig} projectConfig - Loaded project configuration
30
26
  * @param {SiteRoute} route - The resolved route for this page
31
- * @param {Map<string, ContentLoaderEntry[]>} [contentTypes] - Loaded content types
32
27
  * @param {string | null} [projectRoot] - Absolute path to the project root (for import rebasing)
33
28
  * @returns {JxDocument} The mutated document
34
29
  */
@@ -36,7 +31,6 @@ export function injectContext(
36
31
  doc: JxDocument,
37
32
  projectConfig: ProjectConfig,
38
33
  route: SiteRoute,
39
- _contentTypes = new Map<string, ContentLoaderEntry[]>(),
40
34
  projectRoot: string | null = null,
41
35
  ) {
42
36
  if (!doc.state) {
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Db-push — `jx db push [root] [--dry-run] [--connection <name>]` (plan Part 4a).
3
+ *
4
+ * Registry-driven: every `connections` entry resolves to its connector class (by
5
+ * `connector.provider`) and the additive schema sync runs through the class's `deploySchema`
6
+ * capability with the connection's tables from the `data` section. The environment is process.env
7
+ * merged under `<root>/.dev.vars` (wrangler convention, specs/extensions.md §13). After a non-dry
8
+ * apply, each connector's `bindings` fragment is deep-merged into wrangler.jsonc via
9
+ * `applyBindingFragments` (user keys preserved).
10
+ */
11
+
12
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { resolve } from "node:path";
14
+ import { applyBindingFragments } from "@jxsuite/create/scaffold";
15
+ import { buildProjectExtensionRegistry } from "./format-host.ts";
16
+ import { loadProjectConfig } from "./site-loader.ts";
17
+ import type { FormatEntry } from "@jxsuite/schema/format-registry";
18
+
19
+ export interface DbPushOptions {
20
+ dryRun?: boolean;
21
+ /** Restrict the push to one connection name. */
22
+ connection?: string;
23
+ }
24
+
25
+ export interface ConnectionPushResult {
26
+ connection: string;
27
+ provider: string;
28
+ tables: string[];
29
+ statements: string[];
30
+ warnings: string[];
31
+ applied: boolean;
32
+ }
33
+
34
+ export interface DbPushResult {
35
+ results: ConnectionPushResult[];
36
+ /** True when wrangler.jsonc was rewritten with binding fragments. */
37
+ bindingsPatched: boolean;
38
+ wranglerPath: string | null;
39
+ }
40
+
41
+ interface DeployResultLike {
42
+ statements?: string[];
43
+ warnings?: string[];
44
+ applied?: boolean;
45
+ }
46
+
47
+ /**
48
+ * Push the project's table schemas to their connections' databases.
49
+ *
50
+ * @param {string} projectRoot - Absolute path to the project directory
51
+ * @param {DbPushOptions} [options]
52
+ * @returns {Promise<DbPushResult>}
53
+ */
54
+ export async function dbPush(
55
+ projectRoot: string,
56
+ options: DbPushOptions = {},
57
+ ): Promise<DbPushResult> {
58
+ const { dryRun = false, connection: only } = options;
59
+ const { config } = loadProjectConfig(projectRoot);
60
+ const registry = await buildProjectExtensionRegistry(projectRoot, config);
61
+
62
+ const connections = (config.connections ?? {}) as Record<string, Record<string, unknown>>;
63
+ const tables = (config.data ?? {}) as Record<string, { connection?: string }>;
64
+
65
+ const names = Object.keys(connections).filter((name) => !only || name === only);
66
+ if (only && names.length === 0) {
67
+ throw new Error(`Unknown connection "${only}" (no such connections entry in project.json)`);
68
+ }
69
+ if (names.length === 0) {
70
+ throw new Error(
71
+ `project.json has no "connections" section — nothing to push. Declare a connection and ` +
72
+ `its tables in the "data" section first.`,
73
+ );
74
+ }
75
+
76
+ const env: Record<string, unknown> = {
77
+ ...process.env,
78
+ ...readDevVars(projectRoot),
79
+ JX_PROJECT_ROOT: projectRoot,
80
+ };
81
+
82
+ const results: ConnectionPushResult[] = [];
83
+ const fragments: Record<string, unknown>[] = [];
84
+ for (const name of names) {
85
+ const connection: Record<string, unknown> = { ...connections[name], $name: name };
86
+ const provider = String(connection.provider ?? "");
87
+ const entry = connectorByProvider(registry.connectors(), provider);
88
+ if (!entry) {
89
+ throw new Error(
90
+ `Connection "${name}" names provider "${provider}", but no enabled extension provides ` +
91
+ `it (enable the extension in project.json "extensions")`,
92
+ );
93
+ }
94
+ const connectionTables: Record<string, unknown> = {};
95
+ for (const [tableName, def] of Object.entries(tables)) {
96
+ if (def.connection === name) {
97
+ connectionTables[tableName] = def;
98
+ }
99
+ }
100
+
101
+ const deployed = (await entry.call("deploySchema", connectionTables, connection, {
102
+ dryRun,
103
+ env,
104
+ })) as DeployResultLike;
105
+ results.push({
106
+ applied: deployed.applied ?? !dryRun,
107
+ connection: name,
108
+ provider,
109
+ statements: deployed.statements ?? [],
110
+ tables: Object.keys(connectionTables),
111
+ warnings: deployed.warnings ?? [],
112
+ });
113
+
114
+ if (entry.capabilities.bindings) {
115
+ const fragment = (await entry.call("bindings", connection)) as Record<string, unknown>;
116
+ if (fragment && Object.keys(fragment).length > 0) {
117
+ fragments.push(fragment);
118
+ }
119
+ }
120
+ }
121
+
122
+ // Apply binding fragments to wrangler.jsonc (skipped on dry runs and non-wrangler projects).
123
+ const wranglerPath = resolve(projectRoot, "wrangler.jsonc");
124
+ let bindingsPatched = false;
125
+ if (!dryRun && fragments.length > 0 && existsSync(wranglerPath)) {
126
+ const existing = readFileSync(wranglerPath, "utf8");
127
+ const { content, patched } = applyBindingFragments(existing, fragments);
128
+ if (patched && content !== existing) {
129
+ writeFileSync(wranglerPath, content, "utf8");
130
+ bindingsPatched = true;
131
+ }
132
+ }
133
+
134
+ return { bindingsPatched, results, wranglerPath: existsSync(wranglerPath) ? wranglerPath : null };
135
+ }
136
+
137
+ /** Find the connector class serving a provider id. */
138
+ function connectorByProvider(entries: FormatEntry[], provider: string): FormatEntry | undefined {
139
+ return entries.find((entry) => entry.connector?.provider === provider);
140
+ }
141
+
142
+ /**
143
+ * Parse `<root>/.dev.vars` (wrangler convention): KEY=VALUE lines, `#` comments, optional
144
+ * single/double quotes. Kept deliberately tiny; the dev server has its own copy in
145
+ * `@jxsuite/server` (dev-vars.ts) — the compiler cannot depend on the server package.
146
+ *
147
+ * @param {string} projectRoot
148
+ * @returns {Record<string, string>}
149
+ */
150
+ export function readDevVars(projectRoot: string): Record<string, string> {
151
+ const path = resolve(projectRoot, ".dev.vars");
152
+ if (!existsSync(path)) {
153
+ return {};
154
+ }
155
+ const out: Record<string, string> = {};
156
+ for (const rawLine of readFileSync(path, "utf8").split("\n")) {
157
+ const line = rawLine.trim();
158
+ if (line === "" || line.startsWith("#")) {
159
+ continue;
160
+ }
161
+ const eq = line.indexOf("=");
162
+ if (eq === -1) {
163
+ continue;
164
+ }
165
+ const key = line.slice(0, eq).trim();
166
+ let value = line.slice(eq + 1).trim();
167
+ if (
168
+ (value.startsWith('"') && value.endsWith('"')) ||
169
+ (value.startsWith("'") && value.endsWith("'"))
170
+ ) {
171
+ value = value.slice(1, -1);
172
+ }
173
+ if (key !== "") {
174
+ out[key] = value;
175
+ }
176
+ }
177
+ return out;
178
+ }
@@ -1,19 +1,21 @@
1
1
  /**
2
2
  * Format-host — node-side FormatHostIO and project registry construction
3
3
  *
4
- * Provides the filesystem/module I/O the shared format registry needs to run inside the compiler
5
- * (and dev server). Format classes are auto-discovered from the project-level imports map; see
6
- * specs/extensions.md.
4
+ * Provides the filesystem/module I/O the shared extension registry needs to run inside the compiler
5
+ * (and dev server). Extensions are declared explicitly in project.json's `extensions` array and
6
+ * discovered through their jx-extension.json manifests; see specs/extensions.md.
7
7
  */
8
8
 
9
9
  import { readFileSync } from "node:fs";
10
10
  import { dirname, isAbsolute, resolve, sep } from "node:path";
11
11
  import { pathToFileURL } from "node:url";
12
12
  import { createRequire } from "node:module";
13
- import { buildFormatRegistry } from "@jxsuite/schema/format-registry";
14
- import type { FormatHostIO, FormatRegistry } from "@jxsuite/schema/format-registry";
13
+ import { buildExtensionRegistry } from "@jxsuite/schema/extension-registry";
14
+ import type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
15
+ import type { FormatHostIO, FormatRegistry, ProjectBlock } from "@jxsuite/schema/format-registry";
15
16
  import type { ProjectConfig } from "@jxsuite/schema/types";
16
17
 
18
+ export type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
17
19
  export type { FormatRegistry, FormatEntry } from "@jxsuite/schema/format-registry";
18
20
 
19
21
  /**
@@ -73,23 +75,136 @@ export function createNodeFormatIO(projectRoot: string): FormatHostIO {
73
75
  };
74
76
  }
75
77
 
76
- /** Build the format registry for a project from its project.json imports map. */
77
- export async function buildProjectFormatRegistry(
78
+ /** Build the extension registry for a project from its project.json `extensions` array. */
79
+ export async function buildProjectExtensionRegistry(
78
80
  projectRoot: string,
79
81
  projectConfig?: ProjectConfig,
80
- ): Promise<FormatRegistry> {
81
- return buildFormatRegistry(
82
- projectConfig?.imports as Record<string, string> | undefined,
82
+ ): Promise<ExtensionRegistry> {
83
+ return buildExtensionRegistry(
84
+ projectConfig?.extensions,
83
85
  createNodeFormatIO(projectRoot),
84
86
  resolve(projectRoot, "project.json"),
85
87
  );
86
88
  }
87
89
 
90
+ /**
91
+ * Format-dispatch view of the project's extension registry.
92
+ *
93
+ * @deprecated Part-3 cleanup: the desktop app still imports this; use
94
+ * `buildProjectExtensionRegistry(...).then((r) => r.formats)` instead.
95
+ */
96
+ export async function buildProjectFormatRegistry(
97
+ projectRoot: string,
98
+ projectConfig?: ProjectConfig,
99
+ ): Promise<FormatRegistry> {
100
+ const registry = await buildProjectExtensionRegistry(projectRoot, projectConfig);
101
+ return registry.formats;
102
+ }
103
+
104
+ // ─── Studio extensions payload ────────────────────────────────────────────────
105
+
106
+ /** One project-section contribution on the studio wire (specs/extensions.md §9/§9.1). */
107
+ export interface ExtensionsPayloadContribution {
108
+ /** The $prototype-visible class name declaring the `project` block. */
109
+ className: string;
110
+ /** The class descriptor's `project` admission block. */
111
+ project: ProjectBlock;
112
+ /** The class descriptor's `$studio` block (settings section vocabulary), when declared. */
113
+ studio: Record<string, unknown> | null;
114
+ /**
115
+ * The section's value schema — `properties[<key>]` of the extension's project fragment — or null
116
+ * when the extension ships no project fragment (or it lacks the key).
117
+ */
118
+ entrySchema: Record<string, unknown> | null;
119
+ }
120
+
121
+ /** One extension package on the studio wire (the formats route's `extensions` sibling array). */
122
+ export interface ExtensionsPayloadEntry {
123
+ specifier: string;
124
+ name: string;
125
+ title?: string;
126
+ description?: string;
127
+ contributions: ExtensionsPayloadContribution[];
128
+ /**
129
+ * Every manifest class of the extension with its resolved descriptor path, in declaration order
130
+ * (additive; lets the studio address a class by `$src` without hardcoding its package layout).
131
+ * Plain state classes (no admission blocks — `$prototype` targets like TableQuery or Session)
132
+ * carry `state: true` plus their `$studio.stateDefaults` hint (specs/extensions.md §10), so the
133
+ * studio can offer them in its add-state picker and seed new defs (e.g. `timing: "client"`).
134
+ */
135
+ classes: {
136
+ name: string;
137
+ path: string;
138
+ state?: boolean;
139
+ stateDefaults?: Record<string, unknown>;
140
+ }[];
141
+ }
142
+
143
+ /**
144
+ * Build the studio-facing extensions payload from a project's extension registry: per extension,
145
+ * its manifest identity plus every project-section contribution paired with the entry schema read
146
+ * from the extension's shipped project fragment. Unreadable fragments degrade to a null entrySchema
147
+ * (the studio renders the section without field metadata).
148
+ *
149
+ * @param {ExtensionRegistry} registry
150
+ * @returns {ExtensionsPayloadEntry[]}
151
+ */
152
+ export function buildExtensionsPayload(registry: ExtensionRegistry): ExtensionsPayloadEntry[] {
153
+ const payload: ExtensionsPayloadEntry[] = [];
154
+ for (const ext of registry.extensions) {
155
+ let fragmentProperties: Record<string, Record<string, unknown>> = {};
156
+ if (ext.schemas.project) {
157
+ try {
158
+ const fragment = JSON.parse(readFileSync(ext.schemas.project, "utf8")) as {
159
+ properties?: Record<string, Record<string, unknown>>;
160
+ };
161
+ fragmentProperties = fragment.properties ?? {};
162
+ } catch {
163
+ fragmentProperties = {};
164
+ }
165
+ }
166
+ const contributions: ExtensionsPayloadContribution[] = [];
167
+ for (const cls of ext.classes) {
168
+ if (!cls.project) {
169
+ continue;
170
+ }
171
+ contributions.push({
172
+ className: cls.name,
173
+ entrySchema: fragmentProperties[cls.project.key] ?? null,
174
+ project: cls.project,
175
+ studio: cls.studio,
176
+ });
177
+ }
178
+ payload.push({
179
+ classes: ext.classes.map((cls) => {
180
+ const stateDefaults = cls.studio?.stateDefaults as Record<string, unknown> | undefined;
181
+ const isState =
182
+ cls.project === null &&
183
+ cls.server === null &&
184
+ cls.connector === null &&
185
+ cls.extensions.length === 0;
186
+ return {
187
+ name: cls.name,
188
+ path: cls.classPath,
189
+ ...(isState ? { state: true } : {}),
190
+ ...(stateDefaults && typeof stateDefaults === "object" ? { stateDefaults } : {}),
191
+ };
192
+ }),
193
+ contributions,
194
+ name: ext.manifest.name,
195
+ specifier: ext.specifier,
196
+ ...(ext.manifest.title === undefined ? {} : { title: ext.manifest.title }),
197
+ ...(ext.manifest.description === undefined ? {} : { description: ext.manifest.description }),
198
+ });
199
+ }
200
+ return payload;
201
+ }
202
+
88
203
  /** Error message for an unregistered non-JSON extension, naming the fix. */
89
204
  export function unknownFormatError(path: string, ext: string): Error {
90
205
  return new Error(
91
- `No format class imported for "${ext}" (${path}). ` +
92
- `Add a format class to project.json imports, e.g. ` +
93
- `"Markdown": "@jxsuite/parser/Markdown.class.json".`,
206
+ `No format class registered for "${ext}" (${path}). ` +
207
+ `Enable an extension providing this format in project.json "extensions", ` +
208
+ `e.g. "@jxsuite/parser".`,
94
209
  );
95
210
  }