Minifier initial version

- wired up a basic minification package
- accepts config for css or js files and can concat and minify them into a single file for each type
- this will be used for generating merged css and js files for various cards, controlled by theme config
This commit is contained in:
Hannah Wolfe 2021-10-13 13:05:52 +01:00
parent 27cc7a06cb
commit 03b1e9c3bd
14 changed files with 481 additions and 0 deletions

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/node'
]
};

21
ghost/minifier/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013-2021 Ghost Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

56
ghost/minifier/README.md Normal file
View File

@ -0,0 +1,56 @@
# Minifier
## Install
`npm install @tryghost/minifier --save`
or
`yarn add @tryghost/minifier`
## Usage
```
const Minifier = require('@tryghost/minifier');
const minifier = new Minifier({
src: 'my/src/path',
dest: 'my/dest/path'
});
minifier.minify({
'some.css': '*.css',
'then.js': '!(other).js'
});
```
- Minfier constructor requires a src and a dest
- minify() function takes an object with destination file as the key and source glob as the value
- globs can be anything tiny-glob supports
- destination files must end with .css or .js
- src files will be minified according to their destination file extension
## Develop
This is a mono repository, managed with [lerna](https://lernajs.io/).
Follow the instructions for the top-level repo.
1. `git clone` this repo & `cd` into it as usual
2. Run `yarn` to install top-level dependencies.
## Run
- `yarn dev`
## Test
- `yarn lint` run just eslint
- `yarn test` run lint and tests
# Copyright & License
Copyright (c) 2013-2021 Ghost Foundation - Released under the [MIT license](LICENSE).

1
ghost/minifier/index.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./lib/minifier');

View File

@ -0,0 +1,124 @@
const errors = require('@tryghost/errors');
const debug = require('@tryghost/debug')('affsfdfsdfsdfsdffsdsdfsd');
const tpl = require('@tryghost/tpl');
const csso = require('csso');
const uglify = require('uglify-js');
const glob = require('tiny-glob');
const path = require('path');
const fs = require('fs').promises;
const messages = {
badDestination: {
message: 'Unexpected destination {dest}',
context: 'Minifier expected a destination that ended in .css or .js'
},
missingConstructorOption: {
message: 'Minifier missing {opt} option',
context: 'new Minifier({}) requires a {opt} option'
},
globalHelp: 'Refer to the readme for @tryghost/minifier for how to use this module'
};
// public API for minify hooks
class Minifier {
constructor({src, dest}) {
if (!src) {
throw new errors.IncorrectUsageError({
message: tpl(messages.missingConstructorOption.message, {opt: 'src'}),
context: tpl(messages.missingConstructorOption.context, {opt: 'src'}),
help: tpl(messages.globalHelp)
});
}
if (!dest) {
throw new errors.IncorrectUsageError({
message: tpl(messages.missingConstructorOption.message, {opt: 'dest'}),
context: tpl(messages.missingConstructorOption.context, {opt: 'dest'}),
help: tpl(messages.globalHelp)
});
}
this.srcPath = src;
this.destPath = dest;
}
getFullSrc(src) {
return path.join(this.srcPath, src);
}
getFullDest(dest) {
return path.join(this.destPath, dest);
}
async minifyCSS(contents) {
const result = csso.minify(contents);
if (result && result.css) {
return result.css;
}
return null;
}
async minifyJS(contents) {
const result = uglify.minify(contents);
if (result && result.code) {
return result.code;
}
return null;
}
async getMatchingFiles(src) {
return await glob(this.getFullSrc(src));
}
async readFiles(files) {
let mergedFiles = '';
for (const file of files) {
const contents = await fs.readFile(file, 'utf8');
mergedFiles += contents;
}
return mergedFiles;
}
async writeFile(contents, dest) {
if (contents) {
let writePath = this.getFullDest(dest);
await fs.writeFile(writePath, contents);
return writePath;
}
}
async minify(options) {
debug('Begin', options);
const destinations = Object.keys(options);
const minifiedFiles = [];
for (const dest of destinations) {
const src = options[dest];
const files = await this.getMatchingFiles(src);
const contents = await this.readFiles(files);
let minifiedContents;
if (dest.endsWith('.css')) {
minifiedContents = await this.minifyCSS(contents);
} else if (dest.endsWith('.js')) {
minifiedContents = await this.minifyJS(contents);
} else {
throw new errors.IncorrectUsageError({
message: tpl(messages.badDestination.message, {dest}),
context: tpl(messages.badDestination.context),
help: tpl(messages.globalHelp)
});
}
const result = await this.writeFile(minifiedContents, dest);
if (result) {
minifiedFiles.push(result);
}
}
debug('End');
return minifiedFiles;
}
}
module.exports = Minifier;

View File

@ -0,0 +1,35 @@
{
"name": "@tryghost/minifier",
"version": "0.0.0",
"repository": "https://github.com/TryGhost/Utils/tree/main/packages/minifier",
"author": "Ghost Foundation",
"license": "MIT",
"main": "index.js",
"scripts": {
"dev": "echo \"Implement me!\"",
"test": "NODE_ENV=testing c8 --check-coverage mocha './test/**/*.test.js'",
"lint": "eslint . --ext .js --cache",
"posttest": "yarn lint"
},
"files": [
"index.js",
"lib"
],
"publishConfig": {
"access": "public"
},
"devDependencies": {
"c8": "7.10.0",
"mocha": "9.1.2",
"should": "13.2.3",
"sinon": "11.1.2"
},
"dependencies": {
"@tryghost/debug": "^0.1.8",
"@tryghost/errors": "^0.2.16",
"@tryghost/tpl": "^0.1.7",
"csso": "4.2.0",
"tiny-glob": "^0.2.9",
"uglify-js": "3.14.2"
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
plugins: ['ghost'],
extends: [
'plugin:ghost/test'
],
ignorePatterns: ['fixtures']
};

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

@ -0,0 +1,72 @@
// Switch these lines once there are useful utils
// const testUtils = require('./utils');
require('./utils');
const path = require('path');
const fs = require('fs').promises;
const os = require('os');
const Minifier = require('../lib/minifier');
describe('Minifier', function () {
let minifier;
let testDir;
before(async function () {
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'minifier-tests-'));
minifier = new Minifier({
src: path.join(__dirname, 'fixtures', 'basic-cards'),
dest: path.join(os.tmpdir(), 'minifier-tests')
});
});
after(async function () {
await fs.rmdir(testDir);
});
describe('getMatchingFiles expands globs correctly', function () {
it('star glob e.g. css/*.css', async function () {
let result = await minifier.getMatchingFiles('css/*.css');
result.should.be.an.Array().with.lengthOf(2);
result[0].should.eql('test/fixtures/basic-cards/css/bookmark.css');
result[1].should.eql('test/fixtures/basic-cards/css/gallery.css');
});
it('reverse match glob e.g. css/!(bookmark).css', async function () {
let result = await minifier.getMatchingFiles('css/!(bookmark).css');
result.should.be.an.Array().with.lengthOf(1);
result[0].should.eql('test/fixtures/basic-cards/css/gallery.css');
});
it('reverse match glob e.g. css/!(bookmark|gallery).css', async function () {
let result = await minifier.getMatchingFiles('css/!(bookmark|gallery).css');
result.should.be.an.Array().with.lengthOf(0);
});
});
describe('Minify', function () {
it('single type, single file', async function () {
let result = await minifier.minify({
'card.min.js': 'js/*.js'
});
result.should.be.an.Array().with.lengthOf(1);
});
it('single type, multi file', async function () {
let result = await minifier.minify({
'card.min.css': 'css/*.css'
});
result.should.be.an.Array().with.lengthOf(1);
});
it('both css and js types + multiple files', async function () {
let result = await minifier.minify({
'card.min.js': 'js/*.js',
'card.min.css': 'css/*.css'
});
result.should.be.an.Array().with.lengthOf(2);
});
});
});

View File

@ -0,0 +1,11 @@
/**
* Custom Should Assertions
*
* Add any custom assertions to this file.
*/
// Example Assertion
// should.Assertion.add('ExampleAssertion', function () {
// this.params = {operator: 'to be a valid Example Assertion'};
// this.obj.should.be.an.Object;
// });

View File

@ -0,0 +1,11 @@
/**
* Test Utilities
*
* Shared utils for writing tests
*/
// Require overrides - these add globals for tests
require('./overrides');
// Require assertions - adds custom should assertions
require('./assertions');

View File

@ -0,0 +1,10 @@
// This file is required before any test is run
// Taken from the should wiki, this is how to make should global
// Should is a global in our eslint test config
global.should = require('should').noConflict();
should.extend();
// Sinon is a simple case
// Sinon is a global in our eslint test config
global.sinon = require('sinon');