in app/exec/extension/init.ts [161:415]
public async exec(): Promise<InitResult> {
trace.info("");
trace.info(colors.yellow(" -- New Azure DevOps Extension --"));
trace.info("");
trace.info(colors.cyan("For all options and help, run `tfx extension init --help`"));
trace.info("");
const initPath = (await this.commandArgs.path.val())[0];
const noDownload = await this.commandArgs.noDownload.val();
await this.createFolderIfNotExists(initPath);
const isFolder = await this.checkIsFolder(initPath);
if (!isFolder) {
throw new Error("Given path is not a folder: " + initPath);
}
const isAccessible = await this.checkFolderAccessible(initPath);
if (!isAccessible) {
throw new Error("Could not access folder for reading and writing: " + initPath);
}
if (!noDownload) {
const isEmpty = await this.checkFolderIsEmpty(initPath, ["node_modules"]);
if (!isEmpty) {
throw new Error("Folder is not empty: " + initPath);
}
}
const branch = await this.commandArgs.branch.val();
const samplePackageUri = (await this.commandArgs.zipUri.val()).replace("{{branch}}", encodeURIComponent(branch));
const npmPath = await this.commandArgs.npmPath.val();
const extensionPublisher = await this.commandArgs.publisher.val();
const extensionId = await this.commandArgs.extensionId.val();
const extensionName = await this.commandArgs.extensionName.val();
const wantedSamples = await this.commandArgs.samples.val();
// If --no-download is passed, do not download or unpack the zip file. Assume the folder's contents contain the zip file.
if (!noDownload) {
const downloadedZipPath = path.join(initPath, "azure-devops-extension-sample.zip");
const zipFile = fs.createWriteStream(downloadedZipPath);
let bytesReceived = 0;
try {
await new Promise((resolve, reject) => {
trace.info("Downloading sample package from " + samplePackageUri);
http.get(samplePackageUri, response => {
response
.on("data", chunk => {
bytesReceived += chunk.length;
zipFile.write(chunk);
})
.on("end", () => {
zipFile.end(resolve);
})
.on("error", err => {
reject(err);
});
}).on("error", err => {
reject(err);
});
});
} catch (e) {
fs.unlink(downloadedZipPath, err => {});
throw new Error("Failed to download sample package from " + samplePackageUri + ". Error: " + e);
}
trace.info(`Package downloaded to ${downloadedZipPath} (${Math.round(bytesReceived / 1000)} kB)`);
// Crack open the zip file.
try {
await new Promise((resolve, reject) => {
fs.readFile(downloadedZipPath, async (err, data) => {
if (err) {
reject(err);
} else {
await jszip.loadAsync(data).then(async zip => {
// Write each file in the zip to the file system in the same directory as the zip file.
for (const fileName of Object.keys(zip.files)) {
trace.debug("Save file " + fileName);
await zip.files[fileName].async("nodebuffer").then(async buffer => {
trace.debug("Writing buffer for " + fileName);
const noLeadingFolderFileName = fileName.substr(fileName.indexOf("/"));
const fullPath = path.join(initPath, noLeadingFolderFileName);
if (fullPath.endsWith("\\") || fullPath.endsWith("/")) {
// don't need to "write" the folders since they are handled by createFolderIfNotExists().
return;
}
trace.debug("Creating folder if it doesn't exist: " + path.dirname(fullPath));
await this.createFolderIfNotExists(path.dirname(fullPath));
fs.writeFile(fullPath, buffer, err => {
if (err) {
console.log("err: " + err);
reject(err);
}
});
});
}
});
resolve();
}
});
});
} catch (e) {
await this.deleteFolderContents(initPath);
throw new Error(`Error unzipping ${downloadedZipPath}: ${e}`);
}
trace.debug("Delete zip file " + downloadedZipPath);
await promisify(fs.unlink)(downloadedZipPath);
}
trace.debug("Getting available samples");
const samplesPath = path.join(initPath, "src", "Samples");
const samplesList = await promisify(fs.readdir)(samplesPath);
let samplesToRemove = getSamplesToRemove(wantedSamples, samplesList);
const includesHub = samplesToRemove.indexOf("Hub") >= 0;
const includesBcs = samplesToRemove.indexOf("BreadcrumbService") >= 0;
if (includesHub && !includesBcs) {
samplesToRemove.push("BreadcrumbService");
} else if (!includesHub && includesBcs) {
samplesToRemove = samplesToRemove.filter(s => s !== "BreadcrumbService");
}
const includedSamples = samplesList.filter(s => samplesToRemove.indexOf(s) === -1 && s !== "BreadcrumbService");
if (includedSamples.length > 0) {
trace.info("Including the following samples: ");
trace.info(
includedSamples.map(s => {
const text = s === "Hub" ? s + " (with BreadcrumbService)" : s;
return " - " + text;
}),
);
trace.debug("Deleting the following samples: " + samplesToRemove.join(", "));
for (const sampleToRemove of samplesToRemove) {
await this.deleteNonEmptyFolder(path.join(samplesPath, sampleToRemove));
}
} else {
trace.info("Including no samples (starting with an empty project).");
const webpackConfigPath = path.join(initPath, "webpack.config.js");
// Delete all the samples
await this.deleteNonEmptyFolder(samplesPath);
await promisify(fs.unlink)(path.join(initPath, "src", "Common.scss"));
await promisify(fs.unlink)(path.join(initPath, "src", "Common.tsx"));
// Update the webpack config and create a dummy js file in src.
await promisify(fs.unlink)(webpackConfigPath);
await promisify(fs.writeFile)(webpackConfigPath, basicWebpackConfig, "utf8");
await promisify(fs.writeFile)(path.join(initPath, "src", "index.js"), "", "utf8");
// Delete tsconfig, overview.md, and truncate readme
await promisify(fs.unlink)(path.join(initPath, "tsconfig.json"));
await promisify(fs.unlink)(path.join(initPath, "overview.md"));
await promisify(fs.writeFile)(path.join(initPath, "README.md"), "", "utf8");
}
trace.info("Updating azure-devops-extension.json with publisher ID, extension ID, and extension name.");
const mainManifestPath = path.join(initPath, "azure-devops-extension.json");
const manifestContents = await promisify(fs.readFile)(mainManifestPath, "utf8");
const packageJsonPath = path.join(initPath, "package.json");
const packageJsonContents = await promisify(fs.readFile)(packageJsonPath, "utf8");
const newManifest = jsonInPlace(manifestContents)
.set("publisher", extensionPublisher)
.set("id", extensionId)
.set("name", extensionName)
.set("version", "1.0.0")
.set("description", "Azure DevOps Extension")
.set("categories", ["Azure Repos", "Azure Boards", "Azure Pipelines", "Azure Test Plans", "Azure Artifacts"]);
const newPackageJson = jsonInPlace(packageJsonContents)
.set("repository.url", "")
.set("description", extensionName)
.set("name", extensionId)
.set("version", "1.0.0");
const newManifestObj = JSON.parse(newManifest.toString());
if (includedSamples.length === 0) {
newPackageJson
.set("scripts.package-extension", "tfx extension create --manifests azure-devops-extension.json")
.set("scripts.publish-extension", "tfx extension publish --manifests azure-devops-extension.json")
.set("devDependencies", basicDevDependencies);
delete newManifestObj["icons"];
delete newManifestObj["content"];
}
await promisify(fs.writeFile)(mainManifestPath, JSON.stringify(newManifestObj, null, 4), "utf8");
await promisify(fs.writeFile)(packageJsonPath, newPackageJson.toString(), "utf8");
// Check for existence of node_modules. If it's not there, try installing the package.
const nodeModulesPath = path.join(initPath, "node_modules");
const alreadyInstalled = await this.checkFolderAccessible(nodeModulesPath);
if (!alreadyInstalled) {
trace.debug("No node_modules folder found.");
trace.info("Running `" + npmPath + " install` in " + initPath + "... please wait.");
await new Promise((resolve, reject) => {
const npmCommand = exec(
npmPath + " install",
{
cwd: initPath,
},
(err, stdout) => {
if (err) {
reject(err);
} else {
resolve(stdout);
}
},
);
});
} else {
trace.info(`The folder "${nodeModulesPath}" already exists. Foregoing npm install.`);
}
// Yes, this is a lie. We're actually going to run tfx extension create manually.
trace.info("Building sample with `npm run build`");
await new Promise((resolve, reject) => {
const npmCommand = exec(
npmPath + " run compile:dev",
{
cwd: initPath,
},
(err, stdout) => {
if (err) {
reject(err);
} else {
resolve(stdout);
}
},
);
});
trace.debug("Building extension package.");
const manifestGlobs = ["azure-devops-extension.json"];
if (includedSamples.length > 0) {
manifestGlobs.push("src/Samples/**/*.json");
}
const createResult = await createExtension(
{
manifestGlobs: manifestGlobs,
revVersion: false,
bypassValidation: includedSamples.length === 0, // need to bypass validation when there are no contributions
locRoot: null,
manifestJs: null,
env: null,
manifests: null,
overrides: {},
root: initPath,
json5: false,
},
{
locRoot: null,
metadataOnly: false,
outputPath: initPath,
},
);
return { path: initPath } as InitResult;
}