Skip to content

Commit 815edb3

Browse files
Fix globbing of multiple manifests (#522)
* Fixes globbing of multiple manifests Fixes #514 * Bump version --------- Co-authored-by: Jesse Houwing <[email protected]>
1 parent e17e51a commit 815edb3

File tree

4 files changed

+224
-21
lines changed

4 files changed

+224
-21
lines changed

app/exec/extension/_lib/merger.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -49,29 +49,26 @@ export class Merger {
4949
this.manifestBuilders = [];
5050
}
5151

52-
private gatherManifests(): Promise<string[]> {
52+
private async gatherManifests(): Promise<string[]> {
5353
trace.debug("merger.gatherManifests");
5454

5555
if (this.settings.manifestGlobs && this.settings.manifestGlobs.length > 0) {
56-
const globs = this.settings.manifestGlobs.map(p => (path.isAbsolute(p) ? p : path.join(this.settings.root, p)));
56+
const patterns = this.settings.manifestGlobs;
5757

5858
trace.debug("merger.gatherManifestsFromGlob");
59-
const promises = globs.map(pattern => glob(pattern));
60-
61-
return Promise.all(promises)
62-
.then(results => _.uniq(_.flatten<string>(results)))
63-
.then(results => {
64-
if (results.length > 0) {
65-
trace.debug("Merging %s manifests from the following paths: ", results.length.toString());
66-
results.forEach(path => trace.debug(path));
67-
} else {
68-
throw new Error(
69-
"No manifests found from the following glob patterns: \n" + this.settings.manifestGlobs.join("\n"),
70-
);
71-
}
72-
73-
return results;
74-
});
59+
const resultsArrays = await Promise.all(patterns.map(pattern => glob(pattern, { cwd: this.settings.root })));
60+
const relativeResults = _.uniq(_.flatten<string>(resultsArrays));
61+
const results = relativeResults.map(p => path.join(this.settings.root, p));
62+
63+
if (results.length > 0) {
64+
trace.debug("Merging %s manifests from the following paths: ", results.length.toString());
65+
results.forEach(p => trace.debug(p));
66+
return results;
67+
} else {
68+
throw new Error(
69+
"No manifests found from the following glob patterns: \n" + this.settings.manifestGlobs.join("\n"),
70+
);
71+
}
7572
} else {
7673
const manifests = this.settings.manifests;
7774
if (!manifests || manifests.length === 0) {
@@ -83,8 +80,8 @@ export class Merger {
8380
manifests.length.toString(),
8481
manifests.length === 1 ? "" : "s",
8582
);
86-
manifests.forEach(path => trace.debug(path));
87-
return Promise.resolve(this.settings.manifests);
83+
manifests.forEach(p => trace.debug(p));
84+
return this.settings.manifests;
8885
}
8986
}
9087

package-lock.json

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tfx-cli",
3-
"version": "0.22.4",
3+
"version": "0.22.5",
44
"description": "CLI for Azure DevOps Services and Team Foundation Server",
55
"repository": {
66
"type": "git",
@@ -72,6 +72,7 @@
7272
"@types/validator": "^4.5.27",
7373
"@types/winreg": "^1.2.29",
7474
"@types/xml2js": "0.0.27",
75+
"adm-zip": "^0.5.16",
7576
"mocha": "^10.2.0",
7677
"ncp": "^2.0.0",
7778
"rimraf": "^2.6.1",

tests/extension-local-tests.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import assert = require('assert');
22
import { stripColors } from 'colors';
33
import path = require('path');
44
import fs = require('fs');
5+
// eslint-disable-next-line @typescript-eslint/no-var-requires
6+
const AdmZip = require('adm-zip');
57
import { execAsyncWithLogging } from './test-utils/debug-exec';
68

79
// Basic test framework functions to avoid TypeScript errors
@@ -381,6 +383,192 @@ describe('Extension Commands - Local Tests', function() {
381383
})
382384
.catch(done);
383385
});
386+
387+
it('should handle manifest-globs with glob patterns and merge scopes', function (done) {
388+
const complexExtensionPath = path.join(samplesPath, 'complex-extension');
389+
if (!fs.existsSync(complexExtensionPath)) {
390+
console.log('Skipping manifest-globs glob pattern test - sample not found');
391+
done();
392+
return;
393+
}
394+
395+
const outputPath = path.join(complexExtensionPath, 'manifest-globs-scopes-test.vsix');
396+
const manifestsRoot = path.join(complexExtensionPath, 'dist', 'Manifests');
397+
const manifestsSubDir = path.join(manifestsRoot, 'a');
398+
const mainManifestPath = path.join(complexExtensionPath, 'azure-devops-extension.json');
399+
const secondaryManifestPath = path.join(manifestsSubDir, 'manifest-a.json');
400+
401+
const manifestsRootParent = path.dirname(manifestsRoot);
402+
if (!fs.existsSync(manifestsRootParent)) {
403+
fs.mkdirSync(manifestsRootParent);
404+
}
405+
if (!fs.existsSync(manifestsRoot)) {
406+
fs.mkdirSync(manifestsRoot);
407+
}
408+
if (!fs.existsSync(manifestsSubDir)) {
409+
fs.mkdirSync(manifestsSubDir);
410+
}
411+
412+
const primaryManifest = {
413+
"manifestVersion": 1,
414+
"id": "glob-test-extension",
415+
"publisher": "glob-test-publisher",
416+
"version": "1.0.0",
417+
"name": "Glob Test Extension",
418+
"categories": "Azure Boards",
419+
"scopes": [
420+
"vso.analytics"
421+
],
422+
"targets": [
423+
{ "id": "Microsoft.VisualStudio.Services" }
424+
],
425+
"contributions": [
426+
{
427+
"id": "glob-test-hub",
428+
"type": "ms.vss-web.hub",
429+
"targets": ["ms.vss-web.project-hub-group"],
430+
"properties": {
431+
"name": "Glob Test Hub"
432+
}
433+
}
434+
]
435+
};
436+
fs.writeFileSync(mainManifestPath, JSON.stringify(primaryManifest, null, 2));
437+
438+
const secondaryManifest = {
439+
"scopes": [
440+
"vso.work"
441+
]
442+
};
443+
fs.writeFileSync(secondaryManifestPath, JSON.stringify(secondaryManifest, null, 2));
444+
445+
const manifestGlobsArg = 'azure-devops-extension.json dist/Manifests/**/manifest-*.json';
446+
execAsyncWithLogging(
447+
`node "${tfxPath}" extension create --root "${complexExtensionPath}" --output-path "${outputPath}" --manifest-globs ${manifestGlobsArg}`,
448+
'extension create --manifest-globs glob patterns'
449+
)
450+
.then(({ stdout }) => {
451+
const cleanOutput = stripColors(stdout);
452+
assert(cleanOutput.includes('Completed operation: create extension'), 'Should handle manifest-globs with glob patterns');
453+
assert(fs.existsSync(outputPath), 'Should create .vsix file');
454+
455+
// Read extension.vsomanifest (JSON) from VSIX and validate scopes
456+
const zip = new AdmZip(outputPath);
457+
const vsomanifestEntry =
458+
zip.getEntry('extension.vsomanifest') ||
459+
zip.getEntry('extension/extension.vsomanifest');
460+
assert(vsomanifestEntry, 'VSIX must contain extension.vsomanifest');
461+
462+
const vsomanifestJson = JSON.parse(vsomanifestEntry.getData().toString('utf8'));
463+
const scopes: string[] = vsomanifestJson.scopes || [];
464+
465+
assert(scopes.indexOf('vso.analytics') !== -1, 'Resulting manifest should contain vso.analytics scope');
466+
assert(scopes.indexOf('vso.work') !== -1, 'Resulting manifest should contain vso.work scope');
467+
468+
done();
469+
})
470+
.catch(done);
471+
});
472+
473+
it('should resolve manifest-globs to both root and globbed manifests (gatherManifests)', function (done) {
474+
const complexExtensionPath = path.join(samplesPath, 'complex-extension');
475+
if (!fs.existsSync(complexExtensionPath)) {
476+
console.log('Skipping gatherManifests test - sample not found');
477+
done();
478+
return;
479+
}
480+
481+
const manifestsRoot = path.join(complexExtensionPath, 'dist', 'Manifests');
482+
const manifestsSubDir = path.join(manifestsRoot, 'a');
483+
const mainManifestPath = path.join(complexExtensionPath, 'azure-devops-extension.json');
484+
const secondaryManifestPath = path.join(manifestsSubDir, 'manifest-a.json');
485+
486+
const manifestsRootParent = path.dirname(manifestsRoot);
487+
if (!fs.existsSync(manifestsRootParent)) {
488+
fs.mkdirSync(manifestsRootParent);
489+
}
490+
if (!fs.existsSync(manifestsRoot)) {
491+
fs.mkdirSync(manifestsRoot);
492+
}
493+
if (!fs.existsSync(manifestsSubDir)) {
494+
fs.mkdirSync(manifestsSubDir);
495+
}
496+
497+
const primaryManifest = {
498+
"manifestVersion": 1,
499+
"id": "glob-test-extension-gather",
500+
"publisher": "glob-test-publisher",
501+
"version": "1.0.0",
502+
"name": "Glob Test Extension Gather",
503+
"categories": "Azure Boards",
504+
"scopes": [
505+
"vso.analytics"
506+
],
507+
"targets": [
508+
{ "id": "Microsoft.VisualStudio.Services" }
509+
],
510+
"contributions": [
511+
{
512+
"id": "glob-test-hub-gather",
513+
"type": "ms.vss-web.hub",
514+
"targets": ["ms.vss-web.project-hub-group"],
515+
"properties": {
516+
"name": "Glob Test Hub Gather"
517+
}
518+
}
519+
]
520+
};
521+
fs.writeFileSync(mainManifestPath, JSON.stringify(primaryManifest, null, 2));
522+
523+
const secondaryManifest = {
524+
"scopes": [
525+
"vso.work"
526+
]
527+
};
528+
fs.writeFileSync(secondaryManifestPath, JSON.stringify(secondaryManifest, null, 2));
529+
530+
const mergerModulePath = path.resolve(__dirname, '../../_build/exec/extension/_lib/merger');
531+
let MergerCtor: any;
532+
try {
533+
// eslint-disable-next-line @typescript-eslint/no-var-requires
534+
MergerCtor = require(mergerModulePath).Merger || require(mergerModulePath).default;
535+
} catch (e) {
536+
done(e);
537+
return;
538+
}
539+
540+
const settings: any = {
541+
root: complexExtensionPath,
542+
manifests: [],
543+
manifestGlobs: [
544+
'azure-devops-extension.json',
545+
'dist/Manifests/**/manifest-*.json'
546+
],
547+
overrides: {},
548+
noPrompt: true
549+
};
550+
551+
const merger = new MergerCtor(settings);
552+
const gatherFn = (merger as any)['gatherManifests'];
553+
if (typeof gatherFn !== 'function') {
554+
done(new Error('Merger.gatherManifests is not accessible'));
555+
return;
556+
}
557+
558+
Promise.resolve(gatherFn.call(merger))
559+
.then((manifestPaths: string[]) => {
560+
assert(Array.isArray(manifestPaths) && manifestPaths.length >= 2, 'gatherManifests should return at least two manifests');
561+
562+
const normalizedPaths = manifestPaths.map(p => path.normalize(p));
563+
const expectedMain = path.normalize(mainManifestPath);
564+
const expectedSecondary = path.normalize(secondaryManifestPath);
565+
566+
assert(normalizedPaths.indexOf(expectedMain) !== -1, 'gatherManifests should include root manifest');
567+
assert(normalizedPaths.indexOf(expectedSecondary) !== -1, 'gatherManifests should include globbed manifest');
568+
done();
569+
})
570+
.catch(done);
571+
});
384572
});
385573

386574
describe('Extension Creation - Complex Scenarios', function() {

0 commit comments

Comments
 (0)