зеркало из https://github.com/microsoft/tfs-cli.git
Working on extensions.
This commit is contained in:
Родитель
96e503811a
Коммит
6dd5c34644
|
@ -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
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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']);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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,0 +1,8 @@
|
|||
// Type definitions for onecolor (incomplete)
|
||||
|
||||
declare module "onecolor" {
|
||||
|
||||
var onecolor: any;
|
||||
|
||||
export = onecolor;
|
||||
}
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче