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