fix(website): resize showcase images, tighten CI check (#6043)
* fix(website): resize images to width 640 * revert changes first... * resize images * final changes * Add to CI * refactor tests * Fix script * fix script * Final fixes * Oops * relax * fix * crop * Optimize Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
|
@ -6,7 +6,6 @@ build
|
||||||
coverage
|
coverage
|
||||||
jest.config.js
|
jest.config.js
|
||||||
jest.transform.js
|
jest.transform.js
|
||||||
scripts
|
|
||||||
examples/
|
examples/
|
||||||
|
|
||||||
packages/lqip-loader/lib/
|
packages/lqip-loader/lib/
|
||||||
|
|
4
.github/workflows/showcase-test.yml
vendored
|
@ -14,13 +14,11 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js ${{ matrix.node }}
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
|
||||||
cache: yarn
|
cache: yarn
|
||||||
- name: Installation
|
- name: Installation
|
||||||
run: yarn
|
run: yarn
|
||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test website/src/data/__tests__/user.test.ts
|
run: yarn test website/src/data/__tests__/user.test.ts
|
||||||
# TODO another job to optimize the images, see https://github.com/facebook/docusaurus/issues/5980
|
|
||||||
|
|
34
admin/scripts/image-resize.mjs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const images = (
|
||||||
|
await fs.readdir(new URL('../../website/src/data/showcase', import.meta.url))
|
||||||
|
).filter((file) => ['.png', 'jpg', '.jpeg'].includes(path.extname(file)));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
images.map(async (img) => {
|
||||||
|
const imgPath = new URL(
|
||||||
|
`../../website/src/data/showcase/${img}`,
|
||||||
|
import.meta.url,
|
||||||
|
).pathname;
|
||||||
|
const data = await sharp(imgPath)
|
||||||
|
.resize(640, 320, {fit: 'cover', position: 'top'})
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
await fs.writeFile(imgPath.replace(/jpe?g/, 'png'), data);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// You should also run optimizt `find website/src/data/showcase -type f -name '*.png'`.
|
||||||
|
// This is not included here because @funboxteam/optimizt doesn't seem to play well with M1
|
||||||
|
// so I had to run this in a Rosetta terminal.
|
||||||
|
// TODO integrate this as part of the script
|
|
@ -114,6 +114,7 @@
|
||||||
"react-test-renderer": "^17.0.2",
|
"react-test-renderer": "^17.0.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"serve": "^12.0.1",
|
"serve": "^12.0.1",
|
||||||
|
"sharp": "^0.29.1",
|
||||||
"stylelint": "^13.10.0",
|
"stylelint": "^13.10.0",
|
||||||
"tslib": "^2.3.1",
|
"tslib": "^2.3.1",
|
||||||
"typescript": "^4.5.2"
|
"typescript": "^4.5.2"
|
||||||
|
|
|
@ -6,162 +6,93 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TagList, sortedUsers, type User} from '../users';
|
import {TagList, sortedUsers, type User} from '../users';
|
||||||
import {difference} from '@site/src/utils/jsUtils';
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import {Joi} from '@docusaurus/utils-validation';
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import imageSize from 'image-size';
|
import imageSize from 'image-size';
|
||||||
|
|
||||||
describe('users', () => {
|
declare global {
|
||||||
test('are valid', () => {
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
sortedUsers.forEach(ensureUserValid);
|
namespace jest {
|
||||||
});
|
interface Matchers<R> {
|
||||||
|
toHaveGoodDimensions: () => R;
|
||||||
test('have valid images', async () => {
|
|
||||||
const minCardImageWidth = 304;
|
|
||||||
const minCardImageHeight = 150;
|
|
||||||
const minCardImageHeightScaled = 140;
|
|
||||||
const imageDir = path.join(__dirname, '../showcase');
|
|
||||||
|
|
||||||
const files = await fs.readdir(imageDir);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
|
||||||
for (const file of files) {
|
|
||||||
const size = imageSize(path.join(imageDir, file));
|
|
||||||
|
|
||||||
if (size.width! < minCardImageWidth) {
|
|
||||||
throw new Error(
|
|
||||||
`Image width should be >= ${minCardImageWidth}
|
|
||||||
Image=${file}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (size.height! < minCardImageHeight) {
|
|
||||||
throw new Error(
|
|
||||||
`Image height should be >= ${minCardImageHeight}
|
|
||||||
Image=${file}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const scaledHeight = size.height! / (size.width! / minCardImageWidth);
|
|
||||||
if (scaledHeight < minCardImageHeightScaled) {
|
|
||||||
throw new Error(
|
|
||||||
`Image height is too small compared to width
|
|
||||||
After downscaling to width=${minCardImageWidth}, height would be ${scaledHeight} while the minimum is ${minCardImageHeightScaled}
|
|
||||||
Image=${file}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO, refactor legacy test code
|
|
||||||
// Fail-fast on common errors
|
|
||||||
function ensureUserValid(user: User) {
|
|
||||||
function checkFields() {
|
|
||||||
const keys = Object.keys(user);
|
|
||||||
const validKeys = [
|
|
||||||
'title',
|
|
||||||
'description',
|
|
||||||
'preview',
|
|
||||||
'website',
|
|
||||||
'source',
|
|
||||||
'tags',
|
|
||||||
];
|
|
||||||
const unknownKeys = difference(keys, validKeys);
|
|
||||||
if (unknownKeys.length > 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Site contains unknown attribute names=[${unknownKeys.join(',')}]`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkTitle() {
|
|
||||||
if (!user.title) {
|
|
||||||
throw new Error('Site title is missing');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkDescription() {
|
|
||||||
if (!user.description) {
|
|
||||||
throw new Error('Site description is missing');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkWebsite() {
|
|
||||||
if (!user.website) {
|
|
||||||
throw new Error('Site website is missing');
|
|
||||||
}
|
|
||||||
const isHttpUrl =
|
|
||||||
user.website.startsWith('http://') || user.website.startsWith('https://');
|
|
||||||
if (!isHttpUrl) {
|
|
||||||
throw new Error(
|
|
||||||
`Site website does not look like a valid url: ${user.website}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkPreview() {
|
|
||||||
if (
|
|
||||||
!user.preview ||
|
|
||||||
(user.preview instanceof String &&
|
|
||||||
(user.preview.startsWith('http') || user.preview.startsWith('//')))
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Site has bad image preview=[${user.preview}].\nThe image should be hosted on Docusaurus site, and not use remote HTTP or HTTPS URLs`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkTags() {
|
|
||||||
if (
|
|
||||||
!user.tags ||
|
|
||||||
!(user.tags instanceof Array) ||
|
|
||||||
(user.tags as string[]).includes('')
|
|
||||||
) {
|
|
||||||
throw new Error(`Bad showcase tags=[${JSON.stringify(user.tags)}]`);
|
|
||||||
}
|
|
||||||
const unknownTags = difference(user.tags, TagList);
|
|
||||||
if (unknownTags.length > 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Unknown tags=[${unknownTags.join(
|
|
||||||
',',
|
|
||||||
)}\nThe available tags are ${TagList.join(',')}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkOpenSource() {
|
|
||||||
if (typeof user.source === 'undefined') {
|
|
||||||
throw new Error(
|
|
||||||
"The source attribute is required.\nIf your Docusaurus site is not open-source, please make it explicit with 'source: null'",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const hasOpenSourceTag = user.tags.includes('opensource');
|
|
||||||
if (user.source === null && hasOpenSourceTag) {
|
|
||||||
throw new Error(
|
|
||||||
"You can't add the opensource tag to a site that does not have a link to source code.",
|
|
||||||
);
|
|
||||||
} else if (user.source && !hasOpenSourceTag) {
|
|
||||||
throw new Error(
|
|
||||||
"For open-source sites, please add the 'opensource' tag",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
checkFields();
|
|
||||||
checkTitle();
|
|
||||||
checkDescription();
|
|
||||||
checkWebsite();
|
|
||||||
checkPreview();
|
|
||||||
checkTags();
|
|
||||||
checkOpenSource();
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(
|
|
||||||
`Showcase site with title=${user.title} contains errors:\n${
|
|
||||||
(e as Error).message
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expect.extend({
|
||||||
|
toHaveGoodDimensions({width, height}: {width: number; height: number}) {
|
||||||
|
// Put this one first because aspect ratio is harder to fix than resizing (need to take another screenshot)
|
||||||
|
if (width / height < 0.5) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () =>
|
||||||
|
`The preview image's width is ${width} and height is ${height}. To make sure it takes up the entire container in our showcase card, it needs to have a minimum aspect ratio of 2:1. Please make your image taller.`,
|
||||||
|
};
|
||||||
|
} else if (width < 640) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () =>
|
||||||
|
`The preview image's width is ${width}, but we require a minimum 640. You can either resize it locally, or you can wait for the maintainer to resize it for you.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pass: true,
|
||||||
|
message: () => "The preview image's dimensions are good",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('users', () => {
|
||||||
|
sortedUsers.forEach((user) => {
|
||||||
|
test(user.title, () => {
|
||||||
|
Joi.attempt(
|
||||||
|
user,
|
||||||
|
Joi.object<User>({
|
||||||
|
title: Joi.string().required(),
|
||||||
|
description: Joi.string().required(),
|
||||||
|
website: Joi.string()
|
||||||
|
.pattern(/^https?:\/\//)
|
||||||
|
.message('')
|
||||||
|
.required(),
|
||||||
|
// The preview should be jest/emptyModule
|
||||||
|
preview: Joi.object({}).unknown(false).required().messages({
|
||||||
|
'object.base':
|
||||||
|
'The image should be hosted on Docusaurus site, and not use remote HTTP or HTTPS URLs. It must be imported with require().',
|
||||||
|
}),
|
||||||
|
tags: Joi.array()
|
||||||
|
.items(...TagList)
|
||||||
|
.required(),
|
||||||
|
source: Joi.string().allow(null).required().messages({
|
||||||
|
'any.required':
|
||||||
|
"The source attribute is required.\nIf your Docusaurus site is not open-source, please make it explicit with 'source: null'.",
|
||||||
|
}),
|
||||||
|
}).unknown(false),
|
||||||
|
);
|
||||||
|
if (user.tags.includes('opensource') && user.source === null) {
|
||||||
|
throw new Error(
|
||||||
|
"You can't add the 'opensource' tag to a site that does not have a link to source code. Please add your source code, or remove this tag.",
|
||||||
|
);
|
||||||
|
} else if (user.source !== null && !user.tags.includes('opensource')) {
|
||||||
|
throw new Error(
|
||||||
|
"For open-source sites, please add the 'opensource' tag.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageDir = path.join(__dirname, '../showcase');
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(imageDir)
|
||||||
|
.filter((file) => ['.png', 'jpg', '.jpeg'].includes(path.extname(file)));
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
test(file, () => {
|
||||||
|
const size = imageSize(path.join(imageDir, file));
|
||||||
|
|
||||||
|
expect(size).toHaveGoodDimensions();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 585 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 500 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 521 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 623 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 655 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 392 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 400 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 343 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 1 MiB After Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 809 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 512 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 518 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 850 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 312 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 562 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 525 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 521 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 1 MiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 484 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 425 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 379 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 643 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 703 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 377 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 846 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 582 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 1 MiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 799 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 754 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 495 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 802 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 715 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 195 KiB |
BIN
website/src/data/showcase/kotest.png
Normal file
After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 349 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 387 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 310 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 427 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 636 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 381 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 27 KiB |