Skip to content

Commit 3a15368

Browse files
committed
Merge branch 'main' into copilot/update-fumadocs-to-latest
2 parents a6779f8 + dcb9f98 commit 3a15368

7 files changed

Lines changed: 137 additions & 27 deletions

File tree

apps/docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"license": "Apache-2.0",
77
"scripts": {
88
"dev": "next dev",
9-
"build": "pnpm --filter @objectstack/spec gen:docs && next build",
9+
"build": "pnpm --filter @objectstack/spec gen:schema && pnpm --filter @objectstack/spec gen:docs && next build",
1010
"site:start": "next start",
1111
"site:lint": "next lint",
1212
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit",

packages/cli/src/commands/serve.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,17 @@ export const serveCommand = new Command('serve')
207207
}
208208
}
209209

210+
// Wrap raw config objects (no init/start) into AppPlugin
211+
// This handles plugins defined as plain { name, objects, ... } bundles
212+
if (pluginToLoad && typeof pluginToLoad === 'object' && !pluginToLoad.init) {
213+
try {
214+
const { AppPlugin } = await import('@objectstack/runtime');
215+
pluginToLoad = new AppPlugin(pluginToLoad);
216+
} catch (e: any) {
217+
// Fall through to kernel.use which will report the error
218+
}
219+
}
220+
210221
await kernel.use(pluginToLoad);
211222
const pluginName = plugin.name || plugin.constructor?.name || 'unnamed';
212223
trackPlugin(pluginName);

packages/objectql/src/engine.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,92 @@ export class ObjectQL implements IDataEngine {
268268
this.logger.debug('Registered Kind', { kind: kind.name || kind.type, from: id });
269269
}
270270
}
271+
272+
// 7. Recursively register nested plugins
273+
if (Array.isArray(manifest.plugins) && manifest.plugins.length > 0) {
274+
this.logger.debug('Processing nested plugins', { id, count: manifest.plugins.length });
275+
for (const plugin of manifest.plugins) {
276+
if (plugin && typeof plugin === 'object') {
277+
const pluginName = plugin.name || plugin.id || 'unnamed-plugin';
278+
this.logger.debug('Registering nested plugin', { pluginName, parentId: id });
279+
this.registerPlugin(plugin, id, namespace);
280+
}
281+
}
282+
}
283+
}
284+
285+
/**
286+
* Register a nested plugin's metadata (objects, actions, views, etc.)
287+
*
288+
* Unlike registerApp(), this does NOT call SchemaRegistry.installPackage()
289+
* because plugins are not formal manifests — they are lightweight config
290+
* bundles with objects, actions, triggers, and navigation.
291+
*
292+
* @param plugin - The plugin config object
293+
* @param parentId - The parent package ID (for ownership tracking)
294+
* @param parentNamespace - The parent package's namespace (for FQN resolution)
295+
*/
296+
private registerPlugin(plugin: any, parentId: string, parentNamespace?: string) {
297+
const pluginName = plugin.name || plugin.id || 'unnamed';
298+
const pluginNamespace = plugin.namespace || parentNamespace;
299+
300+
// Use parentId as the owning package for namespace consistency.
301+
// The parent package already claimed the namespace — nested plugins
302+
// contribute objects UNDER the parent's ownership.
303+
const ownerId = parentId;
304+
305+
// Register objects (supports both Array and Map formats)
306+
if (plugin.objects) {
307+
try {
308+
if (Array.isArray(plugin.objects)) {
309+
this.logger.debug('Registering plugin objects (Array)', { pluginName, count: plugin.objects.length });
310+
for (const objDef of plugin.objects) {
311+
const fqn = SchemaRegistry.registerObject(objDef, ownerId, pluginNamespace, 'own');
312+
this.logger.debug('Registered Object', { fqn, from: pluginName });
313+
}
314+
} else {
315+
const entries = Object.entries(plugin.objects);
316+
this.logger.debug('Registering plugin objects (Map)', { pluginName, count: entries.length });
317+
for (const [name, objDef] of entries) {
318+
(objDef as any).name = name;
319+
const fqn = SchemaRegistry.registerObject(objDef as any, ownerId, pluginNamespace, 'own');
320+
this.logger.debug('Registered Object', { fqn, from: pluginName });
321+
}
322+
}
323+
} catch (err: any) {
324+
this.logger.warn('Failed to register plugin objects', { pluginName, error: err.message });
325+
}
326+
}
327+
328+
// Register plugin as app if it has navigation (for sidebar display)
329+
if (plugin.name && plugin.navigation) {
330+
try {
331+
SchemaRegistry.registerApp(plugin, ownerId);
332+
this.logger.debug('Registered plugin-as-app', { app: plugin.name, from: pluginName });
333+
} catch (err: any) {
334+
this.logger.warn('Failed to register plugin as app', { pluginName, error: err.message });
335+
}
336+
}
337+
338+
// Register metadata arrays (actions, views, triggers, etc.)
339+
const metadataArrayKeys = [
340+
'actions', 'views', 'pages', 'dashboards', 'reports', 'themes',
341+
'flows', 'workflows', 'approvals', 'webhooks',
342+
'roles', 'permissions', 'profiles', 'sharingRules', 'policies',
343+
'agents', 'ragPipelines', 'apis',
344+
'hooks', 'mappings', 'analyticsCubes', 'connectors',
345+
];
346+
for (const key of metadataArrayKeys) {
347+
const items = (plugin as any)[key];
348+
if (Array.isArray(items) && items.length > 0) {
349+
for (const item of items) {
350+
const itemName = item.name || item.id;
351+
if (itemName) {
352+
SchemaRegistry.registerItem(key, item, 'name' as any, ownerId);
353+
}
354+
}
355+
}
356+
}
271357
}
272358

