Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/11496-retire-cli-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@object-ui/cli': minor
---

**BREAKING (`@object-ui/cli`):** the `objectui add` command is retired, and so are `objectui analyze`'s two flags, `--render-performance` and `--bundle-size`. There is no alias window and no placeholder: the CLI now refuses `add` as an unknown command and each flag as an unknown option (objectui#11496).

(The bump is `minor` by this repo's release model: objectui's major follows the `@objectstack` family major, and its own breaking changes ship as `minor` with the breaking semantics stated here.)

- FROM `objectui add COMPONENT` TO nothing: drop the call. No renderer download or scaffold exists, so there is nothing to replace it with.
- FROM `objectui analyze --render-performance` TO nothing: drop the call. It analysed nothing; it printed the same fixed list of tips in every directory.
- FROM `objectui analyze --bundle-size` TO `objectui analyze`: drop the flag. The bundle-size report is now all `analyze` does.

**Why.** `add` was declared ("Add a new component renderer to your project") and documented with an example, but its action printed "Feature not implemented yet.", wrote nothing and exited 0, so a script or CI step read the no-op as success. No producer of the command was measured, and downloading renderer source into a project would be a new capability with no pull, so the command goes instead. Checking every other command and flag the CLI declares turned up one more no-op: `analyze --render-performance` printed fixed text, byte for byte the same in an empty directory and in a built project, and exited 0. It goes the same way. `--bundle-size` only chose between that section and the bundle report; with one analysis left it would be read by nothing, which is why `generate --output` was retired, so it goes too.

**What changes at the command line.** `objectui add Input` now exits non-zero with commander's unknown-command error and writes nothing. `objectui analyze --render-performance` and `objectui analyze --bundle-size` exit non-zero with the unknown-option error. `objectui analyze` with no flag prints the bundle-size report of `dist/` exactly as before, without the fixed render-performance section. `objectui --help` no longer lists `add`, `objectui analyze --help` lists no flags, and the CLI docs and the package README drop the `add` section and the `analyze` flag table.

**Clause-②: yes.** The published CLI's accept set narrows: a command and two options it used to accept are now refused. No exported symbol moves; `analyze` is not part of the package's exports.
21 changes: 5 additions & 16 deletions content/docs/utilities/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ Your app runs at `http://localhost:3000`.

## Commands

The CLI exposes 15 commands. Run `objectui <command> --help` for the full
flag list.
Run `objectui --help` for the command list, and `objectui <command> --help`
for a command's full flag list.

### `objectui init [name]`

Expand Down Expand Up @@ -280,14 +280,6 @@ failing exit code on a document that does not validate.

Diagnose common environment issues (Node version, deps, config sanity).

### `objectui add <component>`

Add a new component renderer scaffold to your project.

```bash
objectui add Input
```

### `objectui studio`

Launch the visual designer (`@object-ui/plugin-designer`) against the local
Expand All @@ -303,12 +295,9 @@ objectui create plugin my-awesome-widget

### `objectui analyze`

Analyze the built application.

| Flag | Description |
|---|---|
| `--bundle-size` | Inspect bundle size |
| `--render-performance` | Inspect render performance |
Analyze the built application's bundle: the total, JavaScript and CSS sizes of
`dist/` in the current directory (where `objectui build` writes by default),
and its ten largest files. No flags.

## Schema file format

Expand Down
4 changes: 0 additions & 4 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,6 @@ objectui test --ui

Generate new resources (`resource`/`object`, `page`, `plugin`).

### `objectui add <component>`

Add a new component renderer to your project.

### `objectui validate [schema]`

