0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2024-12-30 22:34:01 -05:00

Initial card asset service implementation

- Requires the new @tryghost/minifier package
- Adds a new service that will handle taking config from the theme and optionally including assets for Koenig editor cards
- It supports both css and js as cards may need one or both
- For any given config, the tool can find the matching files to include and concat and minify them into one file per type
- Currently has an override in place so that this is not yet customisable in the theme - will remove this override when we're ready for the feature
This commit is contained in:
Hannah Wolfe 2021-10-13 08:45:56 +01:00
parent df5c87fae3
commit fdf38ba8c6
No known key found for this signature in database
GPG key ID: 9F8C7532D0A6BA55
15 changed files with 514 additions and 15 deletions

3
.gitignore vendored
View file

@ -123,7 +123,8 @@ test/coverage
# Built asset files
/core/built
/core/server/web/admin/views/*.html
/core/frontend/public/ghost.min.css
/core/frontend/public/*.min.css
/core/frontend/public/*.min.js
# Caddyfile - for local development with ssl + caddy
Caddyfile

View file

@ -74,6 +74,7 @@ module.exports = function (grunt) {
'core/server/**/*.js',
'core/shared/**/*.js',
'core/frontend/**/*.js',
'!core/frontend/public/**',
'core/*.js',
'index.js',
'config.*.json',

View file

@ -141,6 +141,9 @@ async function initFrontend() {
const helperService = require('./frontend/services/helpers');
await helperService.init();
const cardAssetService = require('./frontend/services/card-assets');
await cardAssetService.init();
debug('End: initFrontend');
}

View file

