Skip to content

Commit 16728c5

Browse files
Merge pull request #342 from bworline/manifest-js
Allow flexible manifest specification, including production vs. development differences
2 parents b8d1ef8 + 32aa8e0 commit 16728c5

File tree

8 files changed

+237
-130
lines changed

8 files changed

+237
-130
lines changed

app/exec/extension/_lib/interfaces.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,19 @@ export interface MergeSettings {
135135
*/
136136
root: string;
137137

138+
/*
139+
* Manifest in the form of a standard Node.js CommonJS module with an exported function.
140+
* The function takes an environment as a parameter and must return the manifest JSON object.
141+
* Environment variables are specified with the env command line parameter.
142+
* If this is present then manifests, manifestGlobs, json5, override, and overridesFile are ignored.
143+
*/
144+
manifestJs: string;
145+
146+
/*
147+
* A series of environment variables that are passed to the function exported from the manifestJs module.
148+
*/
149+
env: string[];
150+
138151
/*
139152
* List of paths to manifest files
140153
*/

app/exec/extension/_lib/merger.ts

Lines changed: 153 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,45 @@ export class Merger {
9898
}
9999
}
100100

101+
private loadManifestJs(): any {
102+
trace.debug("merger.manifestJs");
103+
104+
// build environment object from --env parameter
105+
const env = {};
106+
(this.settings.env || []).forEach(kvp => {
107+
const [key, ...value] = kvp.split('=');
108+
env[key] = value.join('=');
109+
});
110+
111+
const fullJsFile = path.resolve(this.settings.manifestJs);
112+
const manifestModuleFn = require(fullJsFile);
113+
if (!manifestModuleFn || typeof manifestModuleFn != "function") {
114+
throw new Error(`Missing export function from manifest-js file ${fullJsFile}`)
115+
}
116+
const manifestData = manifestModuleFn(env);
117+
if (!manifestData) {
118+
throw new Error(`The export function from manifest-js file ${fullJsFile} must return the manifest object`)
119+
}
120+
return manifestData;
121+
}
122+
101123
/**
102124
* Finds all manifests and merges them into two JS Objects: vsoManifest and vsixManifest
103125
* @return Q.Promise<SplitManifest> An object containing the two manifests
104126
*/
105-
public merge(): Promise<VsixComponents> {
127+
public async merge(): Promise<VsixComponents> {
106128
trace.debug("merger.merge");
107129

108-
return this.gatherManifests().then(files => {
109-
let overridesProvided = false;
110-
const manifestPromises: Promise<any>[] = [];
111-
files.forEach(file => {
130+
let overridesProvided = false;
131+
const manifestPromises: Promise<any>[] = [];
132+
133+
if (this.settings.manifestJs) {
134+
const result = this.loadManifestJs();
135+
result.__origin = this.settings.manifestJs; // save the origin in order to resolve relative paths later.
136+
manifestPromises.push(Promise.resolve(result));
137+
} else {
138+
let manifestFiles = await this.gatherManifests();
139+
manifestFiles.forEach(file => {
112140
manifestPromises.push(
113141
promisify(readFile)(file, "utf8").then(data => {
114142
const jsonData = data.replace(/^\uFEFF/, "");
@@ -129,141 +157,141 @@ export class Merger {
129157
if (this.settings.overrides) {
130158
overridesProvided = true;
131159
manifestPromises.push(Promise.resolve(this.settings.overrides));
132-
}
160+
}
161+
}
133162

134-
return Promise.all(manifestPromises).then(partials => {
135-
// Determine the targets so we can construct the builders
136-
let targets: TargetDeclaration[] = [];
137-
partials.forEach(partial => {
138-
if (_.isArray(partial["targets"])) {
139-
targets = targets.concat(partial["targets"]);
140-
}
141-
});
142-
this.extensionComposer = ComposerFactory.GetComposer(this.settings, targets);
143-
this.manifestBuilders = this.extensionComposer.getBuilders();
144-
let updateVersionPromise = Promise.resolve<void>(null);
145-
partials.forEach((partial, partialIndex) => {
146-
// Rev the version if necessary
147-
if (this.settings.revVersion) {
148-
if (partial["version"] && partial.__origin) {
149-
try {
150-
const parsedVersion = version.DynamicVersion.parse(partial["version"]);
151-
const newVersion = version.DynamicVersion.increase(parsedVersion);
152-
const newVersionString = newVersion.toString();
153-
partial["version"] = newVersionString;
154-
155-
updateVersionPromise = promisify(readFile)(partial.__origin, "utf8").then(versionPartial => {
156-
try {
157-
let newPartial: any;
158-
if (this.settings.json5) {
159-
const parsed = jju.parse(versionPartial);
160-
parsed["version"] = newVersionString;
161-
newPartial = jju.update(versionPartial, parsed);
162-
} else {
163+
return Promise.all(manifestPromises).then(partials => {
164+
// Determine the targets so we can construct the builders
165+
let targets: TargetDeclaration[] = [];
166+
partials.forEach(partial => {
167+
if (_.isArray(partial["targets"])) {
168+
targets = targets.concat(partial["targets"]);
169+
}
170+
});
171+
this.extensionComposer = ComposerFactory.GetComposer(this.settings, targets);
172+
this.manifestBuilders = this.extensionComposer.getBuilders();
173+
let updateVersionPromise = Promise.resolve<void>(null);
174+
partials.forEach((partial, partialIndex) => {
175+
// Rev the version if necessary
176+
if (this.settings.revVersion) {
177+
if (partial["version"] && partial.__origin) {
178+
try {
179+
const parsedVersion = version.DynamicVersion.parse(partial["version"]);
180+
const newVersion = version.DynamicVersion.increase(parsedVersion);
181+
const newVersionString = newVersion.toString();
182+
partial["version"] = newVersionString;
183+
184+
updateVersionPromise = promisify(readFile)(partial.__origin, "utf8").then(versionPartial => {
185+
try {
186+
let newPartial: any;
187+
if (this.settings.json5) {
188+
const parsed = jju.parse(versionPartial);
189+
parsed["version"] = newVersionString;
190+
newPartial = jju.update(versionPartial, parsed);
191+
} else {
163192
newPartial = jsonInPlace(versionPartial).set("version", newVersionString).toString();
164-
}
165-
return promisify(writeFile)(partial.__origin, newPartial);
166-
} catch (e) {
167-
trace.warn(
168-
"Failed to lex partial as JSON to update the version. Skipping version rev...",
169-
);
170193
}
171-
});
172-
} catch (e) {
173-
trace.warn(
174-
"Could not parse %s as a version (e.g. major.minor.patch). Skipping version rev...",
175-
partial["version"],
176-
);
177-
}
194+
return promisify(writeFile)(partial.__origin, newPartial);
195+
} catch (e) {
196+
trace.warn(
197+
"Failed to lex partial as JSON to update the version. Skipping version rev...",
198+
);
199+
}
200+
});
201+
} catch (e) {
202+
trace.warn(
203+
"Could not parse %s as a version (e.g. major.minor.patch). Skipping version rev...",
204+
partial["version"],
205+
);
178206
}
179207
}
208+
}
180209

181-
// Transform asset paths to be relative to the root of all manifests, verify assets
182-
if (_.isArray(partial["files"])) {
183-
(<Array<FileDeclaration>>partial["files"]).forEach(asset => {
184-
const keys = Object.keys(asset);
185-
if (keys.indexOf("path") < 0) {
186-
throw new Error("Files must have an absolute or relative (to the manifest) path.");
187-
}
188-
let absolutePath;
189-
if (path.isAbsolute(asset.path)) {
190-
absolutePath = asset.path;
191-
} else {
192-
absolutePath = path.join(path.dirname(partial.__origin), asset.path);
193-
}
194-
asset.path = path.relative(this.settings.root, absolutePath);
195-
});
196-
}
197-
// Transform icon paths as above
198-
if (_.isObject(partial["icons"])) {
199-
const icons = partial["icons"];
200-
Object.keys(icons).forEach((iconKind: string) => {
201-
const absolutePath = path.join(path.dirname(partial.__origin), icons[iconKind]);
202-
icons[iconKind] = path.relative(this.settings.root, absolutePath);
203-
});
204-
}
210+
// Transform asset paths to be relative to the root of all manifests, verify assets
211+
if (_.isArray(partial["files"])) {
212+
(<Array<FileDeclaration>>partial["files"]).forEach(asset => {
213+
const keys = Object.keys(asset);
214+
if (keys.indexOf("path") < 0) {
215+
throw new Error("Files must have an absolute or relative (to the manifest) path.");
216+
}
217+
let absolutePath;
218+
if (path.isAbsolute(asset.path)) {
219+
absolutePath = asset.path;
220+
} else {
221+
absolutePath = path.join(path.dirname(partial.__origin), asset.path);
222+
}
223+
asset.path = path.relative(this.settings.root, absolutePath);
224+
});
225+
}
226+
// Transform icon paths as above
227+
if (_.isObject(partial["icons"])) {
228+
const icons = partial["icons"];
229+
Object.keys(icons).forEach((iconKind: string) => {
230+
const absolutePath = path.join(path.dirname(partial.__origin), icons[iconKind]);
231+
icons[iconKind] = path.relative(this.settings.root, absolutePath);
232+
});
233+
}
205234

206-
// Expand any directories listed in the files array
207-
if (_.isArray(partial["files"])) {
208-
for (let i = partial["files"].length - 1; i >= 0; --i) {
209-
const fileDecl: FileDeclaration = partial["files"][i];
210-
const fsPath = path.join(this.settings.root, fileDecl.path);
211-
if (fs.lstatSync(fsPath).isDirectory()) {
212-
Array.prototype.splice.apply(
213-
partial["files"],
214-
(<any[]>[i, 1]).concat(this.pathToFileDeclarations(fsPath, this.settings.root, fileDecl)),
215-
);
216-
}
235+
// Expand any directories listed in the files array
236+
if (_.isArray(partial["files"])) {
237+
for (let i = partial["files"].length - 1; i >= 0; --i) {
238+
const fileDecl: FileDeclaration = partial["files"][i];
239+
const fsPath = path.join(this.settings.root, fileDecl.path);
240+
if (fs.lstatSync(fsPath).isDirectory()) {
241+
Array.prototype.splice.apply(
242+
partial["files"],
243+
(<any[]>[i, 1]).concat(this.pathToFileDeclarations(fsPath, this.settings.root, fileDecl)),
244+
);
217245
}
218246
}
247+
}
219248

220-
// Process each key by each manifest builder.
221-
Object.keys(partial).forEach(key => {
222-
const isOverridePartial = partials.length - 1 === partialIndex && overridesProvided;
223-
if (partial[key] !== undefined && (partial[key] !== null || isOverridePartial)) {
224-
// Notify each manifest builder of the key/value pair
225-
this.manifestBuilders.forEach(builder => {
226-
builder.processKey(key, partial[key], isOverridePartial);
227-
});
228-
}
229-
});
249+
// Process each key by each manifest builder.
250+
Object.keys(partial).forEach(key => {
251+
const isOverridePartial = partials.length - 1 === partialIndex && overridesProvided;
252+
if (partial[key] !== undefined && (partial[key] !== null || isOverridePartial)) {
253+
// Notify each manifest builder of the key/value pair
254+
this.manifestBuilders.forEach(builder => {
255+
builder.processKey(key, partial[key], isOverridePartial);
256+
});
257+
}
230258
});
259+
});
231260

232-
// Generate localization resources
233-
const locPrepper = new loc.LocPrep.LocKeyGenerator(this.manifestBuilders);
234-
const resources = locPrepper.generateLocalizationKeys();
261+
// Generate localization resources
262+
const locPrepper = new loc.LocPrep.LocKeyGenerator(this.manifestBuilders);
263+
const resources = locPrepper.generateLocalizationKeys();
235264

236-
// Build up resource data by reading the translations from disk
237-
return this.buildResourcesData().then(resourceData => {
238-
if (resourceData) {
239-
resourceData["defaults"] = resources.combined;
240-
}
265+
// Build up resource data by reading the translations from disk
266+
return this.buildResourcesData().then(resourceData => {
267+
if (resourceData) {
268+
resourceData["defaults"] = resources.combined;
269+
}
241270

242-
// Build up a master file list
243-
const packageFiles: PackageFiles = {};
244-
this.manifestBuilders.forEach(builder => {
245-
_.assign(packageFiles, builder.files);
246-
});
271+
// Build up a master file list
272+
const packageFiles: PackageFiles = {};
273+
this.manifestBuilders.forEach(builder => {
274+
_.assign(packageFiles, builder.files);
275+
});
247276

248-
const components: VsixComponents = { builders: this.manifestBuilders, resources: resources };
249-
250-
// Finalize each builder
251-
return Promise.all(
252-
[updateVersionPromise].concat(
253-
this.manifestBuilders.map(b => b.finalize(packageFiles, resourceData, this.manifestBuilders)),
254-
),
255-
).then(() => {
256-
// const the composer do validation
257-
return this.extensionComposer.validate(components).then(validationResult => {
258-
if (validationResult.length === 0 || this.settings.bypassValidation) {
259-
return components;
260-
} else {
261-
throw new Error(
262-
"There were errors with your extension. Address the following and re-run the tool.\n" +
263-
validationResult,
264-
);
265-
}
266-
});
277+
const components: VsixComponents = { builders: this.manifestBuilders, resources: resources };
278+
279+
// Finalize each builder
280+
return Promise.all(
281+
[updateVersionPromise].concat(
282+
this.manifestBuilders.map(b => b.finalize(packageFiles, resourceData, this.manifestBuilders)),
283+
),
284+
).then(() => {
285+
// const the composer do validation
286+
return this.extensionComposer.validate(components).then(validationResult => {
287+
if (validationResult.length === 0 || this.settings.bypassValidation) {
288+
return components;
289+
} else {
290+
throw new Error(
291+
"There were errors with your extension. Address the following and re-run the tool.\n" +
292+
validationResult,
293+
);
294+
}
267295
});
268296
});
269297
});

app/exec/extension/create.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export class ExtensionCreate extends extBase.ExtensionBase<CreationResult> {
4747
protected getHelpArgs(): string[] {
4848
return [
4949
"root",
50+
"manifestJs",
51+
"env",
5052
"manifests",
5153
"manifestGlobs",
5254
"json5",

0 commit comments

Comments
 (0)