Validate a schema file against the ObjectUI specification.
Expand Down
122 changes: 108 additions & 14 deletions packages/cli/src/__tests__/cli-bin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/
import { describe, it, expect, beforeAll } from 'vitest';
import { spawnSync } from 'node:child_process';
import { existsSync, mkdtempSync, readdirSync, writeFileSync, readFileSync } from 'node:fs';
import { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync, readFileSync } from 'node:fs';
import { join, resolve, dirname, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { tmpdir } from 'node:os';
Expand All @@ -21,6 +21,13 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
const CLI_BIN = resolve(__dirname, '../../dist/cli.js');
const PKG_PATH = resolve(__dirname, '../../package.json');

/**
* The commands the CLI declares. The enumeration pin below holds this list
* equal to the built CLI's `--help`, both directions (objectui#11496). `add` is
* deliberately absent: it was retired, and the bin refuses it as an unknown
* command. The CLI docs give each of these a `### objectui NAME` heading; keep
* them in step by hand, because nothing in this file reads the docs.
*/
const SUBCOMMANDS = [
'serve',
'dev',
Expand All @@ -31,7 +38,6 @@ const SUBCOMMANDS = [
'test',
'generate',
'doctor',
'add',
'studio',
'check',
'validate',
Expand Down Expand Up @@ -85,6 +91,31 @@ function run(args: string[], opts: { cwd?: string } = {}) {
};
}

/** Every path under `dir`, `/`-separated and sorted: what a run wrote there. */
function tree(dir: string): string[] {
return (readdirSync(dir, { recursive: true }) as string[])
.map((p) => p.split(sep).join('/'))
.sort();
}

/**
* The command names commander prints under `Commands:` in the root `--help`, in
* order. An entry starts with exactly two spaces; a wrapped description line is
* indented further, so it is not an entry. `generate|g` reads as `generate`.
*/
function helpCommandNames(helpText: string): string[] {
const lines = helpText.split('\n');
const start = lines.findIndex((line) => line.trim() === 'Commands:');
if (start === -1) return [];
const names: string[] = [];
for (const line of lines.slice(start + 1)) {
if (line.trim() === '') break;
const entry = /^ {2}(\S+)/.exec(line);
if (entry) names.push(entry[1].split('|')[0]);
}
return names;
}

describe('@object-ui/cli bin', () => {
beforeAll(() => {
// The CLI bin imports @object-ui/types/zod at runtime (validate command),
Expand Down Expand Up @@ -151,14 +182,22 @@ describe('@object-ui/cli bin', () => {
});
});

// objectui#11496. One enumeration over the built CLI's commands, so the next
// declared-but-unbuilt command cannot ship the way `add` did: a retired
// command that comes back, or a new one this list does not name, turns this
// red. It replaces a containment check that a retired command coming back
// would have passed.
describe('--help', () => {
it('lists all 15 documented commands', () => {
it('lists exactly the declared commands, no more and no fewer', () => {
const res = run(['--help']);
expect(res.code).toBe(0);
const out = res.stdout;
for (const cmd of SUBCOMMANDS) {
expect(out, `command "${cmd}" should appear in --help output`).toContain(cmd);
}
expect(res.code, res.stderr).toBe(0);
const listed = helpCommandNames(res.stdout);
// Lit control: commander lists its own `help` command last, so reading it
// proves the parse ran to the end of the Commands section.
expect(listed[listed.length - 1]).toBe('help');
const declared = listed.filter((name) => name !== 'help');
expect([...declared].sort()).toEqual([...SUBCOMMANDS].sort());
expect(new Set(declared).size).toBe(declared.length);
});

it('uses the "objectui" bin name (not "os ui")', () => {
Expand Down Expand Up @@ -208,7 +247,6 @@ describe('@object-ui/cli bin', () => {
['lint', [/--fix/]],
['test', [/-w, --watch/, /-c, --coverage/, /--ui/]],
['generate', [/<type> <name>/]],
['analyze', [/--bundle-size/, /--render-performance/]],
['validate', [/\[schema\]/]],
];
it.each(cases)('%s exposes the documented flags', (cmd, patterns) => {
Expand All @@ -232,11 +270,6 @@ describe('@object-ui/cli bin', () => {
// refusals are read against: same command, same empty directory, one file
// written.
describe('generate refuses its retired flags (objectui#11476, objectui#11488)', () => {
const tree = (dir: string) =>
(readdirSync(dir, { recursive: true }) as string[])
.map((p) => p.split(sep).join('/'))
.sort();

it.each([
['--output', 'custom/'],
['--from', 'openapi.yaml'],
Expand Down Expand Up @@ -266,6 +299,67 @@ describe('@object-ui/cli bin', () => {
});
});

// objectui#11496. `add <component>` was documented as adding a component
// renderer scaffold, but its action printed "Feature not implemented yet.",
// wrote nothing and exited 0, so a script read the no-op as success. The
// command is retired with no placeholder, so the bin must REFUSE it the way it
// refuses any command it never declared. The control is the same directory
// answering a declared command: it is writable, and the tree read sees a write.
describe('the retired add command is refused (objectui#11496)', () => {
it('exits non-zero with the unknown-command error on `add Input`, and writes nothing', () => {
const work = mkdtempSync(join(tmpdir(), 'objectui-cli-add-retired-'));
const res = run(['add', 'Input'], { cwd: work });
expect(res.code, res.stdout + res.stderr).not.toBe(0);
expect(res.stderr).toMatch(/unknown command 'add'/i);
expect(tree(work)).toEqual([]);

const control = run(['g', 'page', 'Probe'], { cwd: work });
expect(control.code, control.stdout + control.stderr).toBe(0);
expect(tree(work)).toEqual(['pages', 'pages/probe.json']);
});
});

// objectui#11496, the family close-out over every command and flag. `analyze
// --render-performance` printed the same fixed list of tips and ticks in any
// directory, a built project or an empty one, and analysed nothing. It is
// retired. `--bundle-size` only chose between that and the bundle report, so
// with nothing left to choose from it would be read by nothing, which is the
// shape `generate --output` was retired for; it is retired with it. `analyze`
// with no flag is the control: it still reports the bundle under `dist/`.
describe('analyze refuses its retired flags (objectui#11496)', () => {
it.each(['--render-performance', '--bundle-size'])(
'exits non-zero naming %s as an unknown option, and writes nothing',
(flag) => {
const work = mkdtempSync(join(tmpdir(), 'objectui-cli-analyze-retired-'));
const res = run(['analyze', flag], { cwd: work });
expect(res.code, res.stdout + res.stderr).not.toBe(0);
expect(res.stderr).toMatch(/unknown option/i);
expect(res.stderr).toContain(flag);
expect(tree(work)).toEqual([]);
},
);

it('still reports the bundle under dist/ without a flag, and no render section', () => {
const work = mkdtempSync(join(tmpdir(), 'objectui-cli-analyze-'));
mkdirSync(join(work, 'dist', 'assets'), { recursive: true });
writeFileSync(join(work, 'dist', 'assets', 'probe-bundle.js'), 'x'.repeat(2048));
const res = run(['analyze'], { cwd: work });
expect(res.code, res.stdout + res.stderr).toBe(0);
expect(res.stdout).toContain('Bundle Size Analysis');
expect(res.stdout).toContain('assets/probe-bundle.js');
expect(res.stdout).not.toMatch(/render performance/i);
});

it('lists neither flag in analyze --help', () => {
const res = run(['analyze', '--help']);
expect(res.code, res.stderr).toBe(0);
// Lit control: the help text that was read is analyze's own usage line.
expect(res.stdout).toMatch(/Usage: objectui analyze\b/);
expect(res.stdout).not.toMatch(/--render-performance\b/);
expect(res.stdout).not.toMatch(/--bundle-size\b/);
});
});

describe('validate', () => {
let work: string;
beforeAll(() => {
Expand Down
20 changes: 2 additions & 18 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { lint } from './commands/lint.js';
import { test } from './commands/test.js';
import { generate } from './commands/generate.js';
import { doctor } from './commands/doctor.js';
import { add } from './commands/add.js';
import { studio } from './commands/studio.js';
import { check } from './commands/check.js';
import { validate } from './commands/validate.js';
Expand Down Expand Up @@ -173,19 +172,6 @@ program
}
});

program
.command('add')
.description('Add a new component renderer to your project')
.argument('<component>', 'Component name (e.g. Input, Grid)')
.action(async (component) => {
try {
await add(component);
} catch (error) {
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
process.exit(1);
}
});

program
.command('studio')
.description('Start the visual designer')
Expand Down Expand Up @@ -246,11 +232,9 @@ program
program
.command('analyze')
.description('Analyze application performance')
.option('--bundle-size', 'Analyze bundle size')
.option('--render-performance', 'Analyze render performance')
.action(async (options) => {
.action(async () => {
try {
await analyze(options);
await analyze();
} catch (error) {
console.error(chalk.red('Error:'), error instanceof Error ? error.message : error);
process.exit(1);
Expand Down
15 changes: 0 additions & 15 deletions packages/cli/src/commands/add.ts

This file was deleted.

49 changes: 9 additions & 40 deletions packages/cli/src/commands/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ import chalk from 'chalk';
import { existsSync, statSync, readdirSync } from 'fs';
import { resolve, join, extname } from 'path';

interface AnalyzeOptions {
bundleSize?: boolean;
renderPerformance?: boolean;
}

/**
* Analyze bundle size by scanning dist directory
*/
Expand Down Expand Up @@ -97,27 +92,6 @@ async function analyzeBundleSize() {
console.log('');
}

/**
* Analyze render performance (placeholder for now)
*/
async function analyzeRenderPerformance() {
console.log(chalk.bold('\n⚡ Render Performance Analysis\n'));

console.log(chalk.gray('Performance analysis features:'));
console.log(chalk.gray(' ✓ Expression caching enabled'));
console.log(chalk.gray(' ✓ Component memoization available'));
console.log(chalk.gray(' ✓ Virtual scrolling support for large lists'));

console.log(chalk.bold('\n💡 Performance Tips:'));
console.log(chalk.gray(' • Use virtual scrolling for lists > 100 items'));
console.log(chalk.gray(' • Cache frequently evaluated expressions'));
console.log(chalk.gray(' • Use React.memo for expensive components'));
console.log(chalk.gray(' • Implement pagination for large datasets'));
console.log(chalk.gray(' • Use code splitting for large apps'));

console.log('');
}

/**
* Format bytes to human-readable string
*/
Expand All @@ -132,22 +106,17 @@ function formatBytes(bytes: number): string {
}

/**
* Analyze application performance
*
* @param options - Analysis options
* Analyze application performance: the bundle-size report over `dist/`.
*
* It used to take `--bundle-size` and `--render-performance`. The second printed
* a fixed list of tips in any directory and analysed nothing, so it was retired,
* and the first went with it, having nothing left to choose between
* (objectui#11496).
*/
export async function analyze(options: AnalyzeOptions = {}) {
export async function analyze() {
console.log(chalk.blue('🔍 ObjectUI Performance Analyzer\n'));

const runAll = !options.bundleSize && !options.renderPerformance;

if (options.bundleSize || runAll) {
await analyzeBundleSize();
}

if (options.renderPerformance || runAll) {
await analyzeRenderPerformance();
}

await analyzeBundleSize();

console.log(chalk.green('✓ Analysis complete!\n'));
}
Loading