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
33 changes: 20 additions & 13 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@ Client library and CLI for the That Open Platform — a cloud platform for build

```
src/
core/client.ts # EngineServicesClient — the main API class
core/client.ts # EngineServicesClient — the main API class
cli/
commands/create.ts # thatopen create — scaffolds new projects
commands/serve.ts # thatopen serve — dev server (esbuild watch + serve)
commands/login.ts # thatopen login — authenticate with the platform
commands/publish.ts # thatopen publish — build and upload to the platform
commands/run.ts # thatopen run — test cloud components locally
templates/ # Template generators for scaffolded projects
lib/ # CLI helper utilities (config, certificates)
built-in/index.ts # Built-in component type stubs + runtime UUID constants
types/ # TypeScript type definitions (items, execution, etc.)
index.ts # Library entry point (re-exports everything)
commands/create.ts # thatopen create — scaffolds new projects
commands/serve.ts # thatopen serve — dev server (esbuild watch + serve)
commands/login.ts # thatopen login — authenticate with the platform
commands/publish.ts # thatopen publish — build and upload to the platform
commands/run.ts # thatopen run — test cloud components locally
commands/create-tests.ts # thatopen create-tests — scaffolds test app + test component
commands/serve-tests.ts # thatopen serve-tests — serves both test projects in parallel
templates/ # Template generators for scaffolded projects
lib/ # CLI helper utilities (config, certificates)
built-in/index.ts # Built-in component type stubs + runtime UUID constants
types/ # TypeScript type definitions (items, execution, etc.)
index.ts # Library entry point (re-exports everything)
```

## Build system
Expand All @@ -47,6 +49,8 @@ npm run test:ui # Interactive browser test page
npm run test:cli-build-app # Scaffold + build a test app
npm run test:cli-build-component # Scaffold + build a test cloud component
npm run test:cli-run-component # Run the test cloud component locally
npm run test:cli-build-tests # Build CLI + scaffold test app & test component into temp/
npm run test:cli-serve-tests # Serve the test app and test component's local server in parallel
```

### Publishing to npm
Expand All @@ -66,7 +70,7 @@ yarn create-version # Build → changeset → version → publish
| **Entry point** | Side effects in `main.ts` (renders UI) | `export async function main()` |
| **Context** | `window.__THATOPEN_CONTEXT__` provides `{ appId, projectId, accessToken, apiUrl }` | Globals: `thatOpenServices`, `executionParams`, `executionReporter`, `OBC`, `THREE`, `fs` |
| **Build output** | IIFE `dist/bundle.js` (all deps bundled) | IIFE `dist/bundle.js` (platform deps externalized) |
| **Template** | `bim` or `default` | `cloud` |
| **Template** | `bim`, `default`, or `test` | `cloud` or `cloud-test` |

### Authentication

Expand Down Expand Up @@ -162,13 +166,16 @@ Full API reference with config interfaces, method signatures, and `@example` blo
## CLI commands

```bash
thatopen create <name> [--template bim|default|cloud] # Scaffold project + auto npm install
thatopen create <name> [--template bim|default|cloud|test|cloud-test]
# Scaffold project + auto npm install
# Use "." as name to scaffold in current directory
thatopen serve [--port N] # Dev server (esbuild watch + serve bundle)
thatopen login [--token T] [--api-url U] [--local] # Authenticate
thatopen publish [--name N] [--version-tag T] [--skip-build] [--app-id ID | --component-id ID]
thatopen run [--params '{}'] [--skip-build] # Test cloud component locally
thatopen local-server [--port N] [--skip-build] # Local execution server (API-compatible)
thatopen create-tests [directory] # Scaffold test app + test component (cleans directory first)
thatopen serve-tests [directory] # Serve test app + test component in parallel
```

## Dependencies
Expand Down
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ npm run run # Build and test locally
| `bim` (default) | `npx thatopen create my-app` | Three.js + BIM viewer + platform UI components |
| `default` | `npx thatopen create my-app --template default` | Minimal app showing platform context |
| `cloud` | `npx thatopen create my-component --template cloud` | Server-side Node.js component |
| `test` | `npx thatopen create my-tests --template test` | Browser test app that exercises every API endpoint |
| `cloud-test` | `npx thatopen create my-tests --template cloud-test` | Cloud component test suite for server-side API testing |

Use `npx thatopen create .` to scaffold in the current directory instead of creating a new one.

Expand Down Expand Up @@ -87,11 +89,13 @@ const client = new EngineServicesClient(ctx.accessToken, ctx.apiUrl, { useBearer

| Command | Description |
|---------|-------------|
| `thatopen create <name> [--template bim\|default\|cloud]` | Scaffold a new project (use `.` for current directory) |
| `thatopen create <name> [--template bim\|default\|cloud\|test\|cloud-test]` | Scaffold a new project (use `.` for current directory) |
| `thatopen serve [--port N]` | Dev server (esbuild watch + serve bundle) |
| `thatopen login [--token T] [--local]` | Authenticate with the platform |
| `thatopen publish` | Build and publish to the platform |
| `thatopen run [--params '{}']` | Build and test a cloud component locally |
| `thatopen create-tests [directory]` | Scaffold both a test app and test cloud component |
| `thatopen serve-tests [directory]` | Serve both the test app and test component in parallel |

## App workflow

Expand Down Expand Up @@ -228,6 +232,28 @@ npm run test:cli-build-component
npm run test:cli-run-component
```

