Skip to content

Commit 30577f5

Browse files
authored
Merge pull request #639 from objectstack-ai/copilot/complete-3-0-development-again
2 parents 962a3e9 + 7914b5e commit 30577f5

12 files changed

Lines changed: 1344 additions & 231 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ jobs:
143143
fi
144144
echo "Build outputs verified successfully"
145145
146+
- name: Analyze bundle size
147+
run: pnpm --filter @objectstack/spec analyze
148+
146149
- name: Upload build artifacts
147150
uses: actions/upload-artifact@v6
148151
with:

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ packages/*/*.tsbuildinfo
4848
# Generated JSON Schema (rebuilt by `pnpm gen:schema` during build)
4949
packages/spec/json-schema/
5050

51+
# Generated bundle size reports
52+
packages/spec/bundle-size-report.json
53+
54+
# Generated SBOM (rebuilt during release)
55+
sbom.json
56+
5157
# Exclude generated JS from TypeScript sources
5258
packages/*/src/**/*.js
5359
!packages/*/src/**/*.config.js

ROADMAP.md

Lines changed: 124 additions & 119 deletions
Large diffs are not rendered by default.

packages/cli/src/commands/generate.ts

Lines changed: 181 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,110 @@ function toSnakeCase(str: string): string {
204204
return str.replace(/[-]/g, '_').replace(/[A-Z]/g, c => `_${c.toLowerCase()}`).replace(/^_/, '');
205205
}
206206

207+
// ─── Field Type Mapping ─────────────────────────────────────────────
208+
209+
const FIELD_TYPE_MAP: Record<string, string> = {
210+
text: 'string',
211+
textarea: 'string',
212+
richtext: 'string',
213+
html: 'string',
214+
markdown: 'string',
215+
number: 'number',
216+
integer: 'number',
217+
currency: 'number',
218+
percent: 'number',
219+
boolean: 'boolean',
220+
date: 'string',
221+
datetime: 'string',
222+
time: 'string',
223+
email: 'string',
224+
phone: 'string',
225+
url: 'string',
226+
select: 'string',
227+
multiselect: 'string[]',
228+
lookup: 'string',
229+
master_detail: 'string',
230+
formula: 'unknown',
231+
autonumber: 'string',
232+
json: 'Record<string, unknown>',
233+
file: 'string',
234+
image: 'string',
235+
password: 'string',
236+
slug: 'string',
237+
uuid: 'string',
238+
ip_address: 'string',
239+
color: 'string',
240+
rating: 'number',
241+
geo_point: '{ lat: number; lng: number }',
242+
vector: 'number[]',
243+
encrypted: 'string',
244+
};
245+
246+
function fieldTypeToTs(fieldType: string, multiple?: boolean): string {
247+
const base = FIELD_TYPE_MAP[fieldType] || 'unknown';
248+
return multiple ? `${base}[]` : base;
249+
}
250+
251+
function generateTypesFromConfig(config: Record<string, unknown>): string {
252+
const lines: string[] = [
253+
'// Auto-generated by ObjectStack CLI — do not edit manually',
254+
`// Generated at ${new Date().toISOString()}`,
255+
'',
256+
"import type { Data } from '@objectstack/spec';",
257+
'',
258+
];
259+
260+
// Extract objects from config (supports both top-level and nested)
261+
const objects: Record<string, unknown>[] = [];
262+
const rawObjects = (config as any).objects ?? (config as any).data?.objects ?? {};
263+
264+
if (Array.isArray(rawObjects)) {
265+
objects.push(...rawObjects);
266+
} else if (typeof rawObjects === 'object') {
267+
for (const val of Object.values(rawObjects)) {
268+
if (val && typeof val === 'object') objects.push(val as Record<string, unknown>);
269+
}
270+
}
271+
272+
if (objects.length === 0) {
273+
lines.push('// No objects found in configuration');
274+
return lines.join('\n') + '\n';
275+
}
276+
277+
for (const obj of objects) {
278+
const name = String(obj.name || 'unknown');
279+
const typeName = name
280+
.split('_')
281+
.map((w: string) => w.charAt(0).toUpperCase() + w.slice(1))
282+
.join('');
283+
const fields = (obj.fields ?? {}) as Record<string, Record<string, unknown>>;
284+
285+
lines.push(`/** ${String(obj.label || typeName)} record type */`);
286+
lines.push(`export interface ${typeName}Record {`);
287+
lines.push(' id: string;');
288+
289+
for (const [fieldName, fieldDef] of Object.entries(fields)) {
290+
const fType = String(fieldDef.type || 'text');
291+
const tsType = fieldTypeToTs(fType, !!fieldDef.multiple);
292+
const required = fieldDef.required ? '' : '?';
293+
if (fieldDef.label) {
294+
lines.push(` /** ${fieldDef.label} */`);
295+
}
296+
lines.push(` ${fieldName}${required}: ${tsType};`);
297+
}
298+
299+
lines.push('}');
300+
lines.push('');
301+
}
302+
303+
return lines.join('\n') + '\n';
304+
}
305+
207306
// ─── Command ────────────────────────────────────────────────────────
208307

