/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; import * as xmldom from 'xmldom'; import * as constants from '../common/constants'; import { Uri } from 'vscode'; import { promises as fs } from 'fs'; import { DataSource } from './dataSources/dataSources'; /** * Class representing a Project, and providing functions for operating on it */ export class Project { public projectFilePath: string; public files: ProjectEntry[] = []; public dataSources: DataSource[] = []; public get projectFolderPath() { return path.dirname(this.projectFilePath); } private projFileXmlDoc: any = undefined; constructor(projectFilePath: string) { this.projectFilePath = projectFilePath; } /** * Reads the project setting and contents from the file */ public async readProjFile() { const projFileText = await fs.readFile(this.projectFilePath); this.projFileXmlDoc = new xmldom.DOMParser().parseFromString(projFileText.toString()); // find all folders and files to include for (let ig = 0; ig < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; ig++) { const itemGroup = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup)[ig]; for (let b = 0; b < itemGroup.getElementsByTagName(constants.Build).length; b++) { this.files.push(this.createProjectEntry(itemGroup.getElementsByTagName(constants.Build)[b].getAttribute(constants.Include), EntryType.File)); } for (let f = 0; f < itemGroup.getElementsByTagName(constants.Folder).length; f++) { this.files.push(this.createProjectEntry(itemGroup.getElementsByTagName(constants.Folder)[f].getAttribute(constants.Include), EntryType.Folder)); } } } /** * Adds a folder to the project, and saves the project file * @param relativeFolderPath Relative path of the folder */ public async addFolderItem(relativeFolderPath: string): Promise { const absoluteFolderPath = path.join(this.projectFolderPath, relativeFolderPath); await fs.mkdir(absoluteFolderPath, { recursive: true }); const folderEntry = this.createProjectEntry(relativeFolderPath, EntryType.Folder); this.files.push(folderEntry); await this.addToProjFile(folderEntry); return folderEntry; } /** * Writes a file to disk, adds that file to the project, and writes it to disk * @param relativeFilePath Relative path of the file * @param contents Contents to be written to the new file */ public async addScriptItem(relativeFilePath: string, contents: string): Promise { const absoluteFilePath = path.join(this.projectFolderPath, relativeFilePath); await fs.mkdir(path.dirname(absoluteFilePath), { recursive: true }); await fs.writeFile(absoluteFilePath, contents); const fileEntry = this.createProjectEntry(relativeFilePath, EntryType.File); this.files.push(fileEntry); await this.addToProjFile(fileEntry); return fileEntry; } private createProjectEntry(relativePath: string, entryType: EntryType): ProjectEntry { return new ProjectEntry(Uri.file(path.join(this.projectFolderPath, relativePath)), relativePath, entryType); } private findOrCreateItemGroup(containedTag?: string): any { let outputItemGroup = undefined; // find any ItemGroup node that contains files; that's where we'll add for (let i = 0; i < this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup).length; i++) { const currentItemGroup = this.projFileXmlDoc.documentElement.getElementsByTagName(constants.ItemGroup)[i]; // if we're not hunting for a particular child type, or if we are and we find it, use the ItemGroup if (!containedTag || currentItemGroup.getElementsByTagName(containedTag).length > 0) { outputItemGroup = currentItemGroup; break; } } // if none already exist, make a new ItemGroup for it if (!outputItemGroup) { outputItemGroup = this.projFileXmlDoc.createElement(constants.ItemGroup); this.projFileXmlDoc.documentElement.appendChild(outputItemGroup); } return outputItemGroup; } private addFileToProjFile(path: string) { const newFileNode = this.projFileXmlDoc.createElement(constants.Build); newFileNode.setAttribute(constants.Include, path); this.findOrCreateItemGroup(constants.Build).appendChild(newFileNode); } private addFolderToProjFile(path: string) { const newFolderNode = this.projFileXmlDoc.createElement(constants.Folder); newFolderNode.setAttribute(constants.Include, path); this.findOrCreateItemGroup(constants.Folder).appendChild(newFolderNode); } private async addToProjFile(entry: ProjectEntry) { switch (entry.type) { case EntryType.File: this.addFileToProjFile(entry.relativePath); break; case EntryType.Folder: this.addFolderToProjFile(entry.relativePath); } await this.serializeToProjFile(this.projFileXmlDoc); } private async serializeToProjFile(projFileContents: any) { const xml = new xmldom.XMLSerializer().serializeToString(projFileContents); // TODO: how to get this to serialize with "pretty" formatting await fs.writeFile(this.projectFilePath, xml); } } /** * Represents an entry in a project file */ export class ProjectEntry { /** * Absolute file system URI */ fsUri: Uri; relativePath: string; type: EntryType; constructor(uri: Uri, relativePath: string, type: EntryType) { this.fsUri = uri; this.relativePath = relativePath; this.type = type; } public toString(): string { return this.fsUri.path; } } export enum EntryType { File, Folder }