Skip to content

Commit

Permalink
Add create-live-compositor package
Browse files Browse the repository at this point in the history
  • Loading branch information
wkozyra95 committed Sep 17, 2024
1 parent facbb86 commit 9f8691d
Show file tree
Hide file tree
Showing 41 changed files with 1,511 additions and 54 deletions.
8 changes: 1 addition & 7 deletions ts/@live-compositor/core/src/compositor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import { intoRegisterOutput } from './api/output';
import { intoRegisterInput } from './api/input';
import { onCompositorEvent } from './event';

export async function createLiveCompositor(manager: CompositorManager): Promise<LiveCompositor> {
const compositor = new LiveCompositor(manager);
await compositor['setupInstance']();
return compositor;
}

export class LiveCompositor {
private manager: CompositorManager;
private api: ApiClient;
Expand All @@ -24,7 +18,7 @@ export class LiveCompositor {
this.store = new _liveCompositorInternals.InstanceContextStore();
}

private async setupInstance() {
public async init(): Promise<void> {
this.manager.registerEventListener((event: unknown) => onCompositorEvent(this.store, event));
await this.manager.setupInstance();
}
Expand Down
2 changes: 1 addition & 1 deletion ts/@live-compositor/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { ApiClient, ApiRequest } from './api';
export { LiveCompositor, createLiveCompositor } from './compositor';
export { LiveCompositor } from './compositor';
export { CompositorManager } from './compositorManager';
10 changes: 3 additions & 7 deletions ts/@live-compositor/node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import {
LiveCompositor as CoreLiveCompositor,
CompositorManager,
createLiveCompositor,
} from '@live-compositor/core';
import { LiveCompositor as CoreLiveCompositor, CompositorManager } from '@live-compositor/core';
import LocallySpawnedInstance from './manager/locallySpawnedInstance';
import ExistingInstance from './manager/existingInstance';

export { LocallySpawnedInstance, ExistingInstance };

export default class LiveCompositor extends CoreLiveCompositor {
public static async create(manager?: CompositorManager): Promise<LiveCompositor> {
return await createLiveCompositor(manager ?? LocallySpawnedInstance.defaultManager());
constructor(manager?: CompositorManager) {
super(manager ?? LocallySpawnedInstance.defaultManager());
}
}
2 changes: 2 additions & 0 deletions ts/create-live-compositor/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
templates
5 changes: 5 additions & 0 deletions ts/create-live-compositor/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": [
"../.eslintrc.base.json"
]
}
2 changes: 2 additions & 0 deletions ts/create-live-compositor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
7 changes: 7 additions & 0 deletions ts/create-live-compositor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# `create-live-compositor`

To start new project using Live Compositor run:

```
npm create live-compositor
```
34 changes: 34 additions & 0 deletions ts/create-live-compositor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "create-live-compositor",
"version": "0.1.0-rc.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"watch": "tsc --watch --preserveWatchOutput",
"build": "tsc",
"clean": "rimraf dist",
"prepublishOnly": "npm run clean && npm run build"
},
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/software-mansion/live-compositor/issues"
},
"homepage": "https://github.com/software-mansion/live-compositor",
"files": [
"/dist",
"/templates"
],
"bin": {
"create-live-compositor": "./dist/index.js"
},
"dependencies": {
"chalk": "^4.1.2",
"prompts": "^2.4.2"
},
"devDependencies": {
"@types/prompts": "^2.4.9"
}
}
10 changes: 10 additions & 0 deletions ts/create-live-compositor/src/createNodeProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ProjectOptions } from './options';
import { ensureProjectDir } from './utils/workingdir';
import { runPackageManagerInstall } from './utils/packageManager';
import { applyTemplate } from './template';

export async function createNodeProject(options: ProjectOptions) {
await ensureProjectDir(options.directory);
await applyTemplate(options.directory, options.runtime.templateName, options.projectName);
await runPackageManagerInstall(options.packageManager, options.directory);
}
5 changes: 5 additions & 0 deletions ts/create-live-compositor/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import run from './run';

