private genContentTypesXml()

in app/exec/extension/_lib/vsix-manifest-builder.ts [588:840]


	private genContentTypesXml(builders: ManifestBuilder[]): Promise<string> {
		let typeMap = VsixManifestBuilder.CONTENT_TYPE_MAP;
		trace.debug("Generating [Content_Types].xml");
		let contentTypes: any = {
			Types: {
				$: {
					xmlns: "http://schemas.openxmlformats.org/package/2006/content-types",
				},
				Default: [],
				Override: [],
			},
		};
		let windows = /^win/.test(process.platform);
		let contentTypePromise;
		const showWarningForExtensionMap: { [ext: string]: boolean } = {};
		if (windows) {
			// On windows, check HKCR to get the content type of the file based on the extension
			let contentTypePromises: Promise<any>[] = [];
			let extensionlessFiles = [];
			let uniqueExtensions = _.uniq<string>(
				Object.keys(this.files).map(f => {
					let extName = path.extname(f) || path.extname(this.files[f].partName);
					const filename = path.basename(f);

					// Look in the best guess table. Or, default to text/plain if the file starts with a "."
					const bestGuess =
						VsixManifestBuilder.BEST_GUESS_CONTENT_TYPES[filename.toUpperCase()] ||
						(filename[0] === "." ? "text/plain" : null);
					if (!extName && !this.files[f].contentType && this.files[f].addressable && !bestGuess) {
						trace.warn(
							"File %s does not have an extension, and its content-type is not declared. Defaulting to application/octet-stream.",
							path.resolve(f),
						);
						this.files[f].contentType = "application/octet-stream";
					} else if (bestGuess) {
						this.files[f].contentType = bestGuess;
					}
					if (this.files[f].contentType) {
						// If there is an override for this file, ignore its extension
						return "";
					}

					// Later, we will show warnings for extensions with unknown content types if there
					// was at least one file with this extension that was addressable.
					if (!showWarningForExtensionMap[extName] && this.files[f].addressable) {
						showWarningForExtensionMap[extName] = true;
					}
					return extName.toLowerCase();
				}),
			);
			uniqueExtensions.forEach(ext => {
				if (!ext.trim()) {
					return;
				}
				if (typeMap[ext]) {
					contentTypes.Types.Default.push({
						$: {
							Extension: ext,
							ContentType: typeMap[ext],
						},
					});
					return;
				}
				let hkcrKey = new winreg({
					hive: winreg.HKCR,
					key: "\\" + ext,
				});
				const regPromise = new Promise((resolve, reject) => {
					hkcrKey.get("Content Type", (err, result) => {
						if (err) {
							reject(err);
						} else {
							resolve(result);
						}
					});
				})
					.then((type: winreg.RegistryItem) => {
						trace.debug("Found content type for %s: %s.", ext, type.value);
						let contentType = "application/octet-stream";
						if (type) {
							contentType = type.value;
						}
						return contentType;
					})
					.catch(err => {
						if (showWarningForExtensionMap[ext]) {
							trace.warn(
								"Could not determine content type for extension %s. Defaulting to application/octet-stream. To override this, add a contentType property to this file entry in the manifest.",
								ext,
							);
						}
						return "application/octet-stream";
					})
					.then(contentType => {
						contentTypes.Types.Default.push({
							$: {
								Extension: ext,
								ContentType: contentType,
							},
						});
					});
				contentTypePromises.push(regPromise);
			});
			contentTypePromise = Promise.all(contentTypePromises);
		} else {
			// If not on windows, run the file --mime-type command to use magic to get the content type.
			// If the file has an extension, rev a hit counter for that extension and the extension
			// If there is no extension, create an <Override> element for the element
			// For each file with an extension that doesn't match the most common type for that extension
			// (tracked by the hit counter), create an <Override> element.
			// Finally, add a <Default> element for each extension mapped to the most common type.

			let contentTypePromises: Promise<any>[] = [];
			let extTypeCounter: { [ext: string]: { [type: string]: string[] } } = {};
			Object.keys(this.files)
				.filter(fileName => {
					return !this.files[fileName].contentType;
				})
				.forEach(fileName => {
					let extension = path.extname(fileName).toLowerCase();
					let mimePromise;
					if (typeMap[extension]) {
						if (!extTypeCounter[extension]) {
							extTypeCounter[extension] = {};
						}
						if (!extTypeCounter[extension][typeMap[extension]]) {
							extTypeCounter[extension][typeMap[extension]] = [];
						}
						extTypeCounter[extension][typeMap[extension]].push(fileName);
						mimePromise = Promise.resolve(null);
						return;
					}
					mimePromise = new Promise((resolve, reject) => {
						let child = childProcess.exec('file --mime-type "' + fileName + '"', (err, stdout, stderr) => {
							try {
								if (err) {
									if (this.files[fileName].addressable) {
										reject(err);
									} else {
										this.files[fileName].contentType = "application/octet-stream";
									}
								} else {
									if (typeof stdout === "string") {
										let magicMime = _.trimEnd(stdout.substr(stdout.lastIndexOf(" ") + 1), "\n");
										trace.debug("Magic mime type for %s is %s.", fileName, magicMime);
										if (magicMime) {
											if (extension) {
												if (!extTypeCounter[extension]) {
													extTypeCounter[extension] = {};
												}
												let hitCounters = extTypeCounter[extension];
												if (!hitCounters[magicMime]) {
													hitCounters[magicMime] = [];
												}
												hitCounters[magicMime].push(fileName);
											} else {
												if (!this.files[fileName].contentType) {
													this.files[fileName].contentType = magicMime;
												}
											}
										} else {
											if (stderr) {
												if (this.files[fileName].addressable) {
													reject(stderr);
												} else {
													this.files[fileName].contentType = "application/octet-stream";
												}
											} else {
												if (this.files[fileName].addressable) {
													trace.warn(
														"Could not determine content type for %s. Defaulting to application/octet-stream. To override this, add a contentType property to this file entry in the manifest.",
														fileName,
													);
												}
												this.files[fileName].contentType = "application/octet-stream";
											}
										}
									}
								}
								resolve(null);
							} catch (e) {
								reject(e);
							}
						});
					});
					contentTypePromises.push(mimePromise);
				});
			contentTypePromise = Promise.all(contentTypePromises).then(() => {
				Object.keys(extTypeCounter).forEach(ext => {
					let hitCounts = extTypeCounter[ext];
					let bestMatch = maxKey<string[]>(hitCounts, i => i.length);
					Object.keys(hitCounts).forEach(type => {
						if (type === bestMatch) {
							return;
						}
						hitCounts[type].forEach(fileName => {
							this.files[fileName].contentType = type;
						});
					});
					contentTypes.Types.Default.push({
						$: {
							Extension: ext,
							ContentType: bestMatch,
						},
					});
				});
			});
		}
		return contentTypePromise.then(() => {
			let seenPartNames = new Set();
			Object.keys(this.files).forEach(filePath => {
				if (this.files[filePath].contentType) {
					let partName = "/" + toZipItemName(this.files[filePath].partName);
					if (!seenPartNames.has(partName)) {
						contentTypes.Types.Override.push({
							$: {
								ContentType: this.files[filePath].contentType,
								PartName: partName,
							},
						});
						seenPartNames.add(partName);
					}
					if ((this.files[filePath] as any)._additionalPackagePaths) {
						for (const additionalPath of (this.files[filePath] as any)._additionalPackagePaths) {							
							let additionalPartName =  "/" + toZipItemName(additionalPath);
							if (!seenPartNames.has(additionalPartName)) {
								contentTypes.Types.Override.push({
									$: {
										ContentType: this.files[filePath].contentType,
										PartName: additionalPartName,
									},
								});
								seenPartNames.add(additionalPartName);
							}
						}
					}
				}
			});
			// Add the Default entries for manifests.
			builders.forEach(builder => {
				let manifestExt = path.extname(builder.getPath());
				if (contentTypes.Types.Default.filter(t => t.$.Extension === manifestExt).length === 0) {
					contentTypes.Types.Default.push({
						$: {
							Extension: manifestExt,
							ContentType: builder.getContentType(),
						},
					});
				}
			});
			return jsonToXml(contentTypes).replace(/\n/g, os.EOL);
		});
	}