import State from './state'; import * as path from 'path'; import pLimit from 'p-limit'; import { NodeFS, AnyFS, MemoryFS } from '@teambit/any-fs'; import Container, { ExecOptions } from './container'; import Console from './console'; import { Exec } from './container'; import ContainerFactory from './container/container-factory'; export type CapsuleBaseOptions = { id: string; }; export interface Serializable { toString(): string; } export default class Capsule { constructor( /** * container implementation the capsule is being executed within. */ protected container: Container, /** * the capsule's file system. */ readonly fs: Y, /** * console for controlling process streams as stdout, stdin and stderr. */ readonly console: Console, /** * capsule's state. */ readonly state: State, /** * config to pass capsule */ readonly config: any = {} ) {} // implement this to handle capsules ids. get id(): string { return ''; } get containerId(): string { return this.container.id; } run(modulePath: string, options?: {}) { return new Promise((resolve, reject) => { console.log(`running script: ${modulePath} in capsule`); return this.exec({ command: ['node', `./node_modules/${modulePath}`] }) .then(exec => { let data = ''; let err = ''; exec.stdout.on('data', (chunk) => { data += chunk.toString('utf8'); }); exec.stderr.on('data', (chunk) => { err += chunk.toString('utf8'); }); exec.on('error', () => { }); exec.on('close', () => { console.log(err, data); if (err !== '') return reject(err); resolve(data); }); }); }); } async runFn(fn: () => Serializable, options?: { cwd: string }) { return new Promise((resolve, reject) => { const serFn = `const fn = ${fn.toString()}\nprocess.stdout.write(fn().toString());`; const fileName = `__run__${Date.now()}.js`; const filepath = options ? options.cwd + fileName : fileName; this.fs.writeFileSync(filepath, serFn); // console.log('running script', serFn); return this.exec({ command: ['node', filepath] }) .then((exec) => { // handle streaming // handle response and serialization // resolve modules from path (e.g. extension node_modules) let data = ''; let err = ''; exec.stdout.on('data', (chunk) => { data += chunk.toString('utf8'); }); exec.stderr.on('data', (chunk) => { err += chunk.toString('utf8'); }); exec.on('error', () => { }); exec.on('close', () => { console.log(err, data); if (err !== '') return reject(err); this.fs.unlinkSync(filepath); resolve(data); }); }); }); } start(): Promise { return this.container.start(); } serialize(): string { return JSON.stringify( Object.assign( { id: this.containerId }, this.config ) ); } log(): Promise { return this.container.log(); } on(event: string, fn: (data: any) => void) { this.container.on(event, fn); } async updateFs(fs: { [path: string]: string }): Promise { const limit = pLimit(1); const queueWrite = Object.keys(fs).map(async filePath => { return limit(async () => { await this.fs.promises.mkdir(path.dirname(filePath), { recursive: true }); await this.fs.promises.writeFile(filePath, fs[filePath]); }); }); await Promise.all(queueWrite); return; } pause() { return this.container.pause(); } resume() { return this.container.resume(); } stop() { return this.container.stop(); } status() { return this.container.inspect(); } async exec(execOpions: ExecOptions): Promise { return this.container.exec(execOpions); } destroy() { return this.container.stop(); } static async create< fs extends AnyFS, Y extends Exec, T extends Capsule >( containerFactory: ContainerFactory, volume: AnyFS = new MemoryFS(), config: any = {}, initialState: State = new State(), console: Console = new Console() ): Promise { const container = await containerFactory.createContainer(config); return new this( container, container.fs, console, initialState, config ) as T; } static async obtain< X extends Exec, fs extends AnyFS, T extends Capsule, Y extends CapsuleBaseOptions >(containerFactory: ContainerFactory, raw: string): Promise { const object: Y = JSON.parse(raw); const container = await containerFactory.obtain(object); return new this(container, container.fs, new Console(), new State()); } }