import { inject, injectable, postConstruct } from 'inversify'; import { join, basename } from 'path'; import * as fs from 'fs'; import { promisify } from 'util'; import { FileUri } from '@theia/core/lib/node/file-uri'; import { notEmpty } from '@theia/core/lib/common/objects'; import { Sketch, SketchContainer } from '../common/protocol/sketches-service'; import { SketchesServiceImpl } from './sketches-service-impl'; import { ExamplesService } from '../common/protocol/examples-service'; import { LibraryLocation, LibraryPackage, LibraryService } from '../common/protocol'; import { ConfigServiceImpl } from './config-service-impl'; @injectable() export class ExamplesServiceImpl implements ExamplesService { @inject(SketchesServiceImpl) protected readonly sketchesService: SketchesServiceImpl; @inject(LibraryService) protected readonly libraryService: LibraryService; @inject(ConfigServiceImpl) protected readonly configService: ConfigServiceImpl; protected _all: SketchContainer[] | undefined; @postConstruct() protected init(): void { this.builtIns(); } async builtIns(): Promise { if (this._all) { return this._all; } const exampleRootPath = join(__dirname, '..', '..', 'Examples'); const exampleNames = await promisify(fs.readdir)(exampleRootPath); this._all = await Promise.all(exampleNames.map(name => join(exampleRootPath, name)).map(path => this.load(path))); return this._all; } // TODO: decide whether it makes sense to cache them. Keys should be: `fqbn` + version of containing core/library. async installed({ fqbn }: { fqbn: string }): Promise<{ user: SketchContainer[], current: SketchContainer[], any: SketchContainer[] }> { const user: SketchContainer[] = []; const current: SketchContainer[] = []; const any: SketchContainer[] = []; if (fqbn) { const packages: LibraryPackage[] = await this.libraryService.list({ fqbn }); for (const pkg of packages) { const container = await this.tryGroupExamples(pkg); const { location } = pkg; if (location === LibraryLocation.USER) { user.push(container); } else if (location === LibraryLocation.PLATFORM_BUILTIN || LibraryLocation.REFERENCED_PLATFORM_BUILTIN) { current.push(container); } else { any.push(container); } } } return { user, current, any }; } /** * The CLI provides direct FS paths to the examples so that menus and menu groups cannot be built for the UI by traversing the * folder hierarchy. This method tries to workaround it by falling back to the `installDirUri` and manually creating the * location of the examples. Otherwise it creates the example container from the direct examples FS paths. */ protected async tryGroupExamples({ label, exampleUris, installDirUri }: LibraryPackage): Promise { const paths = exampleUris.map(uri => FileUri.fsPath(uri)); if (installDirUri) { for (const example of ['example', 'Example', 'EXAMPLE', 'examples', 'Examples', 'EXAMPLES']) { const examplesPath = join(FileUri.fsPath(installDirUri), example); const exists = await promisify(fs.exists)(examplesPath); const isDir = exists && (await promisify(fs.lstat)(examplesPath)).isDirectory(); if (isDir) { const fileNames = await promisify(fs.readdir)(examplesPath); const children: SketchContainer[] = []; const sketches: Sketch[] = []; for (const fileName of fileNames) { const subPath = join(examplesPath, fileName); const subIsDir = (await promisify(fs.lstat)(subPath)).isDirectory(); if (subIsDir) { const sketch = await this.tryLoadSketch(subPath); if (!sketch) { const container = await this.load(subPath); if (container.children.length || container.sketches.length) { children.push(container); } } else { sketches.push(sketch); } } } return { label, children, sketches }; } } } const sketches = await Promise.all(paths.map(path => this.tryLoadSketch(path))); return { label, children: [], sketches: sketches.filter(notEmpty) }; } // Built-ins are included inside the IDE. protected async load(path: string): Promise { if (!await promisify(fs.exists)(path)) { throw new Error('Examples are not available'); } const stat = await promisify(fs.stat)(path); if (!stat.isDirectory) { throw new Error(`${path} is not a directory.`); } const names = await promisify(fs.readdir)(path); const sketches: Sketch[] = []; const children: SketchContainer[] = []; for (const p of names.map(name => join(path, name))) { const stat = await promisify(fs.stat)(p); if (stat.isDirectory()) { const sketch = await this.tryLoadSketch(p); if (sketch) { sketches.push(sketch); } else { const child = await this.load(p); children.push(child); } } } const label = basename(path); return { label, children, sketches }; } protected async group(paths: string[]): Promise> { const map = new Map(); for (const path of paths) { const stat = await promisify(fs.stat)(path); map.set(path, stat); } return map; } protected async tryLoadSketch(path: string): Promise { try { const sketch = await this.sketchesService.loadSketch(FileUri.create(path).toString()); return sketch; } catch { return undefined; } } }