Compare commits
145 commits
azure-pipe
...
master
Author | SHA1 | Date | |
---|---|---|---|
9ccf7e2778 | |||
0d3899d745 | |||
ed9dceb8e8 | |||
ed4c3aaaec | |||
f2c83c1bc3 | |||
|
5ade7bfa42 | ||
|
d971fb692b | ||
|
6b9d1f3f11 | ||
|
2cd3c14d9d | ||
|
789e14b31a | ||
|
d3c2ffc5f4 | ||
|
d15456e941 | ||
|
4b42b6ce38 | ||
|
74172a1b80 | ||
|
fb9170a707 | ||
|
665047b6d2 | ||
|
281482632c | ||
|
4a019da349 | ||
|
bb205c01d0 | ||
|
dc49ab513c | ||
|
3b10000ad4 | ||
|
9633988aa0 | ||
|
bf11c09458 | ||
|
728b7930bc | ||
|
fc75efa9ad | ||
|
16f35b3485 | ||
|
638e34f818 | ||
|
960c841778 | ||
|
b95a11bf0d | ||
|
f61ae38395 | ||
|
2a6d88d7ba | ||
|
820c01424e | ||
|
ad67f3554a | ||
|
41ef704b57 | ||
|
b57d6a60a0 | ||
|
96e7e42bb4 | ||
|
600505a065 | ||
|
6adca5365f | ||
|
a4cca08746 | ||
|
1d582dccc9 | ||
|
2929c77b3a | ||
|
d1609eea71 | ||
|
ba42d4cb40 | ||
|
07ea892302 | ||
|
4960c7c674 | ||
|
dc36c1f16b | ||
|
48e02a8773 | ||
|
68e143f621 | ||
|
33e72f99ca | ||
|
3dc84fbd22 | ||
|
48702544df | ||
|
9a66cdca45 | ||
|
18845b1ea5 | ||
|
2dec836b11 | ||
|
e287784228 | ||
|
f3e7284f8e | ||
|
ff9ce089ec | ||
|
03a95d4886 | ||
|
2be9d7def6 | ||
|
3d7afdded1 | ||
|
fbebf95182 | ||
|
2a6d2e6cea | ||
|
66a41f19ab | ||
|
acd1c3c96b | ||
|
368ffbd535 | ||
|
ee42cf42c0 | ||
|
032cca7738 | ||
|
32d7b80e1d | ||
|
285c1844d0 | ||
|
2ca36aa7fc | ||
|
e4a8df59d3 | ||
|
7eba451e35 | ||
|
6e5c2d5488 | ||
|
ef3322b35a | ||
|
2a02458bad | ||
|
2844e5bed5 | ||
|
845eb79541 | ||
|
e0d73c9b8e | ||
|
2e003b0dc5 | ||
|
4302d01253 | ||
|
2d8dc608e8 | ||
|
f2c29b5440 | ||
|
3bcc6039da | ||
|
aa73aacb0e | ||
|
3fb175aa64 | ||
|
392778b924 | ||
|
e90ffca8ba | ||
|
955e3df4d3 | ||
|
facdf0ac8d | ||
|
8ae7985365 | ||
|
b044011b63 | ||
|
56f916b4fc | ||
|
c5a0e103fc | ||
|
33e3227970 | ||
|
157c01c4a7 | ||
|
d0693862cf | ||
|
282b740301 | ||
|
022b175b0c | ||
|
8455460dbc | ||
|
e80056d64e | ||
|
fca4c7ce19 | ||
|
107f68bfbe | ||
|
a154e8ce27 | ||
|
4903798070 | ||
|
a87a725c30 | ||
|
226554637c | ||
|
1bc8c65547 | ||
|
c6fe5fd5e4 | ||
|
6bb1d40a1b | ||
|
830843f54a | ||
|
f6be2f032f | ||
|
84f537a4fa | ||
|
2303cfafa3 | ||
|
c770da590b | ||
|
fecd9f311d | ||
|
6b344de515 | ||
|
188c304780 | ||
|
bef8e1c7ac | ||
|
79289884af | ||
|
82ce52726f | ||
|
416ed1929b | ||
|
e794552b45 | ||
|
29b7072a88 | ||
|
d999140b44 | ||
|
e8d8304384 | ||
|
6a64c4f294 | ||
|
5d2af9c38a | ||
|
fba5037db2 | ||
|
744a8c64a0 | ||
|
d90d09919f | ||
|
2bf0f3317a | ||
|
ec8398369c | ||
|
cff60534fc | ||
|
18b4584abe | ||
|
786118d097 | ||
|
bf41e290fc | ||
|
c3cfc3a337 | ||
|
b063a7ba7f | ||
|
5477a63e02 | ||
|
778b942b38 | ||
|
fcbfc91cad | ||
|
4c26522fde | ||
|
00b07a2b38 | ||
|
0c526b3e4a | ||
|
ec03eac9eb |
31 changed files with 3137 additions and 1793 deletions
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
|
@ -8,13 +8,14 @@
|
|||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
// "cwd": "<absolute path to your extension>",
|
||||
"program": "${workspaceFolder}/out/vsce",
|
||||
"args": [
|
||||
"--version"
|
||||
// "ls", "package", "publish"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"outputCapture": "std",
|
||||
"preLaunchTask": "compile"
|
||||
"outputCapture": "std"
|
||||
}
|
||||
]
|
||||
}
|
24
.vscode/tasks.json
vendored
24
.vscode/tasks.json
vendored
|
@ -1,25 +1,23 @@
|
|||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "compile",
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": [
|
||||
"$tsc-watch"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"command": "npm",
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"args": [
|
||||
"run",
|
||||
"compile",
|
||||
"--loglevel",
|
||||
"silent"
|
||||
],
|
||||
"problemMatcher": [],
|
||||
"isBackground": true
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test"
|
||||
}
|
||||
]
|
||||
}
|
37
README.md
37
README.md
|
@ -1,13 +1,44 @@
|
|||
# vsce
|
||||
|
||||
> *The Visual Studio Code Extension Manager*
|
||||
|
||||
[![build status](https://travis-ci.org/Microsoft/vscode-vsce.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-vsce)
|
||||
[![Build Status](https://dev.azure.com/vscode/VSCE/_apis/build/status/VSCE?branchName=master)](https://dev.azure.com/vscode/VSCE/_build/latest?definitionId=16&branchName=master) [![npm version](https://badge.fury.io/js/vsce.svg)](https://badge.fury.io/js/vsce)
|
||||
|
||||
### Requirements
|
||||
## Requirements
|
||||
|
||||
- [Node.js](https://nodejs.org/en/) at least `8.x.x`
|
||||
|
||||
### About
|
||||
## Usage
|
||||
|
||||
`vsce` is meant to be mainly used as a command line tool. It can also be used a library since it exposes a small [API](https://github.com/microsoft/vscode-vsce/blob/master/src/api.ts).
|
||||
|
||||
> **Warning:** When using vsce as a library be sure to sanitize any user input used in API calls, as a security measure.
|
||||
|
||||
## Development
|
||||
|
||||
First clone this repository, then:
|
||||
|
||||
```sh
|
||||
yarn
|
||||
yarn watch # or `watch-test` to also run tests
|
||||
```
|
||||
|
||||
Once the watcher is up and running, you can run out of sources with:
|
||||
|
||||
```sh
|
||||
yarn vsce
|
||||
```
|
||||
|
||||
### Publish to NPM
|
||||
|
||||
Simply push a new tag and the CI will automatically publish to NPM. The usual flow is:
|
||||
|
||||
```sh
|
||||
npm version [minor|patch]
|
||||
git push --follow-tags
|
||||
```
|
||||
|
||||
## About
|
||||
|
||||
This tool assists in packaging and publishing Visual Studio Code extensions.
|
||||
|
||||
|
|
|
@ -7,11 +7,11 @@ trigger:
|
|||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: "10.15.1"
|
||||
versionSpec: "8.x"
|
||||
|
||||
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
|
||||
inputs:
|
||||
versionSpec: "1.10.1"
|
||||
versionSpec: "1.x"
|
||||
|
||||
- script: yarn
|
||||
displayName: Install Dependencies
|
||||
|
@ -20,4 +20,12 @@ steps:
|
|||
displayName: Compile
|
||||
|
||||
- script: yarn test
|
||||
displayName: Run Tests
|
||||
displayName: Run Tests
|
||||
|
||||
- task: Npm@1
|
||||
displayName: 'Publish to NPM'
|
||||
inputs:
|
||||
command: publish
|
||||
verbose: false
|
||||
publishEndpoint: 'NPM'
|
||||
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))
|
36
package.json
36
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "vsce",
|
||||
"version": "1.63.0",
|
||||
"name": "@entan.gl/vsce",
|
||||
"version": "1.79.6",
|
||||
"description": "VSCode Extension Manager",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -24,11 +24,13 @@
|
|||
"vsce": "out/vsce"
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "tsc && cpx src/vsce out",
|
||||
"watch": "concurrently \"tsc --watch\" \"cpx --watch src/vsce out\"",
|
||||
"watch-test": "concurrently \"tsc --watch\" \"cpx --watch src/vsce out\" \"mocha --watch\"",
|
||||
"copy-vsce": "mkdir -p out && cp src/vsce out/vsce",
|
||||
"compile": "tsc && npm run copy-vsce",
|
||||
"watch": "npm run copy-vsce && tsc --watch",
|
||||
"watch-test": "npm run copy-vsce && concurrently \"tsc --watch\" \"mocha --watch\"",
|
||||
"test": "mocha",
|
||||
"prepublishOnly": "tsc && cpx src/vsce out && mocha"
|
||||
"prepublishOnly": "tsc && npm run copy-vsce && mocha",
|
||||
"vsce": "out/vsce"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
|
@ -39,10 +41,10 @@
|
|||
"cheerio": "^1.0.0-rc.1",
|
||||
"commander": "^2.8.1",
|
||||
"denodeify": "^1.2.1",
|
||||
"didyoumean": "^1.2.1",
|
||||
"glob": "^7.0.6",
|
||||
"lodash": "^4.17.10",
|
||||
"markdown-it": "^8.3.1",
|
||||
"leven": "^3.1.0",
|
||||
"lodash": "^4.17.15",
|
||||
"markdown-it": "^10.0.0",
|
||||
"mime": "^1.3.4",
|
||||
"minimatch": "^3.0.3",
|
||||
"osenv": "^0.1.3",
|
||||
|
@ -58,23 +60,27 @@
|
|||
"devDependencies": {
|
||||
"@types/cheerio": "^0.22.1",
|
||||
"@types/denodeify": "^1.2.31",
|
||||
"@types/didyoumean": "^1.2.0",
|
||||
"@types/glob": "^7.1.1",
|
||||
"@types/lodash": "^4.14.123",
|
||||
"@types/markdown-it": "0.0.2",
|
||||
"@types/mime": "^1",
|
||||
"@types/minimatch": "^3.0.3",
|
||||
"@types/mocha": "^5.2.6",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/node": "^8",
|
||||
"@types/read": "^0.0.28",
|
||||
"@types/semver": "^6.0.0",
|
||||
"@types/tmp": "^0.1.0",
|
||||
"@types/xml2js": "^0.4.4",
|
||||
"concurrently": "^4.1.0",
|
||||
"cpx": "^1.5.0",
|
||||
"mocha": "^5.2.0",
|
||||
"concurrently": "^5.1.0",
|
||||
"mocha": "^7.1.1",
|
||||
"source-map-support": "^0.4.2",
|
||||
"typescript": "^3.4.3",
|
||||
"xml2js": "^0.4.12"
|
||||
},
|
||||
"mocha": {
|
||||
"require": [
|
||||
"source-map-support/register"
|
||||
],
|
||||
"spec": "out/test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
1301
pnpm-lock.yaml
generated
Normal file
1301
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -12,6 +12,7 @@
|
|||
<Property Id="Microsoft.VisualStudio.Code.Engine" Value="<%- engine %>" />
|
||||
<Property Id="Microsoft.VisualStudio.Code.ExtensionDependencies" Value="<%- extensionDependencies %>" />
|
||||
<Property Id="Microsoft.VisualStudio.Code.ExtensionPack" Value="<%- extensionPack %>" />
|
||||
<Property Id="Microsoft.VisualStudio.Code.ExtensionKind" Value="<%- extensionKind %>" />
|
||||
<Property Id="Microsoft.VisualStudio.Code.LocalizedLanguages" Value="<%- localizedLanguages %>" />
|
||||
<% if (links.repository) { %>
|
||||
<Property Id="Microsoft.VisualStudio.Services.Links.Source" Value="<%- links.repository %>" />
|
||||
|
@ -29,6 +30,7 @@
|
|||
<Property Id="Microsoft.VisualStudio.Services.GitHubFlavoredMarkdown" Value="<%- githubMarkdown %>" />
|
||||
<% if (typeof enableMarketplaceQnA === 'boolean') { %><Property Id="Microsoft.VisualStudio.Services.EnableMarketplaceQnA" Value="<%- enableMarketplaceQnA %>" /><% } %>
|
||||
<% if (customerQnALink) { %><Property Id="Microsoft.VisualStudio.Services.CustomerQnALink" Value="<%- customerQnALink %>" /><% } %>
|
||||
<% if (typeof webExtension === 'boolean') { %><Property Id="Microsoft.VisualStudio.Code.WebExtension" Value="<%- webExtension %>" /><% } %>
|
||||
</Properties>
|
||||
<% if (license) { %><License><%- license %></License><% } %>
|
||||
<% if (icon) { %><Icon><%- icon %></Icon><% } %>
|
||||
|
|
27
src/api.ts
27
src/api.ts
|
@ -30,6 +30,11 @@ export interface ICreateVSIXOptions {
|
|||
* Should use Yarn instead of NPM.
|
||||
*/
|
||||
useYarn?: boolean;
|
||||
|
||||
/**
|
||||
* Select the package manager to use
|
||||
*/
|
||||
usePackageManager?: "yarn" | "pnpm" | "npm";
|
||||
}
|
||||
|
||||
export interface IPublishOptions {
|
||||
|
@ -62,6 +67,11 @@ export interface IPublishOptions {
|
|||
* Should use Yarn instead of NPM.
|
||||
*/
|
||||
useYarn?: boolean;
|
||||
|
||||
/**
|
||||
* Select the package manager to use
|
||||
*/
|
||||
usePackageManager?: "yarn" | "pnpm" | "npm";
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,7 +79,8 @@ export interface IPublishOptions {
|
|||
*/
|
||||
export enum PackageManager {
|
||||
Npm,
|
||||
Yarn
|
||||
Yarn,
|
||||
Pnpm,
|
||||
}
|
||||
|
||||
export interface IListFilesOptions {
|
||||
|
@ -90,6 +101,13 @@ export interface IListFilesOptions {
|
|||
* no dependencies will be included.
|
||||
*/
|
||||
packagedDependencies?: string[];
|
||||
|
||||
/**
|
||||
* The location of an alternative .vscodeignore file to be used.
|
||||
* The `.vscodeignore` file located at the root of the project will be taken
|
||||
* instead, if none is specified.
|
||||
*/
|
||||
ignoreFile?: string;
|
||||
}
|
||||
|
||||
export interface IPublishVSIXOptions {
|
||||
|
@ -115,6 +133,11 @@ export interface IPublishVSIXOptions {
|
|||
* Should use Yarn instead of NPM.
|
||||
*/
|
||||
useYarn?: boolean;
|
||||
|
||||
/**
|
||||
* Select the package manager to use
|
||||
*/
|
||||
usePackageManager?: "yarn" | "pnpm" | "npm";
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -135,7 +158,7 @@ export function publish(options: IPublishOptions = {}): Promise<any> {
|
|||
* Lists the files included in the extension's package.
|
||||
*/
|
||||
export function listFiles(options: IListFilesOptions = {}): Promise<string[]> {
|
||||
return _listFiles(options.cwd, options.packageManager === PackageManager.Yarn, options.packagedDependencies);
|
||||
return _listFiles(options.cwd, options.packageManager === PackageManager.Yarn, <any>({ [PackageManager.Yarn]: "yarn", [PackageManager.Npm]: "npm", [PackageManager.Pnpm]: "pnpm" }[options.packageManager]), options.packagedDependencies, options.ignoreFile);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
43
src/main.ts
43
src/main.ts
|
@ -1,5 +1,5 @@
|
|||
import * as program from 'commander';
|
||||
import * as didYouMean from 'didyoumean';
|
||||
import * as leven from 'leven';
|
||||
|
||||
import { packageCommand, ls } from './package';
|
||||
import { publish, unpublish } from './publish';
|
||||
|
@ -7,12 +7,12 @@ import { show } from './show';
|
|||
import { search } from './search';
|
||||
import { listPublishers, createPublisher, deletePublisher, loginPublisher, logoutPublisher } from './store';
|
||||
import { getLatestVersion } from './npm';
|
||||
import { CancellationToken, isCancelledError, log } from './util';
|
||||
import { CancellationToken, log } from './util';
|
||||
import * as semver from 'semver';
|
||||
import { isatty } from 'tty';
|
||||
const pkg = require('../package.json');
|
||||
|
||||
function fatal<T>(message: any, ...args: any[]): void {
|
||||
function fatal(message: any, ...args: any[]): void {
|
||||
if (message instanceof Error) {
|
||||
message = message.message;
|
||||
|
||||
|
@ -39,14 +39,14 @@ function main(task: Promise<any>): void {
|
|||
if (isatty(1)) {
|
||||
getLatestVersion(pkg.name, token)
|
||||
.then(version => latestVersion = version)
|
||||
.catch(err => !isCancelledError(err) && log.error(err));
|
||||
.catch(_ => { /* noop */ });
|
||||
}
|
||||
|
||||
task
|
||||
.catch(fatal)
|
||||
.then(() => {
|
||||
if (latestVersion && semver.gt(latestVersion, pkg.version)) {
|
||||
log.warn(`\nThe latest version of ${pkg.name} is ${latestVersion} and you have ${pkg.version}.\nUpdate it now: npm install -g ${pkg.name}`);
|
||||
log.info(`\nThe latest version of ${pkg.name} is ${latestVersion} and you have ${pkg.version}.\nUpdate it now: npm install -g ${pkg.name}`);
|
||||
} else {
|
||||
token.cancel();
|
||||
}
|
||||
|
@ -62,35 +62,46 @@ module.exports = function (argv: string[]): void {
|
|||
.command('ls')
|
||||
.description('Lists all the files that will be published')
|
||||
.option('--yarn', 'Use yarn instead of npm')
|
||||
.option('--packageManager [yarn|npm|pnpm]', 'Use yarn, npm, or pnpm package manager')
|
||||
.option('--packagedDependencies <path>', 'Select packages that should be published only (includes dependencies)', (val, all) => all ? all.concat(val) : [val], undefined)
|
||||
.action(({ yarn, packagedDependencies }) => main(ls(undefined, yarn, packagedDependencies)));
|
||||
.option('--ignoreFile [path]', 'Indicate alternative .vscodeignore')
|
||||
.action(({ yarn, packagedDependencies, ignoreFile, packageManager }) => main(ls(undefined, yarn, packagedDependencies, ignoreFile, packageManager)));
|
||||
|
||||
program
|
||||
.command('package')
|
||||
.description('Packages an extension')
|
||||
.option('-o, --out [path]', 'Output .vsix extension file to [path] location')
|
||||
.option('--githubBranch [branch]', 'The GitHub branch used to infer relative links in README.md. Can be overriden by --baseContentUrl and --baseImagesUrl.')
|
||||
.option('--baseContentUrl [url]', 'Prepend all relative links in README.md with this url.')
|
||||
.option('--baseImagesUrl [url]', 'Prepend all relative image links in README.md with this url.')
|
||||
.option('--yarn', 'Use yarn instead of npm')
|
||||
.action(({ out, baseContentUrl, baseImagesUrl, yarn }) => main(packageCommand({ packagePath: out, baseContentUrl, baseImagesUrl, useYarn: yarn })));
|
||||
.option('--packageManager [yarn|npm|pnpm]', 'Use yarn, npm, or pnpm package manager')
|
||||
.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, packageManager }) => main(packageCommand({ packagePath: out, githubBranch, baseContentUrl, baseImagesUrl, useYarn: yarn, ignoreFile, expandGitHubIssueLinks: noGitHubIssueLinking, usePackageManager: packageManager })));
|
||||
|
||||
program
|
||||
.command('publish [<version>]')
|
||||
.description('Publishes an extension')
|
||||
.option('-p, --pat <token>', 'Personal Access Token')
|
||||
.option('-p, --pat <token>', 'Personal Access Token', process.env['VSCE_PAT'])
|
||||
.option('-m, --message <commit message>', 'Commit message used when calling `npm version`.')
|
||||
.option('--packagePath [path]', 'Publish the VSIX package located at the specified path.')
|
||||
.option('--githubBranch [branch]', 'The GitHub branch used to infer relative links in README.md. Can be overriden by --baseContentUrl and --baseImagesUrl.')
|
||||
.option('--baseContentUrl [url]', 'Prepend all relative links in README.md with this url.')
|
||||
.option('--baseImagesUrl [url]', 'Prepend all relative image links in README.md with this url.')
|
||||
.option('--yarn', 'Use yarn instead of npm while packing extension files')
|
||||
.option('--packageManager [yarn|npm|pnpm]', 'Use yarn, npm, or pnpm package manager')
|
||||
.option('--noVerify')
|
||||
.action((version, { pat, message, packagePath, baseContentUrl, baseImagesUrl, yarn, noVerify }) => main(publish({ pat, commitMessage: message, version, packagePath, baseContentUrl, baseImagesUrl, useYarn: yarn, noVerify })));
|
||||
.option('--ignoreFile [path]', 'Indicate alternative .vscodeignore')
|
||||
.option('--web', 'Experimental flag to enable publishing web extensions. Note: This is supported only for selected extensions.')
|
||||
.action((version, { pat, message, packagePath, githubBranch, baseContentUrl, baseImagesUrl, yarn, noVerify, ignoreFile, web, packageManager }) => main(publish({ pat, commitMessage: message, version, packagePath, githubBranch, baseContentUrl, baseImagesUrl, useYarn: yarn, noVerify, ignoreFile, web, usePackageManager: packageManager })));
|
||||
|
||||
program
|
||||
.command('unpublish [<extensionid>]')
|
||||
.description('Unpublishes an extension. Example extension id: microsoft.csharp.')
|
||||
.option('-p, --pat <token>', 'Personal Access Token')
|
||||
.action((id, { pat }) => main(unpublish({ id, pat })));
|
||||
.option('-f, --force', 'Forces Unpublished Extension')
|
||||
.action((id, { pat, force }) => main(unpublish({ id, pat, force })));
|
||||
|
||||
program
|
||||
.command('ls-publishers')
|
||||
|
@ -133,19 +144,13 @@ module.exports = function (argv: string[]): void {
|
|||
.command('*', '', { noHelp: true })
|
||||
.action((cmd: string) => {
|
||||
program.help(help => {
|
||||
const suggestion = didYouMean(cmd, program.commands.map(c => c._name));
|
||||
const availableCommands = program.commands.map(c => c._name);
|
||||
const suggestion = availableCommands.find(c => leven(c, cmd) < c.length * 0.4);
|
||||
|
||||
help = `${help}
|
||||
Unknown command '${cmd}'`;
|
||||
|
||||
if (suggestion) {
|
||||
help = `${help}, did you mean '${suggestion}'?`;
|
||||
} else {
|
||||
help = `${help}.`;
|
||||
}
|
||||
|
||||
return `${help}
|
||||
`;
|
||||
return suggestion ? `${help}, did you mean '${suggestion}'?\n` : `${help}.\n`;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ export interface Contributions {
|
|||
[contributionType: string]: any;
|
||||
}
|
||||
|
||||
export type ExtensionKind = 'ui' | 'workspace' | 'web';
|
||||
|
||||
export interface Manifest {
|
||||
// mandatory (npm)
|
||||
name: string;
|
||||
|
@ -42,6 +44,7 @@ export interface Manifest {
|
|||
_testing?: string;
|
||||
enableProposedApi?: boolean;
|
||||
qna?: 'marketplace' | string | false;
|
||||
extensionKind?: ExtensionKind | ExtensionKind[];
|
||||
|
||||
// optional (npm)
|
||||
author?: string | Person;
|
||||
|
@ -54,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; };
|
||||
|
|
97
src/npm.ts
97
src/npm.ts
|
@ -3,7 +3,7 @@ import * as fs from 'fs';
|
|||
import * as cp from 'child_process';
|
||||
import * as parseSemver from 'parse-semver';
|
||||
import * as _ from 'lodash';
|
||||
import { CancellationToken, log } from './util';
|
||||
import { CancellationToken } from './util';
|
||||
|
||||
interface IOptions {
|
||||
cwd?: string;
|
||||
|
@ -54,7 +54,7 @@ function checkNPM(cancellationToken?: CancellationToken): Promise<void> {
|
|||
|
||||
function getNpmDependencies(cwd: string): Promise<string[]> {
|
||||
return checkNPM()
|
||||
.then(() => exec('npm list --production --parseable --depth=99999', { cwd, maxBuffer: 5000 * 1024 }))
|
||||
.then(() => exec('npm list --production --parseable --depth=99999 --loglevel=error', { cwd, maxBuffer: 5000 * 1024 }))
|
||||
.then(({ stdout }) => stdout
|
||||
.split(/[\r\n]/)
|
||||
.filter(dir => path.isAbsolute(dir)));
|
||||
|
@ -67,25 +67,29 @@ interface YarnTreeNode {
|
|||
|
||||
export interface YarnDependency {
|
||||
name: string;
|
||||
version: string;
|
||||
path: string;
|
||||
children: YarnDependency[];
|
||||
}
|
||||
|
||||
interface PnpmTreeNode {
|
||||
name: string;
|
||||
from: string;
|
||||
version: string;
|
||||
dependencies: { [key:string]:PnpmTreeNode };
|
||||
}
|
||||
|
||||
function asYarnDependency(prefix: string, tree: YarnTreeNode, prune: boolean): YarnDependency | null {
|
||||
if (prune && /@[\^~]/.test(tree.name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let name: string, version: string;
|
||||
let name: string;
|
||||
|
||||
try {
|
||||
const parseResult = parseSemver(tree.name);
|
||||
name = parseResult.name;
|
||||
version = parseResult.version;
|
||||
} catch (err) {
|
||||
log.error('Failed to parse dependency:', tree.name);
|
||||
return null;
|
||||
name = tree.name.replace(/^([^@+])@.*$/, '$1');
|
||||
}
|
||||
|
||||
const dependencyPath = path.join(prefix, name);
|
||||
|
@ -99,7 +103,30 @@ function asYarnDependency(prefix: string, tree: YarnTreeNode, prune: boolean): Y
|
|||
}
|
||||
}
|
||||
|
||||
return { name, version, path: dependencyPath, children };
|
||||
return { name, path: dependencyPath, children };
|
||||
}
|
||||
|
||||
async function asPnpmDependency(prefix: string, tree: PnpmTreeNode, prune: boolean): Promise<YarnDependency | null> {
|
||||
if (prune && /^[\^~]/.test(tree.version)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let name = tree.name || tree.from;
|
||||
|
||||
const dependencyPath = path.join(prefix, name);
|
||||
const children: YarnDependency[] = [];
|
||||
|
||||
const deps = await Promise.all(
|
||||
_.values(tree.dependencies || {})
|
||||
.map(child => asPnpmDependency(prefix, child, prune))
|
||||
);
|
||||
for(const dep of deps) {
|
||||
if (dep) {
|
||||
children.push(dep);
|
||||
}
|
||||
}
|
||||
|
||||
return { name, path: dependencyPath, children };
|
||||
}
|
||||
|
||||
function selectYarnDependencies(deps: YarnDependency[], packagedDependencies: string[]): YarnDependency[] {
|
||||
|
@ -148,6 +175,30 @@ function selectYarnDependencies(deps: YarnDependency[], packagedDependencies: st
|
|||
return reached.values;
|
||||
}
|
||||
|
||||
async function getPnpmProductionDependencies(cwd: string, packagedDependencies?: string[]): Promise<YarnDependency[]> {
|
||||
const raw = await new Promise<string>((c, e) => cp.exec('pnpm list --depth 1000000 --prod --json --silent', { cwd, encoding: 'utf8', env: { ...process.env }, maxBuffer: 5000 * 1024 }, (err, stdout) => err ? e(err) : c(stdout)));
|
||||
const match = /^\s*\[[\s\S]*\]\s*$/m.exec(raw);
|
||||
|
||||
if (!match || match.length !== 1) {
|
||||
throw new Error('Could not parse result of `pnpm list --json`' + raw);
|
||||
}
|
||||
|
||||
const usingPackagedDependencies = Array.isArray(packagedDependencies);
|
||||
const trees = _.values(JSON.parse(match[0])[0].dependencies) as PnpmTreeNode[];
|
||||
|
||||
let result = (
|
||||
await Promise.all(
|
||||
trees.map(tree => asPnpmDependency(path.join(cwd, 'node_modules'), tree, !usingPackagedDependencies))
|
||||
)
|
||||
).filter(dep => !!dep);
|
||||
|
||||
if (usingPackagedDependencies) {
|
||||
result = selectYarnDependencies(result, packagedDependencies);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function getYarnProductionDependencies(cwd: string, packagedDependencies?: string[]): Promise<YarnDependency[]> {
|
||||
const raw = await new Promise<string>((c, e) => cp.exec('yarn list --prod --json', { cwd, encoding: 'utf8', env: { ...process.env }, maxBuffer: 5000 * 1024 }, (err, stdout) => err ? e(err) : c(stdout)));
|
||||
const match = /^{"type":"tree".*$/m.exec(raw);
|
||||
|
@ -182,8 +233,34 @@ async function getYarnDependencies(cwd: string, packagedDependencies?: string[])
|
|||
return _.uniq(result);
|
||||
}
|
||||
|
||||
export function getDependencies(cwd: string, useYarn = false, packagedDependencies?: string[]): Promise<string[]> {
|
||||
return useYarn ? getYarnDependencies(cwd, packagedDependencies) : getNpmDependencies(cwd);
|
||||
async function getPnpmDependencies(cwd: string, packagedDependencies?: string[]): Promise<string[]> {
|
||||
const result: string[] = [cwd];
|
||||
|
||||
if (await new Promise(c => fs.exists(path.join(cwd, 'pnpm-lock.yaml'), c))) {
|
||||
const deps = await getPnpmProductionDependencies(cwd, packagedDependencies);
|
||||
const flatten = (dep: YarnDependency) => { result.push(dep.path); dep.children.forEach(flatten); };
|
||||
deps.forEach(flatten);
|
||||
}
|
||||
|
||||
return _.uniq(result);
|
||||
}
|
||||
|
||||
export function getDependencies(cwd: string, useYarn = false, usePackageManager: "yarn" | "npm" | "pnpm", packagedDependencies?: string[]): Promise<string[]> {
|
||||
if(useYarn) {
|
||||
return getYarnDependencies(cwd, packagedDependencies);
|
||||
}
|
||||
else if(usePackageManager == "npm") {
|
||||
return getNpmDependencies(cwd);
|
||||
}
|
||||
else if(usePackageManager == "pnpm") {
|
||||
return getPnpmDependencies(cwd, packagedDependencies);
|
||||
}
|
||||
else if(usePackageManager == "yarn") {
|
||||
return getYarnDependencies(cwd, packagedDependencies);
|
||||
}
|
||||
else {
|
||||
return getNpmDependencies(cwd);
|
||||
}
|
||||
}
|
||||
|
||||
export function getLatestVersion(name: string, cancellationToken?: CancellationToken): Promise<string> {
|
||||
|
|
368
src/package.ts
368
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,16 +16,11 @@ import { lookup } from 'mime';
|
|||
import * as urljoin from 'url-join';
|
||||
import { validatePublisher, validateExtensionName, validateVersion, validateEngineCompatibility, validateVSCodeTypesCompatibility } from './validation';
|
||||
import { getDependencies } from './npm';
|
||||
|
||||
interface IReadFile {
|
||||
(filePath: string): Promise<Buffer>;
|
||||
(filePath: string, encoding?: string): Promise<string>;
|
||||
}
|
||||
import { IExtensionsReport } from './publicgalleryapi';
|
||||
|
||||
const readFile = denodeify<string, string, string>(fs.readFile);
|
||||
const unlink = denodeify<string, void>(fs.unlink as any);
|
||||
const stat = denodeify(fs.stat);
|
||||
const exec = denodeify<string, { cwd?: string; env?: any; maxBuffer?: number; }, { stdout: string; stderr: string; }>(cp.exec as any, (err, stdout, stderr) => [err, { stdout, stderr }]);
|
||||
const glob = denodeify<string, _glob.IOptions, string[]>(_glob);
|
||||
|
||||
const resourcesPath = path.join(path.dirname(__dirname), 'resources');
|
||||
|
@ -65,22 +60,29 @@ export interface IAsset {
|
|||
export interface IPackageOptions {
|
||||
cwd?: string;
|
||||
packagePath?: string;
|
||||
githubBranch?: string;
|
||||
baseContentUrl?: string;
|
||||
baseImagesUrl?: string;
|
||||
useYarn?: boolean;
|
||||
usePackageManager?: "yarn" | "npm" | "pnpm";
|
||||
dependencyEntryPoints?: string[];
|
||||
ignoreFile?: string;
|
||||
expandGitHubIssueLinks?: boolean;
|
||||
web?: boolean;
|
||||
}
|
||||
|
||||
export interface IProcessor {
|
||||
onFile(file: IFile): Promise<IFile>;
|
||||
onEnd(): Promise<void>;
|
||||
assets: IAsset[];
|
||||
tags: string[];
|
||||
vsix: any;
|
||||
}
|
||||
|
||||
export class BaseProcessor implements IProcessor {
|
||||
constructor(protected manifest: Manifest) { }
|
||||
assets: IAsset[] = [];
|
||||
tags: string[] = [];
|
||||
vsix: any = Object.create(null);
|
||||
onFile(file: IFile): Promise<IFile> { return Promise.resolve(file); }
|
||||
onEnd() { return Promise.resolve(null); }
|
||||
|
@ -175,8 +177,16 @@ const TrustedSVGSources = [
|
|||
'www.versioneye.com'
|
||||
];
|
||||
|
||||
function isHostTrusted(host: string): boolean {
|
||||
return TrustedSVGSources.indexOf(host.toLowerCase()) > -1;
|
||||
function isGitHubRepository(repository: string): boolean {
|
||||
return /^https:\/\/github\.com\/|^git@github\.com:/.test(repository || '');
|
||||
}
|
||||
|
||||
function isGitHubBadge(href: string): boolean {
|
||||
return /^https:\/\/github\.com\/[^/]+\/[^/]+\/workflows\/.*badge\.svg/.test(href || '');
|
||||
}
|
||||
|
||||
function isHostTrusted(url: url.UrlWithStringQuery): boolean {
|
||||
return TrustedSVGSources.indexOf(url.host.toLowerCase()) > -1 || isGitHubBadge(url.href);
|
||||
}
|
||||
|
||||
class ManifestProcessor extends BaseProcessor {
|
||||
|
@ -191,7 +201,7 @@ class ManifestProcessor extends BaseProcessor {
|
|||
}
|
||||
|
||||
const repository = getRepositoryUrl(manifest.repository);
|
||||
const isGitHub = /^https:\/\/github\.com\/|^git@github\.com:/.test(repository || '');
|
||||
const isGitHub = isGitHubRepository(repository);
|
||||
|
||||
let enableMarketplaceQnA: boolean | undefined;
|
||||
let customerQnALink: string | undefined;
|
||||
|
@ -204,6 +214,8 @@ class ManifestProcessor extends BaseProcessor {
|
|||
enableMarketplaceQnA = false;
|
||||
}
|
||||
|
||||
const extensionKind = getExtensionKind(manifest);
|
||||
|
||||
this.vsix = {
|
||||
...this.vsix,
|
||||
id: manifest.name,
|
||||
|
@ -226,6 +238,7 @@ class ManifestProcessor extends BaseProcessor {
|
|||
customerQnALink,
|
||||
extensionDependencies: _(manifest.extensionDependencies || []).uniq().join(','),
|
||||
extensionPack: _(manifest.extensionPack || []).uniq().join(','),
|
||||
extensionKind: extensionKind.join(','),
|
||||
localizedLanguages: (manifest.contributes && manifest.contributes.localizations) ?
|
||||
manifest.contributes.localizations.map(loc => loc.localizedLanguageName || loc.languageName || loc.languageId).join(',') : ''
|
||||
};
|
||||
|
@ -236,6 +249,10 @@ class ManifestProcessor extends BaseProcessor {
|
|||
}
|
||||
|
||||
async onEnd(): Promise<void> {
|
||||
if (typeof this.manifest.extensionKind === 'string') {
|
||||
util.log.warn(`The 'extensionKind' property should be of type 'string[]'. Learn more at: https://aka.ms/vscode/api/incorrect-execution-location`);
|
||||
}
|
||||
|
||||
if (this.manifest.publisher === 'vscode-samples') {
|
||||
throw new Error('It\'s not allowed to use the \'vscode-samples\' publisher. Learn more at: https://code.visualstudio.com/api/working-with-extensions/publishing-extension.');
|
||||
}
|
||||
|
@ -337,10 +354,10 @@ export class TagsProcessor extends BaseProcessor {
|
|||
...descriptionKeywords
|
||||
];
|
||||
|
||||
this.vsix.tags = _(tags)
|
||||
this.tags = _(tags)
|
||||
.uniq() // deduplicate
|
||||
.compact() // remove falsey values
|
||||
.join(',');
|
||||
.value();
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
@ -350,14 +367,20 @@ export class MarkdownProcessor extends BaseProcessor {
|
|||
|
||||
private baseContentUrl: string;
|
||||
private baseImagesUrl: string;
|
||||
private isGitHub: boolean;
|
||||
private repositoryUrl: string;
|
||||
private expandGitHubIssueLinks: boolean;
|
||||
|
||||
constructor(manifest: Manifest, private name: string, private regexp: RegExp, private assetType: string, options: IPackageOptions = {}) {
|
||||
super(manifest);
|
||||
|
||||
const guess = this.guessBaseUrls();
|
||||
const guess = this.guessBaseUrls(options.githubBranch);
|
||||
|
||||
this.baseContentUrl = options.baseContentUrl || (guess && guess.content);
|
||||
this.baseImagesUrl = options.baseImagesUrl || options.baseContentUrl || (guess && guess.images);
|
||||
this.repositoryUrl = (guess && guess.repository);
|
||||
this.isGitHub = isGitHubRepository(this.repositoryUrl);
|
||||
this.expandGitHubIssueLinks = typeof options.expandGitHubIssueLinks === 'boolean' ? options.expandGitHubIssueLinks : true;
|
||||
}
|
||||
|
||||
async onFile(file: IFile): Promise<IFile> {
|
||||
|
@ -372,11 +395,15 @@ export class MarkdownProcessor extends BaseProcessor {
|
|||
let contents = await read(file);
|
||||
|
||||
if (/This is the README for your extension /.test(contents)) {
|
||||
throw new Error(`Make sure to edit the README.md file before you publish your extension.`);
|
||||
throw new Error(`Make sure to edit the README.md file before you package or publish your extension.`);
|
||||
}
|
||||
|
||||
const markdownPathRegex = /(!?)\[([^\]\[]*|!\[[^\]\[]*]\([^\)]+\))\]\(([^\)]+)\)/g;
|
||||
const urlReplace = (all, isImage, title, link) => {
|
||||
const urlReplace = (_, isImage, title, link: string) => {
|
||||
if (/^mailto:/i.test(link)) {
|
||||
return `${isImage}[${title}](${link})`;
|
||||
}
|
||||
|
||||
const isLinkRelative = !/^\w+:\/\//.test(link) && link[0] !== '#';
|
||||
|
||||
if (!this.baseContentUrl && !this.baseImagesUrl) {
|
||||
|
@ -397,8 +424,52 @@ export class MarkdownProcessor extends BaseProcessor {
|
|||
return `${isImage}[${title}](${urljoin(prefix, link)})`;
|
||||
};
|
||||
|
||||
// Replace Markdown links with urls
|
||||
contents = contents.replace(markdownPathRegex, urlReplace);
|
||||
|
||||
// Replace <img> links with urls
|
||||
contents = contents.replace(/<img.+?src=["']([/.\w\s-]+)['"].*?>/g, (all, link) => {
|
||||
const isLinkRelative = !/^\w+:\/\//.test(link) && link[0] !== '#';
|
||||
|
||||
if (!this.baseImagesUrl && isLinkRelative) {
|
||||
throw new Error(`Couldn't detect the repository where this extension is published. The image will be broken in ${this.name}. Please provide the repository URL in package.json or use the --baseContentUrl and --baseImagesUrl options.`);
|
||||
}
|
||||
const prefix = this.baseImagesUrl;
|
||||
|
||||
if (!prefix || !isLinkRelative) {
|
||||
return all;
|
||||
}
|
||||
|
||||
return all.replace(link, urljoin(prefix, link));
|
||||
});
|
||||
|
||||
if (this.isGitHub && this.expandGitHubIssueLinks) {
|
||||
const markdownIssueRegex = /(\s|\n)([\w\d_-]+\/[\w\d_-]+)?#(\d+)\b/g
|
||||
const issueReplace = (all: string, prefix: string, ownerAndRepositoryName: string, issueNumber: string): string => {
|
||||
let result = all;
|
||||
let owner: string;
|
||||
let repositoryName: string;
|
||||
|
||||
if (ownerAndRepositoryName) {
|
||||
[owner, repositoryName] = ownerAndRepositoryName.split('/', 2);
|
||||
}
|
||||
|
||||
if (owner && repositoryName && issueNumber) {
|
||||
// Issue in external repository
|
||||
const issueUrl = urljoin('https://github.com', owner, repositoryName, 'issues', issueNumber);
|
||||
result = prefix + `[${owner}/${repositoryName}#${issueNumber}](${issueUrl})`;
|
||||
|
||||
} else if (!owner && !repositoryName && issueNumber) {
|
||||
// Issue in own repository
|
||||
result = prefix + `[#${issueNumber}](${urljoin(this.repositoryUrl, 'issues', issueNumber)})`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
// Replace Markdown issue references with urls
|
||||
contents = contents.replace(markdownIssueRegex, issueReplace);
|
||||
}
|
||||
|
||||
const html = markdownit({ html: true }).render(contents);
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
|
@ -414,23 +485,23 @@ export class MarkdownProcessor extends BaseProcessor {
|
|||
throw new Error(`Images in ${this.name} must come from an HTTPS source: ${src}`);
|
||||
}
|
||||
|
||||
if (/\.svg$/i.test(srcUrl.pathname) && !isHostTrusted(srcUrl.host)) {
|
||||
if (/\.svg$/i.test(srcUrl.pathname) && (!isHostTrusted(srcUrl))) {
|
||||
throw new Error(`SVGs are restricted in ${this.name}; please use other file image formats, such as PNG: ${src}`);
|
||||
}
|
||||
});
|
||||
|
||||
$('svg').each((_, svg) => {
|
||||
$('svg').each(() => {
|
||||
throw new Error(`SVG tags are not allowed in ${this.name}.`);
|
||||
});
|
||||
|
||||
return {
|
||||
path: file.path,
|
||||
contents: new Buffer(contents)
|
||||
contents: Buffer.from(contents, 'utf8')
|
||||
};
|
||||
}
|
||||
|
||||
// GitHub heuristics
|
||||
private guessBaseUrls(): { content: string; images: string; } {
|
||||
private guessBaseUrls(githubBranch: string | undefined): { content: string; images: string; repository: string } {
|
||||
let repository = null;
|
||||
|
||||
if (typeof this.manifest.repository === 'string') {
|
||||
|
@ -452,10 +523,12 @@ export class MarkdownProcessor extends BaseProcessor {
|
|||
|
||||
const account = match[1];
|
||||
const repositoryName = match[2].replace(/\.git$/i, '');
|
||||
const branchName = githubBranch ? githubBranch : 'master';
|
||||
|
||||
return {
|
||||
content: `https://github.com/${account}/${repositoryName}/blob/master`,
|
||||
images: `https://github.com/${account}/${repositoryName}/raw/master`
|
||||
content: `https://github.com/${account}/${repositoryName}/blob/${branchName}`,
|
||||
images: `https://github.com/${account}/${repositoryName}/raw/${branchName}`,
|
||||
repository: `https://github.com/${account}/${repositoryName}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -476,7 +549,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);
|
||||
|
@ -544,6 +617,92 @@ 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;
|
||||
|
||||
constructor(manifest: Manifest, options: IPackageOptions) {
|
||||
super(manifest);
|
||||
this.isWebKind = options.web && isWebKind(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 a web extension: ${path}`);
|
||||
}
|
||||
this.assets.push({ type: `Microsoft.VisualStudio.Code.WebResources/${path}`, path });
|
||||
}
|
||||
return Promise.resolve(file);
|
||||
}
|
||||
|
||||
async onEnd(): Promise<void> {
|
||||
if (this.assets.length > 25) {
|
||||
throw new Error('Cannot pack more than 25 files in a web extension. Use `vsce ls` to see all the files that will be packed and exclude those which are not needed in .vscodeignore.');
|
||||
}
|
||||
if (this.isWebKind) {
|
||||
this.vsix = {
|
||||
...this.vsix,
|
||||
webExtension: true
|
||||
}
|
||||
this.tags = ['__web_extension'];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class NLSProcessor extends BaseProcessor {
|
||||
|
||||
private translations: { [path: string]: string } = Object.create(null);
|
||||
|
@ -586,6 +745,42 @@ export class NLSProcessor extends BaseProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
export class ValidationProcessor extends BaseProcessor {
|
||||
|
||||
private files = new Map<string, string[]>();
|
||||
private duplicates = new Set<string>();
|
||||
|
||||
async onFile(file: IFile): Promise<IFile> {
|
||||
const lower = file.path.toLowerCase();
|
||||
const existing = this.files.get(lower);
|
||||
|
||||
if (existing) {
|
||||
this.duplicates.add(lower);
|
||||
existing.push(file.path);
|
||||
} else {
|
||||
this.files.set(lower, [file.path]);
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
async onEnd() {
|
||||
if (this.duplicates.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = [`The following files have the same case insensitive path, which isn't supported by the VSIX format:`];
|
||||
|
||||
for (const lower of this.duplicates) {
|
||||
for (const filePath of this.files.get(lower)) {
|
||||
messages.push(` - ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(messages.join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
export function validateManifest(manifest: Manifest): Manifest {
|
||||
validatePublisher(manifest.publisher);
|
||||
validateExtensionName(manifest.name);
|
||||
|
@ -622,7 +817,7 @@ export function validateManifest(manifest: Manifest): Manifest {
|
|||
throw new Error(`Badge URLs must come from an HTTPS source: ${badge.url}`);
|
||||
}
|
||||
|
||||
if (/\.svg$/i.test(srcUrl.pathname) && !isHostTrusted(srcUrl.host)) {
|
||||
if (/\.svg$/i.test(srcUrl.pathname) && (!isHostTrusted(srcUrl))) {
|
||||
throw new Error(`Badge SVGs are restricted. Please use other file image formats, such as PNG: ${badge.url}`);
|
||||
}
|
||||
});
|
||||
|
@ -671,7 +866,7 @@ export function readManifest(cwd = process.cwd(), nls = true): Promise<Manifest>
|
|||
|
||||
}
|
||||
|
||||
export function toVsixManifest(assets: IAsset[], vsix: any, options: IPackageOptions = {}): Promise<string> {
|
||||
export function toVsixManifest(vsix: any): Promise<string> {
|
||||
return readFile(vsixManifestTemplatePath, 'utf8')
|
||||
.then(vsixManifestTemplateStr => _.template(vsixManifestTemplateStr))
|
||||
.then(vsixManifestTemplate => vsixManifestTemplate(vsix));
|
||||
|
@ -725,10 +920,10 @@ const defaultIgnore = [
|
|||
'**/.vscode-test/**'
|
||||
];
|
||||
|
||||
function collectAllFiles(cwd: string, useYarn = false, dependencyEntryPoints?: string[]): Promise<string[]> {
|
||||
return getDependencies(cwd, useYarn, dependencyEntryPoints).then(deps => {
|
||||
function collectAllFiles(cwd: string, useYarn = false, usePackageManager: "yarn" | "npm" | "pnpm" = "npm", dependencyEntryPoints?: string[]): Promise<string[]> {
|
||||
return getDependencies(cwd, useYarn, usePackageManager, dependencyEntryPoints).then(deps => {
|
||||
const promises: Promise<string[]>[] = deps.map(dep => {
|
||||
return glob('**', { cwd: dep, nodir: true, dot: true, ignore: 'node_modules/**' })
|
||||
return glob('**', { cwd: dep, nodir: true, dot: true, follow: usePackageManager == "pnpm", ignore: 'node_modules/**' })
|
||||
.then(files => files
|
||||
.map(f => path.relative(cwd, path.join(dep, f)))
|
||||
.map(f => f.replace(/\\/g, '/')));
|
||||
|
@ -738,12 +933,12 @@ function collectAllFiles(cwd: string, useYarn = false, dependencyEntryPoints?: s
|
|||
});
|
||||
}
|
||||
|
||||
function collectFiles(cwd: string, useYarn = false, dependencyEntryPoints?: string[]): Promise<string[]> {
|
||||
return collectAllFiles(cwd, useYarn, dependencyEntryPoints).then(files => {
|
||||
function collectFiles(cwd: string, useYarn = false, usePackageManager: "yarn" | "npm" | "pnpm" = "npm", dependencyEntryPoints?: string[], ignoreFile?: string): Promise<string[]> {
|
||||
return collectAllFiles(cwd, useYarn, usePackageManager, dependencyEntryPoints).then(files => {
|
||||
files = files.filter(f => !/\r$/m.test(f));
|
||||
|
||||
return readFile(path.join(cwd, '.vscodeignore'), 'utf8')
|
||||
.catch<string>(err => err.code !== 'ENOENT' ? Promise.reject(err) : Promise.resolve(''))
|
||||
return readFile(ignoreFile ? ignoreFile : path.join(cwd, '.vscodeignore'), 'utf8')
|
||||
.catch<string>(err => err.code !== 'ENOENT' ? Promise.reject(err) : ignoreFile ? Promise.reject(err) : Promise.resolve(''))
|
||||
|
||||
// Parse raw ignore by splitting output into lines and filtering out empty lines and comments
|
||||
.then(rawIgnore => rawIgnore.split(/[\n\r]/).map(s => s.trim()).filter(s => !!s).filter(i => !/^\s*#/.test(i)))
|
||||
|
@ -763,18 +958,22 @@ function collectFiles(cwd: string, useYarn = false, dependencyEntryPoints?: stri
|
|||
});
|
||||
}
|
||||
|
||||
export function processFiles(processors: IProcessor[], files: IFile[], options: IPackageOptions = {}): Promise<IFile[]> {
|
||||
export function processFiles(processors: IProcessor[], files: IFile[]): Promise<IFile[]> {
|
||||
const processedFiles = files.map(file => util.chain(file, processors, (file, processor) => processor.onFile(file)));
|
||||
|
||||
return Promise.all(processedFiles).then(files => {
|
||||
return util.sequence(processors.map(p => () => p.onEnd())).then(() => {
|
||||
const assets = _.flatten(processors.map(p => p.assets));
|
||||
const vsix = processors.reduce((r, p) => ({ ...r, ...p.vsix }), { assets });
|
||||
const tags = _(_.flatten(processors.map(p => p.tags)))
|
||||
.uniq() // deduplicate
|
||||
.compact() // remove falsey values
|
||||
.join(',');
|
||||
const vsix = processors.reduce((r, p) => ({ ...r, ...p.vsix }), { assets, tags });
|
||||
|
||||
return Promise.all([toVsixManifest(assets, vsix, options), toContentTypes(files)]).then(result => {
|
||||
return Promise.all([toVsixManifest(vsix), toContentTypes(files)]).then(result => {
|
||||
return [
|
||||
{ path: 'extension.vsixmanifest', contents: new Buffer(result[0], 'utf8') },
|
||||
{ path: '[Content_Types].xml', contents: new Buffer(result[1], 'utf8') },
|
||||
{ path: 'extension.vsixmanifest', contents: Buffer.from(result[0], 'utf8') },
|
||||
{ path: '[Content_Types].xml', contents: Buffer.from(result[1], 'utf8') },
|
||||
...files
|
||||
];
|
||||
});
|
||||
|
@ -790,29 +989,33 @@ export function createDefaultProcessors(manifest: Manifest, options: IPackageOpt
|
|||
new ChangelogProcessor(manifest, options),
|
||||
new LicenseProcessor(manifest),
|
||||
new IconProcessor(manifest),
|
||||
new NLSProcessor(manifest)
|
||||
new NLSProcessor(manifest),
|
||||
new WebExtensionProcessor(manifest, options),
|
||||
new ValidationProcessor(manifest)
|
||||
];
|
||||
}
|
||||
|
||||
export function collect(manifest: Manifest, options: IPackageOptions = {}): Promise<IFile[]> {
|
||||
const cwd = options.cwd || process.cwd();
|
||||
const useYarn = options.useYarn || false;
|
||||
const usePackageManager = options.usePackageManager || "npm";
|
||||
const packagedDependencies = options.dependencyEntryPoints || undefined;
|
||||
const ignoreFile = options.ignoreFile || undefined;
|
||||
const processors = createDefaultProcessors(manifest, options);
|
||||
|
||||
return collectFiles(cwd, useYarn, packagedDependencies).then(fileNames => {
|
||||
return collectFiles(cwd, useYarn, usePackageManager, packagedDependencies, ignoreFile).then(fileNames => {
|
||||
const files = fileNames.map(f => ({ path: `extension/${f}`, localPath: path.join(cwd, f) }));
|
||||
|
||||
return processFiles(processors, files, options);
|
||||
return processFiles(processors, files);
|
||||
});
|
||||
}
|
||||
|
||||
function writeVsix(files: IFile[], packagePath: string): Promise<string> {
|
||||
function writeVsix(files: IFile[], packagePath: string): Promise<void> {
|
||||
return unlink(packagePath)
|
||||
.catch(err => err.code !== 'ENOENT' ? Promise.reject(err) : Promise.resolve(null))
|
||||
.then(() => new Promise<string>((c, e) => {
|
||||
.then(() => new Promise((c, e) => {
|
||||
const zip = new yazl.ZipFile();
|
||||
files.forEach(f => f.contents ? zip.addBuffer(typeof f.contents === 'string' ? new Buffer(f.contents, 'utf8') : f.contents, f.path) : zip.addFile(f.localPath, f.path));
|
||||
files.forEach(f => f.contents ? zip.addBuffer(typeof f.contents === 'string' ? Buffer.from(f.contents, 'utf8') : f.contents, f.path) : zip.addFile(f.localPath, f.path));
|
||||
zip.end();
|
||||
|
||||
const zipStream = fs.createWriteStream(packagePath);
|
||||
|
@ -820,41 +1023,76 @@ function writeVsix(files: IFile[], packagePath: string): Promise<string> {
|
|||
|
||||
zip.outputStream.once('error', e);
|
||||
zipStream.once('error', e);
|
||||
zipStream.once('finish', () => c(packagePath));
|
||||
zipStream.once('finish', () => c());
|
||||
}));
|
||||
}
|
||||
|
||||
function defaultPackagePath(cwd: string, manifest: Manifest): string {
|
||||
return path.join(cwd, `${manifest.name}-${manifest.version}.vsix`);
|
||||
function getDefaultPackageName(manifest: Manifest): string {
|
||||
return `${manifest.name}-${manifest.version}.vsix`;
|
||||
}
|
||||
|
||||
function prepublish(cwd: string, manifest: Manifest): Promise<Manifest> {
|
||||
async function prepublish(cwd: string, manifest: Manifest, useYarn: boolean = false, usePackageManager: "yarn" | "npm" | "pnpm" = "npm"): Promise<void> {
|
||||
if (!manifest.scripts || !manifest.scripts['vscode:prepublish']) {
|
||||
return Promise.resolve(manifest);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(`Executing prepublish script 'npm run vscode:prepublish'...`);
|
||||
let tool = 'npm';
|
||||
if(useYarn) {
|
||||
tool = 'yarn'
|
||||
}
|
||||
else if(usePackageManager == "npm") {
|
||||
tool = 'npm';
|
||||
}
|
||||
else if(usePackageManager == "pnpm") {
|
||||
tool = 'pnpm';
|
||||
}
|
||||
else if(usePackageManager == "yarn") {
|
||||
tool = 'yarn'
|
||||
}
|
||||
|
||||
return exec('npm run vscode:prepublish', { cwd, maxBuffer: 5000 * 1024 })
|
||||
.then(({ stdout, stderr }) => {
|
||||
process.stdout.write(stdout);
|
||||
process.stderr.write(stderr);
|
||||
return Promise.resolve(manifest);
|
||||
})
|
||||
.catch(err => Promise.reject(err.message));
|
||||
console.log(`Executing prepublish script '${tool} run vscode:prepublish'...`);
|
||||
|
||||
await new Promise((c, e) => {
|
||||
const child = cp.spawn(tool, ['run', 'vscode:prepublish'], { cwd, shell: true, stdio: 'inherit' });
|
||||
child.on('exit', code => code === 0 ? c() : e(`${tool} failed with exit code ${code}`));
|
||||
child.on('error', e);
|
||||
});
|
||||
}
|
||||
|
||||
async function getPackagePath(cwd: string, manifest: Manifest, options: IPackageOptions = {}): Promise<string> {
|
||||
if (!options.packagePath) {
|
||||
return path.join(cwd, getDefaultPackageName(manifest));
|
||||
}
|
||||
|
||||
try {
|
||||
const _stat = await stat(options.packagePath);
|
||||
|
||||
if (_stat.isDirectory()) {
|
||||
return path.join(options.packagePath, getDefaultPackageName(manifest));
|
||||
} else {
|
||||
return options.packagePath;
|
||||
}
|
||||
} catch {
|
||||
return options.packagePath;
|
||||
}
|
||||
}
|
||||
|
||||
export async function pack(options: IPackageOptions = {}): Promise<IPackageResult> {
|
||||
const cwd = options.cwd || process.cwd();
|
||||
|
||||
let manifest = await readManifest(cwd);
|
||||
manifest = await prepublish(cwd, manifest);
|
||||
const manifest = await readManifest(cwd);
|
||||
|
||||
await prepublish(cwd, manifest, options.useYarn, options.usePackageManager);
|
||||
|
||||
const files = await collect(manifest, options);
|
||||
if (files.length > 100) {
|
||||
console.log(`This extension consists of ${files.length} separate files. For performance reasons, you should bundle your extension: https://aka.ms/vscode-bundle-extension. You should also exclude unnecessary files by adding them to your .vscodeignore: https://aka.ms/vscode-vscodeignore`);
|
||||
const jsFiles = files.filter(f => /\.js$/i.test(f.path));
|
||||
|
||||
if (files.length > 5000 || jsFiles.length > 100) {
|
||||
console.log(`This extension consists of ${files.length} files, out of which ${jsFiles.length} are JavaScript files. For performance reasons, you should bundle your extension: https://aka.ms/vscode-bundle-extension . You should also exclude unnecessary files by adding them to your .vscodeignore: https://aka.ms/vscode-vscodeignore`);
|
||||
}
|
||||
const packagePath = await writeVsix(files, path.resolve(options.packagePath || defaultPackagePath(cwd, manifest)));
|
||||
|
||||
const packagePath = await getPackagePath(cwd, manifest, options);
|
||||
await writeVsix(files, path.resolve(packagePath));
|
||||
|
||||
return { manifest, packagePath, files };
|
||||
}
|
||||
|
@ -880,17 +1118,17 @@ export async function packageCommand(options: IPackageOptions = {}): Promise<any
|
|||
/**
|
||||
* Lists the files included in the extension's package. Does not run prepublish.
|
||||
*/
|
||||
export function listFiles(cwd = process.cwd(), useYarn = false, packagedDependencies?: string[]): Promise<string[]> {
|
||||
export function listFiles(cwd = process.cwd(), useYarn = false, usePackageManager: "yarn" | "npm" | "pnpm" = "npm", packagedDependencies?: string[], ignoreFile?: string): Promise<string[]> {
|
||||
return readManifest(cwd)
|
||||
.then(manifest => collectFiles(cwd, useYarn, packagedDependencies));
|
||||
.then(() => collectFiles(cwd, useYarn, usePackageManager, packagedDependencies, ignoreFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the files included in the extension's package. Runs prepublish.
|
||||
*/
|
||||
export function ls(cwd = process.cwd(), useYarn = false, packagedDependencies?: string[]): Promise<void> {
|
||||
export function ls(cwd = process.cwd(), useYarn = false, usePackageManager?: "yarn" | "npm" | "pnpm", packagedDependencies?: string[], ignoreFile?: string): Promise<void> {
|
||||
return readManifest(cwd)
|
||||
.then(manifest => prepublish(cwd, manifest))
|
||||
.then(manifest => collectFiles(cwd, useYarn, packagedDependencies))
|
||||
.then(manifest => prepublish(cwd, manifest, useYarn, usePackageManager))
|
||||
.then(() => collectFiles(cwd, useYarn, usePackageManager, packagedDependencies, ignoreFile))
|
||||
.then(files => files.forEach(f => console.log(`${f}`)));
|
||||
}
|
||||
|
|
|
@ -1,24 +1,28 @@
|
|||
import { HttpClient, HttpClientResponse } from 'typed-rest-client/HttpClient';
|
||||
import {
|
||||
PublishedExtension, ExtensionQueryFlags, FilterCriteria, SortOrderType,
|
||||
SortByType, ExtensionQueryFilterType, TypeInfo
|
||||
} from 'azure-devops-node-api/interfaces/GalleryInterfaces';
|
||||
import { PublishedExtension, ExtensionQueryFlags, FilterCriteria, ExtensionQueryFilterType, TypeInfo } from 'azure-devops-node-api/interfaces/GalleryInterfaces';
|
||||
import { IHeaders } from 'azure-devops-node-api/interfaces/common/VsoBaseInterfaces';
|
||||
import { ContractSerializer } from 'azure-devops-node-api/Serialization';
|
||||
|
||||
export interface ExtensionQuery {
|
||||
pageNumber?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: SortByType;
|
||||
sortOrder?: SortOrderType;
|
||||
flags?: ExtensionQueryFlags[];
|
||||
criteria?: FilterCriteria[];
|
||||
assetTypes?: string[];
|
||||
readonly pageNumber?: number;
|
||||
readonly pageSize?: number;
|
||||
readonly flags?: ExtensionQueryFlags[];
|
||||
readonly criteria?: FilterCriteria[];
|
||||
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') { }
|
||||
|
||||
|
@ -29,8 +33,6 @@ export class PublicGalleryAPI {
|
|||
async extensionQuery({
|
||||
pageNumber = 1,
|
||||
pageSize = 1,
|
||||
sortBy = SortByType.Relevance,
|
||||
sortOrder = SortOrderType.Default,
|
||||
flags = [],
|
||||
criteria = [],
|
||||
assetTypes = [],
|
||||
|
@ -52,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: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import * as fs from 'fs';
|
||||
import { ExtensionQueryFlags, PublishedExtension, ExtensionQueryFilterType, PagingDirection, SortByType, SortOrderType } from 'azure-devops-node-api/interfaces/GalleryInterfaces';
|
||||
import { pack, readManifest, IPackage } from './package';
|
||||
import { ExtensionQueryFlags, PublishedExtension } from 'azure-devops-node-api/interfaces/GalleryInterfaces';
|
||||
import { pack, readManifest, IPackage, isWebKind, isSupportedWebExtension } from './package';
|
||||
import * as tmp from 'tmp';
|
||||
import { getPublisher } from './store';
|
||||
import { getGalleryAPI, read, getPublishedUrl, log } from './util';
|
||||
import { validatePublisher } from './validation';
|
||||
import { getGalleryAPI, read, getPublishedUrl, log, getPublicGalleryAPI } from './util';
|
||||
import { Manifest } from './manifest';
|
||||
import * as denodeify from 'denodeify';
|
||||
import * as yauzl from 'yauzl';
|
||||
|
@ -74,7 +73,7 @@ async function _publish(packagePath: string, pat: string, manifest: Manifest): P
|
|||
|
||||
return promise
|
||||
.catch(err => Promise.reject(err.statusCode === 409 ? `${fullName} already exists.` : err))
|
||||
.then(() => log.done(`Published ${fullName}\nYour extension will live at ${getPublishedUrl(name)} (might take a few seconds for it to show up).`));
|
||||
.then(() => log.done(`Published ${fullName}\nYour extension will live at ${getPublishedUrl(name)} (might take a few minutes for it to show up).`));
|
||||
})
|
||||
.catch(err => {
|
||||
const message = err && err.message || '';
|
||||
|
@ -93,17 +92,27 @@ export interface IPublishOptions {
|
|||
commitMessage?: string;
|
||||
cwd?: string;
|
||||
pat?: string;
|
||||
githubBranch?: string;
|
||||
baseContentUrl?: string;
|
||||
baseImagesUrl?: string;
|
||||
useYarn?: boolean;
|
||||
usePackageManager?: "yarn" | "npm" | "pnpm";
|
||||
noVerify?: boolean;
|
||||
ignoreFile?: string;
|
||||
web?: boolean;
|
||||
}
|
||||
|
||||
function versionBump(cwd: string = process.cwd(), version?: string, commitMessage?: string): Promise<void> {
|
||||
async function versionBump(cwd: string = process.cwd(), version?: string, commitMessage?: string): Promise<void> {
|
||||
if (!version) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const manifest = await readManifest(cwd);
|
||||
|
||||
if (manifest.version === version) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (version) {
|
||||
case 'major':
|
||||
case 'minor':
|
||||
|
@ -127,14 +136,15 @@ function versionBump(cwd: string = process.cwd(), version?: string, commitMessag
|
|||
command = `${command} -m "${commitMessage}"`;
|
||||
}
|
||||
|
||||
// call `npm version` to do our dirty work
|
||||
return exec(command, { cwd })
|
||||
.then(({ stdout, stderr }) => {
|
||||
process.stdout.write(stdout);
|
||||
process.stderr.write(stderr);
|
||||
return Promise.resolve(null);
|
||||
})
|
||||
.catch(err => Promise.reject(err.message));
|
||||
try {
|
||||
// call `npm version` to do our dirty work
|
||||
const { stdout, stderr } = await exec(command, { cwd });
|
||||
process.stdout.write(stdout);
|
||||
process.stderr.write(stderr);
|
||||
return null;
|
||||
} catch (err) {
|
||||
throw err.message;
|
||||
}
|
||||
}
|
||||
|
||||
export function publish(options: IPublishOptions = {}): Promise<any> {
|
||||
|
@ -144,25 +154,42 @@ export function publish(options: IPublishOptions = {}): Promise<any> {
|
|||
if (options.version) {
|
||||
return Promise.reject(`Not supported: packagePath and version.`);
|
||||
}
|
||||
if (options.web) {
|
||||
return Promise.reject(`Not supported: packagePath and web.`);
|
||||
}
|
||||
|
||||
promise = readManifestFromPackage(options.packagePath)
|
||||
.then(manifest => ({ manifest, packagePath: options.packagePath }));
|
||||
} else {
|
||||
const cwd = options.cwd;
|
||||
const githubBranch = options.githubBranch;
|
||||
const baseContentUrl = options.baseContentUrl;
|
||||
const baseImagesUrl = options.baseImagesUrl;
|
||||
const useYarn = options.useYarn;
|
||||
const ignoreFile = options.ignoreFile;
|
||||
const web = options.web;
|
||||
const usePackageManager = options.usePackageManager;
|
||||
|
||||
promise = versionBump(options.cwd, options.version, options.commitMessage)
|
||||
.then(() => tmpName())
|
||||
.then(packagePath => pack({ packagePath, cwd, baseContentUrl, baseImagesUrl, useYarn }));
|
||||
.then(packagePath => pack({ packagePath, cwd, githubBranch, baseContentUrl, baseImagesUrl, useYarn, ignoreFile, web, usePackageManager }));
|
||||
}
|
||||
|
||||
return promise.then(({ manifest, packagePath }) => {
|
||||
return promise.then(async ({ manifest, packagePath }) => {
|
||||
if (!options.noVerify && manifest.enableProposedApi) {
|
||||
throw new Error('Extensions using proposed API (enableProposedApi: true) can\'t be published to the Marketplace');
|
||||
}
|
||||
|
||||
if (options.web) {
|
||||
if (!isWebKind(manifest)) {
|
||||
throw new Error('Extensions which are not web kind can\'t be published to the Marketpalce as a web extension');
|
||||
}
|
||||
const extensionsReport = await getPublicGalleryAPI().getExtensionsReport();
|
||||
if (!isSupportedWebExtension(manifest, extensionsReport)) {
|
||||
throw new Error('Extensions which are not supported can\'t be published to the Marketpalce as a web extension');
|
||||
}
|
||||
}
|
||||
|
||||
const patPromise = options.pat
|
||||
? Promise.resolve(options.pat)
|
||||
: getPublisher(manifest.publisher).then(p => p.pat);
|
||||
|
@ -173,29 +200,33 @@ export function publish(options: IPublishOptions = {}): Promise<any> {
|
|||
|
||||
export interface IUnpublishOptions extends IPublishOptions {
|
||||
id?: string;
|
||||
force?: boolean;
|
||||
}
|
||||
|
||||
export function unpublish(options: IUnpublishOptions = {}): Promise<any> {
|
||||
let promise: Promise<{ publisher: string; name: string; }>;
|
||||
export async function unpublish(options: IUnpublishOptions = {}): Promise<any> {
|
||||
let publisher: string, name: string;
|
||||
|
||||
if (options.id) {
|
||||
const [publisher, name] = options.id.split('.');
|
||||
promise = Promise.resolve(({ publisher, name }));
|
||||
[publisher, name] = options.id.split('.');
|
||||
} else {
|
||||
promise = readManifest(options.cwd);
|
||||
const manifest = await readManifest(options.cwd);
|
||||
publisher = manifest.publisher;
|
||||
name = manifest.name;
|
||||
}
|
||||
|
||||
return promise.then(({ publisher, name }) => {
|
||||
const fullName = `${publisher}.${name}`;
|
||||
const pat = options.pat
|
||||
? Promise.resolve(options.pat)
|
||||
: getPublisher(publisher).then(p => p.pat);
|
||||
const fullName = `${publisher}.${name}`;
|
||||
|
||||
return read(`This will FOREVER delete '${fullName}'! Are you sure? [y/N] `)
|
||||
.then(answer => /^y$/i.test(answer) ? null : Promise.reject('Aborted'))
|
||||
.then(() => pat)
|
||||
.then(getGalleryAPI)
|
||||
.then(api => api.deleteExtension(publisher, name))
|
||||
.then(() => log.done(`Deleted extension: ${fullName}!`));
|
||||
});
|
||||
if (!options.force) {
|
||||
const answer = await read(`This will FOREVER delete '${fullName}'! Are you sure? [y/N] `);
|
||||
|
||||
if (!/^y$/i.test(answer)) {
|
||||
throw new Error('Aborted');
|
||||
}
|
||||
}
|
||||
|
||||
const pat = options.pat || (await getPublisher(publisher).then(p => p.pat));
|
||||
const api = await getGalleryAPI(pat);
|
||||
|
||||
await api.deleteExtension(publisher, name);
|
||||
log.done(`Deleted extension: ${fullName}!`);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { tableView, wordTrim } from './viewutils';
|
|||
|
||||
const pageSize = 100;
|
||||
|
||||
export async function search(searchText: string, json: boolean = false, pageNumber: number = 1): Promise<any> {
|
||||
export async function search(searchText: string, json: boolean = false): Promise<any> {
|
||||
const flags = [];
|
||||
const api = getPublicGalleryAPI();
|
||||
const results = await api.extensionQuery({
|
||||
|
|
|
@ -6,7 +6,7 @@ import { validatePublisher } from './validation';
|
|||
import * as denodeify from 'denodeify';
|
||||
|
||||
const readFile = denodeify<string, string, string>(fs.readFile);
|
||||
const writeFile = denodeify<string, string, void>(fs.writeFile as any);
|
||||
const writeFile = denodeify<string, string, object, void>(fs.writeFile as any);
|
||||
const storePath = path.join(home(), '.vsce');
|
||||
|
||||
export interface IPublisher {
|
||||
|
@ -40,7 +40,7 @@ function load(): Promise<IStore> {
|
|||
}
|
||||
|
||||
function save(store: IStore): Promise<IStore> {
|
||||
return writeFile(storePath, JSON.stringify(store))
|
||||
return writeFile(storePath, JSON.stringify(store), {mode: '0600'})
|
||||
.then(() => store);
|
||||
}
|
||||
|
||||
|
|
48
src/test/fixtures/readme/readme.branch.main.expected.md
vendored
Normal file
48
src/test/fixtures/readme/readme.branch.main.expected.md
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
# README
|
||||
|
||||
>**Important:** Once installed the checker will only update if you add the setting `"spellMD.enable": true` to your `.vscode\settings.json` file.
|
||||
|
||||
This README covers off:
|
||||
* [Functionality](#functionality)
|
||||
* [Install](#install)
|
||||
* [Run and Configure](#run-and-configure)
|
||||
* [Known Issues/Bugs](#known-issuesbugs)
|
||||
* [Backlog](#backlog)
|
||||
* [How to Debug](#how-to-debug)
|
||||
|
||||
# Functionality
|
||||
|
||||
Load up a Markdown file and get highlights and hovers for existing issues. Checking will occur as you type in the document.
|
||||
|
||||
![Underscores and hovers](https://github.com/username/repository/raw/main/images/SpellMDDemo1.gif)
|
||||
|
||||
The status bar lets you quickly navigate to any issue and you can see all positions in the gutter.
|
||||
|
||||
[![Jump to issues](https://github.com/username/repository/raw/main/images/SpellMDDemo2.gif)](http://shouldnottouchthis/)
|
||||
[![Jump to issues](https://github.com/username/repository/raw/main/images/SpellMDDemo2.gif)](https://github.com/username/repository/blob/main/monkey)
|
||||
![](https://github.com/username/repository/raw/main/images/SpellMDDemo2.gif)
|
||||
<img src="https://github.com/username/repository/raw/main/images/myImage.gif">
|
||||
|
||||
The `spellMD.json` config file is watched so you can add more ignores or change mappings at will.
|
||||
|
||||
![Add to dictionary](https://github.com/username/repository/raw/main/images/SpellMDDemo3.gif)
|
||||
|
||||
![issue](https://github.com/username/repository/raw/main/issue)
|
||||
|
||||
[mono](https://github.com/username/repository/blob/main/monkey)
|
||||
[not](http://shouldnottouchthis/)
|
||||
[Email me](mailto:example@example.com)
|
||||
|
||||
# Install
|
||||
This extension is published in the VS Code Gallery. So simply hit 'F1' and type 'ext inst' from there select `SpellMD` and follow instructions.
|
||||
|
||||
|
||||
To clone the extension and load locally...
|
||||
|
||||
```
|
||||
git clone https://github.com/Microsoft/vscode-SpellMD.git
|
||||
npm install
|
||||
tsc
|
||||
```
|
||||
|
||||
>**Note:** TypeScript 1.6 or higher is required you can check with `tsc -v` and if you need to upgrade then run `npm install -g typescript`.
|
48
src/test/fixtures/readme/readme.branch.override.content.expected.md
vendored
Normal file
48
src/test/fixtures/readme/readme.branch.override.content.expected.md
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
# README
|
||||
|
||||
>**Important:** Once installed the checker will only update if you add the setting `"spellMD.enable": true` to your `.vscode\settings.json` file.
|
||||
|
||||
This README covers off:
|
||||
* [Functionality](#functionality)
|
||||
* [Install](#install)
|
||||
* [Run and Configure](#run-and-configure)
|
||||
* [Known Issues/Bugs](#known-issuesbugs)
|
||||
* [Backlog](#backlog)
|
||||
* [How to Debug](#how-to-debug)
|
||||
|
||||
# Functionality
|
||||
|
||||
Load up a Markdown file and get highlights and hovers for existing issues. Checking will occur as you type in the document.
|
||||
|
||||
![Underscores and hovers](https://github.com/base/images/SpellMDDemo1.gif)
|
||||
|
||||
The status bar lets you quickly navigate to any issue and you can see all positions in the gutter.
|
||||
|
||||
[![Jump to issues](https://github.com/base/images/SpellMDDemo2.gif)](http://shouldnottouchthis/)
|
||||
[![Jump to issues](https://github.com/base/images/SpellMDDemo2.gif)](https://github.com/base/monkey)
|
||||
![](https://github.com/base/images/SpellMDDemo2.gif)
|
||||
<img src="https://github.com/base/images/myImage.gif">
|
||||
|
||||
The `spellMD.json` config file is watched so you can add more ignores or change mappings at will.
|
||||
|
||||
![Add to dictionary](https://github.com/base/images/SpellMDDemo3.gif)
|
||||
|
||||
![issue](https://github.com/base/issue)
|
||||
|
||||
[mono](https://github.com/base/monkey)
|
||||
[not](http://shouldnottouchthis/)
|
||||
[Email me](mailto:example@example.com)
|
||||
|
||||
# Install
|
||||
This extension is published in the VS Code Gallery. So simply hit 'F1' and type 'ext inst' from there select `SpellMD` and follow instructions.
|
||||
|
||||
|
||||
To clone the extension and load locally...
|
||||
|
||||
```
|
||||
git clone https://github.com/Microsoft/vscode-SpellMD.git
|
||||
npm install
|
||||
tsc
|
||||
```
|
||||
|
||||
>**Note:** TypeScript 1.6 or higher is required you can check with `tsc -v` and if you need to upgrade then run `npm install -g typescript`.
|
48
src/test/fixtures/readme/readme.branch.override.images.expected.md
vendored
Normal file
48
src/test/fixtures/readme/readme.branch.override.images.expected.md
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
# README
|
||||
|
||||
>**Important:** Once installed the checker will only update if you add the setting `"spellMD.enable": true` to your `.vscode\settings.json` file.
|
||||
|
||||
This README covers off:
|
||||
* [Functionality](#functionality)
|
||||
* [Install](#install)
|
||||
* [Run and Configure](#run-and-configure)
|
||||
* [Known Issues/Bugs](#known-issuesbugs)
|
||||
* [Backlog](#backlog)
|
||||
* [How to Debug](#how-to-debug)
|
||||
|
||||
# Functionality
|
||||
|
||||
Load up a Markdown file and get highlights and hovers for existing issues. Checking will occur as you type in the document.
|
||||
|
||||
![Underscores and hovers](https://github.com/base/images/SpellMDDemo1.gif)
|
||||
|
||||
The status bar lets you quickly navigate to any issue and you can see all positions in the gutter.
|
||||
|
||||
[![Jump to issues](https://github.com/base/images/SpellMDDemo2.gif)](http://shouldnottouchthis/)
|
||||
[![Jump to issues](https://github.com/base/images/SpellMDDemo2.gif)](https://github.com/username/repository/blob/main/monkey)
|
||||
![](https://github.com/base/images/SpellMDDemo2.gif)
|
||||
<img src="https://github.com/base/images/myImage.gif">
|
||||
|
||||
The `spellMD.json` config file is watched so you can add more ignores or change mappings at will.
|
||||
|
||||
![Add to dictionary](https://github.com/base/images/SpellMDDemo3.gif)
|
||||
|
||||
![issue](https://github.com/base/issue)
|
||||
|
||||
[mono](https://github.com/username/repository/blob/main/monkey)
|
||||
[not](http://shouldnottouchthis/)
|
||||
[Email me](mailto:example@example.com)
|
||||
|
||||
# Install
|
||||
This extension is published in the VS Code Gallery. So simply hit 'F1' and type 'ext inst' from there select `SpellMD` and follow instructions.
|
||||
|
||||
|
||||
To clone the extension and load locally...
|
||||
|
||||
```
|
||||
git clone https://github.com/Microsoft/vscode-SpellMD.git
|
||||
npm install
|
||||
tsc
|
||||
```
|
||||
|
||||
>**Note:** TypeScript 1.6 or higher is required you can check with `tsc -v` and if you need to upgrade then run `npm install -g typescript`.
|
2
src/test/fixtures/readme/readme.expected.md
vendored
2
src/test/fixtures/readme/readme.expected.md
vendored
|
@ -21,6 +21,7 @@ The status bar lets you quickly navigate to any issue and you can see all positi
|
|||
[![Jump to issues](https://github.com/username/repository/raw/master/images/SpellMDDemo2.gif)](http://shouldnottouchthis/)
|
||||
[![Jump to issues](https://github.com/username/repository/raw/master/images/SpellMDDemo2.gif)](https://github.com/username/repository/blob/master/monkey)
|
||||
![](https://github.com/username/repository/raw/master/images/SpellMDDemo2.gif)
|
||||
<img src="https://github.com/username/repository/raw/master/images/myImage.gif">
|
||||
|
||||
The `spellMD.json` config file is watched so you can add more ignores or change mappings at will.
|
||||
|
||||
|
@ -30,6 +31,7 @@ The `spellMD.json` config file is watched so you can add more ignores or change
|
|||
|
||||
[mono](https://github.com/username/repository/blob/master/monkey)
|
||||
[not](http://shouldnottouchthis/)
|
||||
[Email me](mailto:example@example.com)
|
||||
|
||||
# Install
|
||||
This extension is published in the VS Code Gallery. So simply hit 'F1' and type 'ext inst' from there select `SpellMD` and follow instructions.
|
||||
|
|
15
src/test/fixtures/readme/readme.github.expected.md
vendored
Normal file
15
src/test/fixtures/readme/readme.github.expected.md
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Replace
|
||||
|
||||
[#8](https://github.com/username/repository/issues/8)
|
||||
|
||||
* Some issue in same repository: [#7](https://github.com/username/repository/issues/7)
|
||||
* Some issue in other repository: [other/repositoryName#8](https://github.com/other/repositoryName/issues/8)
|
||||
* Some issue in other repository with fancy name: [my_user-name/my-rep_o12#6](https://github.com/my_user-name/my-rep_o12/issues/6)
|
||||
|
||||
# Do not touch this:
|
||||
* username#4 (no valid github link)
|
||||
* /#7
|
||||
* foo/$234/#7
|
||||
* [#7](http://shouldnottouchthis/)
|
||||
* [other/repositoryName#8](http://shouldnottouchthis/)
|
||||
* [Email me](MAILTO:example@example.com)
|
15
src/test/fixtures/readme/readme.github.md
vendored
Normal file
15
src/test/fixtures/readme/readme.github.md
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Replace
|
||||
|
||||
#8
|
||||
|
||||
* Some issue in same repository: #7
|
||||
* Some issue in other repository: other/repositoryName#8
|
||||
* Some issue in other repository with fancy name: my_user-name/my-rep_o12#6
|
||||
|
||||
# Do not touch this:
|
||||
* username#4 (no valid github link)
|
||||
* /#7
|
||||
* foo/$234/#7
|
||||
* [#7](http://shouldnottouchthis/)
|
||||
* [other/repositoryName#8](http://shouldnottouchthis/)
|
||||
* [Email me](MAILTO:example@example.com)
|
|
@ -21,6 +21,7 @@ The status bar lets you quickly navigate to any issue and you can see all positi
|
|||
[![Jump to issues](https://github.com/username/repository/path/to/images/SpellMDDemo2.gif)](http://shouldnottouchthis/)
|
||||
[![Jump to issues](https://github.com/username/repository/path/to/images/SpellMDDemo2.gif)](https://github.com/username/repository/blob/master/monkey)
|
||||
![](https://github.com/username/repository/path/to/images/SpellMDDemo2.gif)
|
||||
<img src="https://github.com/username/repository/path/to/images/myImage.gif">
|
||||
|
||||
The `spellMD.json` config file is watched so you can add more ignores or change mappings at will.
|
||||
|
||||
|
@ -30,6 +31,7 @@ The `spellMD.json` config file is watched so you can add more ignores or change
|
|||
|
||||
[mono](https://github.com/username/repository/blob/master/monkey)
|
||||
[not](http://shouldnottouchthis/)
|
||||
[Email me](mailto:example@example.com)
|
||||
|
||||
# Install
|
||||
This extension is published in the VS Code Gallery. So simply hit 'F1' and type 'ext inst' from there select `SpellMD` and follow instructions.
|
||||
|
|
2
src/test/fixtures/readme/readme.md
vendored
2
src/test/fixtures/readme/readme.md
vendored
|
@ -21,6 +21,7 @@ The status bar lets you quickly navigate to any issue and you can see all positi
|
|||
[![Jump to issues](images/SpellMDDemo2.gif)](http://shouldnottouchthis/)
|
||||
[![Jump to issues](images/SpellMDDemo2.gif)](monkey)
|
||||
![](images/SpellMDDemo2.gif)
|
||||
<img src="/images/myImage.gif">
|
||||
|
||||
The `spellMD.json` config file is watched so you can add more ignores or change mappings at will.
|
||||
|
||||
|
@ -30,6 +31,7 @@ The `spellMD.json` config file is watched so you can add more ignores or change
|
|||
|
||||
[mono](monkey)
|
||||
[not](http://shouldnottouchthis/)
|
||||
[Email me](mailto:example@example.com)
|
||||
|
||||
# Install
|
||||
This extension is published in the VS Code Gallery. So simply hit 'F1' and type 'ext inst' from there select `SpellMD` and follow instructions.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
readManifest, collect, toContentTypes, ReadmeProcessor,
|
||||
read, processFiles, createDefaultProcessors,
|
||||
toVsixManifest, IFile, validateManifest
|
||||
toVsixManifest, IFile, validateManifest, isSupportedWebExtension, WebExtensionProcessor, IAsset, IPackageOptions
|
||||
} 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;
|
||||
|
@ -65,13 +66,14 @@ type ContentTypes = {
|
|||
const parseXmlManifest = createXMLParser<XMLManifest>();
|
||||
const parseContentTypes = createXMLParser<ContentTypes>();
|
||||
|
||||
function _toVsixManifest(manifest: Manifest, files: IFile[]): Promise<string> {
|
||||
const processors = createDefaultProcessors(manifest);
|
||||
function _toVsixManifest(manifest: Manifest, files: IFile[], options: IPackageOptions = {}): Promise<string> {
|
||||
const processors = createDefaultProcessors(manifest, options);
|
||||
return processFiles(processors, files).then(() => {
|
||||
const assets = _.flatten(processors.map(p => p.assets));
|
||||
const vsix = processors.reduce((r, p) => ({ ...r, ...p.vsix }), { assets });
|
||||
const tags = _(_.flatten(processors.map(p => p.tags))).join(',');
|
||||
const vsix = processors.reduce((r, p) => ({ ...r, ...p.vsix }), { assets, tags });
|
||||
|
||||
return toVsixManifest(assets, vsix);
|
||||
return toVsixManifest(vsix);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -359,7 +361,7 @@ describe('toVsixManifest', () => {
|
|||
};
|
||||
|
||||
const files = [
|
||||
{ path: 'extension/readme.md', contents: new Buffer('') }
|
||||
{ path: 'extension/readme.md', contents: Buffer.from('') }
|
||||
];
|
||||
|
||||
return _toVsixManifest(manifest, files)
|
||||
|
@ -381,7 +383,7 @@ describe('toVsixManifest', () => {
|
|||
};
|
||||
|
||||
const files = [
|
||||
{ path: 'extension/changelog.md', contents: new Buffer('') }
|
||||
{ path: 'extension/changelog.md', contents: Buffer.from('') }
|
||||
];
|
||||
|
||||
return _toVsixManifest(manifest, files)
|
||||
|
@ -1018,8 +1020,8 @@ describe('toVsixManifest', () => {
|
|||
};
|
||||
|
||||
const files = [
|
||||
{ path: 'extension/de.json', contents: new Buffer('') },
|
||||
{ path: 'extension/translations/pt.json', contents: new Buffer('') }
|
||||
{ path: 'extension/de.json', contents: Buffer.from('') },
|
||||
{ path: 'extension/translations/pt.json', contents: Buffer.from('') }
|
||||
];
|
||||
|
||||
return _toVsixManifest(manifest, files)
|
||||
|
@ -1242,84 +1244,215 @@ describe('toVsixManifest', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('qna', () => {
|
||||
it('should use marketplace qna by default', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null)
|
||||
});
|
||||
it('should error with files with same case insensitive name', async () => {
|
||||
const manifest = {
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
description: 'test extension',
|
||||
engines: Object.create(null)
|
||||
};
|
||||
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA');
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink');
|
||||
const files = [
|
||||
{ path: 'extension/file.txt' },
|
||||
{ path: 'extension/FILE.txt' },
|
||||
];
|
||||
|
||||
try {
|
||||
await _toVsixManifest(manifest, files);
|
||||
} catch (err) {
|
||||
assert(/have the same case insensitive path/i.test(err.message));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Should not reach here');
|
||||
});
|
||||
|
||||
it('should expose web extension assets and properties', async () => {
|
||||
const manifest = createManifest({
|
||||
browser: 'browser.js',
|
||||
extensionKind: ['web'],
|
||||
});
|
||||
const files = [
|
||||
{ path: 'extension/browser.js', contents: Buffer.from('') },
|
||||
];
|
||||
|
||||
const vsixManifest = await _toVsixManifest(manifest, files, { web: true })
|
||||
const result = await parseXmlManifest(vsixManifest);
|
||||
const assets = result.PackageManifest.Assets[0].Asset;
|
||||
assert(assets.some(asset => asset.$.Type === 'Microsoft.VisualStudio.Code.WebResources/extension/browser.js' && asset.$.Path === 'extension/browser.js'));
|
||||
|
||||
const properties = result.PackageManifest.Metadata[0].Properties[0].Property;
|
||||
const webExtensionProps = properties.filter(p => p.$.Id === 'Microsoft.VisualStudio.Code.WebExtension');
|
||||
assert.equal(webExtensionProps.length, 1);
|
||||
assert.equal(webExtensionProps[0].$.Value, 'true');
|
||||
});
|
||||
|
||||
it('should expose web extension assets and properties when extension kind is not provided', async () => {
|
||||
const manifest = createManifest({
|
||||
browser: 'browser.js',
|
||||
});
|
||||
const files = [
|
||||
{ path: 'extension/browser.js', contents: Buffer.from('') },
|
||||
];
|
||||
|
||||
const vsixManifest = await _toVsixManifest(manifest, files, { web: true })
|
||||
const result = await parseXmlManifest(vsixManifest);
|
||||
const assets = result.PackageManifest.Assets[0].Asset;
|
||||
assert(assets.some(asset => asset.$.Type === 'Microsoft.VisualStudio.Code.WebResources/extension/browser.js' && asset.$.Path === 'extension/browser.js'));
|
||||
|
||||
const properties = result.PackageManifest.Metadata[0].Properties[0].Property;
|
||||
const webExtensionProps = properties.filter(p => p.$.Id === 'Microsoft.VisualStudio.Code.WebExtension');
|
||||
assert.equal(webExtensionProps.length, 1);
|
||||
assert.equal(webExtensionProps[0].$.Value, 'true');
|
||||
});
|
||||
|
||||
it('should not expose web extension assets and properties for web extension when not asked for', async () => {
|
||||
const manifest = createManifest({
|
||||
browser: 'browser.js',
|
||||
extensionKind: ['web'],
|
||||
});
|
||||
const files = [
|
||||
{ path: 'extension/browser.js', contents: Buffer.from('') },
|
||||
];
|
||||
|
||||
const vsixManifest = await _toVsixManifest(manifest, files)
|
||||
const result = await parseXmlManifest(vsixManifest);
|
||||
const assets = result.PackageManifest.Assets[0].Asset;
|
||||
assert(assets.every(asset => !asset.$.Type.startsWith('Microsoft.VisualStudio.Code.WebResources')));
|
||||
|
||||
const properties = result.PackageManifest.Metadata[0].Properties[0].Property;
|
||||
const webExtensionProps = properties.filter(p => p.$.Id === 'Microsoft.VisualStudio.Code.WebExtension');
|
||||
assert.equal(webExtensionProps.length, 0);
|
||||
});
|
||||
|
||||
it('should not expose web extension assets and properties for non web extension', async () => {
|
||||
const manifest = createManifest({
|
||||
main: 'main.js',
|
||||
});
|
||||
const files = [
|
||||
{ path: 'extension/main.js', contents: Buffer.from('') },
|
||||
];
|
||||
|
||||
const vsixManifest = await _toVsixManifest(manifest, files, { web: true })
|
||||
const result = await parseXmlManifest(vsixManifest);
|
||||
const assets = result.PackageManifest.Assets[0].Asset;
|
||||
assert(assets.every(asset => !asset.$.Type.startsWith('Microsoft.VisualStudio.Code.WebResources')));
|
||||
|
||||
const properties = result.PackageManifest.Metadata[0].Properties[0].Property;
|
||||
const webExtensionProps = properties.filter(p => p.$.Id === 'Microsoft.VisualStudio.Code.WebExtension');
|
||||
assert.equal(webExtensionProps.length, 0);
|
||||
});
|
||||
|
||||
it('should expose extension kind properties when providedd', async () => {
|
||||
const manifest = createManifest({
|
||||
extensionKind: ['ui', 'workspace', 'web'],
|
||||
});
|
||||
const files = [
|
||||
{ path: 'extension/main.js', contents: Buffer.from('') },
|
||||
];
|
||||
|
||||
const vsixManifest = await _toVsixManifest(manifest, files, { web: true })
|
||||
const result = await parseXmlManifest(vsixManifest);
|
||||
const properties = result.PackageManifest.Metadata[0].Properties[0].Property;
|
||||
const extensionKindProps = properties.filter(p => p.$.Id === 'Microsoft.VisualStudio.Code.ExtensionKind');
|
||||
assert.equal(extensionKindProps[0].$.Value, ['ui', 'workspace', 'web'].join(','));
|
||||
});
|
||||
|
||||
it('should expose extension kind properties when derived', async () => {
|
||||
const manifest = createManifest({
|
||||
main: 'main.js',
|
||||
});
|
||||
const files = [
|
||||
{ path: 'extension/main.js', contents: Buffer.from('') },
|
||||
];
|
||||
|
||||
const vsixManifest = await _toVsixManifest(manifest, files, { web: true })
|
||||
const result = await parseXmlManifest(vsixManifest);
|
||||
const properties = result.PackageManifest.Metadata[0].Properties[0].Property;
|
||||
const extensionKindProps = properties.filter(p => p.$.Id === 'Microsoft.VisualStudio.Code.ExtensionKind');
|
||||
assert.equal(extensionKindProps[0].$.Value, 'workspace');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('qna', () => {
|
||||
it('should use marketplace qna by default', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null)
|
||||
});
|
||||
|
||||
it('should not use marketplace in a github repo, without specifying it', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null),
|
||||
repository: 'https://github.com/username/repository'
|
||||
});
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA');
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink');
|
||||
});
|
||||
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA');
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink');
|
||||
it('should not use marketplace in a github repo, without specifying it', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null),
|
||||
repository: 'https://github.com/username/repository'
|
||||
});
|
||||
|
||||
it('should use marketplace in a github repo, when specifying it', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null),
|
||||
repository: 'https://github.com/username/repository',
|
||||
qna: 'marketplace'
|
||||
});
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA');
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink');
|
||||
});
|
||||
|
||||
assertProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA', 'true');
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink');
|
||||
it('should use marketplace in a github repo, when specifying it', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null),
|
||||
repository: 'https://github.com/username/repository',
|
||||
qna: 'marketplace'
|
||||
});
|
||||
|
||||
it('should handle qna=marketplace', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null),
|
||||
qna: 'marketplace'
|
||||
});
|
||||
assertProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA', 'true');
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink');
|
||||
});
|
||||
|
||||
assertProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA', 'true');
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink');
|
||||
it('should handle qna=marketplace', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null),
|
||||
qna: 'marketplace'
|
||||
});
|
||||
|
||||
it('should handle qna=false', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null),
|
||||
qna: false
|
||||
});
|
||||
assertProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA', 'true');
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink');
|
||||
});
|
||||
|
||||
assertProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA', 'false');
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink');
|
||||
it('should handle qna=false', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null),
|
||||
qna: false
|
||||
});
|
||||
|
||||
it('should handle custom qna', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null),
|
||||
qna: 'http://myqna'
|
||||
});
|
||||
assertProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA', 'false');
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink');
|
||||
});
|
||||
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA');
|
||||
assertProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink', 'http://myqna');
|
||||
it('should handle custom qna', async () => {
|
||||
const xmlManifest = await toXMLManifest({
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
engines: Object.create(null),
|
||||
qna: 'http://myqna'
|
||||
});
|
||||
|
||||
assertMissingProperty(xmlManifest, 'Microsoft.VisualStudio.Services.EnableMarketplaceQnA');
|
||||
assertProperty(xmlManifest, 'Microsoft.VisualStudio.Services.CustomerQnALink', 'http://myqna');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1442,6 +1575,102 @@ describe('MarkdownProcessor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should replace relative links with GitHub URLs while respecting githubBranch', () => {
|
||||
const manifest = {
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
description: 'test extension',
|
||||
engines: Object.create(null),
|
||||
repository: 'https://github.com/username/repository'
|
||||
};
|
||||
|
||||
const root = fixture('readme');
|
||||
const processor = new ReadmeProcessor(manifest, {
|
||||
githubBranch: 'main'
|
||||
});
|
||||
const readme = {
|
||||
path: 'extension/readme.md',
|
||||
localPath: path.join(root, 'readme.md')
|
||||
};
|
||||
|
||||
return processor.onFile(readme)
|
||||
.then(file => read(file))
|
||||
.then(actual => {
|
||||
return readFile(path.join(root, 'readme.branch.main.expected.md'), 'utf8')
|
||||
.then(expected => {
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should override image URLs with baseImagesUrl while also respecting githubBranch", () => {
|
||||
const manifest = {
|
||||
name: "test",
|
||||
publisher: "mocha",
|
||||
version: "0.0.1",
|
||||
description: "test extension",
|
||||
engines: Object.create(null),
|
||||
repository: "https://github.com/username/repository",
|
||||
};
|
||||
|
||||
const root = fixture("readme");
|
||||
const processor = new ReadmeProcessor(manifest, {
|
||||
githubBranch: "main",
|
||||
// Override image relative links to point to different base URL
|
||||
baseImagesUrl: "https://github.com/base",
|
||||
});
|
||||
const readme = {
|
||||
path: "extension/readme.md",
|
||||
localPath: path.join(root, "readme.md"),
|
||||
};
|
||||
|
||||
return processor
|
||||
.onFile(readme)
|
||||
.then((file) => read(file))
|
||||
.then((actual) => {
|
||||
return readFile(
|
||||
path.join(root, "readme.branch.override.images.expected.md"),
|
||||
"utf8"
|
||||
).then((expected) => {
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should override githubBranch setting with baseContentUrl", () => {
|
||||
const manifest = {
|
||||
name: "test",
|
||||
publisher: "mocha",
|
||||
version: "0.0.1",
|
||||
description: "test extension",
|
||||
engines: Object.create(null),
|
||||
repository: "https://github.com/username/repository",
|
||||
};
|
||||
|
||||
const root = fixture("readme");
|
||||
const processor = new ReadmeProcessor(manifest, {
|
||||
githubBranch: "main",
|
||||
baseContentUrl: "https://github.com/base",
|
||||
});
|
||||
const readme = {
|
||||
path: "extension/readme.md",
|
||||
localPath: path.join(root, "readme.md"),
|
||||
};
|
||||
|
||||
return processor
|
||||
.onFile(readme)
|
||||
.then((file) => read(file))
|
||||
.then((actual) => {
|
||||
return readFile(
|
||||
path.join(root, "readme.branch.override.content.expected.md"),
|
||||
"utf8"
|
||||
).then((expected) => {
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should infer baseContentUrl if its a github repo (.git)', () => {
|
||||
const manifest = {
|
||||
name: 'test',
|
||||
|
@ -1500,6 +1729,87 @@ describe('MarkdownProcessor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should replace issue links with urls if its a github repo.', () => {
|
||||
const manifest = {
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
description: 'test extension',
|
||||
engines: Object.create(null),
|
||||
repository: 'https://github.com/username/repository.git'
|
||||
};
|
||||
|
||||
const root = fixture('readme');
|
||||
const processor = new ReadmeProcessor(manifest, {});
|
||||
const readme = {
|
||||
path: 'extension/readme.md',
|
||||
localPath: path.join(root, 'readme.github.md')
|
||||
};
|
||||
|
||||
return processor.onFile(readme)
|
||||
.then(file => read(file))
|
||||
.then(actual => {
|
||||
return readFile(path.join(root, 'readme.github.expected.md'), 'utf8')
|
||||
.then(expected => {
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not replace issue links with urls if its a github repo but issue link expansion is disabled.', () => {
|
||||
const manifest = {
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
description: 'test extension',
|
||||
engines: Object.create(null),
|
||||
repository: 'https://github.com/username/repository.git'
|
||||
};
|
||||
|
||||
const root = fixture('readme');
|
||||
const processor = new ReadmeProcessor(manifest, { expandGitHubIssueLinks: false });
|
||||
const readme = {
|
||||
path: 'extension/readme.md',
|
||||
localPath: path.join(root, 'readme.github.md')
|
||||
};
|
||||
|
||||
return processor.onFile(readme)
|
||||
.then(file => read(file))
|
||||
.then(actual => {
|
||||
return readFile(path.join(root, 'readme.github.md'), 'utf8')
|
||||
.then(expected => {
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not replace issue links with urls if its not a github repo.', () => {
|
||||
const manifest = {
|
||||
name: 'test',
|
||||
publisher: 'mocha',
|
||||
version: '0.0.1',
|
||||
description: 'test extension',
|
||||
engines: Object.create(null),
|
||||
repository: 'https://some-other-provider.com/username/repository.git'
|
||||
};
|
||||
|
||||
const root = fixture('readme');
|
||||
const processor = new ReadmeProcessor(manifest, {});
|
||||
const readme = {
|
||||
path: 'extension/readme.md',
|
||||
localPath: path.join(root, 'readme.github.md')
|
||||
};
|
||||
|
||||
return processor.onFile(readme)
|
||||
.then(file => read(file))
|
||||
.then(actual => {
|
||||
return readFile(path.join(root, 'readme.github.md'), 'utf8')
|
||||
.then(expected => {
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should prevent non-HTTPS images', async () => {
|
||||
const manifest = { name: 'test', publisher: 'mocha', version: '0.0.1', engines: Object.create(null), repository: 'https://github.com/username/repository' };
|
||||
const contents = `![title](http://foo.png)`;
|
||||
|
@ -1537,6 +1847,25 @@ describe('MarkdownProcessor', () => {
|
|||
assert(file);
|
||||
});
|
||||
|
||||
it('should allow SVG from GitHub actions in image tag', async () => {
|
||||
const manifest = { name: 'test', publisher: 'mocha', version: '0.0.1', engines: Object.create(null), repository: 'https://github.com/username/repository' };
|
||||
const contents = `![title](https://github.com/fakeuser/fakerepo/workflows/fakeworkflowname/badge.svg)`;
|
||||
const processor = new ReadmeProcessor(manifest, {});
|
||||
const readme = { path: 'extension/readme.md', contents };
|
||||
|
||||
const file = await processor.onFile(readme);
|
||||
assert(file);
|
||||
});
|
||||
|
||||
it('should prevent SVG from a GitHub repo in image tag', async () => {
|
||||
const manifest = { name: 'test', publisher: 'mocha', version: '0.0.1', engines: Object.create(null), repository: 'https://github.com/username/repository' };
|
||||
const contents = `![title](https://github.com/eviluser/evilrepo/blob/master/malicious.svg)`;
|
||||
const processor = new ReadmeProcessor(manifest, {});
|
||||
const readme = { path: 'extension/readme.md', contents };
|
||||
|
||||
await throws(() => processor.onFile(readme));
|
||||
});
|
||||
|
||||
it('should prevent SVGs from not trusted sources in img tags', async () => {
|
||||
const manifest = { name: 'test', publisher: 'mocha', version: '0.0.1', engines: Object.create(null), repository: 'https://github.com/username/repository' };
|
||||
const contents = `<img src="https://foo/hello.svg" />`;
|
||||
|
@ -1573,4 +1902,184 @@ describe('MarkdownProcessor', () => {
|
|||
|
||||
await throws(() => processor.onFile(readme));
|
||||
});
|
||||
|
||||
it('should catch an unchanged README.md', async () => {
|
||||
const manifest = { name: 'test', publisher: 'mocha', version: '0.0.1', engines: Object.create(null), repository: 'https://github.com/username/repository' };
|
||||
const contents = `This is the README for your extension `;
|
||||
const processor = new ReadmeProcessor(manifest, {});
|
||||
const readme = { path: 'extension/readme.md', contents };
|
||||
|
||||
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 file', async () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
const file = { path: 'extension/browser.js', contents: '' };
|
||||
|
||||
await processor.onFile(file);
|
||||
await processor.onEnd();
|
||||
|
||||
const expected: IAsset[] = [{ type: `Microsoft.VisualStudio.Code.WebResources/${file.path}`, path: file.path }];
|
||||
assert.deepEqual(processor.assets, expected);
|
||||
});
|
||||
|
||||
it('should include file when extension kind is not specified', async () => {
|
||||
const manifest = createManifest({ browser: 'browser.js' });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
const file = { path: 'extension/browser.js', contents: '' };
|
||||
|
||||
await processor.onFile(file);
|
||||
await processor.onEnd();
|
||||
|
||||
const expected: IAsset[] = [{ type: `Microsoft.VisualStudio.Code.WebResources/${file.path}`, path: file.path }];
|
||||
assert.deepEqual(processor.assets, expected);
|
||||
});
|
||||
|
||||
it('should not include file when not asked for', async () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: false });
|
||||
const file = { path: 'extension/browser.js', contents: '' };
|
||||
|
||||
await processor.onFile(file);
|
||||
await processor.onEnd();
|
||||
|
||||
assert.deepEqual(processor.assets, []);
|
||||
});
|
||||
|
||||
it('should not include file for non web extension', async () => {
|
||||
const manifest = createManifest({ extensionKind: ['ui'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
const file = { path: 'extension/browser.js', contents: '' };
|
||||
|
||||
await processor.onFile(file);
|
||||
await processor.onEnd();
|
||||
|
||||
assert.deepEqual(processor.assets, []);
|
||||
});
|
||||
|
||||
it('should include manifest', async () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
const manifestFile = { path: 'extension/package.json', contents: JSON.stringify(manifest) };
|
||||
|
||||
await processor.onFile(manifestFile);
|
||||
await processor.onEnd();
|
||||
|
||||
const expected: IAsset[] = [{ type: `Microsoft.VisualStudio.Code.WebResources/${manifestFile.path}`, path: manifestFile.path }];
|
||||
assert.deepEqual(processor.assets, expected);
|
||||
});
|
||||
|
||||
it('should fail for svg file', async () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
|
||||
try {
|
||||
await processor.onFile({ path: 'extension/sample.svg', contents: '' });
|
||||
} catch (error) {
|
||||
return; // expected
|
||||
}
|
||||
|
||||
assert.fail('Should fail');
|
||||
});
|
||||
|
||||
it('should include max 25 files', async () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
|
||||
const expected: IAsset[] = [];
|
||||
for (let i = 1; i <= 25; i++) {
|
||||
const file = { path: `extension/${i}.json`, contents: `${i}` };
|
||||
await processor.onFile(file);
|
||||
expected.push({ type: `Microsoft.VisualStudio.Code.WebResources/${file.path}`, path: file.path });
|
||||
}
|
||||
|
||||
await processor.onEnd();
|
||||
|
||||
assert.deepEqual(processor.assets.length, 25);
|
||||
assert.deepEqual(processor.assets, expected);
|
||||
});
|
||||
|
||||
it('should throw an error if there are more than 25 files', async () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
|
||||
for (let i = 1; i <= 26; i++) {
|
||||
await processor.onFile({ path: `extension/${i}.json`, contents: `${i}` });
|
||||
}
|
||||
|
||||
try {
|
||||
await processor.onEnd();
|
||||
} catch (error) {
|
||||
return; // expected error
|
||||
}
|
||||
assert.fail('Should fail');
|
||||
});
|
||||
|
||||
it('should include web extension property & tag', async () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
|
||||
await processor.onEnd();
|
||||
|
||||
assert.equal(processor.vsix.webExtension, true);
|
||||
assert.deepEqual(processor.tags, ['__web_extension']);
|
||||
});
|
||||
|
||||
it('should include web extension property & tag when extension kind is not provided', async () => {
|
||||
const manifest = createManifest({ browser: 'browser.js' });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
|
||||
await processor.onEnd();
|
||||
|
||||
assert.equal(processor.vsix.webExtension, true);
|
||||
assert.deepEqual(processor.tags, ['__web_extension']);
|
||||
});
|
||||
|
||||
it('should not include web extension property & tag when not asked for', async () => {
|
||||
const manifest = createManifest({ extensionKind: ['web'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: false });
|
||||
|
||||
await processor.onEnd();
|
||||
|
||||
assert.equal(processor.vsix.webExtension, undefined);
|
||||
assert.deepEqual(processor.tags, []);
|
||||
});
|
||||
|
||||
it('should not include web extension property & tag for non web extension', async () => {
|
||||
const manifest = createManifest({ extensionKind: ['ui'] });
|
||||
const processor = new WebExtensionProcessor(manifest, { web: true });
|
||||
|
||||
await processor.onEnd();
|
||||
|
||||
assert.equal(processor.vsix.webExtension, undefined);
|
||||
assert.deepEqual(processor.tags, []);
|
||||
});
|
||||
|
||||
|
||||
});
|
|
@ -109,6 +109,7 @@ describe('validateVSCodeTypesCompatibility', () => {
|
|||
|
||||
validateVSCodeTypesCompatibility('1.30.0', '1.30.0');
|
||||
validateVSCodeTypesCompatibility('1.30.0', '1.20.0');
|
||||
validateVSCodeTypesCompatibility('1.46.0', '1.45.1');
|
||||
|
||||
assert.throws(() => validateVSCodeTypesCompatibility('1.30.0', '1.40.0'));
|
||||
assert.throws(() => validateVSCodeTypesCompatibility('1.30.0', '^1.40.0'));
|
||||
|
|
|
@ -91,13 +91,13 @@ export function validateVSCodeTypesCompatibility(engineVersion: string, typeVers
|
|||
|
||||
const error = new Error(`@types/vscode ${typeVersion} greater than engines.vscode ${engineVersion}. Consider upgrade engines.vscode or use an older @types/vscode version`);
|
||||
|
||||
if (typeof typeMajor === 'number' && typeof engineMajor === 'number' && typeMajor > engineMajor) {
|
||||
if (typeMajor > engineMajor) {
|
||||
throw error;
|
||||
}
|
||||
if (typeof typeMinor === 'number' && typeof engineMinor === 'number' && typeMinor > engineMinor) {
|
||||
if (typeMajor === engineMajor && typeMinor > engineMinor) {
|
||||
throw error;
|
||||
}
|
||||
if (typeof typePatch === 'number' && typeof enginePatch === 'number' && typePatch > enginePatch) {
|
||||
if (typeMajor === engineMajor && typeMinor === engineMinor && typePatch > enginePatch) {
|
||||
throw error;
|
||||
}
|
||||
}
|
|
@ -13,16 +13,9 @@ const columns = process.stdout.columns ? process.stdout.columns : 80;
|
|||
// unicode charset. For now we use fallback icons when on windows.
|
||||
const useFallbackIcons = process.platform === 'win32';
|
||||
|
||||
export const icons = useFallbackIcons?
|
||||
{
|
||||
download: '\u{2193}',
|
||||
star: '\u{2665}',
|
||||
emptyStar: '\u{2022}',
|
||||
} : {
|
||||
download: '\u{2913}',
|
||||
star: '\u{2605}',
|
||||
emptyStar: '\u{2606}',
|
||||
};
|
||||
export const icons = useFallbackIcons
|
||||
? { download: '\u{2193}', star: '\u{2665}', emptyStar: '\u{2022}', }
|
||||
: { download: '\u{2913}', star: '\u{2605}', emptyStar: '\u{2606}', };
|
||||
|
||||
export function formatDate(date) { return date.toLocaleString(fixedLocale, format.date); }
|
||||
export function formatTime(date) { return date.toLocaleString(fixedLocale, format.time); }
|
||||
|
@ -53,11 +46,11 @@ export function wordWrap(text: string, width: number = columns): string {
|
|||
return text
|
||||
.replace(/^\s+/, '')
|
||||
.split('')
|
||||
.reduce(([out, buffer, pos], ch, i) => {
|
||||
.reduce(([out, buffer, pos], ch) => {
|
||||
const nl = pos === maxWidth ? `\n${indent}` : '';
|
||||
const newPos: number = nl ? 0 : +pos + 1;
|
||||
return / |-|,|\./.test(ch) ?
|
||||
[`${out}${buffer}${ch}${nl}`, '', newPos] : [`${out}${nl}`, buffer+ch, newPos];
|
||||
[`${out}${buffer}${ch}${nl}`, '', newPos] : [`${out}${nl}`, buffer + ch, newPos];
|
||||
}, [indent, '', 0])
|
||||
.slice(0, 2)
|
||||
.join('');
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
--require source-map-support/register out/test
|
|
@ -3,7 +3,9 @@
|
|||
"target": "es2015",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"outDir": "out"
|
||||
"outDir": "out",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
|
Loading…
Add table
Reference in a new issue