Finish up localization support.

This commit is contained in:
Trevor Gau 2016-09-20 08:03:22 -04:00
Родитель 2c029d29dc
Коммит 2fd91dc70f
25 изменённых файлов: 443 добавлений и 260 удалений

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

@ -31,3 +31,6 @@ node_modules
# VS Code Settings
.settings
# Generated by nexe
tmp/

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

@ -4,4 +4,5 @@ docs/
tests/
_build/tests
gulpfile.js
tsconfig.json
tsconfig.json
tmp/

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

@ -1,41 +1,87 @@
{
"version": "0.1.0",
// List of configurations. Add new configurations or edit existing ones.
"configurations": [
{
// Name of configuration; appears in the launch configuration drop down menu.
"name": "Launch ./_build/app/tfx-cli.js",
// Type of configuration.
"type": "node",
// Workspace relative or absolute path to the program.
"program": "${workspaceRoot}/_build/app/tfx-cli.js",
// Automatically stop program after launch.
"stopOnEntry": true,
// Command line arguments passed to the program.
"args": ["workitem", "update", "--work-item-id", "11", "--values", "{\"title\":\"hello\"}"],
// Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace.
"cwd": "${workspaceRoot}",
// 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": "${workspaceRoot}/_build/app"
},
{
"name": "Attach",
"type": "node",
// TCP/IP address. Default is "localhost".
"address": "localhost",
// Port to attach to.
"port": 5858,
"sourceMaps": false
}
]
}
"version": "0.2.0",
"configurations": [
{
"name": "Extension Create",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/app/app.ts",
"stopOnEntry": false,
"args": [
"extension",
"create",
"--loc-root",
"C:\\vso\\obj\\Debug.AnyCPU\\Vssf.WebPlatform\\MS.VS.Extension.WebPlatform\\resjson",
"--manifests",
"vss-web.json",
"--override",
"{\"version\": \"0.0.1\"}"
],
"cwd": "C:\\vso\\src\\Vssf\\WebPlatform\\ExtensionPackages\\WebPlatform",
"preLaunchTask": null,
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "development",
"TFX_TRACE": "1"
},
"console": "internalConsole",
"sourceMaps": true,
"outDir": "${workspaceRoot}/_build"
},
{
"name": "Resources Create",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/app/app.ts",
"stopOnEntry": false,
"args": [
"extension",
"resources",
"create",
"--manifests",
"vss-web.json",
"--override",
"{\"version\": \"0.0.1\"}"
],
"cwd": "C:\\vso\\src\\Vssf\\WebPlatform\\ExtensionPackages\\WebPlatform",
"preLaunchTask": null,
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "development",
"TFX_TRACE": "1"
},
"console": "internalConsole",
"sourceMaps": true,
"outDir": "${workspaceRoot}/_build"
},
{
"name": "HelpScreen",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/app/app.ts",
"stopOnEntry": false,
"args": [
],
"cwd": "${workspaceRoot}",
"preLaunchTask": null,
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "development",
"TFX_TRACE": "1"
},
"console": "internalConsole",
"sourceMaps": true,
"outDir": "${workspaceRoot}/_build"
}
]
}

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

@ -0,0 +1,11 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/.DS_Store": true,
"tmp/": true
},
"editor.insertSpaces": false
}

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

@ -0,0 +1,15 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "0.1.0",
"command": "npm",
"isShellCommand": true,
"showOutput": "always",
"suppressTaskName": true,
"tasks": [
{
"taskName": "build",
"args": ["run", "build"]
}
]
}

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

@ -4,7 +4,6 @@ import errHandler = require("./lib/errorhandler");
import loader = require("./lib/loader");
import path = require("path");
// Set app root
common.APP_ROOT = __dirname;

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

@ -63,10 +63,10 @@ export abstract class ExtensionComposer {
return "All 'files' must include a 'path'.";
}
if (asset.Type && asset.Addressable) {
if (usedAssetTypes[asset.Type]) {
if (usedAssetTypes[asset.Type + "|" + asset.lang]) {
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;
usedAssetTypes[asset.Type + "|" + asset.Lang] = asset.Path;
}
}
}

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

