public async exec()

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;
	}