209-
export const generateCommand = new Command('generate')
210-
.alias('g')
211-
.description('Generate metadata files (object, view, action, flow, agent, dashboard, app)')
308+
const generateMetadataCommand = new Command('metadata')
309+
.alias('m')
310+
.description('Generate metadata scaffold (object, view, action, flow, agent, dashboard, app)')
212311
.argument('<type>', 'Metadata type to generate')
213312
.argument('<name>', 'Name for the metadata (use kebab-case)')
214313
.option('-d, --dir <directory>', 'Target directory (overrides default)')
@@ -297,3 +396,82 @@ export const generateCommand = new Command('generate')
297396
process.exit(1);
298397
}
299398
});
399+
400+
const generateTypesCommand = new Command('types')
401+
.description('Generate TypeScript type definitions from ObjectStack configuration')
402+
.argument('[config]', 'Configuration file path')
403+
.option('-o, --output <file>', 'Output file path', 'src/types/objectstack.d.ts')
404+
.option('--dry-run', 'Show what would be generated without writing files')
405+
.action(async (configPath, options) => {
406+
printHeader('Generate Types');
407+
408+
try {
409+
const { loadConfig } = await import('../utils/config.js');
410+
printInfo('Loading configuration...');
411+
const { config, absolutePath } = await loadConfig(configPath);
412+
413+
console.log(` ${chalk.dim('Config:')} ${chalk.white(absolutePath)}`);
414+
console.log(` ${chalk.dim('Output:')} ${chalk.white(options.output)}`);
415+
console.log('');
416+
417+
const content = generateTypesFromConfig(config as Record<string, unknown>);
418+
419+
if (options.dryRun) {
420+
printInfo('Dry run — no files written');
421+
console.log('');
422+
for (const line of content.split('\n')) {
423+
console.log(chalk.dim(` ${line}`));
424+
}
425+
console.log('');
426+
return;
427+
}
428+
429+
const outPath = path.resolve(process.cwd(), options.output);
430+
const outDir = path.dirname(outPath);
431+
if (!fs.existsSync(outDir)) {
432+
fs.mkdirSync(outDir, { recursive: true });
433+
}
434+
fs.writeFileSync(outPath, content);
435+
printSuccess(`Generated types at ${options.output}`);
436+
console.log('');
437+
438+
} catch (error: any) {
439+
printError(error.message || String(error));
440+
process.exit(1);
441+
}
442+
});
443+
444+
export const generateCommand = new Command('generate')
445+
.alias('g')
446+
.description('Generate metadata files or TypeScript types')
447+
.argument('[type]', 'Metadata type to generate (object, view, action, flow, agent, dashboard, app)')
448+
.argument('[name]', 'Name for the metadata (use kebab-case)')
449+
.option('-d, --dir <directory>', 'Target directory (overrides default)')
450+
.option('--dry-run', 'Show what would be created without writing files')
451+
.addCommand(generateTypesCommand)
452+
.action(async (type: string | undefined, name: string | undefined, options) => {
453+
if (!type) {
454+
printHeader('Generate');
455+
console.log(chalk.bold(' Sub-commands:'));
456+
console.log(` ${chalk.cyan('types'.padEnd(12))} Generate TypeScript type definitions from config`);
457+
console.log('');
458+
console.log(chalk.bold(' Metadata types:'));
459+
for (const [key, gen] of Object.entries(GENERATORS)) {
460+
console.log(` ${chalk.cyan(key.padEnd(12))} ${chalk.dim(gen.description)}`);
461+
}
462+
console.log('');
463+
console.log(chalk.dim(' Usage: objectstack generate <type> <name>'));
464+
console.log(chalk.dim(' Usage: objectstack generate types [config]'));
465+
return;
466+
}
467+
468+
// Delegate to metadata command action
469+
if (!name) {
470+
printError('Missing required argument: <name>');
471+
console.log(chalk.dim(' Usage: objectstack generate <type> <name>'));
472+
process.exit(1);
473+
}
474+
475+
// Execute metadata generation inline
476+
await generateMetadataCommand.parseAsync([type, name, ...process.argv.slice(4)], { from: 'user' });
477+
});

0 commit comments

Comments
 (0)