refactor: migrate lqip-loader to TS, fix typing for Webpack Loaders (#5779)

This commit is contained in:
Joshua Chen 2021-10-27 22:38:11 +08:00 committed by GitHub
parent ca5d70d7fb
commit 68c970175a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 254 additions and 265 deletions

View file

@ -2,10 +2,14 @@
"name": "@docusaurus/lqip-loader",
"version": "2.0.0-beta.8",
"description": "Low Quality Image Placeholders (LQIP) loader for webpack.",
"main": "src/index.js",
"main": "lib/index.js",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc",
"watch": "tsc --watch"
},
"repository": {
"type": "git",
"url": "https://github.com/facebook/docusaurus.git",
@ -20,5 +24,8 @@
},
"engines": {
"node": ">=12.13.0"
},
"devDependencies": {
"@types/sharp": "^0.29.2"
}
}

View file

@ -10,14 +10,14 @@ import Vibrant from 'node-vibrant';
import {Palette} from 'node-vibrant/lib/color';
import {toPalette, toBase64} from '../utils';
import lqip from '../lqip';
import * as lqip from '../lqip';
describe('lqip-loader', () => {
describe('toBase64', () => {
test('should return a properly formatted Base64 image string', () => {
const expected = 'data:image/jpeg;base64,hello world';
const expected = 'data:image/jpeg;base64,aGVsbG8gd29ybGQ=';
const mockedMimeType = 'image/jpeg';
const mockedBase64Data = 'hello world';
const mockedBase64Data = Buffer.from('hello world');
expect(toBase64(mockedMimeType, mockedBase64Data)).toEqual(expected);
});
});

View file

@ -1,83 +0,0 @@
/**
* 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.
*/
const lqip = require('./lqip');
module.exports = function (contentBuffer) {
if (this.cacheable) {
this.cacheable();
}
const callback = this.async();
const imgPath = this.resourcePath;
const config = this.getOptions() || {};
config.base64 = 'base64' in config ? config.base64 : true;
config.palette = 'palette' in config ? config.palette : false;
let content = contentBuffer.toString('utf8');
const contentIsUrlExport =
/^(?:export default|module.exports =) "data:(.*)base64,(.*)/.test(content);
const contentIsFileExport = /^(?:export default|module.exports =) (.*)/.test(
content,
);
let source = '';
const SOURCE_CHUNK = 1;
if (contentIsUrlExport) {
source = content.match(/^(?:export default|module.exports =) (.*)/)[
SOURCE_CHUNK
];
} else {
if (!contentIsFileExport) {
// eslint-disable-next-line global-require
const fileLoader = require('file-loader');
content = fileLoader.call(this, contentBuffer);
}
source = content.match(/^(?:export default|module.exports =) (.*);/)[
SOURCE_CHUNK
];
}
const outputPromises = [];
if (config.base64 === true) {
outputPromises.push(lqip.base64(imgPath));
} else {
outputPromises.push(null);
}
// color palette generation is set to false by default
// since it is little bit slower than base64 generation
if (config.palette === true) {
outputPromises.push(lqip.palette(imgPath));
} else {
outputPromises.push(null);
}
Promise.all(outputPromises)
.then((data) => {
if (data) {
const [preSrc, palette] = data;
const finalObject = JSON.stringify({src: 'STUB', preSrc, palette});
const result = `module.exports = ${finalObject.replace(
'"STUB"',
source,
)};`;
callback(null, result);
} else {
callback('ERROR', null);
}
})
.catch((error) => {
console.error(error);
callback(error, null);
});
};
module.exports.raw = true;

View file

@ -0,0 +1,83 @@
/**
* 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.
*/
import * as lqip from './lqip';
import type {LoaderContext} from 'webpack';
type Options = {
base64: boolean;
palette: boolean;
};
async function lqipLoader(
this: LoaderContext<Options>,
contentBuffer: Buffer,
): Promise<void> {
if (this.cacheable) {
this.cacheable();
}
const callback = this.async();
const imgPath = this.resourcePath;
const config = this.getOptions() || {};
config.base64 = 'base64' in config ? config.base64 : true;
config.palette = 'palette' in config ? config.palette : false;
let content = contentBuffer.toString('utf8');
const contentIsUrlExport =
/^(?:export default|module.exports =) "data:(.*)base64,(.*)/.test(content);
const contentIsFileExport = /^(?:export default|module.exports =) (.*)/.test(
content,
);
let source = '';
const SOURCE_CHUNK = 1;
if (contentIsUrlExport) {
source = content.match(/^(?:export default|module.exports =) (.*)/)![
SOURCE_CHUNK
];
} else {
if (!contentIsFileExport) {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
const fileLoader = require('file-loader');
content = fileLoader.call(this, contentBuffer);
}
source = content.match(/^(?:export default|module.exports =) (.*);/)![
SOURCE_CHUNK
];
}
const outputPromises: [Promise<string> | null, Promise<string[]> | null] = [
config.base64 === true ? lqip.base64(imgPath) : null,
// color palette generation is set to false by default
// since it is little bit slower than base64 generation
config.palette === true ? lqip.palette(imgPath) : null,
];
try {
const data = await Promise.all(outputPromises);
if (data) {
const [preSrc, palette] = data;
const finalObject = JSON.stringify({src: 'STUB', preSrc, palette});
const result = `module.exports = ${finalObject.replace(
'"STUB"',
source,
)};`;
callback(null, result);
} else {
callback(new Error('ERROR'), undefined);
}
} catch (error) {
console.error(error);
callback(new Error('ERROR'), undefined);
}
}
lqipLoader.raw = true;
export default lqipLoader;

View file

@ -1,75 +0,0 @@
/**
* 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.
*/
const Vibrant = require('node-vibrant');
const path = require('path');
const sharp = require('sharp');
const {version} = require('../package.json');
const {toPalette, toBase64} = require('./utils');
const ERROR_EXT = `Error: Input file is missing or uses unsupported image format, lqip v${version}`;
const SUPPORTED_MIMES = {
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
png: 'image/png',
};
const base64 = (file) => {
return new Promise((resolve, reject) => {
let extension = path.extname(file) || '';
extension = extension.split('.').pop();
if (!SUPPORTED_MIMES[extension]) {
return reject(ERROR_EXT);
}
return sharp(file)
.resize(10)
.toBuffer()
.then((data) => {
if (data) {
return resolve(toBase64(SUPPORTED_MIMES[extension], data));
}
return reject(
new Error('Unhandled promise rejection in base64 promise'),
);
})
.catch((err) => {
return reject(err);
});
});
};
const palette = (file) => {
return new Promise((resolve, reject) => {
const vibrant = new Vibrant(file, {});
vibrant
.getPalette()
.then((pal) => {
if (pal) {
return resolve(toPalette(pal));
}
return reject(
new Error('Unhandled promise rejection in colorPalette', pal),
);
})
.catch((err) => {
return reject(err);
});
});
};
process.on('unhandledRejection', (up) => {
throw up;
});
module.exports = {
base64,
palette,
};

View file

@ -0,0 +1,52 @@
/**
* 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.
*/
import Vibrant from 'node-vibrant';
import path from 'path';
import sharp from 'sharp';
import {toPalette, toBase64} from './utils';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {version} = require('../package.json');
const ERROR_EXT = `Error: Input file is missing or uses unsupported image format, lqip v${version}`;
const SUPPORTED_MIMES: Record<string, string> = {
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
png: 'image/png',
};
async function base64(file: string): Promise<string> {
let extension = path.extname(file) || '';
extension = extension.split('.').pop()!;
if (!SUPPORTED_MIMES[extension]) {
throw new Error(ERROR_EXT);
}
const data = await sharp(file).resize(10).toBuffer();
if (data) {
return toBase64(SUPPORTED_MIMES[extension], data);
}
throw new Error('Unhandled promise rejection in base64 promise');
}
async function palette(file: string): Promise<string[]> {
const vibrant = new Vibrant(file, {});
const pal = await vibrant.getPalette();
if (pal) {
return toPalette(pal);
}
throw new Error(`Unhandled promise rejection in colorPalette ${pal}`);
}
process.on('unhandledRejection', (up) => {
throw up;
});
export {base64, palette};

View file

@ -1,51 +0,0 @@
/**
* 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.
*/
// @ts-check
const {sortBy} = require('lodash');
/**
* toBase64
* @description it returns a Base64 image string with required formatting
* to work on the web (<img src=".." /> or in CSS url('..'))
*
* @param {string} extMimeType: image mime type string
* @param data: base64 string
* @returns {string}
*/
const toBase64 = (extMimeType, data) => {
return `data:${extMimeType};base64,${data.toString('base64')}`;
};
/**
* toPalette
* @description takes a color swatch object, converts it to an array & returns
* only hex color
*
* @param {import("node-vibrant/lib/color").Palette} swatch
* @returns {string[]}
*/
const toPalette = (swatch) => {
/** @type {Array<{popularity: number, hex: string}>} */
let palette = Object.keys(swatch).reduce((result, key) => {
if (swatch[key] !== null) {
result.push({
popularity: swatch[key].getPopulation(),
hex: swatch[key].getHex(),
});
}
return result;
}, []);
palette = sortBy(palette, ['popularity']);
return palette.map((color) => color.hex).reverse();
};
module.exports = {
toBase64,
toPalette,
};

View file

@ -0,0 +1,37 @@
/**
* 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.
*/
import {sortBy} from 'lodash';
import type {Palette} from 'node-vibrant/lib/color';
/**
* it returns a Base64 image string with required formatting
* to work on the web (<img src=".." /> or in CSS url('..'))
*/
const toBase64 = (extMimeType: string, data: Buffer): string => {
return `data:${extMimeType};base64,${data.toString('base64')}`;
};
/**
* takes a color swatch object, converts it to an array & returns
* only hex color
*/
const toPalette = (swatch: Palette): string[] => {
let palette = Object.keys(swatch).reduce((result, key) => {
if (swatch[key] !== null) {
result.push({
popularity: swatch[key]!.getPopulation(),
hex: swatch[key]!.getHex(),
});
}
return result;
}, [] as {popularity: number; hex: string}[]);
palette = sortBy(palette, ['popularity']);
return palette.map((color) => color.hex).reverse();
};
export {toBase64, toPalette};

View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"rootDir": "src",
"outDir": "lib"
}
}