This commit is contained in:
Trevor Gau 2015-10-07 16:54:06 -04:00
Родитель 96e503811a
Коммит 6dd5c34644
28 изменённых файлов: 2089 добавлений и 946 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -28,3 +28,6 @@ build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
# VS Code Settings
.settings

42
.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,42 @@
{
"version": "0.1.0",
// List of configurations. Add new configurations or edit existing ones.
// ONLY "node" and "mono" are supported, change "type" to switch.
"configurations": [
{
// Name of configuration; appears in the launch configuration drop down menu.
"name": "Launch tfx-cli TypeScript",
// Type of configuration. Possible values: "node", "mono".
"type": "node",
// Workspace relative or absolute path to the program.
"program": "./app/tfx-cli.ts",
// Automatically stop program after launch.
"stopOnEntry": false,
// Command line arguments passed to the program.
"args": ["extension", "create"],
// Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace.
"cwd": "C:\\vso-extension-samples\\contributions-guide",
// Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH.
"runtimeExecutable": null,
// Optional arguments passed to the runtime executable.
"runtimeArgs": ["--nolazy"],
// Environment variables passed to the program.
"env": {
"NODE_ENV": "development"
},
// Use JavaScript source maps (if they exist).
"sourceMaps": true,
// If JavaScript source maps are enabled, the generated code is expected in this directory.
"outDir": "c:/tfs-cli/_build/app"
},
{
"name": "Attach",
"type": "node",
// TCP/IP address. Default is "localhost".
"address": "localhost",
// Port to attach to.
"port": 5858,
"sourceMaps": false
}
]
}

Просмотреть файл

@ -1,24 +1,14 @@
/// <reference path="../../definitions/tsd.d.ts" />
import _ = require("lodash");
import { Merger } from "../lib/extensions/merger";
import { VsixManifestBuilder } from "../lib/extensions/vsix-manifest-builder";
import { VsoManifestBuilder } from "../lib/extensions/targets/VSO/vso-manifest-builder";
import { MergeSettings, PackageSettings } from "../lib/extensions/interfaces";
import { VsixWriter } from "../lib/extensions/vsix-writer";
import argm = require('../lib/arguments');
import childProcess = require("child_process");
import cmdm = require('../lib/tfcommand');
import cm = require('../lib/common');
import fs = require("fs");
import gallerym = require('vso-node-api/GalleryApi');
import galleryifm = require('vso-node-api/interfaces/GalleryInterfaces');
import glob = require("glob");
import os = require('os');
import path = require("path");
import Q = require('q');
import stream = require("stream");
import tmp = require("tmp");
import winreg = require('winreg');
import xml = require("xml2js");
import zip = require("jszip");
import mkdirp = require("mkdirp");
import trace = require('../lib/trace');
var defaultManifest = require("./resources/default-manifest.json");
export function describe(): string {
@ -39,6 +29,7 @@ export var hideBanner: boolean = false;
export interface ExtensionCreateArguments {
outputpath: string;
root?: string;
locRoot?: string;
manifestglob?: string[];
settings?: string;
override?: any;
@ -58,6 +49,7 @@ export class ExtensionCreate extends cmdm.TfCommand {
this.optionalArguments = [
argm.ROOT,
argm.LOC_ROOT,
argm.MANIFEST_GLOB,
argm.SETTINGS,
argm.OVERRIDE,
@ -73,22 +65,24 @@ export class ExtensionCreate extends cmdm.TfCommand {
trace.debug("Begin package creation");
var args = <ExtensionCreateArguments><any>rawArgs;
var merger = new Merger({
let mergeSettings: MergeSettings = {
root: args.root,
manifestGlobs: args.manifestglob,
overrides: _.assign({}, args.override, {
publisher: args.publisher,
extensionid: args.extensionid
}),
overrides: args.override,
bypassValidation: args.bypassvalidation
});
};
let packageSettings: PackageSettings = {
outputPath: args.outputpath,
locRoot: args.locRoot
};
var merger = new Merger(mergeSettings, [VsixManifestBuilder, VsoManifestBuilder]);
trace.debug("Merge partial manifests");
return merger.merge().then(({ vsixManifest, manifests, files }) => {
return merger.merge().then((components) => {
trace.success("Merged successfully");
var vsixWriter = new VsixWriter(vsixManifest, manifests, files);
var vsixWriter = new VsixWriter(packageSettings, components);
trace.debug("Beginning writing VSIX");
return vsixWriter.writeVsix(args[argm.OUTPUT_PATH.name]).then((outPath: string) => {
return vsixWriter.writeVsix().then((outPath: string) => {
trace.debug("VSIX written to: %s", outPath);
return outPath;
});
@ -103,914 +97,4 @@ export class ExtensionCreate extends cmdm.TfCommand {
trace.success('Successfully created package at ' + outPath);
}
}
/**
* Combines the vsix and vso manifests into one object
*/
export interface VsixComponents {
vsixManifest: VsixManifest;
manifests: Manifest[];
files: PackageFiles;
}
/**
* Represents a part in an OPC package
*/
export interface PackagePart {
contentType?: string;
partName: string;
}
/**
* List of files in the package, mapped to null, or, if it can't be properly auto-
* detected, a content type.
*/
export interface PackageFiles {
[path: string]: PackagePart;
}
/**
* Describes a file in a manifest
*/
export interface FileDeclaration {
assetType?: string;
contentType?: string;
auto?: boolean;
path: string;
partName: string;
}
/**
* Settings for doing the merging
*/
export interface MergeSettings {
/**
* Root of source manifests
*/
root: string;
/**
* List of globs for searching for partial manifests
*/
manifestGlobs: string[];
/**
* Highest priority partial manifest
*/
overrides: any;
/**
* True to bypass validation during packaging.
*/
bypassValidation: boolean;
}
export /* abstract */ class Manifest {
public type: string;
public path: string;
constructor(protected data: any) {
// noop
}
public toJSON(): string {
return this.data;
}
public merge(key: string, value: any, packageFiles: PackageFiles, override: boolean): void {
// noop
}
public validate(): string[] {
return [];
}
public write(stream: stream.Writable): Q.Promise<any> {
return Q.resolve(null);
}
protected singleValueProperty(path: string, value: any, manifestKey: string, override: boolean = false): boolean {
let existingValue = _.get(this.data, path);
if (!override && existingValue !== undefined) {
trace.warn("Multiple values found for '%s'. Ignoring future occurrences and using the value '%s'.", manifestKey, JSON.stringify(existingValue, null, 4));
return false;
} else {
_.set(this.data, path, value);
return true;
}
}
protected handleDelimitedList(value: any, path: string, delimiter: string = ",", uniq: boolean = true): void {
if (_.isString(value)) {
value = value.split(delimiter);
_.remove(value, v => v === "");
}
var items = _.get(this.data, path, "").split(delimiter);
_.remove(items, v => v === "");
let val = items.concat(value);
if (uniq) {
val = _.uniq(val);
}
_.set(this.data, path, val.join(delimiter));
}
}
function removeMetaKeys(obj: any): any {
return _.omit(obj, (v, k) => _.startsWith(k, "__meta_"));
}
export class VsoManifest extends Manifest {
public path = "extension.vsomanifest";
public type = "Microsoft.VisualStudio.Services.Manifest";
constructor() {
super({
manifestVersion: 1,
scopes: [],
contributions: [],
});
}
public merge(key: string, value: any, packageFiles: PackageFiles, override: boolean): void {
switch(key.toLowerCase()) {
case "version":
this.data.version = value;
break;
case "name":
this.data.name = value;
break;
case "description":
this.data.description = value;
break;
case "eventcallbacks":
if (_.isObject(value)) {
if (!this.data.eventCallbacks) {
this.data.eventCallbacks = {};
}
_.merge(this.data.eventCallbacks, value);
}
break;
case "manifestversion":
let version = value;
if (_.isString(version)) {
version = parseFloat(version);
}
if (!version) {
version = 1;
}
this.singleValueProperty("manifestVersion", version, key, true);
break;
case "scopes":
if (_.isArray(value)) {
this.data.scopes = _.uniq(this.data.scopes.concat(value));
}
break;
case "baseuri":
this.singleValueProperty("baseUri", value, key, override);
break;
case "contributions":
if (_.isArray(value)) {
this.data.contributions = this.data.contributions.concat(value);
}
break;
case "contributiontypes":
if (_.isArray(value)) {
if (!this.data.contributionTypes) {
this.data.contributionTypes = [];
}
this.data.contributionTypes = this.data.contributionTypes.concat(value);
}
break;
case "namespace":
case "extensionid":
case "id":
case "icons":
case "public":
case "publisher":
case "releasenotes":
case "tags":
case "vsoflags":
case "galleryflags":
case "categories":
case "files":
break;
default:
if (key.substr(0, 2) !== "__") {
this.singleValueProperty(key, value, key, override);
}
break;
}
}
/**
* Writes the vso manifest to given stream.
* @param stream.Writable Stream to write the vso manifest (json)
* @return Q.Promise<any> A promise that is resolved when the stream has been ended
*/
public write(stream: stream.Writable): Q.Promise<any> {
const contents = JSON.stringify(removeMetaKeys(this.data), null, 4).replace(/\n/g, os.EOL);
return Q.ninvoke<any>(stream, "end", contents, "utf8");
}
}
export class VsixManifest extends Manifest {
private didCleanupAssets = false;
public path: string = "extension.vsixmanifest";
constructor(public root: string, private manifests: Manifest[]) {
super(_.cloneDeep(defaultManifest));
}
public get assets():any[] {
if (!this.didCleanupAssets) {
// Remove any vso manifest assets, then add the correct entry.
let assets = _.get<any[]>(this.data, "PackageManifest.Assets[0].Asset");
if (assets) {
_.remove(assets, (asset) => {
let type = _.get(asset, "$.Type", "x").toLowerCase();
return type === "microsoft.vso.manifest" || type === "microsoft.visualstudio.services.manifest";
});
} else {
assets = [];
_.set<any, any>(this.data, "PackageManifest.Assets[0].Asset[0]", assets);
}
assets.push(...this.manifests.map(manifest => ({$:{
Type: manifest.type,
Path: manifest.path
}})));
this.didCleanupAssets = true;
}
return _.get(this.data, "PackageManifest.Assets[0].Asset", [])
.filter(asset => asset.$ && !_.contains(this.manifests.map(m => m.type), asset.$.Type));
}
public merge(key: string, value: any, packageFiles: PackageFiles, override: boolean): void {
switch(key.toLowerCase()) {
case "namespace":
case "extensionid":
case "id":
if (_.isString(value)) {
this.singleValueProperty("PackageManifest.Metadata[0].Identity[0].$.Id", value.replace(/\./g, "-"), "namespace/extensionId/id", override);
}
break;
case "version":
this.singleValueProperty("PackageManifest.Metadata[0].Identity[0].$.Version", value, key, override);
break;
case "name":
this.singleValueProperty("PackageManifest.Metadata[0].DisplayName[0]", value, key, override);
break;
case "description":
this.data.PackageManifest.Metadata[0].Description[0]._ = value;
break;
case "icons":
if (_.isString(value["default"])) {
let assets = _.get<any>(this.data, "PackageManifest.Assets[0].Asset");
let iconPath = value["default"].replace(/\\/g, "/");
assets.push({
"$": {
"Type": "Microsoft.VisualStudio.Services.Icons.Default",
"d:Source": "File",
"Path": iconPath
}
});
// Default icon is also the package icon
this.singleValueProperty("PackageManifest.Metadata[0].Icon[0]", iconPath, "icons['default']", override);
}
if (_.isString(value["wide"])) {
let assets = _.get<any>(this.data, "PackageManifest.Assets[0].Asset");
assets.push({
"$": {
"Type": "Microsoft.VisualStudio.Services.Icons.Wide",
"d:Source": "File",
"Path": value["wide"].replace(/\\/g, "/")
}
});
}
break;
case "public":
if (typeof value === "boolean") {
let flags = _.get(this.data, "PackageManifest.Metadata[0].GalleryFlags[0]", "").split(",");
_.remove(flags, v => v === "");
if (value === true) {
flags.push("Public");
}
_.set(this.data, "PackageManifest.Metadata[0].GalleryFlags[0]", _.uniq(flags).join(","));
}
break;
case "publisher":
this.singleValueProperty("PackageManifest.Metadata[0].Identity[0].$.Publisher", value, key, override);
break;
case "releasenotes":
this.singleValueProperty("PackageManifest.Metadata[0].ReleaseNotes[0]", value, key, override);
break;
case "tags":
this.handleDelimitedList(value, "PackageManifest.Metadata[0].Tags[0]");
break;
case "vsoflags":
case "galleryflags":
// Gallery Flags are space-separated since it's a Flags enum.
this.handleDelimitedList(value, "PackageManifest.Metadata[0].GalleryFlags[0]", " ");
break;
case "categories":
this.handleDelimitedList(value, "PackageManifest.Metadata[0].Categories[0]");
break;
case "files":
if (_.isArray(value)) {
value.forEach((asset: FileDeclaration) => {
let assetPath = asset.path.replace(/\\/g, "/");
if (!asset.auto || !packageFiles[assetPath]) {
packageFiles[assetPath] = {
partName: asset.partName || assetPath
};
}
if (asset.contentType) {
packageFiles[assetPath].contentType = asset.contentType;
}
if (asset.assetType) {
this.data.PackageManifest.Assets[0].Asset.push({
"$": {
"Type": asset.assetType,
"d:Source": "File",
"Path": assetPath
}
});
}
if (asset.assetType === "Microsoft.VisualStudio.Services.Icons.Default") {
this.data.PackageManifest.Metadata[0].Icon = [assetPath];
}
});
}
break;
}
}
public validate(): string[] {
return Object
.keys(VsixManifest.vsixValidators)
.map(path => VsixManifest.vsixValidators[path](_.get(this.data, path)))
.filter(r => !!r);
}
/**
* Writes the vsix manifest to given stream.
* @param stream.Writable Stream to write the vsix manifest (xml)
* @return Q.Promise<any> A promise that is resolved when the stream has been ended
*/
public write(stream: stream.Writable): Q.Promise<any> {
const builder = new xml.Builder({
indent: " ",
newline: os.EOL,
pretty: true,
xmldec: {
encoding: "utf-8",
standalone: null,
version: "1.0"
}
});
const vsix = builder.buildObject(removeMetaKeys(this.data));
return Q.ninvoke<any>(stream, 'end', vsix, "utf8");
}
/**
* If outPath is {auto}, generate an automatic file name.
* Otherwise, try to determine if outPath is a directory (checking for a . in the filename)
* If it is, generate an automatic filename in the given outpath
* Otherwise, outPath doesn't change.
*/
public getOutputPath(outPath: string): string {
let newPath = outPath;
let pub = _.get(this.data, "PackageManifest.Metadata[0].Identity[0].$.Publisher");
let ns = _.get(this.data, "PackageManifest.Metadata[0].Identity[0].$.Id");
let version = _.get(this.data, "PackageManifest.Metadata[0].Identity[0].$.Version");
let autoName = pub + "." + ns + "-" + version + ".vsix";
if (outPath === "{auto}") {
return path.resolve(autoName);
} else {
let basename = path.basename(outPath);
if (basename.indexOf(".") > 0) { // conscious use of >
return path.resolve(outPath);
} else {
return path.resolve(path.join(outPath, autoName));
}
}
}
private static vsixValidators: {[path: string]: (value) => string} = {
"PackageManifest.Metadata[0].Identity[0].$.Id": (value) => {
if (/^[A-z0-9_-]+$/.test(value)) {
return null;
} else {
return "'extensionId' may only include letters, numbers, underscores, and dashes.";
}
},
"PackageManifest.Metadata[0].Identity[0].$.Version": (value) => {
if (typeof value === "string" && value.length > 0) {
return null;
} else {
return "'version' must be provided.";
}
},
"PackageManifest.Metadata[0].DisplayName[0]": (value) => {
if (typeof value === "string" && value.length > 0) {
return null;
} else {
return "'name' must be provided.";
}
},
"PackageManifest.Assets[0].Asset": (value) => {
let usedAssetTypes = {};
if (_.isArray(value)) {
for (let i = 0; i < value.length; ++i) {
let asset = value[i].$;
if (asset) {
if (!asset.Path) {
return "All 'files' must include a 'path'.";
}
if (asset.Type) {
if (usedAssetTypes[asset.Type]) {
return "Cannot have multiple files with the same 'assetType'.\nFile1: " + usedAssetTypes[asset.Type] + ", File 2: " + asset.Path + " (asset type: " + asset.Type + ")";
} else {
usedAssetTypes[asset.Type] = asset.Path;
}
}
}
}
}
return null;
},
"PackageManifest.Metadata[0].Identity[0].$.Publisher": (value) => {
if (typeof value === "string" && value.length > 0) {
return null;
} else {
return "'publisher' must be provided.";
}
},
"PackageManifest.Metadata[0].Categories[0]": (value) => {
if (!value) {
return null;
}
let categories = value.split(",");
let validCategories = [
"Build and release",
"Collaboration",
"Customer support",
"Planning",
"Productivity",
"Sync and integration",
"Testing"
];
_.remove(categories, c => !c);
let badCategories = categories.filter(c => validCategories.indexOf(c) === -1);
return badCategories.length ? "The following categories are not valid: " + badCategories.join(", ") + ". Valid categories are: " + validCategories.join(", ") + "." : null;
}
}
}
/**
* Facilitates the gathering/reading of partial manifests and creating the merged
* manifests (one vsoManifest and one vsixManifest)
*/
export class Merger {
/**
* constructor
* @param string Root path for locating candidate manifests
*/
constructor(private settings: MergeSettings) {
// noop
}
private gatherManifests(): Q.Promise<string[]> {
trace.debug('merger.gatherManifests');
const globs = this.settings.manifestGlobs.map(p => path.isAbsolute(p) ? p : path.join(this.settings.root, p));
trace.debug('merger.gatherManifestsFromGlob');
const promises = globs.map(pattern => Q.nfcall<string[]>(glob, pattern));
return Q.all(promises)
.then(results => _.unique(_.flatten<string>(results)))
.then(results => {
if (results.length > 0) {
trace.debug("Merging %s manifests from the following paths: ", results.length.toString());
results.forEach(path => trace.debug(path));
} else {
throw new Error("No manifests found from the following glob patterns: \n" + this.settings.manifestGlobs.join("\n"));
}
return results;
});
}
/**
* Finds all manifests and merges them into two JS Objects: vsoManifest and vsixManifest
* @return Q.Promise<SplitManifest> An object containing the two manifests
*/
public merge(): Q.Promise<VsixComponents> {
trace.debug('merger.merge')
return this.gatherManifests().then(files => {
let overridesProvided = false;
let manifestPromises: Q.Promise<any>[] = [];
files.forEach((file) => {
manifestPromises.push(Q.nfcall<any>(fs.readFile, file, "utf8").then((data) => {
let jsonData = data.replace(/^\uFEFF/, '');
try {
let result = JSON.parse(jsonData);
result.__origin = file; // save the origin in order to resolve relative paths later.
return result;
} catch (err) {
trace.error("Error parsing the JSON in %s: ", file);
trace.debug(jsonData, null);
throw err;
}
}));
});
// Add the overrides if necessary
if (this.settings.overrides) {
overridesProvided = true;
manifestPromises.push(Q.resolve(this.settings.overrides));
}
let vsoManifest = new VsoManifest();
let vsixManifest = new VsixManifest(this.settings.root, [vsoManifest]);
let packageFiles: PackageFiles = {};
return Q.all(manifestPromises).then(partials => {
partials.forEach((partial, partialIndex) => {
// Transform asset paths to be relative to the root of all manifests, verify assets
if (_.isArray(partial["files"])) {
(<Array<FileDeclaration>>partial["files"]).forEach((asset) => {
let keys = Object.keys(asset);
if (keys.indexOf("path") < 0) {
throw new Error("Files must have an absolute or relative (to the manifest) path.");
}
let absolutePath;
if (path.isAbsolute(asset.path)) {
absolutePath = asset.path;
} else {
absolutePath = path.join(path.dirname(partial.__origin), asset.path);
}
asset.path = path.relative(this.settings.root, absolutePath);
});
}
// Transform icon paths as above
if (_.isObject(partial["icons"])) {
let icons = partial["icons"];
Object.keys(icons).forEach((iconKind: string) => {
let absolutePath = path.join(path.dirname(partial.__origin), icons[iconKind]);
icons[iconKind] = path.relative(this.settings.root, absolutePath);
});
}
// Expand any directories listed in the files array
let pathToFileDeclarations = (fsPath: string, root: string): FileDeclaration[] => {
let files: FileDeclaration[] = [];
if (fs.lstatSync(fsPath).isDirectory()) {
trace.debug("Path '%s` is a directory. Adding all contained files (recursive).", fsPath);
fs.readdirSync(fsPath).forEach((dirChildPath) => {
trace.debug("-- %s", dirChildPath);
files = files.concat(pathToFileDeclarations(path.join(fsPath, dirChildPath), root));
});
} else {
let relativePath = path.relative(root, fsPath);
files.push({path: relativePath, partName: relativePath, auto: true});
}
return files;
};
if (_.isArray(partial["files"])) {
for (let i = partial["files"].length - 1; i >= 0; --i) {
let fileDecl: FileDeclaration = partial["files"][i];
let fsPath = path.join(this.settings.root, fileDecl.path);
if (fs.lstatSync(fsPath).isDirectory()) {
Array.prototype.splice.apply(partial["files"], (<any[]>[i, 1]).concat(pathToFileDeclarations(fsPath, this.settings.root)));
}
}
}
// Merge each key of each partial manifest into the joined manifests
Object.keys(partial).forEach((key) => {
if (partial[key] !== undefined && partial[key] !== null) {
vsixManifest.merge(key, partial[key], packageFiles, partials.length - 1 === partialIndex && overridesProvided);
vsoManifest.merge(key, partial[key], packageFiles, partials.length - 1 === partialIndex && overridesProvided);
}
});
});
trace.debug("VSO Manifest: " + JSON.stringify(vsoManifest));
trace.debug("VSIX Manifest: " + JSON.stringify(vsixManifest));
let validationResult = [...vsixManifest.validate(), ...vsoManifest.validate()];
if (validationResult.length === 0 || this.settings.bypassValidation) {
return <VsixComponents>{ vsixManifest, manifests: [vsoManifest], files: packageFiles };
} else {
throw new Error("There were errors with your manifests. Address the following errors and re-run the tool.\n" + validationResult);
}
});
});
}
}
/**
* Facilitates packaging the vsix and writing it to a file
*/
export class VsixWriter {
private static VSO_MANIFEST_FILENAME: string = "extension.vsomanifest";
private static VSIX_MANIFEST_FILENAME: string = "extension.vsixmanifest";
private static CONTENT_TYPES_FILENAME: string = "[Content_Types].xml";
/**
* List of known file types to use in the [Content_Types].xml file in the VSIX package.
*/
private static CONTENT_TYPE_MAP: {[key: string]: string} = {
".bat": "application/bat",
".gif": "image/gif",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".json": "application/json",
".md": "text/markdown",
".pdf": "application/pdf",
".png": "image/png",
".ps1": "text/ps1",
".vsixmanifest": "text/xml",
".vsomanifest": "application/json"
};
/**
* constructor
* @param any vsoManifest JS Object representing a vso manifest
* @param any vsixManifest JS Object representing the XML for a vsix manifest
*/
constructor(private vsixManifest: VsixManifest, private manifests: Manifest[], private files: PackageFiles) {
// noop
}
/**
* Write a vsix package to the given file name
* @param stream.Writable Stream to write the vsix package
*/
public writeVsix(outPath: string): Q.Promise<any> {
let outputPath = this.vsixManifest.getOutputPath(outPath);
let vsixzip = new zip();
let root = this.vsixManifest.root;
if (!root) {
throw new Error("Manifest root unknown. Manifest objects should have a __meta_root key specifying the absolute path to the root of assets.");
}
// Add assets to vsix archive
let overrides: {[partName: string]: PackagePart} = {};
Object.keys(this.files).forEach((file) => {
if (_.endsWith(file, VsixWriter.VSO_MANIFEST_FILENAME)) {
return;
}
let partName = this.files[file].partName.replace(/\\/g, "/");
let fsPath = path.join(root, file);
vsixzip.file(partName, fs.readFileSync(path.join(root, file)));
if (this.files[file].contentType) {
overrides[partName] = this.files[file];
}
});
this.vsixManifest.assets.forEach(asset => {
vsixzip.file((<string>asset.$.Path).replace(/\\/g, "/"), fs.readFileSync(path.join(root, asset.$.Path)));
});
// Write the manifests to a temporary path and add them to the zip
return Q.nfcall(tmp.dir, {unsafeCleanup: true}).then((result) => {
let tmpPath = result[0];
let manifests = [<Manifest> this.vsixManifest].concat(this.manifests);
return Q.all(manifests.map(manifest => {
const manifestPath = path.join(tmpPath, manifest.path);
const stream = fs.createWriteStream(manifestPath);
return manifest.write(stream).then(() => {
vsixzip.file(manifest.path, fs.readFileSync(manifestPath, "utf-8"));
});
}));
}).then(() => {
return this.genContentTypesXml(Object.keys(vsixzip.files), overrides);
}).then((contentTypesXml) => {
vsixzip.file(VsixWriter.CONTENT_TYPES_FILENAME, contentTypesXml);
let buffer = vsixzip.generate({
type: "nodebuffer",
compression: "DEFLATE",
compressionOptions: { level: 9 },
platform: process.platform
});
trace.debug("Writing vsix to: %s", outputPath);
return Q.nfcall(mkdirp, path.dirname(outputPath))
.then(() => Q.nfcall(fs.writeFile, outputPath, buffer))
.then(() => outputPath);
});
}
/**
* Generates the required [Content_Types].xml file for the vsix package.
* This xml contains a <Default> entry for each different file extension
* found in the package, mapping it to the appropriate MIME type.
*/
private genContentTypesXml(fileNames: string[], overrides: {[partName: string]: PackagePart}): Q.Promise<string> {
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;
if (windows) {
// On windows, check HKCR to get the content type of the file based on the extension
let contentTypePromises: Q.Promise<any>[] = [];
let extensionlessFiles = [];
let uniqueExtensions = _.unique<string>(fileNames.map((f) => {
let extName = path.extname(f);
if (!extName && !overrides[f]) {
trace.warn("File %s does not have an extension, and its content-type is not declared. Defaulting to application/octet-stream.", path.resolve(f));
}
if (overrides[f]) {
// If there is an override for this file, ignore its extension
return "";
}
return extName;
}));
uniqueExtensions.forEach((ext) => {
if (!ext.trim()) {
return;
}
if (!ext) {
return;
}
if (VsixWriter.CONTENT_TYPE_MAP[ext.toLowerCase()]) {
contentTypes.Types.Default.push({
$: {
Extension: ext,
ContentType: VsixWriter.CONTENT_TYPE_MAP[ext.toLowerCase()]
}
});
return;
}
let hkcrKey = new winreg({
hive: winreg.HKCR,
key: "\\" + ext.toLowerCase()
});
let regPromise = Q.ninvoke(hkcrKey, "get", "Content Type").then((type: WinregValue) => {
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) => {
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 = Q.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: Q.Promise<any>[] = [];
let extTypeCounter: {[ext: string]: {[type: string]: string[]}} = {};
fileNames.forEach((fileName) => {
let extension = path.extname(fileName);
let mimePromise;
if (VsixWriter.CONTENT_TYPE_MAP[extension]) {
if (!extTypeCounter[extension]) {
extTypeCounter[extension] = {};
}
if (!extTypeCounter[extension][VsixWriter.CONTENT_TYPE_MAP[extension]]) {
extTypeCounter[extension][VsixWriter.CONTENT_TYPE_MAP[extension]] = [];
}
extTypeCounter[extension][VsixWriter.CONTENT_TYPE_MAP[extension]].push(fileName);
mimePromise = Q.resolve(null);
return;
}
mimePromise = Q.Promise((resolve, reject, notify) => {
let child = childProcess.exec("file --mime-type \"" + fileName + "\"", (err, stdout, stderr) => {
try {
if (err) {
reject(err);
}
let stdoutStr = stdout.toString("utf8");
let magicMime = _.trimRight(stdoutStr.substr(stdoutStr.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 (!overrides[fileName]) {
overrides[fileName].contentType = magicMime;
}
}
} else {
if (stderr) {
reject(stderr.toString("utf8"));
} else {
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);
overrides[fileName].contentType = "application/octet-stream";
}
}
resolve(null);
} catch (e) {
reject(e);
}
});
});
contentTypePromises.push(mimePromise);
});
contentTypePromise = Q.all(contentTypePromises).then(() => {
Object.keys(extTypeCounter).forEach((ext) => {
let hitCounts = extTypeCounter[ext];
let bestMatch = this.maxKey<string[]>(hitCounts, (i => i.length));
Object.keys(hitCounts).forEach((type) => {
if (type === bestMatch) {
return;
}
hitCounts[type].forEach((fileName) => {
overrides[fileName].contentType = type;
});
});
contentTypes.Types.Default.push({
$: {
Extension: ext,
ContentType: bestMatch
}
});
});
});
}
return contentTypePromise.then(() => {
Object.keys(overrides).forEach((partName) => {
contentTypes.Types.Override.push({
$: {
ContentType: overrides[partName].contentType,
PartName: "/" + _.trimLeft(partName, "/")
}
})
});
let builder = new xml.Builder({
indent: " ",
newline: os.EOL,
pretty: true,
xmldec: {
encoding: "utf-8",
standalone: null,
version: "1.0"
}
});
return builder.buildObject(contentTypes);
});
}
private maxKey<T>(obj: {[key: string]: T}, func: (input: T) => number): string {
let maxProp;
for (let prop in obj) {
if (!maxProp || func(obj[prop]) > func(obj[maxProp])) {
maxProp = prop;
}
}
return maxProp;
}
}

Просмотреть файл

@ -4,7 +4,7 @@ import _ = require("lodash");
import argm = require('../lib/arguments');
import cmdm = require('../lib/tfcommand');
import cm = require('../lib/common');
import extinfom = require('../lib/extensioninfo');
import extinfom = require('../lib/extensions/extensioninfo');
import fs = require('fs');
import gallerym = require('vso-node-api/GalleryApi');
import galleryifm = require('vso-node-api/interfaces/GalleryInterfaces');

Просмотреть файл

@ -4,7 +4,7 @@ import argm = require('../lib/arguments');
import cmdm = require('../lib/tfcommand');
import cm = require('../lib/common');
import createm = require('./extension-create');
import extinfom = require('../lib/extensioninfo');
import extinfom = require('../lib/extensions/extensioninfo');
import fs = require('fs');
import gallerym = require('vso-node-api/GalleryApi');
import galleryifm = require('vso-node-api/interfaces/GalleryInterfaces');

Просмотреть файл

@ -3,7 +3,7 @@ import cm = require('../lib/common');
import gallerym = require('vso-node-api/GalleryApi');
import galleryifm = require('vso-node-api/interfaces/GalleryInterfaces');
import argm = require('../lib/arguments');
import extinfom = require('../lib/extensioninfo');
import extinfom = require('../lib/extensions/extensioninfo');
import Q = require('q');
var trace = require('../lib/trace');

Просмотреть файл

@ -3,7 +3,7 @@ import cm = require('../lib/common');
import gallerym = require('vso-node-api/GalleryApi');
import galleryifm = require('vso-node-api/interfaces/GalleryInterfaces');
import argm = require('../lib/arguments');
import extinfom = require('../lib/extensioninfo');
import extinfom = require('../lib/extensions/extensioninfo');
import Q = require('q');
var trace = require('../lib/trace');

Просмотреть файл

@ -3,7 +3,7 @@ import cm = require('../lib/common');
import gallerym = require('vso-node-api/GalleryApi');
import galleryifm = require('vso-node-api/interfaces/GalleryInterfaces');
import argm = require('../lib/arguments');
import extinfom = require('../lib/extensioninfo');
import extinfom = require('../lib/extensions/extensioninfo');
import Q = require('q');
var trace = require('../lib/trace');

Просмотреть файл

@ -0,0 +1,89 @@
import cmdm = require('../lib/tfcommand');
import cm = require('../lib/common');
import witifm = require('vso-node-api/interfaces/WorkItemTrackingInterfaces');
import VSSInterfaces = require('vso-node-api/interfaces/common/VSSInterfaces');
import witm = require('vso-node-api/WorkItemTrackingApi');
import argm = require('../lib/arguments');
var trace = require('../lib/trace');
export function describe(): string {
return 'create a workitem';
}
export function getCommand(): cmdm.TfCommand {
// this just offers description for help and to offer sub commands
return new WorkItemCreate;
}
// requires auth, connect etc...
export var isServerOperation: boolean = true;
// unless you have a good reason, should not hide
export var hideBanner: boolean = false;
export class WorkItemCreate extends cmdm.TfCommand {
requiredArguments = [argm.PROJECT_NAME, argm.WORKITEMTYPE, argm.TITLE];
optionalArguments = [argm.ASSIGNEDTO, argm.DESCRIPTION];
public exec(args: string[], options: cm.IOptions): any {
trace('workitem-create.exec');
var witapi: witm.IQWorkItemTrackingApi = this.getWebApi().getQWorkItemTrackingApi();
return this.checkArguments(args, options).then( (allArguments) => {
var workitemtype: string = allArguments[argm.WORKITEMTYPE.name];
var assignedto: string = allArguments[argm.ASSIGNEDTO.name];
var title: string = allArguments[argm.TITLE.name];
var description: string = allArguments[argm.DESCRIPTION.name];
var project: string = allArguments[argm.PROJECT_NAME.name];
var workItemId: number = allArguments[argm.WORKITEM_ID.name];
var patchDoc: VSSInterfaces.JsonPatchOperation[] = [];
patchDoc.push({
op: VSSInterfaces.Operation.Add,
path: "/fields/System.Title",
value: title,
from: null
});
if(assignedto) {
patchDoc.push({
op: VSSInterfaces.Operation.Add,
path: "/fields/System.AssignedTo",
value: assignedto,
from: null
});
}
if(description) {
patchDoc.push({
op: VSSInterfaces.Operation.Add,
path: "/fields/System.Description",
value: description,
from: null
});
}
return witapi.updateWorkItemTemplate(null, <VSSInterfaces.JsonPatchDocument>patchDoc, project, workitemtype);
});
}
public output(data: any): void {
if (!data) {
throw new Error('no results');
}
var workitem: witifm.WorkItem = data;
console.log();
console.log('created workitem @ ' + workitem.id);
console.log();
console.log('id : ' + workitem.id);
console.log('rev : ' + workitem.rev);
console.log('type : ' + workitem.fields['System.WorkItemType']);
console.log('state : ' + workitem.fields['System.State']);
console.log('title : ' + workitem.fields['System.Title']);
console.log('assigned to : ' + workitem.fields['System.AssignedTo']);
}
}

Просмотреть файл

@ -0,0 +1,65 @@
import cmdm = require('../lib/tfcommand');
import cm = require('../lib/common');
import witifm = require('vso-node-api/interfaces/WorkItemTrackingInterfaces');
import witm = require('vso-node-api/WorkItemTrackingApi');
import argm = require('../lib/arguments');
var trace = require('../lib/trace');
export function describe(): string {
return 'get a list of workitems given query';
}
export function getCommand(): cmdm.TfCommand {
// this just offers description for help and to offer sub commands
return new WorkItemQuery;
}
// requires auth, connect etc...
export var isServerOperation: boolean = true;
// unless you have a good reason, should not hide
export var hideBanner: boolean = false;
export class WorkItemQuery extends cmdm.TfCommand {
requiredArguments = [argm.PROJECT_NAME, argm.QUERY];
public exec(args: string[], options: cm.IOptions): any {
trace('workitem-list.exec');
var witapi: witm.IQWorkItemTrackingApi = this.getWebApi().getQWorkItemTrackingApi();
return this.checkArguments(args, options).then( (allArguments) => {
var project: string = allArguments[argm.PROJECT_NAME.name];
var query: string = allArguments[argm.QUERY.name];
var wiql: witifm.Wiql = { query: query }
var workItemIds: witifm.WorkItemReference[] = [];
return witapi.queryByWiql(wiql, project).then((result: witifm.WorkItemQueryResult) => {
var workItemIds = result.workItems.map(val => val.id).slice(0,Math.min(200, result.workItems.length));
var fieldRefs = result.columns.map(val => val.referenceName).slice(0,Math.min(20, result.columns.length));
return witapi.getWorkItems(workItemIds, fieldRefs);
});
});
}
public output(data: any): void {
if (!data) {
throw new Error('no results');
}
if (!(data instanceof Array)) {
throw new Error('expected an array of workitems');
}
data.forEach((workitem: witifm.WorkItem) => {
console.log();
console.log('id : ' + workitem.id);
console.log('rev : ' + workitem.rev);
console.log('type : ' + workitem.fields['System.WorkItemType']);
console.log('state : ' + workitem.fields['System.State']);
console.log('title : ' + workitem.fields['System.Title']);
console.log('assigned to : ' + workitem.fields['System.AssignedTo']);
});
}
}

52
app/exec/workitem-show.ts Normal file
Просмотреть файл

@ -0,0 +1,52 @@
import cmdm = require('../lib/tfcommand');
import cm = require('../lib/common');
import witifm = require('vso-node-api/interfaces/WorkItemTrackingInterfaces');
import witm = require('vso-node-api/WorkItemTrackingApi');
import argm = require('../lib/arguments');
var trace = require('../lib/trace');
export function describe(): string {
return 'show a work item given an id';
}
export function getCommand(): cmdm.TfCommand {
// this just offers description for help and to offer sub commands
return new WorkItemShow;
}
// requires auth, connect etc...
export var isServerOperation: boolean = true;
// unless you have a good reason, should not hide
export var hideBanner: boolean = false;
export class WorkItemShow extends cmdm.TfCommand {
public requiredArguments = [argm.WORKITEM_ID];
public exec(args: string[], options: cm.IOptions): any {
trace('workitem-show.exec');
var witapi: witm.IQWorkItemTrackingApi = this.getWebApi().getQWorkItemTrackingApi();
return this.checkArguments(args, options).then( (allArguments) => {
var project: string = allArguments[argm.PROJECT_NAME.name];
var workItemId: number = allArguments[argm.WORKITEM_ID.name];
return witapi.getWorkItem(workItemId);
});
}
public output(data: any): void {
if (!data) {
throw new Error('no results');
}
var workitem: witifm.WorkItem = data;
console.log();
console.log('id : ' + workitem.id);
console.log('rev : ' + workitem.rev);
console.log('type : ' + workitem.fields['System.WorkItemType']);
console.log('state : ' + workitem.fields['System.State']);
console.log('title : ' + workitem.fields['System.Title']);
console.log('assigned to : ' + workitem.fields['System.AssignedTo']);
}
}

11
app/exec/workitem.ts Normal file
Просмотреть файл

@ -0,0 +1,11 @@
import cmdm = require('../lib/tfcommand');
export function describe(): string {
return 'manage workitems';
}
export function getCommand(): cmdm.TfCommand {
// this just offers description for help and to offer sub commands
return null;
}

Просмотреть файл

@ -126,3 +126,12 @@ export var UNSHARE_WITH: ArrayArgument = new ArrayArgument('with', 'accounts to
export var VSIX_PATH: FilePathArgument = new FilePathArgument('vsix', 'path to vsix');
export var BYPASS_VALIDATION: BooleanArgument = new BooleanArgument("bypassvalidation", "bypass local validation during packaging", false);
export var MARKET: BooleanArgument = new BooleanArgument("market", "login to the Market", false);
export var LOC_ROOT: StringArgument = new StringArgument("locRoot", "root for localization files");
///WORK
export var WORKITEM_ID: IntArgument = new IntArgument('id', 'workitemid');
export var QUERY: StringArgument = new StringArgument('query');
export var TOP: IntArgument = new IntArgument('top');
export var TITLE: StringArgument = new StringArgument('title');
export var ASSIGNEDTO: StringArgument = new StringArgument('assignedto');
export var WORKITEMTYPE: StringArgument = new StringArgument('workitemtype');

Просмотреть файл

@ -1,8 +1,8 @@
/// <reference path="../../definitions/tsd.d.ts" />
/// <reference path="../../../definitions/tsd.d.ts" />
import _ = require('lodash');
import Q = require('q');
import trace = require('./trace');
import trace = require('../trace');
import fs = require('fs');
import xml2js = require("xml2js");
import zip = require("jszip");

Просмотреть файл

@ -0,0 +1,241 @@
/**
* Represents a part in an OPC package
*/
export interface PackagePart {
contentType?: string;
partName: string;
}
/**
* List of files in the package, mapped to null, or, if it can't be properly auto-
* detected, a content type.
*/
export interface PackageFiles {
[path: string]: FileDeclaration;
}
/**
* Describes a file in a manifest
*/
export interface FileDeclaration {
assetType?: string;
contentType?: string;
auto?: boolean;
path: string;
content?: string;
partName?: string;
lang?: string;
addressable?: boolean;
}
/**
* Describes a base asset declaration
*/
export interface AssetDeclaration {
path: string;
contentType?: string;
}
/**
* Describes a screenshot in the manifest
*/
export interface ScreenshotDeclaration extends AssetDeclaration {
}
/**
* Describes a details file in the manifest
*/
export interface DetailsDeclaration extends AssetDeclaration {
}
/**
* Describes a link in the manifest
*/
export interface LinkDeclaration {
url: string;
}
/**
* Describes a set of links keyed off the link type in the manifest.
*/
export interface Links {
[type: string]: LinkDeclaration;
}
/**
* Describes a target in the manifest
*/
export interface TargetDeclaration {
id: string;
version?: string;
}
/**
* Describes the extension's branding in the manifest.
*/
export interface BrandingDeclaration {
color: string;
theme: string;
}
/**
* Settings for doing the merging
*/
export interface MergeSettings {
/**
* Root of source manifests
*/
root: string;
/**
* List of globs for searching for partial manifests
*/
manifestGlobs: string[];
/**
* Highest priority partial manifest
*/
overrides: any;
/**
* True to bypass validation during packaging.
*/
bypassValidation: boolean;
}
export interface PackageSettings {
/**
* Path to the generated vsix
*/
outputPath: string;
/**
* Path to the root of localized resource files
*/
locRoot: string;
}
/*** Types related to localized resources ***/
export interface ResourcesFile {
[key: string]: string;
}
// Models the schema outlined at https://msdn.microsoft.com/en-us/library/dd997147.aspx
export interface VsixLanguagePack {
VsixLanguagePack: {
$: {
Version: string;
xmlns: string;
};
LocalizedName: [string];
LocalizedDescription: [string];
LocalizedReleaseNotes: [string];
License: [string];
MoreInfoUrl: [string];
};
}
export interface ResourceSet {
manifestResources: { [manifestType: string]: ResourcesFile};
combined: ResourcesFile;
}
/*** Types for VSIX Manifest ****/
export namespace Vsix {
export interface PackageManifestAttr {
Version?: string;
xmlns?: string;
"xmlns:d"?: string;
}
export interface IdentityAttr {
Language?: string;
Id?: string;
Version?: string;
Publisher?: string;
}
export interface Identity {
$?: IdentityAttr;
}
export interface DescriptionAttr {
"xml:space"?: string;
}
export interface Description {
$?: DescriptionAttr;
_?: string;
}
export interface Properties {
Property?: Property[];
}
export interface PropertyAttr {
Id?: string;
Value?: string;
}
export interface Property {
$?: PropertyAttr;
}
export interface Metadata {
Identity?: [Identity];
DisplayName?: [string];
Description?: [Description];
ReleaseNotes?: [string];
Tags?: [string];
GalleryFlags?: [string];
Categories?: [string];
Icon?: [string];
Properties?: [Properties];
}
export interface InstallationTargetAttr {
Id?: string;
Version?: string;
}
export interface InstallationTarget {
$?: InstallationTargetAttr;
}
export interface Installation {
InstallationTarget?: [InstallationTarget];
}
export interface AssetAttr {
Type?: string;
"d:Source"?: string;
Path?: string;
Addressable?: string;
}
export interface Asset {
$?: AssetAttr;
}
export interface Assets {
Asset?: Asset[];
}
export interface PackageManifest {
$?: PackageManifestAttr;
Metadata?: [Metadata];
Installation?: [Installation];
Dependencies?: [string];
Assets?: [Assets];
}
export interface VsixManifest {
PackageManifest?: PackageManifest;
}
}

188
app/lib/extensions/loc.ts Normal file
Просмотреть файл

@ -0,0 +1,188 @@
import { ResourcesFile, VsixLanguagePack, ResourceSet } from "./interfaces";
import { ManifestBuilder } from "./manifest";
import { VsixManifestBuilder } from "./vsix-manifest-builder";
import _ = require("lodash");
import fs = require("fs");
import trace = require("../trace");
import mkdirp = require('mkdirp');
import path = require("path");
import Q = require("q");
export module LocPrep {
/**
* Creates a deep copy of document, replacing resource keys with the values from
* the resources object.
* If a resource cannot be found, the same string from the defaults document will be substituted.
* The defaults object must have the same structure/schema as document.
*/
export function makeReplacements(document: any, resources: ResourcesFile, defaults: ResourcesFile): any {
let locDocument = _.isArray(document) ? [] : {};
for (let key in document) {
if (propertyIsComment(key)) {
continue;
} else if (_.isObject(document[key])) {
locDocument[key] = makeReplacements(document[key], resources, defaults);
} else if (_.isString(document[key]) && _.startsWith(document[key], "resource:")) {
let resourceKey = document[key].substr("resource:".length).trim();
let replacement = resources[resourceKey];
if (!_.isString(replacement)) {
replacement = defaults[resourceKey];
trace.warn("Could not find a replacement for resource key %s. Falling back to '%s'.", resourceKey, replacement);
}
locDocument[key] = replacement;
} else {
locDocument[key] = document[key];
}
}
return locDocument;
}
/**
* If the resjsonPath setting is set...
* Check if the path exists. If it does, check if it's a directory.
* If it's a directory, write to path + extension.resjson
* All other cases just write to path.
*/
export function writeResourceFile(fullResjsonPath: string, resources: ResourcesFile): Q.Promise<void> {
return Q.Promise<boolean>((resolve, reject, notify) => {
fs.exists(fullResjsonPath, (exists) => {
resolve(exists);
});
}).then<string>((exists) => {
if (exists) {
return Q.nfcall(fs.lstat, fullResjsonPath).then((obj: fs.Stats) => {
return obj.isDirectory();
}).then<string>((isDir) => {
if (isDir) {
return path.join(fullResjsonPath, "extension.resjson");
} else {
return fullResjsonPath;
}
});
} else {
return Q.resolve(fullResjsonPath)
}
}).then((determinedPath) => {
return Q.nfcall(mkdirp, path.dirname(determinedPath)).then(() => {
return Q.nfcall<void>(fs.writeFile, determinedPath, JSON.stringify(resources, null, 4), "utf8");
});
});
}
export function propertyIsComment(property: string): boolean {
return _.startsWith(property, "_") && _.endsWith(property, ".comment");
}
export class LocKeyGenerator {
private static I18N_PREFIX = "i18n:";
private combined: ResourcesFile;
private resourceFileMap: {[manifestType: string]: ResourcesFile};
private vsixManifestBuilder: VsixManifestBuilder;
constructor(private manifestBuilders: ManifestBuilder[]) {
this.initStringObjs();
// find the vsixmanifest and pull it out because we treat it a bit differently
let vsixManifest = manifestBuilders.filter(b => b.getType() === VsixManifestBuilder.manifestType);
if (vsixManifest.length === 1) {
this.vsixManifestBuilder = <VsixManifestBuilder>vsixManifest[0];
} else {
throw "Found " + vsixManifest.length + " vsix manifest builders (expected 1). Something is not right!";
}
}
private initStringObjs() {
this.resourceFileMap = {};
this.manifestBuilders.forEach((b) => {
this.resourceFileMap[b.getType()] = {};
});
this.combined = {};
}
/**
* Destructive method modifies the manifests by replacing i18nable strings with resource:
* keys. Adds all the original resources to the resources object.
*/
public generateLocalizationKeys(): ResourceSet {
this.initStringObjs();
this.manifestBuilders.forEach((builder) => {
if (builder.getType() !== VsixManifestBuilder.manifestType) {
this.jsonReplaceWithKeysAndGenerateDefaultStrings(builder);
}
});
this.vsixGenerateDefaultStrings();
return {
manifestResources: this.resourceFileMap,
combined: this.generateCombinedResourceFile()
}
}
private generateCombinedResourceFile(): ResourcesFile {
let combined: ResourcesFile = {};
let resValues = Object.keys(this.resourceFileMap).map(k => this.resourceFileMap[k]);
// the .d.ts file falls short in this case
let anyAssign: any = _.assign;
anyAssign(combined, ...resValues);
return combined;
}
private addResource(builderType: string, sourceKey: string, resourceKey: string, obj: any) {
let resourceVal = this.removeI18nPrefix(obj[sourceKey]);
this.resourceFileMap[builderType][resourceKey] = resourceVal;
let comment = obj["_" + sourceKey + ".comment"];
if (comment) {
this.resourceFileMap[builderType]["_" + resourceKey + ".comment"] = comment;
}
obj[sourceKey] = "resource:" + resourceKey;
}
private removeI18nPrefix(str: string): string {
if (_.startsWith(str, LocKeyGenerator.I18N_PREFIX)) {
return str.substr(LocKeyGenerator.I18N_PREFIX.length);
}
return str;
}
private vsixGenerateDefaultStrings(): void {
let vsixManifest = this.vsixManifestBuilder.getData();
let displayName = this.removeI18nPrefix(_.get<string>(vsixManifest, "PackageManifest.Metadata[0].DisplayName[0]"));
let description = this.removeI18nPrefix(_.get<string>(vsixManifest, "PackageManifest.Metadata[0].Description[0]._"));
let releaseNotes = this.removeI18nPrefix(_.get<string>(vsixManifest, "PackageManifest.Metadata[0].ReleaseNotes[0]"));
let vsixRes: ResourcesFile = {};
if (displayName) {
vsixRes["displayName"] = displayName;
_.set<any, string>(vsixManifest, "PackageManifest.Metadata[0].DisplayName[0]", displayName);
}
if (displayName) {
vsixRes["description"] = description;
_.set<any, string>(vsixManifest, "PackageManifest.Metadata[0].Description[0]._", description);
}
if (releaseNotes) {
vsixRes["releaseNotes"] = releaseNotes;
_.set<any, string>(vsixManifest, "PackageManifest.Metadata[0].ReleaseNotes[0]", releaseNotes);
}
this.resourceFileMap[this.vsixManifestBuilder.getType()] = vsixRes;
}
private jsonReplaceWithKeysAndGenerateDefaultStrings(builder: ManifestBuilder, json: any = null, path: string = ""): void {
if (!json) {
json = builder.getData();
}
for (let key in json) {
let val = json[key];
if (_.isObject(val)) {
let nextPath = builder.getLocKeyPath(path + key + ".");
while (_.endsWith(nextPath, ".")) {
nextPath = nextPath.substr(0, nextPath.length - 1);
}
this.jsonReplaceWithKeysAndGenerateDefaultStrings(builder, val, nextPath);
} else if (_.isString(val) && _.startsWith(val, LocKeyGenerator.I18N_PREFIX)) {
this.addResource(builder.getType(), key, path + key, json)
}
}
}
}
}

Просмотреть файл

@ -0,0 +1,190 @@
/// <reference path="../../../definitions/tsd.d.ts" />
import { PackageFiles, FileDeclaration, ResourcesFile } from "./interfaces";
import { cleanAssetPath, removeMetaKeys } from "./utils";
import _ = require("lodash");
import os = require("os");
import Q = require("q");
import stream = require("stream");
import trace = require('../trace');
export abstract class ManifestBuilder {
protected packageFiles: PackageFiles = { };
protected data: any = { };
constructor() { }
/**
* Explains the type of manifest builder
*/
public abstract getType(): string;
/**
* Gets the package path to this manifest
*/
public abstract getPath(): string;
/**
* Gets the path to the localized resource associated with this manifest
*/
public getLocPath(): string {
return this.getPath();
}
/**
* Given a key/value pair, decide how this effects the manifest
*/
public abstract processKey(key: string, value: any, override: boolean): void;
/**
* Return a string[] of current validation errors
*/
public abstract validate(): Q.Promise<string[]>;
/**
* Called just before the package is written to make any final adjustments.
*/
public finalize(files: PackageFiles): Q.Promise<void> {
return Q.resolve<void>(null);
}
/**
* Gives the manifest the chance to transform the key that is used when generating the localization
* strings file. Path will be a dot-separated set of keys to address the string (or another
* object/array) in question. See vso-manifest-builder for an example.
*/
public getLocKeyPath(path: string): string {
return path;
}
/**
* Write this manifest to a stream.
*/
public getResult(): string {
return JSON.stringify(removeMetaKeys(this.data), null, 4).replace(/\n/g, os.EOL);
}
/**
* Gets the contents of the file that will serve as localization for this asset.
* Default implementation returns JSON with all strings replaced given by the translations/defaults objects.
*/
public getLocResult(translations: ResourcesFile, defaults: ResourcesFile): string {
return JSON.stringify(this._getLocResult(this.expandResourceFile(translations), this.expandResourceFile(defaults)), null, 4);
}
private _getLocResult(translations: any, defaults: any, locData = {}, currentPath = "") {
let currentData = currentPath ? _.get(this.data, currentPath) : this.data;
// CurrentData should be guaranteed to be
// This magically works for arrays too, just go with it.
Object.keys(currentData).forEach((key) => {
let nextPath = currentPath + "." + key;
if (_.isString(currentData[key])) {
let translation = _.get(translations, nextPath);
if (translation !== undefined) {
_.set(locData, nextPath, translation);
} else {
let defaultString = _.get(defaults, nextPath);
if (defaultString !== undefined) {
_.set(locData, nextPath, defaultString);
} else {
throw "Couldn't find a default string - this is definitely a bug.";
}
}
} else if (_.isObject(currentData[key])) {
this._getLocResult(translations, defaults, locData, nextPath);
} else {
// must be a number of boolean
_.set(locData, nextPath, currentData[key]);
}
});
return locData;
}
/**
* Resource files are flat key-value pairs where the key is the json "path" to the original element.
* This routine expands the resource files back into their original schema
*/
private expandResourceFile(resources: ResourcesFile): any {
let expanded = {};
Object.keys(resources).forEach((path) => {
_.set(expanded, path, resources[path]);
});
return expanded;
}
/**
* Get the raw JSON data. Please do not modify it.
*/
public getData(): any {
return this.data;
}
/**
* Get a list of files to be included in the package
*/
public get files(): PackageFiles {
return this.packageFiles;
}
/**
* Set 'value' to data[path] in this manifest if it has not been set, or if override is true.
* If it has been set, issue a warning.
*/
protected singleValueProperty(path: string, value: any, manifestKey: string, override: boolean = false): boolean {
let existingValue = _.get(this.data, path);
if (!override && existingValue !== undefined) {
trace.warn("Multiple values found for '%s'. Ignoring future occurrences and using the value '%s'.", manifestKey, JSON.stringify(existingValue, null, 4));
return false;
} else {
_.set(this.data, path, value);
return true;
}
}
/**
* Read a value as a delimited string or array and concat it to the existing list at data[path]
*/
protected handleDelimitedList(value: any, path: string, delimiter: string = ",", uniq: boolean = true): void {
if (_.isString(value)) {
value = value.split(delimiter);
_.remove(value, v => v === "");
}
var items = _.get(this.data, path, "").split(delimiter);
_.remove(items, v => v === "");
let val = items.concat(value);
if (uniq) {
val = _.uniq(val);
}
_.set(this.data, path, val.join(delimiter));
}
/**
* Add a file to the vsix package
*/
protected addFile(file: FileDeclaration) {
file.path = cleanAssetPath(file.path);
if (!file.partName) {
file.partName = file.path;
}
if (!file.partName) {
throw "Every file must have a file name.";
}
// Files added recursively, i.e. from a directory, get lower
// priority than those specified explicitly. Therefore, if
// the file has already been added to the package list, don't
// re-add (overwrite) with this file if it is an auto (from a dir)
if (file.auto && this.packageFiles[file.path]) {
// Don't add files discovered via directory if they've already
// been added.
} else {
this.packageFiles[file.path] = file;
}
if (file.contentType && this.packageFiles[file.path]) {
this.packageFiles[file.path].contentType = file.contentType;
}
}
}

Просмотреть файл

@ -0,0 +1,187 @@
/// <reference path="../../../definitions/tsd.d.ts" />
import { ManifestBuilder } from "./manifest"
import { FileDeclaration, MergeSettings, PackageFiles, ResourceSet } from "./interfaces"
import _ = require("lodash");
import fs = require("fs");
import glob = require("glob");
import loc = require("./loc");
import path = require("path");
import Q = require("q");
import trace = require('../trace');
/**
* Combines the vsix and vso manifests into one object
*/
export interface VsixComponents {
builders: ManifestBuilder[];
resources: ResourceSet;
}
/**
* Facilitates the gathering/reading of partial manifests and creating the merged
* manifests (one for each manifest builder)
*/
export class Merger {
private manifestBuilders: ManifestBuilder[];
/**
* constructor. Instantiates one of each manifest builder.
*/
constructor(private settings: MergeSettings, builderTypes: { new(): ManifestBuilder }[] ) {
builderTypes.forEach((builderType) => {
this.manifestBuilders.push(new builderType());
});
}
private gatherManifests(): Q.Promise<string[]> {
trace.debug('merger.gatherManifests');
const globs = this.settings.manifestGlobs.map(p => path.isAbsolute(p) ? p : path.join(this.settings.root, p));
trace.debug('merger.gatherManifestsFromGlob');
const promises = globs.map(pattern => Q.nfcall<string[]>(glob, pattern));
return Q.all(promises)
.then(results => _.unique(_.flatten<string>(results)))
.then(results => {
if (results.length > 0) {
trace.debug("Merging %s manifests from the following paths: ", results.length.toString());
results.forEach(path => trace.debug(path));
} else {
throw new Error("No manifests found from the following glob patterns: \n" + this.settings.manifestGlobs.join("\n"));
}
return results;
});
}
/**
* Finds all manifests and merges them into two JS Objects: vsoManifest and vsixManifest
* @return Q.Promise<SplitManifest> An object containing the two manifests
*/
public merge(): Q.Promise<VsixComponents> {
trace.debug('merger.merge')
return this.gatherManifests().then(files => {
let overridesProvided = false;
let manifestPromises: Q.Promise<any>[] = [];
files.forEach((file) => {
manifestPromises.push(Q.nfcall<any>(fs.readFile, file, "utf8").then((data) => {
let jsonData = data.replace(/^\uFEFF/, '');
try {
let result = JSON.parse(jsonData);
result.__origin = file; // save the origin in order to resolve relative paths later.
return result;
} catch (err) {
trace.error("Error parsing the JSON in %s: ", file);
trace.debug(jsonData, null);
throw err;
}
}));
});
// Add the overrides if necessary
if (this.settings.overrides) {
overridesProvided = true;
manifestPromises.push(Q.resolve(this.settings.overrides));
}
return Q.all(manifestPromises).then(partials => {
partials.forEach((partial, partialIndex) => {
// Transform asset paths to be relative to the root of all manifests, verify assets
if (_.isArray(partial["files"])) {
(<Array<FileDeclaration>>partial["files"]).forEach((asset) => {
let keys = Object.keys(asset);
if (keys.indexOf("path") < 0) {
throw new Error("Files must have an absolute or relative (to the manifest) path.");
}
let absolutePath;
if (path.isAbsolute(asset.path)) {
absolutePath = asset.path;
} else {
absolutePath = path.join(path.dirname(partial.__origin), asset.path);
}
asset.path = path.relative(this.settings.root, absolutePath);
});
}
// Transform icon paths as above
if (_.isObject(partial["icons"])) {
let icons = partial["icons"];
Object.keys(icons).forEach((iconKind: string) => {
let absolutePath = path.join(path.dirname(partial.__origin), icons[iconKind]);
icons[iconKind] = path.relative(this.settings.root, absolutePath);
});
}
// Expand any directories listed in the files array
if (_.isArray(partial["files"])) {
for (let i = partial["files"].length - 1; i >= 0; --i) {
let fileDecl: FileDeclaration = partial["files"][i];
let fsPath = path.join(this.settings.root, fileDecl.path);
if (fs.lstatSync(fsPath).isDirectory()) {
Array.prototype.splice.apply(partial["files"], (<any[]>[i, 1]).concat(this.pathToFileDeclarations(fsPath, this.settings.root)));
}
}
}
// Process each key by each manifest builder.
Object.keys(partial).forEach((key) => {
if (partial[key] !== undefined && partial[key] !== null) {
// Notify each manifest builder of the key/value pair
this.manifestBuilders.forEach((builder) => {
builder.processKey(key, partial[key], partials.length - 1 === partialIndex && overridesProvided);
});
}
});
});
// Generate localization resources
let locPrepper = new loc.LocPrep.LocKeyGenerator(this.manifestBuilders);
let resources = locPrepper.generateLocalizationKeys();
// Build up a master file list
let packageFiles: PackageFiles = {};
this.manifestBuilders.forEach((builder) => {
_.assign(packageFiles, builder.files);
});
// Finalize each builder
this.manifestBuilders.forEach(b => b.finalize(packageFiles));
// Let each builder validate, then concat the results.
return Q.all(this.manifestBuilders.map(b => b.validate())).then((results) => {
let flattened = results.reduce((a, b) => a.concat(b), []);
if (flattened.length === 0 || this.settings.bypassValidation) {
return {
builders: this.manifestBuilders,
resources: resources
};
} else {
throw new Error("There were errors with your manifests. Address the following errors and re-run the tool.\n" + flattened);
}
});
});
});
}
/**
* Recursively converts a given path to a flat list of FileDeclaration
*/
private pathToFileDeclarations(fsPath: string, root: string): FileDeclaration[] {
let files: FileDeclaration[] = [];
if (fs.lstatSync(fsPath).isDirectory()) {
trace.debug("Path '%s` is a directory. Adding all contained files (recursive).", fsPath);
fs.readdirSync(fsPath).forEach((dirChildPath) => {
trace.debug("-- %s", dirChildPath);
files = files.concat(this.pathToFileDeclarations(path.join(fsPath, dirChildPath), root));
});
} else {
let relativePath = path.relative(root, fsPath);
files.push({path: relativePath, partName: relativePath, auto: true});
}
return files;
}
}

Просмотреть файл

@ -0,0 +1,137 @@
import { ManifestBuilder } from "../../manifest"
import { PackageFiles } from "../../interfaces"
import os = require("os");
import stream = require("stream");
export class VsoManifestBuilder extends ManifestBuilder {
/**
* Gets the package path to this manifest.
*/
public getPath(): string {
return "extension.vsomanifest";
}
public static manifestType = "Microsoft.VisualStudio.Services.Manifest";
/**
* Explains the type of manifest builder
*/
public getType(): string {
return VsoManifestBuilder.manifestType;
}
public validate(): Q.Promise<string[]> {
let errors = [];
if (this.data.contributions.length === 0 && this.data.contributionTypes.length === 0) {
errors.push("Your extension must define at least one contribution or contribution type.");
}
return Q.resolve(errors);
}
public finalize(files: PackageFiles): Q.Promise<void> {
// Ensure some default values are set
if (!this.data.contributions) {
this.data.contributions = [];
}
if (!this.data.scopes) {
this.data.scopes = [];
}
if (!this.data.contributionTypes) {
this.data.contributionTypes = [];
}
if (!this.data.manifestVersion) {
this.data.manifestVersion = 1;
}
return Q.resolve<void>(null);
}
/**
* Some elements of this file are arrays, which would typically produce a localization
* key like "contributions.3.name". We want to turn the 3 into the contribution id to
* make it more friendly to translators.
*/
public getLocKeyPath(path: string): string {
let pathParts = path.split(".").filter(p => !!p);
if (pathParts && pathParts.length >= 2) {
let cIndex = parseInt(pathParts[1]);
if (pathParts[0] === "contributions" && !isNaN(cIndex) && this.data.contributions[cIndex] && this.data.contributions[cIndex].id) {
return "contributions" + this.data.contributions[cIndex].id;
} else {
return path;
}
}
}
public processKey(key: string, value: any, override: boolean): void {
switch(key.toLowerCase()) {
case "eventcallbacks":
if (_.isObject(value)) {
this.singleValueProperty("eventCallbacks", value, key, override);
}
break;
case "manifestversion":
let version = value;
if (_.isString(version)) {
version = parseFloat(version);
}
this.singleValueProperty("manifestVersion", version, key, override);
break;
case "scopes":
if (_.isArray(value)) {
if (!this.data.scopes) {
this.data.scopes = [];
}
this.data.scopes = _.uniq(this.data.scopes.concat(value));
}
break;
case "baseuri":
this.singleValueProperty("baseUri", value, key, override);
break;
case "contributions":
if (_.isArray(value)) {
if (!this.data.contributions) {
this.data.contributions = [];
}
this.data.contributions = this.data.contributions.concat(value);
}
break;
case "contributiontypes":
if (_.isArray(value)) {
if (!this.data.contributionTypes) {
this.data.contributionTypes = [];
}
this.data.contributionTypes = this.data.contributionTypes.concat(value);
}
break;
// Ignore all the vsixmanifest keys so we can take a default case below.
case "namespace":
case "extensionid":
case "id":
case "version":
case "name":
case "description":
case "icons":
case "screenshots":
case "details":
case "targets":
case "links":
case "branding":
case "public":
case "publisher":
case "releasenotes":
case "tags":
case "flags":
case "vsoflags":
case "galleryflags":
case "categories":
case "files":
break;
default:
if (key.substr(0, 2) !== "__") {
this.singleValueProperty(key, value, key, override);
}
break;
}
}
}

Просмотреть файл

@ -0,0 +1,57 @@
import _ = require("lodash");
import os = require("os");
import xml = require("xml2js");
export function removeMetaKeys(obj: any): any {
return _.omit(obj, (v, k) => _.startsWith(k, "__meta_"));
}
export function cleanAssetPath(assetPath: string) {
if (!assetPath) {
return null;
}
let cleanPath = assetPath.replace(/\\/g, "/");
if (!_.startsWith(cleanPath, "/")) {
cleanPath = "/" + cleanPath;
}
return cleanPath;
}
/**
* OPC Convention implementation. See
* http://www.ecma-international.org/news/TC45_current_work/tc45-2006-335.pdf §10.1.3.2 & §10.2.3
*/
export function toZipItemName(partName: string): string {
let cleanPartName = cleanAssetPath(partName);
if (_.startsWith(cleanPartName, "/")) {
return cleanPartName.substr(1);
} else {
return cleanPartName;
}
}
export function jsonToXml(json: any): string {
let builder = new xml.Builder(DEFAULT_XML_BUILDER_SETTINGS);
return builder.buildObject(json);
}
export function maxKey<T>(obj: {[key: string]: T}, func: (input: T) => number): string {
let maxProp;
for (let prop in obj) {
if (!maxProp || func(obj[prop]) > func(obj[maxProp])) {
maxProp = prop;
}
}
return maxProp;
}
export var DEFAULT_XML_BUILDER_SETTINGS: xml.BuilderOptions = {
indent: " ",
newline: os.EOL,
pretty: true,
xmldec: {
encoding: "utf-8",
standalone: null,
version: "1.0"
}
};

Просмотреть файл

@ -0,0 +1,565 @@
import { ManifestBuilder } from "./manifest";
import { FileDeclaration, PackageFiles, ResourcesFile, ScreenshotDeclaration, TargetDeclaration, Vsix, VsixLanguagePack } from "./interfaces";
import { cleanAssetPath, jsonToXml, maxKey, removeMetaKeys, toZipItemName } from "./utils";
import _ = require("lodash");
import childProcess = require("child_process");
import onecolor = require("onecolor");
import os = require("os");
import path = require("path");
import stream = require("stream");
import trace = require("../trace");
import winreg = require("winreg");
import xml = require("xml2js");
export class VsixManifestBuilder extends ManifestBuilder {
/**
* List of known file types to use in the [Content_Types].xml file in the VSIX package.
*/
private static CONTENT_TYPE_MAP: {[key: string]: string} = {
".md": "text/markdown",
".pdf": "application/pdf",
".png": "image/png",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".gif": "image/gif",
".bat": "application/bat",
".json": "application/json",
".vsixlangpack": "text/xml",
".vsixmanifest": "text/xml",
".vsomanifest": "application/json",
".ps1": "text/ps1"
};
private static vsixValidators: {[path: string]: (value) => string} = {
"PackageManifest.Metadata[0].Identity[0].$.Id": (value) => {
if (/^[A-z0-9_-]+$/.test(value)) {
return null;
} else {
return "'extensionId' may only include letters, numbers, underscores, and dashes.";
}
},
"PackageManifest.Metadata[0].Identity[0].$.Version": (value) => {
if (typeof value === "string" && value.length > 0) {
return null;
} else {
return "'version' must be provided.";
}
},
"PackageManifest.Metadata[0].DisplayName[0]": (value) => {
if (typeof value === "string" && value.length > 0) {
return null;
} else {
return "'name' must be provided.";
}
},
"PackageManifest.Assets[0].Asset": (value) => {
let usedAssetTypes = {};
if (_.isArray(value)) {
for (let i = 0; i < value.length; ++i) {
let asset = value[i].$;
if (asset) {
if (!asset.Path) {
return "All 'files' must include a 'path'.";
}
if (asset.Type && asset.Addressable) {
if (usedAssetTypes[asset.Type]) {
return "Cannot have multiple 'addressable' files with the same 'assetType'.\nFile1: " + usedAssetTypes[asset.Type] + ", File 2: " + asset.Path + " (asset type: " + asset.Type + ")";
} else {
usedAssetTypes[asset.Type] = asset.Path;
}
}
}
}
}
return null;
},
"PackageManifest.Metadata[0].Identity[0].$.Publisher": (value) => {
if (typeof value === "string" && value.length > 0) {
return null;
} else {
return "'publisher' must be provided.";
}
},
"PackageManifest.Metadata[0].Categories[0]": (value) => {
if (!value) {
return null;
}
let categories = value.split(",");
if (categories.length > 1) {
return "For now, extensions are limited to a single category.";
}
let validCategories = [
"Build and release",
"Collaboration",
"Customer support",
"Planning",
"Productivity",
"Sync and integration",
"Testing"
];
_.remove(categories, c => !c);
let badCategories = categories.filter(c => validCategories.indexOf(c) < 0);
return badCategories.length ? "The following categories are not valid: " + badCategories.join(", ") + ". Valid categories are: " + validCategories.join(", ") + "." : null;
},
"PackageManifest.Installation[0].InstallationTarget": (value) => {
if (_.isArray(value) && value.length > 0) {
return null;
}
return "Your manifest must include at least one 'target'.";
}
};
public static manifestType = "vsix";
/**
* Explains the type of manifest builder
*/
public getType(): string {
return VsixManifestBuilder.manifestType;
}
/**
* Gets the package path to this manifest
*/
public getPath(): string {
return "extension.vsixmanifest";
}
/**
* VSIX Manifest loc assets are vsixlangpack files.
*/
public getLocPath(): string {
return "Extension.vsixlangpack";
}
/**
* Gets the contents of the vsixLangPack file for this manifest
*/
public getLocResult(translations: ResourcesFile, defaults: ResourcesFile): string {
let langPack = this.generateVsixLangPack(translations, defaults);
return jsonToXml(langPack);
}
private generateVsixLangPack(translations: ResourcesFile, defaults: ResourcesFile): VsixLanguagePack {
return <VsixLanguagePack>{
VsixLanguagePack: {
$: {
Version: "1.0.0",
xmlns: "http://schemas.microsoft.com/developer/vsx-schema-lp/2010"
},
LocalizedName: [translations["displayName"] || defaults["displayName"]],
LocalizedDescription: [translations["description"] || defaults["description"]],
LocalizedReleaseNotes: [translations["releaseNotes"] || defaults["releaseNotes"]],
License: [null],
MoreInfoUrl: [null]
}
};
}
/**
* Add an asset: add a file to the vsix package and if there is an assetType on the
* file, add an <Asset> entry in the vsixmanifest.
*/
private addAsset(file: FileDeclaration) {
this.addFile(file);
}
/**
* Add an <Asset> entry to the vsixmanifest.
*/
private addAssetToManifest(assetPath: string, type: string, addressable: boolean = false, lang: string = null): void {
let cleanAssetPath = toZipItemName(assetPath);
let asset = {
"Type": type,
"d:Source": "File",
"Path": cleanAssetPath
};
if (addressable) {
asset["Addressable"] = "true";
}
if (lang) {
asset["Lang"] = lang;
}
this.data.PackageManifest.Assets[0].Asset.push({
"$": asset
});
if (type === "Microsoft.VisualStudio.Services.Icons.Default") {
this.data.PackageManifest.Metadata[0].Icon = [cleanAssetPath];
}
}
/**
* Add a property to the vsixmanifest.
*/
private addProperty(id: string, value: string) {
let defaultProperties = [];
let existingProperties = _.get<any[]>(this.data, "PackageManifest.Metadata[0].Properties[0].Property", defaultProperties);
if (defaultProperties === existingProperties) {
_.set(this.data, "PackageManifest.Metadata[0].Properties[0].Property", defaultProperties);
}
existingProperties.push({
$: {
Id: id,
Value: value
}
});
}
/**
* Given a key/value pair, decide how this effects the manifest
*/
public processKey(key: string, value: any, override: boolean): void {
switch(key.toLowerCase()) {
case "namespace":
case "extensionid":
case "id":
if (_.isString(value)) {
this.singleValueProperty("PackageManifest.Metadata[0].Identity[0].$.Id", value, "namespace/extensionId/id", override);
}
break;
case "version":
this.singleValueProperty("PackageManifest.Metadata[0].Identity[0].$.Version", value, key, override);
break;
case "name":
this.singleValueProperty("PackageManifest.Metadata[0].DisplayName[0]", value, key, override);
break;
case "description":
this.singleValueProperty("PackageManifest.Metadata[0].Description[0]._", value, key, override);
break;
case "icons":
Object.keys(value).forEach((key) => {
let iconType = _.startCase(key.toLowerCase());
let fileDecl: FileDeclaration = {
path: value[key],
addressable: true,
assetType: "Microsoft.VisualStudio.Services.Icons." + iconType,
partName: value[key]
};
this.addAsset(fileDecl);
});
break;
case "screenshots":
if (_.isArray(value)) {
let screenshotIndex = 0;
value.forEach((screenshot: ScreenshotDeclaration) => {
let fileDecl: FileDeclaration = {
path: screenshot.path,
addressable: true,
assetType: "Microsoft.VisualStudio.Services.Screenshots." + (++screenshotIndex),
contentType: screenshot.contentType
};
this.addAsset(fileDecl);
});
}
break;
case "details":
if (_.isObject(value) && value.path) {
let fileDecl: FileDeclaration = {
path: value.path,
addressable: true,
assetType: "Microsoft.VisualStudio.Services.Content.Details",
contentType: value.contentType
};
this.addAsset(fileDecl);
}
break;
case "targets":
if (_.isArray(value)) {
let existingTargets = _.get<any[]>(this.data, "PackageManifest.Installation[0].InstallationTarget", []);
value.forEach((target: TargetDeclaration) => {
if (!target.id) {
return;
}
let newTargetAttrs = {
Id: target.id
};
if (target.version) {
newTargetAttrs["Version"] = target.version;
}
existingTargets.push({
$: newTargetAttrs
});
});
}
break;
case "links":
if (_.isObject(value)) {
Object.keys(value).forEach((linkType) => {
let url = _.get<string>(value, linkType + ".uri") || _.get<string>(value, linkType + ".url");
if (url) {
let linkTypeCased = _.capitalize(_.camelCase(linkType));
this.addProperty("Microsoft.VisualStudio.Services.Links." + linkTypeCased, url);
} else {
trace.warn("'uri' property not found for link: '%s'... ignoring.", linkType);
}
});
}
break;
case "branding":
if (_.isObject(value)) {
Object.keys(value).forEach((brandingType) => {
let brandingTypeCased = _.capitalize(_.camelCase(brandingType));
let brandingValue = value[brandingType];
if (brandingTypeCased === "Color") {
try {
brandingValue = onecolor(brandingValue).hex();
} catch (e) {
throw "Could not parse branding color as a valid color. Please use a hex or rgb format, e.g. #00ff00 or rgb(0, 255, 0)";
}
}
this.addProperty("Microsoft.VisualStudio.Services.Branding." + brandingTypeCased, brandingValue);
});
}
break;
case "public":
if (typeof value === "boolean") {
let flags = _.get(this.data, "PackageManifest.Metadata[0].GalleryFlags[0]", "").split(",");
_.remove(flags, v => v === "");
if (value === true) {
flags.push("Public");
}
_.set(this.data, "PackageManifest.Metadata[0].GalleryFlags[0]", _.uniq(flags).join(","));
}
break;
case "publisher":
this.singleValueProperty("PackageManifest.Metadata[0].Identity[0].$.Publisher", value, key, override);
break;
case "releasenotes":
this.singleValueProperty("PackageManifest.Metadata[0].ReleaseNotes[0]", value, key, override);
break;
case "tags":
this.handleDelimitedList(value, "PackageManifest.Metadata[0].Tags[0]");
break;
case "flags":
case "vsoflags":
case "galleryflags":
// Gallery Flags are space-separated since it's a Flags enum.
this.handleDelimitedList(value, "PackageManifest.Metadata[0].GalleryFlags[0]", " ", true);
break;
case "categories":
this.handleDelimitedList(value, "PackageManifest.Metadata[0].Categories[0]");
break;
case "files":
if (_.isArray(value)) {
value.forEach((asset: FileDeclaration) => {
this.addAsset(asset);
});
}
break;
}
}
/**
* Return a string[] of current validation errors
*/
public validate(): Q.Promise<string[]> {
return Q.resolve(Object.keys(VsixManifestBuilder.vsixValidators).map(path => VsixManifestBuilder.vsixValidators[path](_.get(this.data, path))).filter(r => !!r));
}
/**
* --Ensures an <Asset> entry is added for each file as appropriate
* --Builds the [Content_Types].xml file
*/
public finalize(files: PackageFiles): Q.Promise<void> {
Object.keys(files).forEach((fileName) => {
let file = files[fileName];
// Add all assets to manifest except the vsixmanifest (duh)
if (file.assetType && file.path !== this.getPath()) {
this.addAssetToManifest(file.path, file.assetType, file.addressable, file.lang);
}
});
// The vsixmanifest will be responsible for generating the [Content_Types].xml file
// Obviously this is kind of strange, but hey ho.
return this.genContentTypesXml().then((result) => {
this.addFile({
path: null,
content: result,
partName: "[Content_Types].xml"
});
});
}
/**
* Gets the string representation (XML) of this manifest
*/
public getResult(): string {
return jsonToXml(removeMetaKeys(this.data)).replace(/\n/g, os.EOL);
}
/**
* Generates the required [Content_Types].xml file for the vsix package.
* This xml contains a <Default> entry for each different file extension
* found in the package, mapping it to the appropriate MIME type.
*/
private genContentTypesXml(): Q.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;
if (windows) {
// On windows, check HKCR to get the content type of the file based on the extension
let contentTypePromises: Q.Promise<any>[] = [];
let extensionlessFiles = [];
let uniqueExtensions = _.unique<string>(Object.keys(this.files).map((f) => {
let extName = path.extname(f);
if (!extName && !this.files[f].contentType) {
trace.warn("File %s does not have an extension, and its content-type is not declared. Defaulting to application/octet-stream.", path.resolve(f));
}
if (this.files[f].contentType) {
// If there is an override for this file, ignore its extension
return "";
}
return extName;
}));
uniqueExtensions.forEach((ext) => {
if (!ext.trim()) {
return;
}
if (!ext) {
return;
}
if (typeMap[ext.toLowerCase()]) {
contentTypes.Types.Default.push({
$: {
Extension: ext,
ContentType: typeMap[ext.toLowerCase()]
}
});
return;
}
let hkcrKey = new winreg({
hive: winreg.HKCR,
key: "\\" + ext.toLowerCase()
});
let regPromise = Q.ninvoke(hkcrKey, "get", "Content Type").then((type: WinregValue) => {
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) => {
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 = Q.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: Q.Promise<any>[] = [];
let extTypeCounter: {[ext: string]: {[type: string]: string[]}} = {};
Object.keys(this.files).forEach((fileName) => {
let extension = path.extname(fileName);
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 = Q.resolve(null);
return;
}
mimePromise = Q.Promise((resolve, reject, notify) => {
let child = childProcess.exec("file --mime-type \"" + fileName + "\"", (err, stdout, stderr) => {
try {
if (err) {
reject(err);
}
let stdoutStr = stdout.toString("utf8");
let magicMime = _.trimRight(stdoutStr.substr(stdoutStr.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) {
reject(stderr.toString("utf8"));
} else {
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 = Q.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(() => {
Object.keys(this.files).forEach((partName) => {
contentTypes.Types.Override.push({
$: {
ContentType: this.files[partName].contentType,
PartName: "/" + _.trimLeft(partName, "/")
}
});
});
return jsonToXml(contentTypes).replace(/\n/g, os.EOL);
});
}
}

Просмотреть файл

@ -0,0 +1,213 @@
import { ManifestBuilder } from "./manifest";
import { VsixManifestBuilder } from "./vsix-manifest-builder";
import { FileDeclaration, PackageSettings, PackageFiles, PackagePart, ResourceSet, ResourcesFile } from "./interfaces";
import { VsixComponents } from "./merger";
import { cleanAssetPath, removeMetaKeys, toZipItemName } from "./utils";
import { LocPrep } from "./loc";
import childProcess = require("child_process");
import fs = require("fs");
import mkdirp = require("mkdirp");
import os = require("os");
import path = require("path");
import Q = require("q");
import trace = require('../trace');
import winreg = require("winreg");
import xml = require("xml2js");
import zip = require("jszip");
/**
* Facilitates packaging the vsix and writing it to a file
*/
export class VsixWriter {
private manifestBuilders: ManifestBuilder[];
private resources: ResourceSet;
private static VSO_MANIFEST_FILENAME: string = "extension.vsomanifest";
private static VSIX_MANIFEST_FILENAME: string = "extension.vsixmanifest";
private static CONTENT_TYPES_FILENAME: string = "[Content_Types].xml";
public static DEFAULT_XML_BUILDER_SETTINGS: xml.BuilderOptions = {
indent: " ",
newline: os.EOL,
pretty: true,
xmldec: {
encoding: "utf-8",
standalone: null,
version: "1.0"
}
};
/**
* List of known file types to use in the [Content_Types].xml file in the VSIX package.
*/
private static CONTENT_TYPE_MAP: {[key: string]: string} = {
".md": "text/markdown",
".pdf": "application/pdf",
".png": "image/png",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".gif": "image/gif",
".bat": "application/bat",
".json": "application/json",
".vsixlangpack": "text/xml",
".vsixmanifest": "text/xml",
".vsomanifest": "application/json",
".ps1": "text/ps1"
};
/**
* constructor
* @param any vsoManifest JS Object representing a vso manifest
* @param any vsixManifest JS Object representing the XML for a vsix manifest
*/
constructor(private settings: PackageSettings, components: VsixComponents) {
this.manifestBuilders = components.builders;
this.resources = components.resources;
}
/**
* If outPath is {auto}, generate an automatic file name.
* Otherwise, try to determine if outPath is a directory (checking for a . in the filename)
* If it is, generate an automatic filename in the given outpath
* Otherwise, outPath doesn't change.
*/
private getOutputPath(outPath: string): string {
// Find the vsix manifest, if it exists
let vsixBuilders = this.manifestBuilders.filter(b => b.getType() === VsixManifestBuilder.manifestType);
let autoName = "extension.vsix";
if (vsixBuilders.length === 1) {
let vsixManifest = vsixBuilders[0].getData();
let pub = _.get(vsixManifest, "PackageManifest.Metadata[0].Identity[0].$.Publisher");
let ns = _.get(vsixManifest, "PackageManifest.Metadata[0].Identity[0].$.Id");
let version = _.get(vsixManifest, "PackageManifest.Metadata[0].Identity[0].$.Version");
autoName = pub + "." + ns + "-" + version + ".vsix";
}
if (outPath === "{auto}") {
return path.resolve(autoName);
} else {
let basename = path.basename(outPath);
if (basename.indexOf(".") > 0) { // conscious use of >
return path.resolve(outPath);
} else {
return path.resolve(path.join(outPath, autoName));
}
}
}
/**
* Write a vsix package to the given file name
*/
public writeVsix(): Q.Promise<string> {
let outputPath = this.getOutputPath(this.settings.outputPath);
let vsix = new zip();
let builderPromises: Q.Promise<void>[] = [];
this.manifestBuilders.forEach((builder) => {
// Add the package files
let readFilePromises: Q.Promise<void>[] = [];
Object.keys(builder.files).forEach((path) => {
let readFilePromise = Q.nfcall(fs.readFile, path, "utf8").then((result) => {
vsix.file(toZipItemName(path), result);
});
readFilePromises.push(readFilePromise);
});
let builderPromise = Q.all(readFilePromises).then(() => {
// Add the manifest itself
vsix.file(toZipItemName(builder.getPath()), builder.getResult());
});
builderPromises.push(builderPromise);
});
return Q.all(builderPromises).then(() => {
return this.addResourceStrings(vsix);
}).then(() => {
trace.debug("Writing vsix to: %s", outputPath);
return Q.nfcall(mkdirp, path.dirname(outputPath)).then(() => {
let buffer = vsix.generate({
type: "nodebuffer",
compression: "DEFLATE"
});
return Q.nfcall(fs.writeFile, outputPath, buffer).then(() => outputPath);
});
});
}
/**
* For each folder F under the localization folder (--loc-root),
* look for a resources.resjson file within F. If it exists, split the
* resources.resjson into one file per manifest. Add
* each to the vsix archive as F/<manifest_loc_path> and F/Extension.vsixlangpack
*/
private addResourceStrings(vsix: zip): Q.Promise<void[]> {
// Make sure locRoot is set, that it refers to a directory, and
// iterate each subdirectory of that.
if (!this.settings.locRoot) {
return Q.resolve<void[]>(null);
}
let stringsPath = path.resolve(this.settings.locRoot);
return Q.Promise((resolve, reject, notify) => {
fs.exists(stringsPath, (exists) => {
resolve(exists);
});
}).then<boolean>((exists) => {
if (exists) {
return Q.nfcall(fs.lstat, stringsPath).then((stats: fs.Stats) => {
if (stats.isDirectory()) {
return true;
}
});
} else {
return Q.resolve(false);
}
}).then<void[]>((stringsFolderExists) => {
if (!stringsFolderExists) {
return Q.resolve<void[]>(null);
}
return Q.nfcall(fs.readdir, stringsPath).then((files: string[]) => {
let promises: Q.Promise<void>[] = [];
files.forEach((languageTag) => {
var filePath = path.join(stringsPath, languageTag);
let promise = Q.nfcall(fs.lstat, filePath).then((fileStats: fs.Stats) => {
if (fileStats.isDirectory()) {
let resourcePath = path.join(filePath, "resources.resjson");
return Q.Promise<boolean>((resolve, reject, notify) => {
fs.exists(resourcePath, (exists) => {
resolve(exists);
});
}).then<void>((exists: boolean) => {
if (exists) {
// A resources.resjson file exists in <locRoot>/<language_tag>/
// return Q.nfcall<string>(fs.readFile, resourcePath, "utf8").then<void>((contents: string) => {
// let resourcesObj = JSON.parse(contents);
// let locGen = new LocPrep.LocKeyGenerator(null, null);
// let splitRes = locGen.splitIntoVsoAndVsixResourceObjs(resourcesObj);
// let locManifestPath = languageTag + "/" + VsixWriter.VSO_MANIFEST_FILENAME;
// vsix.file(toZipItemName(locManifestPath), this.getVsoManifestString(splitRes.vsoResources));
// this.vsixManifest.PackageManifest.Assets[0].Asset.push({
// "$": {
// Lang: languageTag,
// Type: "Microsoft.VisualStudio.Services.Manifest",
// Path: locManifestPath,
// Addressable: "true",
// "d:Source": "File"
// }
// });
// let builder = new xml.Builder(VsixWriter.DEFAULT_XML_BUILDER_SETTINGS);
// let vsixLangPackStr = builder.buildObject(splitRes.vsixResources);
// vsix.file(toZipItemName(languageTag + "/Extension.vsixlangpack"), vsixLangPackStr);
// });
} else {
return Q.resolve<void>(null);
}
});
}
});
promises.push(promise);
});
return Q.all(promises);
});
});
}
}

0
app/tfx-cli.js → app/tfx-cli.ts Executable file → Normal file
Просмотреть файл

8
definitions/onecolor/onecolor.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,8 @@
// Type definitions for onecolor (incomplete)
declare module "onecolor" {
var onecolor: any;
export = onecolor;
}

1
definitions/tsd.d.ts поставляемый
Просмотреть файл

@ -14,3 +14,4 @@
/// <reference path="xml2js/xml2js.d.ts" />
/// <reference path="winreg/winreg.d.ts" />
/// <reference path="mkdirp/mkdirp.d.ts" />
/// <reference path="onecolor/onecolor.d.ts" />

3
definitions/winreg/winreg.d.ts поставляемый
Просмотреть файл

@ -54,5 +54,4 @@ interface Winreg {
interface WinregCallback<T> {
(err: NodeJS.ErrnoException, val: T): void;
}
declare var winreg: Winreg;
declare var winreg: Winreg;

Просмотреть файл

@ -27,6 +27,7 @@
"minimist": "^1.1.2",
"mkdirp": "^0.5.1",
"node-uuid": "^1.4.3",
"onecolor": "^2.5.0",
"os-homedir": "^1.0.1",
"q": "^1.4.1",
"read": "^1.0.6",
@ -44,7 +45,7 @@
"gulp": "^3.9.0",
"gulp-filter": "^3.0.1",
"gulp-mocha": "2.0.0",
"gulp-tsb": "^1.5.1",
"gulp-tsb": "^1.6.2",
"minimatch": "^2.0.8",
"mocha": "^2.2.5"
},

Просмотреть файл

@ -2,6 +2,7 @@
"compilerOptions": {
"module": "commonjs",
"target": "ES5",
"declaration": false
"declaration": false,
"sourceMap": true
}
}