support web extensions
This commit is contained in:
parent
9633988aa0
commit
3b10000ad4
6 changed files with 211 additions and 8 deletions
|
@ -76,7 +76,8 @@ module.exports = function (argv: string[]): void {
|
|||
.option('--yarn', 'Use yarn instead of npm')
|
||||
.option('--ignoreFile [path]', 'Indicate alternative .vscodeignore')
|
||||
.option('--noGitHubIssueLinking', 'Prevent automatic expansion of GitHub-style issue syntax into links')
|
||||
.action(({ out, githubBranch, baseContentUrl, baseImagesUrl, yarn, ignoreFile, noGitHubIssueLinking }) => main(packageCommand({ packagePath: out, githubBranch, baseContentUrl, baseImagesUrl, useYarn: yarn, ignoreFile, expandGitHubIssueLinks: noGitHubIssueLinking })));
|
||||
.option('--web', 'Enable packing as web extension. Note: This is a preview feature and is supported only for selected extensions.')
|
||||
.action(({ out, githubBranch, baseContentUrl, baseImagesUrl, yarn, ignoreFile, noGitHubIssueLinking, web }) => main(packageCommand({ packagePath: out, githubBranch, baseContentUrl, baseImagesUrl, useYarn: yarn, ignoreFile, expandGitHubIssueLinks: noGitHubIssueLinking, web })));
|
||||
|
||||
program
|
||||
.command('publish [<version>]')
|
||||
|
@ -90,7 +91,8 @@ module.exports = function (argv: string[]): void {
|
|||
.option('--yarn', 'Use yarn instead of npm while packing extension files')
|
||||
.option('--noVerify')
|
||||
.option('--ignoreFile [path]', 'Indicate alternative .vscodeignore')
|
||||
.action((version, { pat, message, packagePath, githubBranch, baseContentUrl, baseImagesUrl, yarn, noVerify, ignoreFile }) => main(publish({ pat, commitMessage: message, version, packagePath, githubBranch, baseContentUrl, baseImagesUrl, useYarn: yarn, noVerify, ignoreFile })));
|
||||
.option('--web', 'Enable packing as web extension. Note: This is a preview feature and is supported only for selected extensions.')
|
||||
.action((version, { pat, message, packagePath, githubBranch, baseContentUrl, baseImagesUrl, yarn, noVerify, ignoreFile, web }) => main(publish({ pat, commitMessage: message, version, packagePath, githubBranch, baseContentUrl, baseImagesUrl, useYarn: yarn, noVerify, ignoreFile, web })));
|
||||
|
||||
program
|
||||
.command('unpublish [<extensionid>]')
|
||||
|
|
|
@ -21,6 +21,8 @@ export interface Contributions {
|
|||
[contributionType: string]: any;
|
||||
}
|
||||
|
||||
export type ExtensionKind = 'ui' | 'workspace' | 'web';
|
||||
|
||||
export interface Manifest {
|
||||
// mandatory (npm)
|
||||
name: string;
|
||||
|
@ -42,7 +44,7 @@ export interface Manifest {
|
|||
_testing?: string;
|
||||
enableProposedApi?: boolean;
|
||||
qna?: 'marketplace' | string | false;
|
||||
extensionKind?: string[];
|
||||
extensionKind?: ExtensionKind | ExtensionKind[];
|
||||
|
||||
// optional (npm)
|
||||
author?: string | Person;
|
||||
|
@ -55,6 +57,7 @@ export interface Manifest {
|
|||
license?: string;
|
||||
contributors?: string | Person[];
|
||||
main?: string;
|
||||
browser?: string;
|
||||
repository?: string | { type?: string; url?: string; };
|
||||
scripts?: { [name: string]: string; };
|
||||
dependencies?: { [name: string]: string; };
|
||||
|
|
103
src/package.ts
103
src/package.ts
|
@ -3,7 +3,7 @@ import * as path from 'path';
|
|||
import * as cp from 'child_process';
|
||||
import * as _ from 'lodash';
|
||||
import * as yazl from 'yazl';
|
||||
import { Manifest } from './manifest';
|
||||
import { ExtensionKind, Manifest } from './manifest';
|
||||
import { ITranslations, patchNLS } from './nls';
|
||||
import * as util from './util';
|
||||
import * as _glob from 'glob';
|
||||
|
@ -16,6 +16,7 @@ import { lookup } from 'mime';
|
|||
import * as urljoin from 'url-join';
|
||||
import { validatePublisher, validateExtensionName, validateVersion, validateEngineCompatibility, validateVSCodeTypesCompatibility } from './validation';
|
||||
import { getDependencies } from './npm';
|
||||
import { IExtensionsReport } from './publicgalleryapi';
|
||||
|
||||
const readFile = denodeify<string, string, string>(fs.readFile);
|
||||
const unlink = denodeify<string, void>(fs.unlink as any);
|
||||
|
@ -66,6 +67,7 @@ export interface IPackageOptions {
|
|||
dependencyEntryPoints?: string[];
|
||||
ignoreFile?: string;
|
||||
expandGitHubIssueLinks?: boolean;
|
||||
web?: boolean;
|
||||
}
|
||||
|
||||
export interface IProcessor {
|
||||
|
@ -541,7 +543,7 @@ export class ChangelogProcessor extends MarkdownProcessor {
|
|||
class LicenseProcessor extends BaseProcessor {
|
||||
|
||||
private didFindLicense = false;
|
||||
private filter: (name: string) => boolean;
|
||||
filter: (name: string) => boolean;
|
||||
|
||||
constructor(manifest: Manifest) {
|
||||
super(manifest);
|
||||
|
@ -609,6 +611,89 @@ class IconProcessor extends BaseProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
export function isSupportedWebExtension(manifest: Manifest, extensionsReport: IExtensionsReport): boolean {
|
||||
const id = `${manifest.publisher}.${manifest.name}`;
|
||||
return extensionsReport.web.publishers.some(publisher => manifest.publisher === publisher)
|
||||
|| extensionsReport.web.extensions.some(extension => extension === id);
|
||||
}
|
||||
|
||||
export function isWebKind(manifest: Manifest): boolean {
|
||||
const extensionKind = getExtensionKind(manifest);
|
||||
return extensionKind.some(kind => kind === 'web');
|
||||
}
|
||||
|
||||
const workspaceExtensionPoints: string[] = ['terminal', 'debuggers', 'jsonValidation'];
|
||||
|
||||
function getExtensionKind(manifest: Manifest): ExtensionKind[] {
|
||||
// check the manifest
|
||||
if (manifest.extensionKind) {
|
||||
return Array.isArray(manifest.extensionKind)
|
||||
? manifest.extensionKind
|
||||
: manifest.extensionKind === 'ui' ? ['ui', 'workspace'] : [manifest.extensionKind]
|
||||
}
|
||||
|
||||
// Not an UI extension if it has main
|
||||
if (manifest.main) {
|
||||
if (manifest.browser) {
|
||||
return ['workspace', 'web'];
|
||||
}
|
||||
return ['workspace'];
|
||||
}
|
||||
|
||||
if (manifest.browser) {
|
||||
return ['web'];
|
||||
}
|
||||
|
||||
const isNonEmptyArray = obj => Array.isArray(obj) && obj.length > 0;
|
||||
// Not an UI nor web extension if it has dependencies or an extension pack
|
||||
if (isNonEmptyArray(manifest.extensionDependencies) || isNonEmptyArray(manifest.extensionPack)) {
|
||||
return ['workspace'];
|
||||
}
|
||||
|
||||
if (manifest.contributes) {
|
||||
// Not an UI nor web extension if it has workspace contributions
|
||||
for (const contribution of Object.keys(manifest.contributes)) {
|
||||
if (workspaceExtensionPoints.indexOf(contribution) !== -1) {
|
||||
return ['workspace'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['ui', 'workspace', 'web'];
|
||||
}
|
||||
|
||||
export class WebExtensionProcessor extends BaseProcessor {
|
||||
|
||||
private readonly isWebKind: boolean = false;
|
||||
private readonly licenseProcessor: LicenseProcessor;
|
||||
|
||||
constructor(manifest: Manifest, options: IPackageOptions) {
|
||||
super(manifest);
|
||||
this.isWebKind = options.web && isWebKind(manifest);
|
||||
this.licenseProcessor = new LicenseProcessor(manifest);
|
||||
}
|
||||
|
||||
onFile(file: IFile): Promise<IFile> {
|
||||
if (this.isWebKind) {
|
||||
const path = util.normalize(file.path);
|
||||
if (/\.svg$/i.test(path)) {
|
||||
throw new Error(`SVGs can't be used in web extensions: ${path}`);
|
||||
}
|
||||
if (
|
||||
!/^extension\/readme.md$/i.test(path) // exclude read me
|
||||
&& !/^extension\/changelog.md$/i.test(path) // exclude changelog
|
||||
&& !/^extension\/package.json$/i.test(path) // exclude package.json
|
||||
&& !this.licenseProcessor.filter(path) // exclude licenses
|
||||
&& !/^extension\/*node_modules\/*/i.test(path) // exclude node_modules
|
||||
) {
|
||||
this.assets.push({ type: `Microsoft.VisualStudio.Code.WebResources/${path}`, path });
|
||||
}
|
||||
}
|
||||
return Promise.resolve(file);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class NLSProcessor extends BaseProcessor {
|
||||
|
||||
private translations: { [path: string]: string } = Object.create(null);
|
||||
|
@ -870,6 +955,11 @@ export function processFiles(processors: IProcessor[], files: IFile[]): Promise<
|
|||
return Promise.all(processedFiles).then(files => {
|
||||
return util.sequence(processors.map(p => () => p.onEnd())).then(() => {
|
||||
const assets = _.flatten(processors.map(p => p.assets));
|
||||
|
||||
if (assets.length >= 50) {
|
||||
throw new Error('Cannot have more than 50 assets');
|
||||
}
|
||||
|
||||
const vsix = processors.reduce((r, p) => ({ ...r, ...p.vsix }), { assets });
|
||||
|
||||
return Promise.all([toVsixManifest(vsix), toContentTypes(files)]).then(result => {
|
||||
|
@ -892,6 +982,7 @@ export function createDefaultProcessors(manifest: Manifest, options: IPackageOpt
|
|||
new LicenseProcessor(manifest),
|
||||
new IconProcessor(manifest),
|
||||
new NLSProcessor(manifest),
|
||||
new WebExtensionProcessor(manifest, options),
|
||||
new ValidationProcessor(manifest)
|
||||
];
|
||||
}
|
||||
|
@ -968,6 +1059,14 @@ export async function pack(options: IPackageOptions = {}): Promise<IPackageResul
|
|||
const cwd = options.cwd || process.cwd();
|
||||
|
||||
const manifest = await readManifest(cwd);
|
||||
|
||||
if (options.web && isWebKind(manifest)) {
|
||||
const extensionsReport = await util.getPublicGalleryAPI().getExtensionsReport();
|
||||
if (!isSupportedWebExtension(manifest, extensionsReport)) {
|
||||
throw new Error(`Cannot pack as web extension as it is not supported`);
|
||||
}
|
||||
}
|
||||
|
||||
await prepublish(cwd, manifest, options.useYarn);
|
||||
|
||||
const files = await collect(manifest, options);
|
||||
|
|
|
@ -11,9 +11,18 @@ export interface ExtensionQuery {
|
|||
readonly assetTypes?: string[];
|
||||
}
|
||||
|
||||
export interface IExtensionsReport {
|
||||
malicious: string[];
|
||||
web: {
|
||||
publishers: string[],
|
||||
extensions: string[],
|
||||
};
|
||||
}
|
||||
|
||||
export class PublicGalleryAPI {
|
||||
|
||||
private client = new HttpClient('vsce');
|
||||
private readonly extensionsReportUrl = 'https://az764295.vo.msecnd.net/extensions/marketplace.json';
|
||||
private readonly client = new HttpClient('vsce');
|
||||
|
||||
constructor(private baseUrl: string, private apiVersion = '3.0-preview.1') { }
|
||||
|
||||
|
@ -45,4 +54,13 @@ export class PublicGalleryAPI {
|
|||
const extensions = await this.extensionQuery(query);
|
||||
return extensions.filter(({ publisher: { publisherName: publisher }, extensionName: name }) => extensionId.toLowerCase() === `${publisher}.${name}`.toLowerCase())[0];
|
||||
}
|
||||
|
||||
async getExtensionsReport(): Promise<IExtensionsReport> {
|
||||
const res = await this.client.get(this.extensionsReportUrl);
|
||||
const raw = <Partial<IExtensionsReport>>JSON.parse(await res.readBody());
|
||||
return {
|
||||
malicious: raw.malicious || [],
|
||||
web: raw.web || { publishers: [], extensions: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,6 +98,7 @@ export interface IPublishOptions {
|
|||
useYarn?: boolean;
|
||||
noVerify?: boolean;
|
||||
ignoreFile?: string;
|
||||
web?: boolean;
|
||||
}
|
||||
|
||||
async function versionBump(cwd: string = process.cwd(), version?: string, commitMessage?: string): Promise<void> {
|
||||
|
@ -162,10 +163,11 @@ export function publish(options: IPublishOptions = {}): Promise<any> {
|
|||
const baseImagesUrl = options.baseImagesUrl;
|
||||
const useYarn = options.useYarn;
|
||||
const ignoreFile = options.ignoreFile;
|
||||
const web = options.web;
|
||||
|
||||
promise = versionBump(options.cwd, options.version, options.commitMessage)
|
||||
.then(() => tmpName())
|
||||
.then(packagePath => pack({ packagePath, cwd, githubBranch, baseContentUrl, baseImagesUrl, useYarn, ignoreFile }));
|
||||
.then(packagePath => pack({ packagePath, cwd, githubBranch, baseContentUrl, baseImagesUrl, useYarn, ignoreFile, web }));
|
||||
}
|
||||
|
||||
return promise.then(({ manifest, packagePath }) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
readManifest, collect, toContentTypes, ReadmeProcessor,
|
||||
read, processFiles, createDefaultProcessors,
|
||||
toVsixManifest, IFile, validateManifest
|
||||
toVsixManifest, IFile, validateManifest, isSupportedWebExtension, WebExtensionProcessor, IAsset
|
||||
} from '../package';
|
||||
import { Manifest } from '../manifest';
|
||||
import * as path from 'path';
|
||||
|
@ -10,6 +10,7 @@ import * as assert from 'assert';
|
|||
import { parseString } from 'xml2js';
|
||||
import * as denodeify from 'denodeify';
|
||||
import * as _ from 'lodash';
|
||||
import { IExtensionsReport } from '../publicgalleryapi';
|
||||
|
||||
// don't warn in tests
|
||||
console.warn = () => null;
|
||||
|
@ -1803,3 +1804,81 @@ describe('MarkdownProcessor', () => {
|
|||
await throws(() => processor.onFile(readme));
|
||||
})
|
||||
});
|
||||
|
||||
describe('isSupportedWebExtension', () => {
|
||||
|
||||
it('should return true if extension report has extension', () => {
|
||||
const manifest = createManifest({ name: 'test', publisher: 'mocha' });
|
||||
const extensionReport: IExtensionsReport = { malicious: [], web: { extensions: ['mocha.test'], publishers: [] } };
|
||||
assert.ok(isSupportedWebExtension(manifest, extensionReport));
|
||||
});
|
||||
|
||||
it('should return true if extension report has publisher', () => {
|
||||
const manifest = createManifest({ name: 'test', publisher: 'mocha' });
|
||||
const extensionReport: IExtensionsReport = { malicious: [], web: { extensions: [], publishers: ['mocha'] } };
|
||||
assert.ok(isSupportedWebExtension(manifest, extensionReport));
|
||||
});
|
||||
|
||||
it('should return false if extension report does not has extension', () => {
|
||||
const manifest = createManifest({ name: 'test', publisher: 'mocha' });
|
||||
const extensionReport: IExtensionsReport = { malicious: [], web: { extensions: [], publishers: [] } };
|
||||
assert.ok(!isSupportedWebExtension(manifest, extensionReport));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('WebExtensionProcessor', () => {
|
||||
|
||||
it('should include browser file', () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
const file = { path: 'extension/browser.js', contents: '' };
|
||||
|
||||
processor.onFile(file);
|
||||
|
||||
const expected: IAsset[] = [{ type: `Microsoft.VisualStudio.Code.WebResources/${file.path}`, path: file.path }];
|
||||
assert.deepEqual(processor.assets, expected);
|
||||
});
|
||||
|
||||
it('should exclude manifest', () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
const manifestFile = { path: 'extension/package.json', contents: JSON.stringify(manifest) };
|
||||
|
||||
processor.onFile(manifestFile);
|
||||
|
||||
assert.deepEqual(processor.assets, []);
|
||||
});
|
||||
|
||||
it('should exclude changelog', () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
const changelogFile = { path: 'extension/changelog.md', contents: '' };
|
||||
|
||||
processor.onFile(changelogFile);
|
||||
|
||||
assert.deepEqual(processor.assets, []);
|
||||
});
|
||||
|
||||
it('should exclude readme', () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
const readMeFile = { path: 'extension/readme.md', contents: '' };
|
||||
|
||||
processor.onFile(readMeFile);
|
||||
|
||||
assert.deepEqual(processor.assets, []);
|
||||
});
|
||||
|
||||
it('should exclude files from node_modules', () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
|
||||
processor.onFile({ path: 'extension/node_modules/sample.t.ds', contents: '' });
|
||||
processor.onFile({ path: 'extension/node_modules/a/sample.js', contents: '' });
|
||||
processor.onFile({ path: 'extension/node_modules/a/b/c/sample.js', contents: '' });
|
||||
|
||||
assert.deepEqual(processor.assets, []);
|
||||
});
|
||||
|
||||
});
|
Loading…
Add table
Reference in a new issue