273359
/**

packages/spec/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,7 @@
112112
"@vitest/coverage-v8": "^4.0.18",
113113
"tsx": "^4.21.0",
114114
"typescript": "^5.3.0",
115-
"vitest": "^4.0.18",
116-
"zod-to-json-schema": "^3.25.1"
115+
"vitest": "^4.0.18"
117116
},
118117
"dependencies": {
119118
"zod": "^4.3.6"

packages/spec/scripts/build-docs.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,11 @@ Object.entries(CATEGORIES).forEach(([category, title]) => {
329329
// BUT 'index' must be in 'meta.json' pages? No, index is implicit usually.
330330

331331
// However, Fumadocs often treats folder/index.mdx as the page for the folder.
332-
fs.writeFileSync(path.join(DOCS_ROOT, category, 'index.mdx'), mdx);
332+
// Ensure directory exists before writing
333+
const categoryDir = path.join(DOCS_ROOT, category);
334+
if (!fs.existsSync(categoryDir)) fs.mkdirSync(categoryDir, { recursive: true });
335+
336+
fs.writeFileSync(path.join(categoryDir, 'index.mdx'), mdx);
333337
console.log(`✓ Generated ${category}/index.mdx`);
334338
});
335339

packages/spec/scripts/build-schemas.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import fs from 'fs';
44
import path from 'path';
55
import { z } from 'zod';
6-
import { zodToJsonSchema } from 'zod-to-json-schema';
76
import * as Protocol from '../src/index';
87

98
const OUT_DIR = path.resolve(__dirname, '../json-schema');
@@ -92,8 +91,20 @@ ensureDir(OUT_DIR);
9291
console.log(`Generating JSON Schemas to ${OUT_DIR}...`);
9392

9493
let count = 0;
94+
let skippedCount = 0;
9595
let errorCount = 0;
9696

97+
// Error messages for schema types that inherently cannot be represented in JSON Schema.
98+
// These are expected warnings, not build-breaking errors.
99+
const KNOWN_UNSUPPORTED_PATTERNS = [
100+
'cannot be represented in JSON Schema',
101+
];
102+
103+
function isKnownUnsupported(error: unknown): boolean {
104+
const msg = error instanceof Error ? error.message : String(error);
105+
return KNOWN_UNSUPPORTED_PATTERNS.some((p) => msg.includes(p));
106+
}
107+
97108
// Protocol now exports namespaces (Data, UI, System, AI, API)
98109
// We need to iterate through each namespace
99110
for (const [namespaceName, namespaceExports] of Object.entries(Protocol)) {
@@ -118,31 +129,42 @@ for (const [namespaceName, namespaceExports] of Object.entries(Protocol)) {
118129
const schemaName = key.endsWith('Schema') ? key.replace('Schema', '') : key;
119130

120131
try {
121-
// Convert to JSON Schema
122-
const jsonSchema = zodToJsonSchema(value, {
123-
name: schemaName,
124-
$refStrategy: "none" // We want self-contained schemas for now
132+
// Convert to JSON Schema using Zod v4's built-in toJSONSchema()
133+
const jsonSchema = z.toJSONSchema(value, {
134+
target: 'draft-2020-12',
125135
});
126136

127137
const fileName = `${schemaName}.json`;
128138
const filePath = path.join(categoryDir, fileName);
129139

130140
writeFileWithRetry(filePath, JSON.stringify(jsonSchema, null, 2));
131-
console.log(`✓ ${namespaceName.toLowerCase()}/${fileName}`);
141+
console.log(` ${namespaceName.toLowerCase()}/${fileName}`);
132142
count++;
133143
} catch (error) {
134-
console.error(`Failed to generate schema for ${namespaceName}.${key}:`, error);
135-
errorCount++;
144+
if (isKnownUnsupported(error)) {
145+
// Functions, transforms, Date types etc. have no JSON Schema representation — skip gracefully
146+
const msg = error instanceof Error ? error.message : String(error);
147+
console.warn(` ⊘ ${namespaceName}.${key}: ${msg} (skipped)`);
148+
skippedCount++;
149+
} else {
150+
console.error(` ✗ Failed to generate schema for ${namespaceName}.${key}:`, error);
151+
errorCount++;
152+
}
136153
}
137154
}
138155
}
139156
}
140157
}
141158

159+
console.log(`\n─── Summary ───`);
160+
console.log(` Generated: ${count}`);
161+
if (skippedCount > 0) {
162+
console.log(` Skipped: ${skippedCount} (unsupported types: function, transform, date)`);
163+
}
164+
142165
if (errorCount > 0) {
143-
console.error(`\n❌ Completed with ${errorCount} error(s). ${count} schemas generated successfully.`);
144-
console.error(`\nNote: Partial schema generation occurred. Some schemas may be missing.`);
145-
console.error(`This typically indicates a Zod schema definition error or file system issue.`);
166+
console.error(` Errors: ${errorCount}`);
167+
console.error(`\n❌ Build failed with ${errorCount} unexpected error(s).`);
146168
process.exit(1);
147169
}
148170

pnpm-lock.yaml

Lines changed: 0 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)