arduino-ide/arduino-ide-extension/src/node/examples-service-impl.ts
2025-03-29 01:33:25 +09:00

226 lines
7.2 KiB
TypeScript

import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { join } from 'node:path';
import fs from 'node:fs';
import { FileUri } from '@theia/core/lib/common/file-uri';
import {
SketchRef,
SketchContainer,
} from '../common/protocol/sketches-service';
import { ExamplesService } from '../common/protocol/examples-service';
import { LibraryLocation, LibraryPackage } from '../common/protocol';
import { URI } from '@theia/core/lib/common/uri';
import { Path } from '@theia/core/lib/common/path';
import { LibraryServiceImpl } from './library-service-impl';
import { examplesPath } from './resources';
interface BuiltInSketchRef {
readonly name: string;
readonly relativePath: string;
}
namespace BuiltInSketchRef {
export function toSketchRef(
{ name, relativePath }: BuiltInSketchRef,
root: URI
): SketchRef {
return {
name,
uri: root.resolve(relativePath).toString(),
};
}
}
interface BuiltInSketchContainer {
readonly label: string;
readonly children: BuiltInSketchContainer[];
readonly sketches: BuiltInSketchRef[];
}
namespace BuiltInSketchContainer {
export function toSketchContainer(
source: BuiltInSketchContainer,
root: URI
): SketchContainer {
return {
label: source.label,
children: source.children.map((child) => toSketchContainer(child, root)),
sketches: source.sketches.map((child) =>
BuiltInSketchRef.toSketchRef(child, root)
),
};
}
}
@injectable()
export class BuiltInExamplesServiceImpl {
protected _builtIns: SketchContainer[] | undefined;
@postConstruct()
protected init(): void {
this.builtIns();
}
async builtIns(): Promise<SketchContainer[]> {
if (this._builtIns) {
return this._builtIns;
}
const examplesRootPath = examplesPath;
const examplesRootUri = FileUri.create(examplesRootPath);
const rawJson = await fs.promises.readFile(
join(examplesRootPath, 'examples.json'),
{ encoding: 'utf8' }
);
const examples: BuiltInSketchContainer[] = JSON.parse(rawJson);
this._builtIns = examples.map((container) =>
BuiltInSketchContainer.toSketchContainer(container, examplesRootUri)
);
return this._builtIns;
}
}
@injectable()
export class ExamplesServiceImpl implements ExamplesService {
@inject(LibraryServiceImpl)
private readonly libraryService: LibraryServiceImpl;
@inject(BuiltInExamplesServiceImpl)
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
builtIns(): Promise<SketchContainer[]> {
return this.builtInExamplesService.builtIns();
}
async installed({ fqbn }: { fqbn?: string }): Promise<{
user: SketchContainer[];
current: SketchContainer[];
any: SketchContainer[];
}> {
const user: SketchContainer[] = [];
const current: SketchContainer[] = [];
const any: SketchContainer[] = [];
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 };
}
async find(options: { libraryName: string }): Promise<SketchContainer[]> {
const { libraryName } = options;
const packages = await this.libraryService.list({ libraryName });
return Promise.all(
packages
.filter(({ location }) => location === LibraryLocation.USER)
.map((pkg) => this.tryGroupExamples(pkg))
);
}
/**
* 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.
*/
private async tryGroupExamples({
name,
exampleUris,
installDirUri,
}: LibraryPackage): Promise<SketchContainer> {
const container = SketchContainer.create(name);
if (!installDirUri || !exampleUris.length) {
return container;
}
// Args example:
// exampleUris
// 0:'file:///Users/a.kitta/Documents/Arduino/libraries/ATOM_DTU_CAT1/examples/MQTT'
// 1:'file:///Users/a.kitta/Documents/Arduino/libraries/ATOM_DTU_CAT1/examples/Modbus/ModBus-RTU/Master'
// 2:'file:///Users/a.kitta/Documents/Arduino/libraries/ATOM_DTU_CAT1/examples/Modbus/ModBus-RTU/Slave'
// installDirUri
// 'file:///Users/a.kitta/Documents/Arduino/libraries/ATOM_DTU_CAT1'
// Expected menu structure:
// ATOM_DTU_CAT1 > Modbus > ModBus-RTU > Master
// | > Slave
// > MQTT
const logInfo = (ref: SketchRef) =>
`Example URI: ${ref.uri}, install location URI: ${installDirUri}.`;
for (const ref of exampleUris.map(SketchRef.fromUri)) {
const path = new URI(installDirUri).relative(new URI(ref.uri));
if (!path) {
console.warn(
`Could not resolve the sketch location from its install location. Skipping. ${logInfo(
ref
)}`
);
continue;
}
if (path.isAbsolute) {
console.warn(
`Expected a relative path between the sketch and the install locations. Skipping. Path was: ${path}. ${logInfo(
ref
)}`
);
continue;
}
const pathSegments = path.toString().split(Path.separator);
if (pathSegments.length < 2) {
console.warn(
`Expected at least two segments long relative path. Skipping. Path segments were: ${pathSegments}. ${logInfo(
ref
)}`
);
continue;
}
// the relative must start start with `example` or `Examples` or `EXAMPLE`, .etc. It's open source.
if (!/^examples?$/gi.test(pathSegments[0])) {
console.warn(
`First segment must start with "examples-like". More formally: \`/^examples?$/gi\`. Path segments were: ${pathSegments}. ${logInfo(
ref
)}`
);
}
const getOrCreateChildContainer = (
label: string,
parent: SketchContainer
) => {
let child = parent.children.find(
({ label: childLabel }) => childLabel === label
);
if (!child) {
child = SketchContainer.create(label);
parent.children.push(child);
}
return child;
};
const refContainer = pathSegments.reduce(
(container, segment, index, segments) => {
if (index === 0) {
// skip the first "example-like" segment
return container;
}
if (index === segments.length - 1) {
// if last segment, it's the example sketch itself, do not create container for it.
return container;
}
return getOrCreateChildContainer(segment, container);
},
container
);
refContainer.sketches.push(ref);
}
return container;
}
}