void run();
85 changes: 85 additions & 0 deletions ts/create-live-compositor/src/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Choice, selectPrompt, textPrompt } from './utils/prompts';
import path from 'path';
import { PackageManager } from './utils/packageManager';

export type ProjectOptions = {
projectName: string;
directory: string;
packageManager: PackageManager;
runtime: BrowserOptions | NodeOptions;
};

type BrowserOptions = {
type: 'browser';
embeddedWasm: boolean;
templateName: 'vite' | 'next';
};

type NodeOptions = {
type: 'node';
templateName: string;
};

type Runtime = 'node' | 'browser';

const packageManagers: Choice<PackageManager>[] = [
{ value: 'npm', title: 'npm' },
{ value: 'yarn', title: 'yarn' },
{ value: 'pnpm', title: 'pnpm' },
];

export async function resolveOptions(): Promise<ProjectOptions> {
const projectName = await textPrompt('Project name: ', 'live-compositor-app');
// TODO: replace
// const runtime = await selectPrompt('Select environment:', [
// { value: 'node', title: 'Node.js' },
// { value: 'browser', title: 'Browser' },
// ] as const);
const runtime: Runtime = 'node' as any;

const packageManager = await selectPrompt('Select package manager: ', packageManagers);

let runtimeOptions: ProjectOptions['runtime'];
if (runtime === 'browser') {
runtimeOptions = await resolveBrowserOptions();
} else if (runtime === 'node') {
runtimeOptions = await resolveNodeOptions();
} else {
throw new Error('Unknown runtime');
}

return {
runtime: runtimeOptions,
packageManager,
projectName,
directory: path.join(process.cwd(), 'app'),
};
}

export async function resolveBrowserOptions(): Promise<BrowserOptions> {
const usageType = await selectPrompt('Where do you want to run the LiveCompositor server?', [
{ value: 'external', title: 'Run as an external instance and communicate over the network.' },
{ value: 'wasm', title: 'Embed LiveCompositor in the browser and render using WebGL.' },
]);
const templateName = await selectPrompt('Select project template:', [
{ value: 'vite', title: 'Vite + React' },
{ value: 'next', title: 'Next.js' },
] as const);

return {
type: 'browser',
embeddedWasm: usageType === 'wasm',
templateName,
};
}

export async function resolveNodeOptions(): Promise<NodeOptions> {
const templateName = await selectPrompt('Select project template: ', [
{ title: 'minimal', value: 'node-minimal' },
{ title: 'Express.js + Redux', value: 'node-express-redux' },
] as const);
return {
type: 'node',
templateName,
};
}
15 changes: 15 additions & 0 deletions ts/create-live-compositor/src/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import chalk from 'chalk';
import { resolveOptions } from './options';
import { createNodeProject } from './createNodeProject';

export default async function () {
const options = await resolveOptions();
if (options.runtime.type === 'node') {
console.log('Generating Node.js LiveCompositor project');
await createNodeProject(options);
} else {
throw new Error('Unknown project type.');
}
console.log();
console.log(chalk.green('Project created successfully.'));
}
23 changes: 23 additions & 0 deletions ts/create-live-compositor/src/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import fs from 'fs-extra';
import path from 'path';

const TEMPLATES_ROOT = path.join(__dirname, '../templates');

