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

@ -10,6 +10,7 @@ website/
scripts
examples/
packages/lqip-loader/lib/
packages/docusaurus/lib/
packages/docusaurus-*/lib/*
packages/docusaurus-*/lib-next/

1
.gitignore vendored
View file

@ -20,6 +20,7 @@ test-website
test-website-in-workspace
packages/create-docusaurus/lib/
packages/lqip-loader/lib/
packages/docusaurus/lib/
packages/docusaurus-*/lib/*
packages/docusaurus-*/lib-next/

View file

@ -4,6 +4,7 @@ node_modules
build
coverage
.docusaurus
packages/lqip-loader/lib/
packages/docusaurus/lib/
packages/docusaurus-*/lib/*
packages/docusaurus-*/lib-next/

View file

@ -22,12 +22,7 @@ import transformImage from './remark/transformImage';
import transformLinks from './remark/transformLinks';
import {getFileLoaderUtils} from '@docusaurus/core/lib/webpack/utils';
import type {RemarkAndRehypePluginOptions} from '@docusaurus/mdx-loader';
// TODO temporary until Webpack5 export this type
// see https://github.com/webpack/webpack/issues/11630
interface Loader extends Function {
(this: any, source: string): Promise<string | Buffer | void | undefined>;
}
import type {LoaderContext} from 'webpack';
const {
loaders: {inlineMarkdownImageFileLoader},
@ -40,6 +35,19 @@ const DEFAULT_OPTIONS: RemarkAndRehypePluginOptions = {
beforeDefaultRehypePlugins: [],
};
type Options = RemarkAndRehypePluginOptions & {
staticDir?: string;
isMDXPartial?: (filePath: string) => boolean;
isMDXPartialFrontMatterWarningDisabled?: boolean;
removeContentTitle?: boolean;
metadataPath?: string | ((filePath: string) => string);
createAssets?: (metadata: {
frontMatter: Record<string, unknown>;
metadata: Record<string, unknown>;
}) => Record<string, unknown>;
filepath: string;
};
// When this throws, it generally means that there's no metadata file associated with this MDX document
// It can happen when using MDX partials (usually starting with _)
// That's why it's important to provide the "isMDXPartial" function in config
@ -94,7 +102,10 @@ function createAssetsExportCode(assets: Record<string, unknown>) {
return `{\n${codeLines.join('\n')}\n}`;
}
const docusaurusMdxLoader: Loader = async function (fileString) {
export default async function mdxLoader(
this: LoaderContext<Options>,
fileString: string,
): Promise<void> {
const callback = this.async();
const filePath = this.resourcePath;
const reqOptions = this.getOptions() || {};
@ -107,7 +118,7 @@ const docusaurusMdxLoader: Loader = async function (fileString) {
const hasFrontMatter = Object.keys(frontMatter).length > 0;
const options = {
const options: Options = {
...reqOptions,
remarkPlugins: [
...(reqOptions.beforeDefaultRemarkPlugins || []),
@ -119,7 +130,6 @@ const docusaurusMdxLoader: Loader = async function (fileString) {
rehypePlugins: [
...(reqOptions.beforeDefaultRehypePlugins || []),
...DEFAULT_OPTIONS.rehypePlugins,
...(reqOptions.rehypePlugins || []),
],
filepath: filePath,
@ -129,7 +139,7 @@ const docusaurusMdxLoader: Loader = async function (fileString) {
try {
result = await mdx(content, options);
} catch (err) {
return callback(err);
return callback(err as Error);
}
// MDX partials are MDX files starting with _ or in a folder starting with _
@ -195,6 +205,4 @@ ${result}
`;
return callback(null, code);
};
export default docusaurusMdxLoader;
}

View file

@ -6,9 +6,11 @@
*/
declare module '@docusaurus/mdx-loader' {
type RemarkOrRehypePlugin =
// eslint-disable-next-line @typescript-eslint/ban-types
[Function, Record<string, unknown>] | Function;
import type {Plugin} from 'unified';
export type RemarkOrRehypePlugin =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[Plugin<any[]>, Record<string, unknown>] | Plugin<any[]>;
export type RemarkAndRehypePluginOptions = {
remarkPlugins: RemarkOrRehypePlugin[];
rehypePlugins: RemarkOrRehypePlugin[];
@ -19,15 +21,16 @@ declare module '@docusaurus/mdx-loader' {
// TODO Types provided by MDX 2.0 https://github.com/mdx-js/mdx/blob/main/packages/mdx/types/index.d.ts
declare module '@mdx-js/mdx' {
import type {Plugin, Processor} from 'unified';
import type {Processor} from 'unified';
import type {RemarkOrRehypePlugin} from '@docusaurus/mdx-loader';
namespace mdx {
interface Options {
filepath?: string;
skipExport?: boolean;
wrapExport?: string;
remarkPlugins?: Plugin[];
rehypePlugins?: Plugin[];
remarkPlugins?: RemarkOrRehypePlugin[];
rehypePlugins?: RemarkOrRehypePlugin[];
}
function sync(content: string, options?: Options): string;

View file

@ -8,18 +8,16 @@
import {truncate, linkify} from './blogUtils';
import {parseQuery} from 'loader-utils';
import {BlogMarkdownLoaderOptions} from './types';
import type {LoaderContext} from 'webpack';
// TODO temporary until Webpack5 export this type
// see https://github.com/webpack/webpack/issues/11630
interface Loader extends Function {
(this: any, source: string): string | Buffer | void | undefined;
}
const markdownLoader: Loader = function (source) {
export default function markdownLoader(
this: LoaderContext<BlogMarkdownLoaderOptions>,
source: string,
): void {
const filePath = this.resourcePath;
const fileString = source as string;
const fileString = source;
const callback = this.async();
const markdownLoaderOptions = this.getOptions() as BlogMarkdownLoaderOptions;
const markdownLoaderOptions = this.getOptions();
// Linkify blog posts
let finalContent = linkify({
@ -38,6 +36,4 @@ const markdownLoader: Loader = function (source) {
}
return callback && callback(null, finalContent);
};
export default markdownLoader;
}

View file

@ -7,20 +7,16 @@
import {linkify} from './linkify';
import {DocsMarkdownOption} from '../types';
import type {LoaderContext} from 'webpack';
// TODO temporary until Webpack5 export this type
// see https://github.com/webpack/webpack/issues/11630
interface Loader extends Function {
(this: any, source: string): string | Buffer | void | undefined;
}
const markdownLoader: Loader = function (source) {
const fileString = source as string;
export default function markdownLoader(
this: LoaderContext<DocsMarkdownOption>,
source: string,
): void {
const fileString = source;
const callback = this.async();
const options = this.getOptions() as DocsMarkdownOption;
const options = this.getOptions();
return (
callback && callback(null, linkify(fileString, this.resourcePath, options))
);
};
export default markdownLoader;
}

View file

@ -5,13 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
// TODO temporary until Webpack5 export this type
// see https://github.com/webpack/webpack/issues/11630
interface Loader extends Function {
(this: any, source: string): string | Buffer | void | undefined;
}
import type {LoaderContext} from 'webpack';
const markdownLoader: Loader = function (fileString) {
export default function markdownLoader(
this: LoaderContext<undefined>,
fileString: string,
): void {
const callback = this.async();
// const options = this.getOptions();
@ -20,6 +19,4 @@ const markdownLoader: Loader = function (fileString) {
// fileString = linkify(fileString)
return callback && callback(null, fileString);
};
export default markdownLoader;
}

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"
}
}

View file

@ -4549,6 +4549,13 @@
"@types/mime" "^1"
"@types/node" "*"
"@types/sharp@^0.29.2":
version "0.29.2"
resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.29.2.tgz#b4e932e982e258d1013236c8b4bcc14f9883c9a3"
integrity sha512-tIbMvtPa8kMyFMKNhpsPT1HO3CgXLuiCAA8bxHAGAZLyALpYvYc4hUu3pu0+3oExQA5LwvHrWp+OilgXCYVQgg==
dependencies:
"@types/node" "*"
"@types/shelljs@^0.8.6":
version "0.8.9"
resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.9.tgz#45dd8501aa9882976ca3610517dac3831c2fbbf4"