@ -15,8 +15,8 @@ export interface PackageFiles {
}
/**
* Describes a file in a manifest
*/
* Describes a file in a manifest
*/
export interface FileDeclaration {
/**
* The type of this asset (Type attribute in the vsixmanifest's <Asset> entry)
@ -154,6 +154,11 @@ export interface MergeSettings {
* True to rev the version of the extension before packaging.
*/
revVersion: boolean;
/**
* Path to the root of localized resource files
*/
locRoot: string;
}
export interface PackageSettings {
@ -166,7 +171,6 @@ export interface PackageSettings {
* Path to the root of localized resource files
*/
locRoot: string;
}
export interface PublishSettings {
@ -222,6 +226,11 @@ export interface ResourceSet {
combined: ResourcesFile;
}
export interface LocalizedResources {
[languageTag: string]: ResourcesFile;
defaults: ResourcesFile;
}
/*** Types for VSIX Manifest ****/
export namespace Vsix {

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

@ -109,11 +109,9 @@ export module LocPrep {
public generateLocalizationKeys(): ResourceSet {
this.initStringObjs();
this.manifestBuilders.forEach((builder) => {
if (builder.getType() !== VsixManifestBuilder.manifestType) {
this.jsonReplaceWithKeysAndGenerateDefaultStrings(builder);
}
this.jsonReplaceWithKeysAndGenerateDefaultStrings(builder);
});
this.vsixGenerateDefaultStrings();
return {
manifestResources: this.resourceFileMap,
combined: this.generateCombinedResourceFile()
@ -148,27 +146,6 @@ export module LocPrep {
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();
@ -176,13 +153,10 @@ export module LocPrep {
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);
}
let nextPath = path + key + ".";
this.jsonReplaceWithKeysAndGenerateDefaultStrings(builder, val, nextPath);
} else if (_.isString(val) && _.startsWith(val, LocKeyGenerator.I18N_PREFIX)) {
this.addResource(builder.getType(), key, path + key, json)
this.addResource(builder.getType(), key, builder.getLocKeyPath(path + key), json)
}
}
}

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

@ -1,6 +1,6 @@
import { PackageFiles, FileDeclaration, ResourcesFile } from "./interfaces";
import { PackageFiles, FileDeclaration, LocalizedResources, ResourcesFile } from "./interfaces";
import { cleanAssetPath, forwardSlashesPath, removeMetaKeys, toZipItemName } from "./utils";
import _ = require("lodash");
import common = require("../../../lib/common");
@ -12,8 +12,9 @@ import trace = require('../../../lib/trace');
export abstract class ManifestBuilder {
protected packageFiles: PackageFiles = { };
protected lcPartNames: {[filename: string]: string} = { };
protected lcPartNames: {[filename: string]: string} = { };
protected data: any = { };
private static resourcePrefix = "resource:";
constructor(private extRoot: string) { }
@ -47,7 +48,7 @@ export abstract class ManifestBuilder {
/**
* Called just before the package is written to make any final adjustments.
*/
public finalize(files: PackageFiles, builders: ManifestBuilder[]): Promise<void> {
public finalize(files: PackageFiles, resourceData: LocalizedResources, builders: ManifestBuilder[]): Promise<void> {
return Promise.resolve<void>(null);
}
@ -71,35 +72,42 @@ export abstract class ManifestBuilder {
* 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);
public getLocResult(translations: ResourcesFile, defaults: ResourcesFile): FileDeclaration[] {
return [{
partName: this.getPath(),
path: null,
content: 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;
private _getLocResult(translations: any, defaults: any, locData = {}, currentPath: string[] = []) {
// CurrentData should be guaranteed to be
// deep iterate through this.data. If the value is a string that starts with
// resource:, use the key to look in translations and defaults to find the real string.
// Do the replacement
// 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);
let currentData = currentPath.length > 0 ? _.get(this.data, currentPath) : this.data;
Object.keys(currentData).forEach(key => {
const val = currentData[key];
if (typeof val === "string" && val.substr(0, ManifestBuilder.resourcePrefix.length) === ManifestBuilder.resourcePrefix) {
const locKey = val.substr(ManifestBuilder.resourcePrefix.length);
const localized = _.get(translations, locKey) || _.get(defaults, locKey);
if (localized) {
_.set(locData, currentPath.concat(key), localized);
} 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.";
}
throw new Error("Could not find translation or default value for resource " + locKey);
}
} else if (_.isObject(currentData[key])) {
this._getLocResult(translations, defaults, locData, nextPath);
} else {
// must be a number of boolean
_.set(locData, nextPath, currentData[key]);
if (typeof val === "object") {
if (_.isArray(val)) {
_.set(locData, currentPath.concat(key), []);
} else {
_.set(locData, currentPath.concat(key), {});
}
this._getLocResult(translations, defaults, locData, currentPath.concat(key));
} else {
_.set(locData, currentPath.concat(key), val);
}
}
});
return locData;
@ -177,7 +185,7 @@ export abstract class ManifestBuilder {
file.partName = "/" + path.relative(this.extRoot, file.path);
}
if (!file.partName) {
throw "Every file must have a path specified name.";
throw new Error("Every file must have a path specified name.");
}
file.partName = forwardSlashesPath(file.partName);
@ -208,11 +216,13 @@ export abstract class ManifestBuilder {
this.packageFiles[file.path || common.newGuid()] = file;
this.lcPartNames[file.partName.toLowerCase()] = file.partName;
} else {
throw "All files in the package must have a case-insensitive unique filename. Trying to add " + file.partName + ", but " + existPartName + " was already added to the package.";
throw new Error("All files in the package must have a case-insensitive unique filename. Trying to add " + file.partName + ", but " + existPartName + " was already added to the package.");
}
}
if (file.contentType && this.packageFiles[file.path]) {
this.packageFiles[file.path].contentType = file.contentType;
}
return file;
}
}

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

@ -1,9 +1,7 @@
import { ManifestBuilder } from "./manifest";
import { ComposerFactory } from "./extension-composer-factory";
import { ExtensionComposer } from "./extension-composer";
import { FileDeclaration, MergeSettings, PackageFiles, ResourceSet, TargetDeclaration } from "./interfaces";
import { FileDeclaration, LocalizedResources, MergeSettings, PackageFiles, ResourceSet, ResourcesFile, TargetDeclaration } from "./interfaces";
import _ = require("lodash");
import fs = require("fs");
import glob = require("glob");
@ -192,29 +190,108 @@ export class Merger {
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);
});
// Build up resource data by reading the translations from disk
return this.buildResourcesData().then(resourceData => {
if (resourceData) {
resourceData["defaults"] = resources.combined;
}
let components: VsixComponents = { builders: this.manifestBuilders, resources: resources };
// Build up a master file list
let packageFiles: PackageFiles = {};
this.manifestBuilders.forEach((builder) => {
_.assign(packageFiles, builder.files);
});
// Finalize each builder
return Promise.all([updateVersionPromise].concat(this.manifestBuilders.map(b => b.finalize(packageFiles, this.manifestBuilders)))).then(() => {
// Let the composer do validation
return this.extensionComposer.validate(components).then((validationResult) => {
if (validationResult.length === 0 || this.settings.bypassValidation) {
return components;
} else {
throw new Error("There were errors with your extension. Address the following and re-run the tool.\n" + validationResult);
}
let components: VsixComponents = { builders: this.manifestBuilders, resources: resources };
// Finalize each builder
return Promise.all([updateVersionPromise].concat(this.manifestBuilders.map(b => b.finalize(packageFiles, resourceData, this.manifestBuilders)))).then(() => {
// Let the composer do validation
return this.extensionComposer.validate(components).then((validationResult) => {
if (validationResult.length === 0 || this.settings.bypassValidation) {
return components;
} else {
throw new Error("There were errors with your extension. Address the following and re-run the tool.\n" + validationResult);
}
});
});
});
});
});
}
/**
* 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 buildResourcesData(): Promise<LocalizedResources> {
// Make sure locRoot is set, that it refers to a directory, and
// iterate each subdirectory of that.
if (!this.settings.locRoot) {
return Promise.resolve<void[]>(null);
}
let stringsPath = path.resolve(this.settings.locRoot);
const data: LocalizedResources = {defaults: null};
// Check that --loc-root exists and is a directory.
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 Promise.resolve<void[]>(null);
}
// stringsPath exists and is a directory - read it.
return <Promise<void[]>><any>Q.nfcall(fs.readdir, stringsPath).then((files: string[]) => {
let promises: 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()) {
// We're under a language tag directory within locRoot. Look for
// resources.resjson and use that to generate manfiest files
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);
data[languageTag] = resourcesObj;
});
}
});
}
});
promises.push(promise);
});
return Promise.all(promises);
});
}).then(() => {
return data;
});
}
/**
* Recursively converts a given path to a flat list of FileDeclaration
* @TODO: Async.

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

@ -1,5 +1,5 @@
import { ManifestBuilder } from "../../manifest"
import { PackageFiles } from "../../interfaces"
import { LocalizedResources, PackageFiles } from "../../interfaces"
import _ = require("lodash");
import os = require("os");
@ -26,7 +26,7 @@ export class VsoManifestBuilder extends ManifestBuilder {
return "application/json";
}
public finalize(files: PackageFiles): Promise<void> {
public finalize(files: PackageFiles, resourceData: LocalizedResources): Promise<void> {
// Ensure some default values are set
if (!this.data.contributions) {
this.data.contributions = [];
@ -53,10 +53,14 @@ export class VsoManifestBuilder extends ManifestBuilder {
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;
return _.trimEnd("contributions." + this.data.contributions[cIndex].id + "." + pathParts.slice(2).join("."));
} else if (pathParts[0] === "contributionTypes" && !isNaN(cIndex) && this.data.contributionTypes[cIndex] && this.data.contributionTypes[cIndex].id) {
return _.trimEnd("contributionTypes." + this.data.contributionTypes[cIndex].id + "." + pathParts.slice(2).join("."));
} else {
return path;
}
} else {
return path;
}
}

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

@ -1,5 +1,5 @@
import { ManifestBuilder } from "./manifest";
import { FileDeclaration, PackageFiles, ResourcesFile, ScreenshotDeclaration, TargetDeclaration, BadgeDeclaration, Vsix, VsixLanguagePack } from "./interfaces";
import { FileDeclaration, LocalizedResources, PackageFiles, ResourcesFile, ScreenshotDeclaration, TargetDeclaration, BadgeDeclaration, Vsix, VsixLanguagePack } from "./interfaces";
import { cleanAssetPath, jsonToXml, maxKey, removeMetaKeys, toZipItemName } from "./utils";
import _ = require("lodash");
import childProcess = require("child_process");
@ -81,9 +81,13 @@ export class VsixManifestBuilder extends ManifestBuilder {
/**
* Gets the contents of the vsixLangPack file for this manifest
*/
public getLocResult(translations: ResourcesFile, defaults: ResourcesFile): string {
public getLocResult(translations: ResourcesFile, defaults: ResourcesFile): FileDeclaration[] {
let langPack = this.generateVsixLangPack(translations, defaults);
return jsonToXml(langPack);
return [{
partName: "Extension.vsixlangpack",
path: null,
content: jsonToXml(langPack)
}];
}
private generateVsixLangPack(translations: ResourcesFile, defaults: ResourcesFile): VsixLanguagePack {
@ -93,9 +97,9 @@ export class VsixManifestBuilder extends ManifestBuilder {
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"]],
LocalizedName: [translations["displayName"] || defaults["displayName"] || null],
LocalizedDescription: [translations["description"] || defaults["description"] || null],
LocalizedReleaseNotes: [translations["releaseNotes"] || defaults["releaseNotes"] || null],
License: [null],
MoreInfoUrl: [null]
}
@ -371,6 +375,21 @@ export class VsixManifestBuilder extends ManifestBuilder {
return _.get<string>(this.data, "PackageManifest.Metadata[0].Identity[0].$.Id");
}
/**
* The JSON structure is fairly exotic since the result is an XML file,
* so change those exotic keys to easy-to-read ones.
*/
public getLocKeyPath(path: string): string {
switch (path) {
case "PackageManifest.Metadata.0.Description.0._":
return "description";
case "PackageManifest.Metadata.0.DisplayName.0" :
return "displayName";
default :
return path;
}
}
/**
* Get the publisher this vsixmanifest goes to
*/
@ -382,7 +401,7 @@ export class VsixManifestBuilder extends ManifestBuilder {
* --Ensures an <Asset> entry is added for each file as appropriate
* --Builds the [Content_Types].xml file
*/
public finalize(files: PackageFiles, builders: ManifestBuilder[]): Promise<void> {
public finalize(files: PackageFiles, resourceData: LocalizedResources, builders: ManifestBuilder[]): Promise<void> {
// Default installation target to VSS if not provided (and log warning)
let installationTarget = _.get<any[]>(this.data, "PackageManifest.Installation[0].InstallationTarget");
if (!(_.isArray(installationTarget) && installationTarget.length > 0)) {
@ -396,6 +415,32 @@ export class VsixManifestBuilder extends ManifestBuilder {
]);
}
if (resourceData) {
Object.keys(resourceData).forEach(languageTag => {
if (languageTag === "defaults") {
return;
}
builders.forEach(builder => {
const locResult = builder.getLocResult(resourceData[languageTag], resourceData.defaults);
locResult.forEach(lr => {
lr.lang = languageTag;
lr.partName = `${languageTag}/${lr.partName}`;
if (lr.partName.indexOf("vsixlangpack") === -1) {
lr.assetType = builder.getType();
lr.addressable = true;
} else {
lr.addressable = false;
}
const file = this.addFile(lr);
if (file.assetType) {
this.addAssetToManifest(file.partName, file.assetType, file.addressable, file.lang);
}
});
});
});
}
Object.keys(files).forEach((fileName) => {
let file = files[fileName];
@ -413,10 +458,11 @@ export class VsixManifestBuilder extends ManifestBuilder {
}
});
// The vsixmanifest will be responsible for generating the [Content_Types].xml file
// Obviously this is kind of strange, but hey ho.
return this.genContentTypesXml(builders).then((result) => {
this.addFile({
this.addAsset({
path: null,
content: result,
partName: "/[Content_Types].xml"
@ -424,6 +470,7 @@ export class VsixManifestBuilder extends ManifestBuilder {
});
}
/**
* Gets the string representation (XML) of this manifest
*/
@ -456,7 +503,7 @@ export class VsixManifestBuilder extends ManifestBuilder {
let contentTypePromises: Promise<any>[] = [];
let extensionlessFiles = [];
let uniqueExtensions = _.uniq<string>(Object.keys(this.files).map((f) => {
let extName = path.extname(f);
let extName = path.extname(f) || path.extname(this.files[f].partName);
const filename = path.basename(f);
// Look in the best guess table. Or, default to text/plain if the file starts with a "."

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

@ -53,17 +53,18 @@ export class VsixWriter {
* 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.
* If filename is generated automatically, use fileExt as the extension
*/
private getOutputPath(outPath: string): string {
public getOutputPath(outPath: string, fileExt: string = "vsix"): string {
// Find the vsix manifest, if it exists
let vsixBuilders = this.manifestBuilders.filter(b => b.getType() === VsixManifestBuilder.manifestType);
let autoName = "extension.vsix";
let autoName = "extension." + fileExt;
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";
autoName = `${pub}.${ns}-${version}.${fileExt}`;
}
if (outPath === "{auto}") {
@ -148,8 +149,6 @@ export class VsixWriter {
builderPromises.push(builderPromise);
});
return Promise.all(builderPromises).then(() => {
return this.addResourceStrings(vsix);
}).then(() => {
trace.debug("Writing vsix to: %s", outputPath);
return Q.nfcall(mkdirp, path.dirname(outputPath)).then(() => {
@ -175,6 +174,8 @@ export class VsixWriter {
return Promise.resolve<void[]>(null);
}
let stringsPath = path.resolve(this.settings.locRoot);
// Check that --loc-root exists and is a directory.
return Q.Promise((resolve, reject, notify) => {
fs.exists(stringsPath, (exists) => {
resolve(exists);
@ -189,16 +190,21 @@ export class VsixWriter {
} else {
return Q.resolve(false);
}
}).then<void[]>((stringsFolderExists) => {
}).then<void[]>((stringsFolderExists) => {1
if (!stringsFolderExists) {
return Promise.resolve<void[]>(null);
}
// stringsPath exists and is a directory - read it.
return <Promise<void[]>><any>Q.nfcall(fs.readdir, stringsPath).then((files: string[]) => {
let promises: 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()) {
// We're under a language tag directory within locRoot. Look for
// resources.resjson and use that to generate manfiest files
let resourcePath = path.join(filePath, "resources.resjson");
return Q.Promise<boolean>((resolve, reject, notify) => {
fs.exists(resourcePath, (exists) => {
@ -207,26 +213,36 @@ export class VsixWriter {
}).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"
// }
// });
return Q.nfcall<string>(fs.readFile, resourcePath, "utf8").then<void>((contents: string) => {
let resourcesObj = JSON.parse(contents);
// let builder = new xml.Builder(VsixWriter.DEFAULT_XML_BUILDER_SETTINGS);
// let vsixLangPackStr = builder.buildObject(splitRes.vsixResources);
// vsix.file(toZipItemName(languageTag + "/Extension.vsixlangpack"), vsixLangPackStr);
// });
// For each language, go through each builder and generate its
// localized resources.
this.manifestBuilders.forEach(builder => {
const locFiles = builder.getLocResult(resourcesObj, null);
locFiles.forEach(locFile => {
});
});
let locGen = new LocPrep.LocKeyGenerator(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 Promise.resolve<void>(null);
}

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

@ -1,5 +1,3 @@
import { Merger } from "./_lib/merger";
import { VsixManifestBuilder } from "./_lib/vsix-manifest-builder";
import { MergeSettings, PackageSettings } from "./_lib/interfaces";

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

@ -68,6 +68,7 @@ export class ExtensionBase<T> extends TfCommand<ExtensionArguments, T> {
protected getMergeSettings(): Promise<MergeSettings> {
return Promise.all([
this.commandArgs.root.val(),
this.commandArgs.locRoot.val(),
this.commandArgs.manifests.val(),
this.commandArgs.manifestGlobs.val(),
this.commandArgs.override.val(),
@ -77,7 +78,7 @@ export class ExtensionBase<T> extends TfCommand<ExtensionArguments, T> {
this.commandArgs.publisher.val(true),
this.commandArgs.extensionId.val(true)
]).then<MergeSettings>((values) => {
const [root, manifests, manifestGlob, override, overridesFile, revVersion, bypassValidation, publisher, extensionId] = values;
const [root, locRoot, manifests, manifestGlob, override, overridesFile, revVersion, bypassValidation, publisher, extensionId] = values;
if (publisher) {
_.set(override, "publisher", publisher);
}
@ -106,6 +107,7 @@ export class ExtensionBase<T> extends TfCommand<ExtensionArguments, T> {
_.merge(mergedOverrides, contentJSON, override);
return {
root: root[0],
locRoot: locRoot && locRoot[0],
manifests: manifests,
manifestGlobs: manifestGlob,
overrides: mergedOverrides,

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

@ -0,0 +1,47 @@
import { Merger } from "../_lib/merger";
import { TfCommand } from "../../../lib/tfcommand";
import { VsixWriter } from "../_lib/vsix-writer";
import * as Loc from "../_lib/loc";
import colors = require("colors");
import extBase = require("../default");
import trace = require('../../../lib/trace');
export function getCommand(args: string[]): TfCommand<extBase.ExtensionArguments, GenResourcesResult> {
return new GenerateExtensionResources(args);
}
export interface GenResourcesResult {
resjsonPath: string;
}
export class GenerateExtensionResources extends extBase.ExtensionBase<GenResourcesResult> {
protected description = "Create a vsix package for an extension.";
constructor(passedArgs: string[]) {
super(passedArgs, false);
}
protected getHelpArgs(): string[] {
return ["root", "manifests", "manifestGlobs", "override", "overridesFile", "revVersion", "bypassValidation", "publisher", "extensionId", "outputPath", "locRoot"];
}
public exec(): Promise<GenResourcesResult> {
return this.getMergeSettings().then(mergeSettings => {
return this.getPackageSettings().then(packageSettings => {
return new Merger(mergeSettings).merge().then(components => {
const writer = new VsixWriter(packageSettings, components);
const resjsonPath = writer.getOutputPath(packageSettings.outputPath, "resjson");
Loc.LocPrep.writeResourceFile(resjsonPath, components.resources.combined);
return <GenResourcesResult>{
resjsonPath: writer.getOutputPath(packageSettings.outputPath, "resjson")
};
});
});
});
}
protected friendlyOutput(data: GenResourcesResult): void {
trace.info(colors.green("\n=== Completed operation: generate extension resources ==="));
trace.info(" - .resjson: %s", data.resjsonPath);
}
}

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

@ -21,12 +21,12 @@ export function getCommand(): Promise<TFXCommand> {
let currentHierarchy = hierarchy;
let inArgs = false;
args.forEach((arg) => {
if (currentHierarchy && currentHierarchy[arg] !== undefined) {
currentHierarchy = currentHierarchy[arg];
execPath.push(arg);
} else if (arg.substr(0, 2) === "--" || inArgs) {
if (arg.substr(0, 2) === "--" || inArgs) {
commandArgs.push(arg);
inArgs = true;
} else if ((currentHierarchy && currentHierarchy[arg] !== undefined)) {
currentHierarchy = currentHierarchy[arg];
execPath.push(arg);
} else {
throw "Command '" + arg + "' not found. For help, type tfx " + execPath.join(" ") + " --help";
}
@ -44,7 +44,7 @@ function getCommandHierarchy(root: string): Promise<CommandHierarchy> {
return fs.readdir(root).then((files) => {
let filePromises = [];
files.forEach((file) => {
if (file.substr(0, 1) === "_") {
if (file.startsWith("_") || file.endsWith(".map")) {
return;
}
let fullPath = path.resolve(root, file);

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

@ -51,4 +51,4 @@ export function load(execPath: string[], args): Promise<TfCommand<any, any>> {
});
});
});
}
}

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

@ -5,10 +5,10 @@ import Q = require("q");
// This is an fs lib that uses Q instead of callbacks.
export var W_OK = fs.W_OK;
export var R_OK = fs.R_OK;
export var X_OK = fs.X_OK;
export var F_OK = fs.F_OK;
export var W_OK = fs.constants ? fs.constants.W_OK : (fs as any).W_OK; // back-compat
export var R_OK = fs.constants ? fs.constants.R_OK : (fs as any).R_OK; // back-compat
export var X_OK = fs.constants ? fs.constants.X_OK : (fs as any).X_OK; // back-compat
export var F_OK = fs.constants ? fs.constants.F_OK : (fs as any).F_OK; // back-compat
export function readdir(path: string): Promise<string[]> {
return Q.nfcall<string[]>(fs.readdir, path);
@ -63,7 +63,7 @@ export function fileAccess(path: string, mode: number = F_OK): Promise<boolean>
export function canWriteTo(path: string): Promise<boolean> {
return exists(path).then((exists) => {
if (exists) {
return fileAccess(path, fs.W_OK);
return fileAccess(path, W_OK);
} else {
return true;
}

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

@ -12,29 +12,12 @@ Install Dev Dependencies from root of repo:
## Build
We build the product using gulp. Just type `gulp` in the root of the repo.
We build the product using npm. Just type `npm run build` in the root of the repo.
This builds the product in the _build/app directory
```bash
~/Projects/tfs-cli$ gulp
[16:26:47] Using gulpfile ~/Projects/tfs-cli/gulpfile.js
[16:26:47] Starting 'clean'...
[16:26:47] Finished 'clean' after 16 ms
[16:26:47] Starting 'compileApp'...
[16:26:47] Starting 'compileTests'...
[16:26:47] Starting 'resources'...
[16:26:47] Finished 'resources' after 39 ms
[16:26:47] Starting 'copy'...
[16:26:47] Finished 'copy' after 15 ms
[16:26:47] Compiling TypeScript files using tsc version 1.5.3
[16:26:47] Compiling TypeScript files using tsc version 1.5.3
[16:26:49] Finished 'compileTests' after 2.06 s
[16:26:49] Finished 'compileApp' after 2.87 s
[16:26:49] Starting 'build'...
[16:26:49] Finished 'build' after 16 μs
[16:26:49] Starting 'default'...
[16:26:49] Finished 'default' after 3.56 μs
C:\tfs-cli>npm run build
```
## Install for Verification

1
gulp.cmd Normal file
Просмотреть файл

@ -0,0 +1 @@
echo Gulp is no longer used to build this repo. Please type 'npm run build' to build TFX. See the README for more details.

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

@ -1,59 +0,0 @@
var fs = require('fs');
var path = require('path');
var gulp = require('gulp');
var del = require('del');
var mocha = require('gulp-mocha');
var tsb = require('gulp-tsb');
var filter = require('gulp-filter');
var minimist = require('minimist');
var shell = require('shelljs');
var tsconfig = require('./tsconfig.json');
var compilation = tsb.create(tsconfig.compilerOptions);
var sources = [
'app/**',
'tests/**',
'typings/**/*.d.ts'
];
gulp.task('copy', ['clean'], function() {
return gulp.src(['package.json', './README.md', './app/tfs-cli.js'])
.pipe(gulp.dest(path.join('_build', 'app')));
});
gulp.task('compile', ['clean', 'copy'], function () {
var tsFilter = filter('**/*.ts', { restore: true });
return gulp.src(sources, { base: '.' })
.pipe(tsFilter)
.pipe(compilation())
.pipe(tsFilter.restore)
.pipe(gulp.dest('_build'));
});
gulp.task('build', ['clean', 'compile', 'copy']);
var mopts = {
boolean: 'ci',
string: 'suite',
default: { ci: false, suite: '*' }
};
var options = minimist(process.argv.slice(2), mopts);
gulp.task('test', function () {
var suitePath = path.join('_build', 'tests', '*.js');
console.log(suitePath);
if (options.suite !== '*') {
suitePath = path.join('_build', 'tests', options.suite + '.js');
}
return gulp.src([suitePath])
.pipe(mocha({ reporter: 'spec', ui: 'bdd', useColors: !options.ci }));
});
gulp.task('clean', function (done) {
del(['_build'], done);
});
gulp.task('default', ['build']);

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

@ -14,7 +14,10 @@
"scripts": {
"clean": "rimraf _build",
"prebuild": "npm run clean",
"build": "tsc -p . && ncp app/tfx-cli.js _build/tfx-cli.js && ncp package.json _build/package.json"
"build": "tsc -p .",
"postbuild": "npm run copy",
"copy": "ncp app/tfx-cli.js _build/tfx-cli.js && ncp package.json _build/package.json",
"prepublish": "npm run build"
},
"dependencies": {
"@types/colors": "^0.6.31",
@ -33,7 +36,7 @@
"archiver": "0.14.4",
"async": "^1.4.0",
"colors": "^1.1.2",
"copy-paste": "^1.1.3",
"copy-paste": "^1.3.0",
"glob": "5.0.10",
"inquirer": "0.8.5",
"json-in-place": "^1.0.1",
@ -57,16 +60,12 @@
"xml2js": "^0.4.16"
},
"devDependencies": {
"del": "^1.2.0",
"gulp": "^3.9.0",
"gulp-filter": "^3.0.1",
"gulp-mocha": "2.0.0",
"gulp-tsb": "^1.10.2",
"minimatch": "^2.0.8",
"mocha": "^2.2.5",
"ncp": "^2.0.0",
"rimraf": "^2.5.4",
"typescript": "^2.0.2"
"typescript": "^2.0.2",
"webpack": "^1.13.2"
},
"author": "",
"license": "MIT"

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

@ -4,7 +4,7 @@
"target": "es5",
"moduleResolution": "node",
"declaration": false,
"sourceMap": false,
"sourceMap": true,
"newLine": "LF",
"lib": ["es5", "es2015", "es6"],
"outDir": "_build"