export async function applyTemplate(
destination: string,
templateName: string,
projectName: string
): Promise<void> {
const templatePath = path.join(TEMPLATES_ROOT, templateName);
await fs.copy(templatePath, destination);

await fs.remove(path.join(destination, 'node_modules'));
await fs.remove(path.join(destination, 'dist'));

const packageJsonPath = path.join(destination, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
delete packageJson?.scripts?.['start'];
delete packageJson['private'];
packageJson.name = projectName;
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n', 'utf8');
}
13 changes: 13 additions & 0 deletions ts/create-live-compositor/src/utils/packageManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { spawn } from './spawn';

export type PackageManager = 'npm' | 'yarn' | 'pnpm';

export async function runPackageManagerInstall(pm: PackageManager, cwd?: string): Promise<void> {
const args: string[] = [];
if (['pnpm', 'npm'].includes(pm)) {
args.push('install');
}
await spawn(pm, args, {
cwd: cwd ?? process.cwd(),
});
}
38 changes: 38 additions & 0 deletions ts/create-live-compositor/src/utils/prompts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { constants } from 'os';
import prompts, { Answers, Options, Choice as PromptChoice, PromptObject } from 'prompts';

export interface Choice<T> extends PromptChoice {
value: T;
}

async function promptWrapper<T extends string = string>(
questions: PromptObject<T> | Array<PromptObject<T>>,
options?: Options
): Promise<Answers<T>> {
return await prompts<T>(questions, {
onCancel() {
process.exit(constants.signals.SIGINT + 128); // Exit code 130 used when process is interrupted with ctrl+c.
},
...options,
});
}

export async function confirmPrompt(message: string, initial?: boolean): Promise<boolean> {
const { value } = await promptWrapper({
type: 'confirm',
message,
name: 'value',
initial: initial ?? false,
});
return !!value;
}

export async function selectPrompt<T>(message: string, choices: Choice<T>[]): Promise<T> {
const { value } = await promptWrapper({ type: 'select', message, name: 'value', choices });
return value;
}

export async function textPrompt(message: string, initial?: string): Promise<string> {
const { value } = await promptWrapper({ type: 'text', message, name: 'value', initial });
return value;
}
23 changes: 23 additions & 0 deletions ts/create-live-compositor/src/utils/spawn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ChildProcess, SpawnOptions, spawn as nodeSpawn } from 'child_process';

export interface SpawnPromise extends Promise<void> {
child: ChildProcess;
}

export function spawn(command: string, args: string[], options: SpawnOptions): SpawnPromise {
const child = nodeSpawn(command, args, {
stdio: 'inherit',
...options,
});
const promise = new Promise((res, rej) => {
child.on('exit', code => {
if (code === 0) {
res();
} else {
rej(new Error(`Command "${command} ${args.join(' ')}" failed with exit code ${code}.`));
}
});
}) as SpawnPromise;
promise.child = child;
return promise;
}
20 changes: 20 additions & 0 deletions ts/create-live-compositor/src/utils/workingdir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import fs from 'fs-extra';
import { confirmPrompt } from './prompts';

export async function ensureProjectDir(directory: string) {
const alreadyExists = await fs.pathExists(directory);
if (alreadyExists) {
const stat = await fs.stat(directory);
// remove cwd unless it's an empty directory
if (!stat.isDirectory || (await fs.readdir(directory)).length > 0) {
if (await confirmPrompt(`Path "${directory}" already exists, Do you want to override it?`)) {
console.log(`Removing ${directory}.`);
await fs.remove(directory);
} else {
console.error('Aboring ...');
process.exit(1);
}
}
}
await fs.mkdirp(directory);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "node-express-redux",
"private": true,
"version": "1.0.0",
"description": "Live Compositor app",
"main": "./dist/index.js",
"license": "MIT",
"scripts": {
"start": "ts-node ./src/index.ts",
"build": "tsc"
},
"dependencies": {
"@live-compositor/node": "0.1.0-rc.0",
"@reduxjs/toolkit": "^2.2.7",
"express": "^4.21.0",
"live-compositor": "0.1.0-rc.0",
"react": "^18.3.1",
"react-redux": "^9.1.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.14.10",
"@types/react": "^18.2.25",
"typescript": "^5.5.3"
}
}
Loading

0 comments on commit 9f8691d

Please sign in to comment.