Skip to content

Commit 196cce4

Browse files
authored
Merge pull request #1212 from microsoft/copilot/validate-extension-dependencies-case
Validate extension dependencies use lowercase letters and warn about deprecated github.copilot
2 parents 1f11bf2 + 82c80d6 commit 196cce4

File tree

4 files changed

+147
-0
lines changed

4 files changed

+147
-0
lines changed

src/package.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
validateEngineCompatibility,
2222
validateVSCodeTypesCompatibility,
2323
validatePublisher,
24+
validateExtensionDependencies,
2425
} from './validation';
2526
import { detectYarn, getDependencies } from './npm';
2627
import * as GitHost from 'hosted-git-info';
@@ -1427,6 +1428,10 @@ export function validateManifestForPackaging(manifest: UnverifiedManifest): Mani
14271428
}
14281429
}
14291430

1431+
// Validate extension dependencies and extension pack use lowercase IDs
1432+
validateExtensionDependencies(manifest.extensionDependencies, 'extensionDependencies');
1433+
validateExtensionDependencies(manifest.extensionPack, 'extensionPack');
1434+
14301435
return {
14311436
...manifest,
14321437
name,

src/test/package.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,62 @@ describe('validateManifest', () => {
604604
validateManifestForPackaging(createManifest({ sponsor: { url: 'http://www.foo.com' } }));
605605
});
606606

607+
it('should validate extensionDependencies are lowercase', () => {
608+
// Valid lowercase dependencies
609+
validateManifestForPackaging(createManifest({ extensionDependencies: ['publisher.extension'] }));
610+
validateManifestForPackaging(createManifest({ extensionDependencies: ['pub.ext', 'another.dep'] }));
611+
validateManifestForPackaging(createManifest({ extensionDependencies: ['pub-name.ext-name'] }));
612+
613+
// Invalid uppercase dependencies
614+
assert.throws(() => validateManifestForPackaging(createManifest({ extensionDependencies: ['Publisher.extension'] })));
615+
assert.throws(() => validateManifestForPackaging(createManifest({ extensionDependencies: ['publisher.Extension'] })));
616+
assert.throws(() => validateManifestForPackaging(createManifest({ extensionDependencies: ['PUBLISHER.EXTENSION'] })));
617+
assert.throws(() => validateManifestForPackaging(createManifest({ extensionDependencies: ['valid.ext', 'Invalid.Ext'] })));
618+
});
619+
620+
it('should validate extensionPack are lowercase', () => {
621+
// Valid lowercase pack
622+
validateManifestForPackaging(createManifest({ extensionPack: ['publisher.extension'] }));
623+
validateManifestForPackaging(createManifest({ extensionPack: ['pub.ext', 'another.dep'] }));
624+
625+
// Invalid uppercase pack
626+
assert.throws(() => validateManifestForPackaging(createManifest({ extensionPack: ['Publisher.extension'] })));
627+
assert.throws(() => validateManifestForPackaging(createManifest({ extensionPack: ['publisher.Extension'] })));
628+
assert.throws(() => validateManifestForPackaging(createManifest({ extensionPack: ['valid.ext', 'Invalid.Ext'] })));
629+
});
630+
631+
it('should warn about deprecated github.copilot dependency', () => {
632+
const originalLogWarn = log.warn;
633+
const warnings: string[] = [];
634+
log.warn = (message: string) => warnings.push(message);
635+
636+
try {
637+
// Test with extensionDependencies
638+
validateManifestForPackaging(createManifest({ extensionDependencies: ['github.copilot'] }));
639+
assert.strictEqual(warnings.length, 1);
640+
assert.ok(warnings[0].includes('github.copilot'));
641+
assert.ok(warnings[0].includes('deprecated'));
642+
assert.ok(warnings[0].includes('github.copilot-chat'));
643+
644+
// Reset warnings
645+
warnings.length = 0;
646+
647+
// Test with extensionPack
648+
validateManifestForPackaging(createManifest({ extensionPack: ['github.copilot'] }));
649+
assert.strictEqual(warnings.length, 1);
650+
assert.ok(warnings[0].includes('github.copilot'));
651+
652+
// Reset warnings
653+
warnings.length = 0;
654+
655+
// Test with no github.copilot dependency
656+
validateManifestForPackaging(createManifest({ extensionDependencies: ['other.extension'] }));
657+
assert.strictEqual(warnings.length, 0);
658+
} finally {
659+
log.warn = originalLogWarn;
660+
}
661+
});
662+
607663
it('should validate pricing', () => {
608664
assert.throws(() => validateManifestForPackaging(createManifest({ pricing: 'Paid' })));
609665
validateManifestForPackaging(createManifest({ pricing: 'Trial' }));

src/test/validation.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
validateVersion,
66
validateEngineCompatibility,
77
validateVSCodeTypesCompatibility,
8+
validateExtensionDependencies,
89
} from '../validation';
910

1011
describe('validatePublisher', () => {
@@ -135,3 +136,53 @@ describe('validateVSCodeTypesCompatibility', () => {
135136
assert.throws(() => validateVSCodeTypesCompatibility('1.5', '1.30'));
136137
});
137138
});
139+
140+
describe('validateExtensionDependencies', () => {
141+
it('should allow empty or undefined dependencies', () => {
142+
validateExtensionDependencies(undefined, 'extensionDependencies');
143+
validateExtensionDependencies([], 'extensionDependencies');
144+
});
145+
146+
it('should allow lowercase extension IDs', () => {
147+
validateExtensionDependencies(['publisher.extension'], 'extensionDependencies');
148+
validateExtensionDependencies(['publisher.extension-name'], 'extensionDependencies');
149+
validateExtensionDependencies(['publisher-name.extension-name'], 'extensionDependencies');
150+
validateExtensionDependencies(['pub123.ext456'], 'extensionDependencies');
151+
validateExtensionDependencies(
152+
['publisher1.extension1', 'publisher2.extension2'],
153+
'extensionDependencies'
154+
);
155+
});
156+
157+
it('should reject uppercase letters in extension IDs', () => {
158+
assert.throws(() => validateExtensionDependencies(['Publisher.extension'], 'extensionDependencies'));
159+
assert.throws(() => validateExtensionDependencies(['publisher.Extension'], 'extensionDependencies'));
160+
assert.throws(() => validateExtensionDependencies(['Publisher.Extension'], 'extensionDependencies'));
161+
assert.throws(() => validateExtensionDependencies(['PUBLISHER.EXTENSION'], 'extensionDependencies'));
162+
});
163+
164+
it('should reject mixed case in extension IDs', () => {
165+
assert.throws(() => validateExtensionDependencies(['MyPublisher.my-extension'], 'extensionDependencies'));
166+
assert.throws(() => validateExtensionDependencies(['my-publisher.MyExtension'], 'extensionDependencies'));
167+
});
168+
169+
it('should work with extensionPack field', () => {
170+
validateExtensionDependencies(['publisher.extension'], 'extensionPack');
171+
assert.throws(() => validateExtensionDependencies(['Publisher.Extension'], 'extensionPack'));
172+
});
173+
174+
it('should list all invalid dependencies in error message', () => {
175+
try {
176+
validateExtensionDependencies(
177+
['valid.extension', 'Invalid.Extension', 'another.Invalid', 'Another.Invalid'],
178+
'extensionDependencies'
179+
);
180+
assert.fail('Should have thrown an error');
181+
} catch (error: any) {
182+
assert.ok(error.message.includes('Invalid.Extension'));
183+
assert.ok(error.message.includes('Another.Invalid'));
184+
assert.ok(error.message.includes('another.Invalid'));
185+
assert.ok(!error.message.includes('valid.extension'));
186+
}
187+
});
188+
});

src/validation.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as semver from 'semver';
22
import parseSemver from 'parse-semver';
3+
import { log } from './util';
34

45
const nameRegex = /^[a-z0-9][a-z0-9\-]*$/i;
56

@@ -115,3 +116,37 @@ export function validateVSCodeTypesCompatibility(engineVersion: string, typeVers
115116
throw error;
116117
}
117118
}
119+
120+
/**
121+
* Validates that extension IDs use only lowercase letters.
122+
* This validation ensures compliance with VS Code extension marketplace requirements.
123+
*
124+
* Note: This only validates the case (lowercase). It does not validate the format
125+
* or structure of extension IDs (e.g., presence of dot separator, valid characters).
126+
*/
127+
export function validateExtensionDependencies(dependencies: string[] | undefined, fieldName: string): void {
128+
if (!dependencies || dependencies.length === 0) {
129+
return;
130+
}
131+
132+
const invalidDependencies: string[] = [];
133+
134+
for (const dep of dependencies) {
135+
// Check if extension ID uses only lowercase letters
136+
// Note: This does not validate the format of the extension ID itself
137+
if (dep !== dep.toLowerCase()) {
138+
invalidDependencies.push(dep);
139+
}
140+
141+
if (dep === 'github.copilot') {
142+
log.warn(`The "github.copilot" extension is being deprecated in favor of the "github.copilot-chat" extension. Please use "github.copilot-chat" in ${fieldName} instead.`);
143+
}
144+
}
145+
146+
if (invalidDependencies.length > 0) {
147+
const depList = invalidDependencies.map(d => `"${d}"`).join(', ');
148+
throw new Error(
149+
`The extension IDs in "${fieldName}" must use lowercase letters only. Invalid IDs: ${depList}.`
150+
);
151+
}
152+
}

0 commit comments

Comments
 (0)