### Running the platform API test suite

The test suite consists of two projects scaffolded together: a **test app** (browser-based, template `test`) and a **test cloud component** (server-side, template `cloud-test`). Both exercise every `EngineServicesClient` endpoint.

```bash
# 1. Build the CLI and scaffold both test projects into a temp/ directory
# (deletes temp/ first if it already exists)
yarn test:cli-build-tests

# 2. Serve both the test app and the test component's local server
yarn test:cli-serve-tests
```

Then open your project on [platform.thatopen.com](https://platform.thatopen.com) and click the debug button. The test app will show a panel with:

- **Context** — current app/project/API info
- **Execution Config** — input fields for a deployed Component ID and the local server URL (defaults to `http://localhost:4001`)
- **Controls** — "Run All Tests" button
- **Results** — test results grouped by API area (Context & Auth, Folders, Files, Hidden Files, Icons, Components, Apps, Execution, Built-in Components)

When execution tests run, the cloud component's output appears in the same Results section as additional groups prefixed with "Local Component:" or "Deployed Component:".

### Publishing a new version

Publishing is handled automatically by CI when a PR with changesets is merged to `main`.
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
"test:cli-serve-app": "cd temp/test-app && thatopen serve",
"test:cli-build-component": "npm run build:cli && node test/setup-test-component.mjs",
"test:cli-run-component": "cd temp/test-component && thatopen run --skip-build",
"test:cli-local-server": "cd temp/test-component && thatopen local-server",
"test:cli-serve-component": "cd temp/test-component && thatopen local-server",
"test:cli-build-tests": "npm run build:cli && thatopen create-tests temp",
"test:cli-serve-tests": "thatopen serve-tests temp",
"changeset": "changeset",
"version": "changeset version"
},
Expand Down
74 changes: 74 additions & 0 deletions src/cli/commands/create-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Command } from 'commander';
import { resolve } from 'node:path';
import { existsSync, rmSync, mkdirSync } from 'node:fs';
import { spawnSync } from 'node:child_process';

