// ***************************************************************************** // Copyright (C) 2024 Typefox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { inject, injectable, named } from '@theia/core/shared/inversify'; import { WorkspaceServer } from '@theia/workspace/lib/common'; import { DevContainerFile } from '../electron-common/remote-container-connection-provider'; import { DevContainerConfiguration } from './devcontainer-file'; import { parse } from 'jsonc-parser'; import * as fs from '@theia/core/shared/fs-extra'; import { ContributionProvider, Path, URI } from '@theia/core'; import { VariableContext, VariableResolverContribution } from './devcontainer-contributions/variable-resolver-contribution'; const VARIABLE_REGEX = /\$\{(.+?)(?::(.+?))?\}/g; @injectable() export class DevContainerFileService { @inject(WorkspaceServer) protected readonly workspaceServer: WorkspaceServer; @inject(ContributionProvider) @named(VariableResolverContribution) protected readonly variableResolverContributions: ContributionProvider; protected resolveVariable(value: string, context?: VariableContext): string { return value.replace(VARIABLE_REGEX, (match, type, variable) => { for (const contribution of this.variableResolverContributions.getContributions()) { if (contribution.canResolve(type, context)) { return contribution.resolve(variable ?? type, context); } } return match; }); } protected resolveVariablesRecursively(obj: T, context?: VariableContext): T { if (typeof obj === 'string') { return this.resolveVariable(obj, context) as T; } else if (Array.isArray(obj)) { return obj.map(item => this.resolveVariablesRecursively(item, context)) as T; } else if (obj && typeof obj === 'object') { const newObj: Record = {}; for (const [key, value] of Object.entries(obj)) { newObj[key] = this.resolveVariablesRecursively(value, context); } return newObj as T; } return obj; } async getConfiguration(path: string, context?: VariableContext): Promise { let configuration: DevContainerConfiguration = parse(await fs.readFile(path, 'utf-8').catch(() => '0')) as DevContainerConfiguration; if (!configuration) { throw new Error(`devcontainer file ${path} could not be parsed`); } configuration = this.resolveVariablesRecursively(configuration, context); configuration.location = path; return configuration; } async getAvailableFiles(workspace: string): Promise { const devcontainerPath = new URI(workspace).path.join('.devcontainer').fsPath(); return (await this.searchForDevontainerJsonFiles(devcontainerPath, 1)).map(file => ({ name: parse(fs.readFileSync(file, 'utf-8')).name ?? 'devcontainer', path: file })); } protected async searchForDevontainerJsonFiles(directory: string, depth: number): Promise { if (depth < 0 || !await fs.pathExists(directory)) { return []; } const filesPaths = (await fs.readdir(directory)).map(file => new Path(directory).join(file).fsPath()); const devcontainerFiles = []; for (const file of filesPaths) { if (file.endsWith('devcontainer.json')) { devcontainerFiles.push(file); } else if ((await fs.stat(file)).isDirectory()) { devcontainerFiles.push(...await this.searchForDevontainerJsonFiles(file, depth - 1)); } } return devcontainerFiles; } }