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
|
||||
jest.config.js
|
||||
jest.transform.js
|
||||
scripts
|
||||
examples/
|
||||
|
||||
packages/lqip-loader/lib/
|
||||
|
|
4
.github/workflows/showcase-test.yml
vendored
|
@ -14,13 +14,11 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node }}
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: yarn
|
||||
- name: Installation
|
||||
run: yarn
|
||||
- name: Test
|
||||
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",
|
||||
"rimraf": "^3.0.2",
|
||||
"serve": "^12.0.1",
|
||||
"sharp": "^0.29.1",
|
||||
"stylelint": "^13.10.0",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.5.2"
|
||||
|
|
|
@ -6,162 +6,93 @@
|
|||
*/
|
||||
|
||||
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 path from 'path';
|
||||
import imageSize from 'image-size';
|
||||
|
||||
describe('users', () => {
|
||||
test('are valid', () => {
|
||||
sortedUsers.forEach(ensureUserValid);
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace jest {
|
||||
interface Matchers<R> {
|
||||
toHaveGoodDimensions: () => R;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('have valid images', async () => {
|
||||
const minCardImageWidth = 304;
|
||||
const minCardImageHeight = 150;
|
||||
const minCardImageHeightScaled = 140;
|
||||
const imageDir = path.join(__dirname, '../showcase');
|
||||
const files = fs
|
||||
.readdirSync(imageDir)
|
||||
.filter((file) => ['.png', 'jpg', '.jpeg'].includes(path.extname(file)));
|
||||
|
||||
const files = await fs.readdir(imageDir);
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const file of files) {
|
||||
files.forEach((file) => {
|
||||
test(file, () => {
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
expect(size).toHaveGoodDimensions();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 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
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
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 |