export const createTestsCommand = new Command('create-tests')
.argument(
'<directory>',
'Parent directory for both projects (required to prevent accidental deletion)',
)
.option('--app-name <name>', 'Name of the test app project', 'test-app')
.option(
'--component-name <name>',
'Name of the test component project',
'test-component',
)
.description(
'Scaffold both a test app (--template test) and a test cloud component (--template cloud-test)',
)
.action(
async (
directory: string,
opts: { appName: string; componentName: string },
) => {
const parentDir = resolve(process.cwd(), directory);
const bin = process.argv[1]; // path to the thatopen CLI entry point

// Clean up previous test suite if it exists
if (existsSync(parentDir)) {
console.log(`Removing existing directory "${directory}"...`);
rmSync(parentDir, { recursive: true, force: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: I believe if someone calls thatopen create-tests without any arguments in their repo root this would nuke the repository. Deleting the subdirectories only could be safer in this case or we could force the argument to make it a bit safer.

}
mkdirSync(parentDir, { recursive: true });

console.log('Creating test suite...');
console.log('');

// 1 ── Test App
console.log(`── Creating test app "${opts.appName}" ──`);
const appResult = spawnSync(
process.execPath,
[bin, 'create', opts.appName, '-t', 'test'],
{ cwd: parentDir, stdio: 'inherit' },
);
if (appResult.status !== 0) {
process.exit(appResult.status ?? 1);
}

console.log('');

// 2 ── Test Cloud Component
console.log(`── Creating test component "${opts.componentName}" ──`);
const compResult = spawnSync(
process.execPath,
[bin, 'create', opts.componentName, '-t', 'cloud-test'],
{ cwd: parentDir, stdio: 'inherit' },
);
if (compResult.status !== 0) {
process.exit(compResult.status ?? 1);
}

console.log('');
console.log('Test suite ready!');
console.log('');
console.log('Quick start:');
console.log(
` 1. cd ${opts.componentName} && npm run run # Run the test component locally`,
);
console.log(
` 2. cd ${opts.appName} && npm run dev # Start the test app`,
);
console.log('');
},
);
16 changes: 13 additions & 3 deletions src/cli/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { getIndexHtml } from '../templates/index-html';
import { getMainTs } from '../templates/main-js';
import { getMainBim } from '../templates/main-bim';
import { getMainCloud } from '../templates/main-cloud';
import { getMainTest } from '../templates/main-test';
import { getMainCloudTest } from '../templates/main-cloud-test';
import { getViteConfig } from '../templates/vite-config';
import { getPackageJson } from '../templates/package-json';
import { getContextMdBim, getContextMdDefault, getContextMdCloud } from '../templates/context-md';
import { getContextMdBim, getContextMdDefault, getContextMdCloud, getContextMdTest, getContextMdCloudTest } from '../templates/context-md';
import { getTsconfig } from '../templates/tsconfig';
import { writeLocalConfig } from '../lib/config';

const TEMPLATES = ['default', 'bim', 'cloud'] as const;
const TEMPLATES = ['default', 'bim', 'cloud', 'test', 'cloud-test'] as const;
type Template = (typeof TEMPLATES)[number];

function getMainSource(template: Template): string {
Expand All @@ -21,6 +23,10 @@ function getMainSource(template: Template): string {
return getMainBim();
case 'cloud':
return getMainCloud();
case 'test':
return getMainTest();
case 'cloud-test':
return getMainCloudTest();
default:
return getMainTs();
}
Expand All @@ -32,6 +38,10 @@ function getContextMd(template: Template): string {
return getContextMdBim();
case 'cloud':
return getContextMdCloud();
case 'test':
return getContextMdTest();
case 'cloud-test':
return getContextMdCloudTest();
default:
return getContextMdDefault();
}
Expand All @@ -49,7 +59,7 @@ export const createCommand = new Command('create')
process.exit(1);
}

const isCloud = template === 'cloud';
const isCloud = template === 'cloud' || template === 'cloud-test';
const projectKind = isCloud ? 'cloud component' : 'app';
const useCurrentDir = projectName === '.';

Expand Down
96 changes: 96 additions & 0 deletions src/cli/commands/serve-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Command } from 'commander';
import { resolve } from 'node:path';
import { spawn } from 'node:child_process';

export const serveTestsCommand = new Command('serve-tests')
.argument(
'<directory>',
'Parent directory containing both projects',
)
.option('--app-name <name>', 'Name of the test app project', 'test-app')
.option(
'--component-name <name>',
'Name of the test component project',
'test-component',
)
.option('--app-port <port>', 'Port for the app bundle server', '4000')
.option('--component-port <port>', 'Port for the component local server', '4001')
.description(
'Serve both the test app (thatopen serve) and the test component (thatopen local-server) in parallel',
)
.action(
async (
directory: string,
opts: {
appName: string;
componentName: string;
appPort: string;
componentPort: string;
},
) => {
const parentDir = resolve(process.cwd(), directory);
const bin = process.argv[1];

const appDir = resolve(parentDir, opts.appName);
const componentDir = resolve(parentDir, opts.componentName);

console.log('Starting test suite servers...');
console.log('');

const children: ReturnType<typeof spawn>[] = [];

// Helper: spawn a child with prefixed stdout/stderr
function startProcess(
label: string,
cwd: string,
args: string[],
) {
const child = spawn(process.execPath, [bin, ...args], {
cwd,
stdio: ['ignore', 'pipe', 'pipe'],
});

const prefix = `[${label}]`;

child.stdout.on('data', (data: Buffer) => {
for (const line of data.toString().split('\n')) {
if (line) console.log(`${prefix} ${line}`);
}
});

child.stderr.on('data', (data: Buffer) => {
for (const line of data.toString().split('\n')) {
if (line) console.error(`${prefix} ${line}`);
}
});

child.on('exit', (code) => {
console.log(`${prefix} exited with code ${code}`);
});

children.push(child);
}

// 1 — Component local-server (start first so it's ready when the app connects)
startProcess('component', componentDir, [
'local-server',
'--port',
opts.componentPort,
]);

// 2 — App serve
startProcess('app', appDir, ['serve', '--port', opts.appPort]);

console.log(`[component] ${componentDir} → http://localhost:${opts.componentPort}`);
console.log(`[app] ${appDir} → http://localhost:${opts.appPort}`);
console.log('');

// Forward SIGINT to children
process.on('SIGINT', () => {
for (const child of children) {
child.kill('SIGINT');
}
setTimeout(() => process.exit(0), 500);
});
},
);
4 changes: 4 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { publishCommand } from './commands/publish';
import { serveCommand } from './commands/serve';
import { runCommand } from './commands/run';
import { localServerCommand } from './commands/local-server';
import { createTestsCommand } from './commands/create-tests';
import { serveTestsCommand } from './commands/serve-tests';

const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));

Expand All @@ -23,5 +25,7 @@ program.addCommand(publishCommand);
program.addCommand(serveCommand);
program.addCommand(runCommand);
program.addCommand(localServerCommand);
program.addCommand(createTestsCommand);
program.addCommand(serveTestsCommand);

program.parse(process.argv);
Loading