@ -85,8 +85,22 @@ class Bridge {
}
}
getCardAssetConfig() {
if (this.getActiveTheme()) {
return this.getActiveTheme().config('card_assets');
} else {
return true;
}
}
reloadFrontend() {
const apiVersion = this.getFrontendApiVersion();
const cardAssetConfig = this.getCardAssetConfig();
debug('reload card assets config', cardAssetConfig);
const cardAssetService = require('./frontend/services/card-assets');
cardAssetService.load(cardAssetConfig);
debug('reload frontend', apiVersion);
const siteApp = require('./frontend/web/site');
siteApp.reload({apiVersion});

View file

@ -0,0 +1,16 @@
const debug = require('@tryghost/debug')('card-assets');
const themeEngine = require('../theme-engine');
const CardAssetService = require('./service');
let cardAssetService = new CardAssetService();
const initFn = async () => {
const cardAssetConfig = themeEngine.getActive().config('card_assets');
debug('initialising with config', cardAssetConfig);
await cardAssetService.load(cardAssetConfig);
};
module.exports = cardAssetService;
module.exports.init = initFn;

View file

@ -0,0 +1,96 @@
const Minifier = require('@tryghost/minifier');
const _ = require('lodash');
const path = require('path');
const fs = require('fs').promises;
const defaultConfig = false;
class CardAssetService {
constructor(options = {}) {
// @TODO: use our config paths concept
this.src = options.src || path.join(__dirname, '../../src/cards');
this.dest = options.dest || path.join(__dirname, '../../public');
this.config = 'config' in options ? options.config : defaultConfig;
this.minifier = new Minifier({src: this.src, dest: this.dest});
this.files = [];
}
generateGlobs() {
// CASE: The theme has asked for all card assets to be included by default
if (this.config === true) {
return {
'cards.min.css': 'css/*.css',
'cards.min.js': 'js/*.js'
};
}
// CASE: the theme has declared an include directive, we should include exactly these assets
// Include rules take precedence over exclude rules.
if (_.has(this.config, 'include')) {
return {
'cards.min.css': `css/(${this.config.include.join('|')}).css`,
'cards.min.js': `js/(${this.config.include.join('|')}).js`
};
}
// CASE: the theme has declared an exclude directive, we should include exactly these assets
if (_.has(this.config, 'exclude')) {
return {
'cards.min.css': `css/!(${this.config.exclude.join('|')}).css`,
'cards.min.js': `js/!(${this.config.exclude.join('|')}).js`
};
}
// CASE: theme has asked that no assets be included
// CASE: we didn't understand config, don't do anything
return {};
}
async minify(globs) {
return await this.minifier.minify(globs);
}
async clearFiles() {
this.files = [];
// @deprecated switch this to use fs.rm when we drop support for Node v12
try {
await fs.unlink(path.join(this.dest, 'cards.min.css'));
} catch (error) {
// Don't worry if the file didn't exist
if (error.code !== 'ENOENT') {
throw error;
}
}
try {
await fs.unlink(path.join(this.dest, 'cards.min.js'));
} catch (error) {
// Don't worry if the file didn't exist
if (error.code !== 'ENOENT') {
throw error;
}
}
}
/**
* A theme can declare which cards it supports, and we'll do the rest
*
* @param {Array|boolean} config
* @returns
*/
async load(config) {
if (config) {
this.config = config;
}
await this.clearFiles();
const globs = this.generateGlobs();
this.files = await this.minify(globs);
}
}
module.exports = CardAssetService;

View file

@ -1,3 +1,4 @@
{
"posts_per_page": 5
"posts_per_page": 5,
"card_assets": false
}

View file

@ -1,6 +1,6 @@
const _ = require('lodash');
const defaultConfig = require('./defaults');
const allowedKeys = ['posts_per_page', 'image_sizes'];
const allowedKeys = ['posts_per_page', 'image_sizes', 'card_assets'];
module.exports.create = function configLoader(packageJson) {
let config = _.cloneDeep(defaultConfig);
@ -9,5 +9,9 @@ module.exports.create = function configLoader(packageJson) {
config = _.assign(config, _.pick(packageJson.config, allowedKeys));
}
// @TOD0: remove this guard when we're ready
// Temporary override to prevent themes from controlling this until we're ready
config.card_assets = defaultConfig.card_assets;
return config;
};

View file

@ -0,0 +1,83 @@
/* style.css */
.kg-bookmark-card {
width: 100%;
position: relative;
}
.kg-bookmark-container {
display: flex;
flex-wrap: wrap;
flex-direction: row-reverse;
color: currentColor;
font-family: inherit;
text-decoration: none;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.kg-bookmark-container:hover {
text-decoration: none;
}
.kg-bookmark-content {
flex-basis: 0;
flex-grow: 999;
padding: 20px;
order: 1;
}
.kg-bookmark-title {
font-weight: 600;
}
.kg-bookmark-metadata,
.kg-bookmark-description {
margin-top: .5em;
}
.kg-bookmark-metadata {
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.kg-bookmark-description {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.kg-bookmark-icon {
display: inline-block;
width: 1em;
height: 1em;
vertical-align: text-bottom;
margin-right: .5em;
margin-bottom: .05em;
}
.kg-bookmark-thumbnail {
display: flex;
flex-basis: 24rem;
flex-grow: 1;
}
.kg-bookmark-thumbnail img {
max-width: 100%;
height: auto;
vertical-align: bottom;
object-fit: cover;
}
.kg-bookmark-author {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.kg-bookmark-publisher::before {
content: "•";
margin: 0 .5em;
}

View file

@ -0,0 +1,36 @@
.kg-gallery-card {
margin: 0 0 1.5em;
}
.kg-gallery-card figcaption {
margin: -1.0em 0 1.5em;
}
.kg-gallery-container {
display: flex;
flex-direction: column;
margin: 1.5em auto;
max-width: 1040px;
width: 100vw;
}
.kg-gallery-row {
display: flex;
flex-direction: row;
justify-content: center;
}
.kg-gallery-image img {
display: block;
margin: 0;
width: 100%;
height: 100%;
}
.kg-gallery-row:not(:first-of-type) {
margin: 0.75em 0 0 0;
}
.kg-gallery-image:not(:first-of-type) {
margin: 0 0 0 0.75em;
}

View file

@ -0,0 +1,8 @@
var images = document.querySelectorAll('.kg-gallery-image img');
images.forEach(function (image) {
var container = image.closest('.kg-gallery-image');
var width = image.attributes.width.value;
var height = image.attributes.height.value;
var ratio = width / height;
container.style.flex = ratio + ' 1 0%';
})

View file

@ -37,7 +37,7 @@
"test:int:slow": "yarn test:integration --reporter=mocha-slow-test-reporter",
"test:e2e:slow": "yarn test:e2e --reporter=mocha-slow-test-reporter",
"test:reg:slow": "mocha --require=./test/utils/overrides.js --exit --trace-warnings --recursive --extension=test.js './test/regression' --timeout=60000 --reporter=mocha-slow-test-reporter",
"cov:unit": "c8 report --all -n 'core/{*.js,frontend,server,shared}' --reporter text --reporter html",
"cov:unit": "c8 report --all -n 'core/{*.js,frontend,server,shared}' -x 'core/frontend/public' --reporter text --reporter html",
"lint:server": "eslint --ignore-path .eslintignore 'core/server/**/*.js' 'core/*.js' '*.js'",
"lint:shared": "eslint --ignore-path .eslintignore 'core/shared/**/*.js'",
"lint:frontend": "eslint --ignore-path .eslintignore 'core/frontend/**/*.js'",
@ -85,6 +85,7 @@
"@tryghost/members-offers": "0.10.1",
"@tryghost/members-ssr": "1.0.15",
"@tryghost/metrics": "1.0.0",
"@tryghost/minifier": "0.1.0",
"@tryghost/mw-session-from-token": "0.1.26",
"@tryghost/nodemailer": "0.3.6",
"@tryghost/package-json": "1.0.6",

View file

@ -0,0 +1,176 @@
const should = require('should');
const sinon = require('sinon');
const path = require('path');
const fs = require('fs').promises;
const os = require('os');
const cardAssetService = require('../../../../core/frontend/services/card-assets');
const CardAssetService = require('../../../../core/frontend/services/card-assets/service');
const themeEngine = require('../../../../core/frontend/services/theme-engine');
describe('Card Asset Init', function () {
it('calls loader with config', function () {
sinon.stub(themeEngine, 'getActive').returns({
config: function (key) {
if (key === 'card_assets') {
return 'random-test-value';
}
}
});
let serviceStub = sinon.stub(cardAssetService, 'load');
cardAssetService.init();
serviceStub.calledOnce.should.eql(true);
serviceStub.calledWith('random-test-value').should.eql(true);
});
});
describe('Card Asset Service', function () {
let testDir,
srcDir,
destDir;
before(async function () {
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ghost-tests-'));
srcDir = path.join(testDir, 'src');
destDir = path.join(testDir, 'dest');
await fs.mkdir(srcDir);
await fs.mkdir(destDir);
await fs.mkdir(path.join(srcDir, 'css'));
await fs.mkdir(path.join(srcDir, 'js'));
});
after(async function () {
await fs.rmdir(testDir, {recursive: true});
});
it('can load nothing', async function () {
const cardAssets = new CardAssetService({
src: srcDir,
dest: destDir
});
await cardAssets.load();
cardAssets.files.should.eql([]);
});
it('can load a single css file', async function () {
const cardAssets = new CardAssetService({
src: srcDir,
dest: destDir
});
await fs.writeFile(path.join(srcDir, 'css', 'test.css'), '.test { color: #fff }');
await cardAssets.load(true);
cardAssets.files.should.eql(['cards.min.css']);
});
it('can clearFiles', async function () {
const cardAssets = new CardAssetService({
src: srcDir,
dest: destDir
});
await fs.writeFile(path.join(destDir, 'cards.min.css'), 'test-css');
await fs.writeFile(path.join(destDir, 'cards.min.js'), 'test-js');
await cardAssets.clearFiles();
try {
await fs.readFile(path.join(destDir, 'cards.min.css'), 'utf-8');
should.fail(cardAssets, 'CSS file should not exist');
} catch (error) {
if (error instanceof should.AssertionError) {
throw error;
}
error.code.should.eql('ENOENT');
}
try {
await fs.readFile(path.join(destDir, 'cards.min.js'), 'utf-8');
should.fail(cardAssets, 'JS file should not exist');
} catch (error) {
if (error instanceof should.AssertionError) {
throw error;
}
error.code.should.eql('ENOENT');
}
});
describe('Generate the correct glob strings', function () {
// @TODO: change the default
it('DEFAULT CASE: do nothing [temp]', function () {
const cardAssets = new CardAssetService();
cardAssets.generateGlobs().should.eql({});
});
it('CASE: card_assets = true, all cards assets should be included', function () {
const cardAssets = new CardAssetService({
config: true
});
cardAssets.generateGlobs().should.eql({
'cards.min.css': 'css/*.css',
'cards.min.js': 'js/*.js'
});
});
it('CASE: card_assets = false, no card assets should be included', function () {
const cardAssets = new CardAssetService({
config: false
});
cardAssets.generateGlobs().should.eql({});
});
it('CASE: card_assets is an object with an exclude property, generate inverse match strings', function () {
const cardAssets = new CardAssetService({
config: {
exclude: ['bookmarks']
}
});
cardAssets.generateGlobs().should.eql({
'cards.min.css': 'css/!(bookmarks).css',
'cards.min.js': 'js/!(bookmarks).js'
});
});
it('CASE: card_assets is an object with an include property, generate match strings', function () {
const cardAssets = new CardAssetService({
config: {
include: ['gallery']
}
});
cardAssets.generateGlobs().should.eql({
'cards.min.css': 'css/(gallery).css',
'cards.min.js': 'js/(gallery).js'
});
});
it('CASE: card_assets has include and exclude, include should win', function () {
const cardAssets = new CardAssetService({
config: {
include: ['gallery'],
exclude: ['bookmark']
}
});
cardAssets.generateGlobs().should.eql({
'cards.min.css': 'css/(gallery).css',
'cards.min.js': 'js/(gallery).js'
});
});
});
});

View file

@ -11,25 +11,37 @@ describe('Themes', function () {
it('handles no package.json', function () {
const config = themeConfig.create();
config.should.eql({posts_per_page: 5});
config.should.eql({
posts_per_page: 5,
card_assets: false
});
});
it('handles package.json without config', function () {
const config = themeConfig.create({name: 'casper'});
config.should.eql({posts_per_page: 5});
config.should.eql({
posts_per_page: 5,
card_assets: false
});
});
it('handles allows package.json to overrideg default', function () {
const config = themeConfig.create({name: 'casper', config: {posts_per_page: 3}});
config.should.eql({posts_per_page: 3});
config.should.eql({
posts_per_page: 3,
card_assets: false
});
});
it('handles ignores non-allowed config', function () {
const config = themeConfig.create({name: 'casper', config: {magic: 'roundabout'}});
config.should.eql({posts_per_page: 5});
config.should.eql({
posts_per_page: 5,
card_assets: false
});
});
});
});

View file

@ -1350,7 +1350,7 @@
"@tryghost/tpl" "^0.1.4"
lodash "^4.17.21"
"@tryghost/debug@0.1.9", "@tryghost/debug@^0.1.2", "@tryghost/debug@^0.1.4", "@tryghost/debug@^0.1.5", "@tryghost/debug@^0.1.9":
"@tryghost/debug@0.1.9", "@tryghost/debug@^0.1.2", "@tryghost/debug@^0.1.4", "@tryghost/debug@^0.1.5", "@tryghost/debug@^0.1.8", "@tryghost/debug@^0.1.9":
version "0.1.9"
resolved "https://registry.yarnpkg.com/@tryghost/debug/-/debug-0.1.9.tgz#9cb4b5debca96ffb010b73811f01da6ca581935d"
integrity sha512-//j7JykptEvKWuxNnE8FgjNrUqkZReVEonMvMZdP25JUqza/3gNhju6vcwsl9lZwGk4Lnn/NEGZil7Gg+Y76BQ==
@ -1395,7 +1395,7 @@
ghost-ignition "^4.2.4"
lodash "^4.17.20"
"@tryghost/errors@0.2.17", "@tryghost/errors@^0.2.10", "@tryghost/errors@^0.2.11", "@tryghost/errors@^0.2.12", "@tryghost/errors@^0.2.13", "@tryghost/errors@^0.2.14", "@tryghost/errors@^0.2.17", "@tryghost/errors@^0.2.9":
"@tryghost/errors@0.2.17", "@tryghost/errors@^0.2.10", "@tryghost/errors@^0.2.11", "@tryghost/errors@^0.2.12", "@tryghost/errors@^0.2.13", "@tryghost/errors@^0.2.14", "@tryghost/errors@^0.2.16", "@tryghost/errors@^0.2.17", "@tryghost/errors@^0.2.9":
version "0.2.17"
resolved "https://registry.yarnpkg.com/@tryghost/errors/-/errors-0.2.17.tgz#9b89f3845256ace5650593f41cc86d64965b56ed"
integrity sha512-Mj+bedWOwfooNA8fQdp6gIcRvWcKhJ/hOyGzu6OLFDLgEosFEeuFgXE6SsAWkf9+9NTYX30w88qGIWZqOhEAmQ==
@ -1666,6 +1666,18 @@
optionalDependencies:
promise.allsettled "^1.0.5"
"@tryghost/minifier@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@tryghost/minifier/-/minifier-0.1.0.tgz#121f30694c5a0e3755dfe77a1d5df6dd5191e862"
integrity sha512-ofP+wuYV/je7IrfzwYQVR7FGtsXNSHrjpTxcqIvP6SuNhJu0npob6k/67T+95sOyy+Qu24HK8su277azmgDxXA==
dependencies:
"@tryghost/debug" "^0.1.8"
"@tryghost/errors" "^0.2.16"
"@tryghost/tpl" "^0.1.7"
csso "4.2.0"
terser "^5.9.0"
tiny-glob "^0.2.9"
"@tryghost/mobiledoc-kit@^0.12.4-ghost.1":
version "0.12.4-ghost.1"
resolved "https://registry.yarnpkg.com/@tryghost/mobiledoc-kit/-/mobiledoc-kit-0.12.4-ghost.1.tgz#32060242b4c7e787a9605ba856454c6a26141925"
@ -1781,7 +1793,7 @@
dependencies:
unidecode "^0.1.8"
"@tryghost/tpl@0.1.8", "@tryghost/tpl@^0.1.2", "@tryghost/tpl@^0.1.3", "@tryghost/tpl@^0.1.4", "@tryghost/tpl@^0.1.5", "@tryghost/tpl@^0.1.8":
"@tryghost/tpl@0.1.8", "@tryghost/tpl@^0.1.2", "@tryghost/tpl@^0.1.3", "@tryghost/tpl@^0.1.4", "@tryghost/tpl@^0.1.5", "@tryghost/tpl@^0.1.7", "@tryghost/tpl@^0.1.8":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@tryghost/tpl/-/tpl-0.1.8.tgz#28acd930b11b71a23372f1855cb8b92282c20bdd"
integrity sha512-0M02hZ3VNhlH9KeUXV75l3UUeNSRhmXUZfWn4vrosu8B3YCwIx+Q/cpWl5DiZ0QuIaVT7YwYDEHtFMgsKk0gXQ==
@ -3163,7 +3175,7 @@ commander@5.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
commander@^2.19.0:
commander@^2.19.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@ -3556,7 +3568,7 @@ cssnano@5.0.9:
lilconfig "^2.0.3"
yaml "^1.10.2"
csso@^4.2.0:
csso@4.2.0, csso@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
@ -5320,6 +5332,16 @@ globals@^13.6.0, globals@^13.9.0:
dependencies:
type-fest "^0.20.2"
globalyzer@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465"
integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==
globrex@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
globule@^1.0.0:
version "1.3.3"
resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.3.tgz#811919eeac1ab7344e905f2e3be80a13447973c2"
@ -10163,6 +10185,14 @@ source-map-resolve@^0.5.0:
source-map-url "^0.4.0"
urix "^0.1.0"
source-map-support@~0.5.20:
version "0.5.20"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"
integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map-url@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
@ -10173,12 +10203,12 @@ source-map@^0.5.0, source-map@^0.5.6:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
source-map@^0.6.1, source-map@~0.6.1:
source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.7.3:
source-map@^0.7.3, source-map@~0.7.2:
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
@ -10613,6 +10643,15 @@ tarn@^3.0.1:
resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec"
integrity sha512-6usSlV9KyHsspvwu2duKH+FMUhqJnAh6J5J/4MITl8s94iSUQTLkJggdiewKv4RyARQccnigV48Z+khiuVZDJw==
terser@^5.9.0:
version "5.9.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.9.0.tgz#47d6e629a522963240f2b55fcaa3c99083d2c351"
integrity sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==
dependencies:
commander "^2.20.0"
source-map "~0.7.2"
source-map-support "~0.5.20"
test-exclude@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
@ -10647,6 +10686,14 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-glob@^0.2.9:
version "0.2.9"
resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2"
integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==
dependencies:
globalyzer "0.1.0"
globrex "^0.1.2"
tiny-lr@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/tiny-lr/-/tiny-lr-1.1.1.tgz#9fa547412f238fedb068ee295af8b682c98b2aab"