Feat/expose sharp options (#1029)

Co-authored-by: khakimvinh <kha.kim.vinh@gmail.com>
Co-authored-by: Elliot DeNolf <denolfe@gmail.com>
This commit is contained in:
Dan Ribbens
2022-09-12 13:29:07 -04:00
committed by GitHub
parent 391c9d8682
commit 7acf944a28
15 changed files with 224 additions and 81 deletions

View File

@@ -45,6 +45,8 @@ Every Payload Collection can opt-in to supporting Uploads by specifying the `upl
| **`staticURL`** \* | The base URL path to use to access your uploads. Example: `/media` |
| **`staticDir`** \* | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. |
| **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) |
| **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) |
| **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) |
| **`adminThumbnail`** | Set the way that the Admin panel will display thumbnails for this Collection. [More](#admin-thumbnails) |
| **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) |
| **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) |
@@ -68,13 +70,13 @@ const Media: CollectionConfig = {
name: 'thumbnail',
width: 400,
height: 300,
crop: 'centre',
position: 'centre',
},
{
name: 'card',
width: 768,
height: 1024,
crop: 'centre',
position: 'centre',
},
{
name: 'tablet',
@@ -84,7 +86,7 @@ const Media: CollectionConfig = {
// but it will retain its original aspect ratio
// and calculate a height automatically.
height: null,
crop: 'centre',
position: 'centre',
},
],
adminThumbnail: 'thumbnail',

View File

@@ -108,6 +108,7 @@
"express-graphql": "0.12.0",
"express-rate-limit": "^5.1.3",
"file-loader": "^6.2.0",
"file-type": "16.5.4",
"find-up": "4.1.0",
"flatley": "^5.2.0",
"fs-extra": "^10.0.0",
@@ -128,7 +129,6 @@
"jwt-decode": "^3.1.2",
"method-override": "^3.0.0",
"micro-memoize": "^4.0.9",
"mime": "^2.5.0",
"mini-css-extract-plugin": "1.3.3",
"minimist": "^1.2.0",
"mkdirp": "^1.0.4",

View File

@@ -135,11 +135,28 @@ const collectionSchema = joi.object().keys({
width: joi.number().allow(null),
height: joi.number().allow(null),
crop: joi.string(), // TODO: add further specificity with joi.xor
}),
}).unknown(),
),
mimeTypes: joi.array().items(joi.string()),
staticOptions: joi.object(),
handlers: joi.array().items(joi.func()),
resizeOptions: joi.object().keys({
width: joi.number().allow(null),
height: joi.number().allow(null),
fit: joi.string(),
position: joi.alternatives().try(
joi.string(),
joi.number(),
),
background: joi.string(),
kernel: joi.string(),
withoutEnlargement: joi.bool(),
fastShrinkOnLoad: joi.bool(),
}).allow(null),
formatOptions: joi.object().keys({
format: joi.string(),
options: joi.object(),
}),
}),
joi.boolean(),
),

View File

@@ -5,6 +5,7 @@ import { Document } from '../../../types';
import getFileByPath from '../../../uploads/getFileByPath';
import create from '../create';
import { getDataLoader } from '../../dataloader';
import { File } from '../../../uploads/types';
export type Options<T> = {
@@ -18,7 +19,7 @@ export type Options<T> = {
disableVerificationEmail?: boolean
showHiddenFields?: boolean
filePath?: string
file?: UploadedFile
file?: File
overwriteExistingFiles?: boolean
req?: PayloadRequest
draft?: boolean
@@ -49,7 +50,7 @@ export default async function createLocal<T = any>(payload: Payload, options: Op
req.fallbackLocale = fallbackLocale || req?.fallbackLocale || null;
req.payload = payload;
req.files = {
file: (file as UploadedFile) ?? (getFileByPath(filePath) as UploadedFile),
file: (file ?? (await getFileByPath(filePath))) as UploadedFile,
};
if (typeof user !== 'undefined') req.user = user;

View File

@@ -49,7 +49,7 @@ export default async function updateLocal<T = any>(payload: Payload, options: Op
fallbackLocale,
payload,
files: {
file: file ?? getFileByPath(filePath),
file: file ?? await getFileByPath(filePath),
},
} as PayloadRequest;

View File

@@ -1,19 +1,19 @@
import fs from 'fs';
import mime from 'mime';
import { fromFile } from 'file-type';
import path from 'path';
import { File } from './types';
const getFileByPath = (filePath: string): File => {
const getFileByPath = async (filePath: string): Promise<File> => {
if (typeof filePath === 'string') {
const data = fs.readFileSync(filePath);
const mimetype = mime.getType(filePath);
const mimetype = fromFile(filePath);
const { size } = fs.statSync(filePath);
const name = path.basename(filePath);
return {
data,
mimetype,
mimetype: (await mimetype).mime,
name,
size,
};

View File

@@ -5,7 +5,7 @@ import fileExists from './fileExists';
const incrementName = (name: string) => {
const extension = name.split('.').pop();
const baseFilename = sanitize(name.substr(0, name.lastIndexOf('.')) || name);
const baseFilename = sanitize(name.substring(0, name.lastIndexOf('.')) || name);
let incrementedName = baseFilename;
const regex = /(.*)-(\d)$/;
const found = baseFilename.match(regex);
@@ -15,8 +15,7 @@ const incrementName = (name: string) => {
const matchedName = found[1];
const matchedNumber = found[2];
const incremented = Number(matchedNumber) + 1;
const newName = `${matchedName}-${incremented}`;
incrementedName = newName;
incrementedName = `${matchedName}-${incremented}`;
}
return `${incrementedName}.${extension}`;
};

View File

@@ -1,6 +1,7 @@
import fs from 'fs';
import sharp from 'sharp';
import sanitize from 'sanitize-filename';
import { fromBuffer } from 'file-type';
import { ProbedImageSize } from './getImageSize';
import fileExists from './fileExists';
import { SanitizedCollectionConfig } from '../collections/config/types';
@@ -8,18 +9,25 @@ import { FileSizes, ImageSize } from './types';
import { PayloadRequest } from '../express/types';
type Args = {
req: PayloadRequest,
file: Buffer,
dimensions: ProbedImageSize,
staticPath: string,
config: SanitizedCollectionConfig,
savedFilename: string,
mimeType: string,
req: PayloadRequest
file: Buffer
dimensions: ProbedImageSize
staticPath: string
config: SanitizedCollectionConfig
savedFilename: string
mimeType: string
}
function getOutputImage(sourceImage: string, size: ImageSize) {
type OutputImage = {
name: string,
extension: string,
width: number,
height: number
}
function getOutputImage(sourceImage: string, size: ImageSize): OutputImage {
const extension = sourceImage.split('.').pop();
const name = sanitize(sourceImage.substr(0, sourceImage.lastIndexOf('.')) || sourceImage);
const name = sanitize(sourceImage.substring(0, sourceImage.lastIndexOf('.')) || sourceImage);
return {
name,
@@ -29,14 +37,6 @@ function getOutputImage(sourceImage: string, size: ImageSize) {
};
}
/**
* @description
* @param staticPath Path to save images
* @param config Payload config
* @param savedFilename
* @param mimeType
* @returns image sizes keyed to strings
*/
export default async function resizeAndSave({
req,
file,
@@ -44,17 +44,17 @@ export default async function resizeAndSave({
staticPath,
config,
savedFilename,
mimeType,
}: Args): Promise<FileSizes> {
const { imageSizes, disableLocalStorage } = config.upload;
const sizes = imageSizes
.filter((desiredSize) => desiredSize.width <= dimensions.width || desiredSize.height <= dimensions.height)
.filter((desiredSize) => needsResize(desiredSize, dimensions))
.map(async (desiredSize) => {
const resized = await sharp(file)
.resize(desiredSize.width, desiredSize.height, {
position: desiredSize.crop || 'centre',
});
let resized = await sharp(file).resize(desiredSize);
if (desiredSize.formatOptions) {
resized = resized.toFormat(desiredSize.formatOptions.format, desiredSize.formatOptions.options);
}
const bufferObject = await resized.toBuffer({
resolveWithObject: true,
@@ -63,7 +63,7 @@ export default async function resizeAndSave({
req.payloadUploadSizes[desiredSize.name] = bufferObject.data;
const outputImage = getOutputImage(savedFilename, desiredSize);
const imageNameWithDimensions = `${outputImage.name}-${bufferObject.info.width}x${bufferObject.info.height}.${outputImage.extension}`;
const imageNameWithDimensions = createImageName(outputImage, bufferObject);
const imagePath = `${staticPath}/${imageNameWithDimensions}`;
const fileAlreadyExists = await fileExists(imagePath);
@@ -81,20 +81,34 @@ export default async function resizeAndSave({
height: bufferObject.info.height,
filename: imageNameWithDimensions,
filesize: bufferObject.info.size,
mimeType,
mimeType: (await fromBuffer(bufferObject.data)).mime,
};
});
const savedSizes = await Promise.all(sizes);
return savedSizes.reduce((results, size) => ({
...results,
[size.name]: {
width: size.width,
height: size.height,
filename: size.filename,
mimeType: size.mimeType,
filesize: size.filesize,
},
}), {});
return savedSizes.reduce(
(results, size) => ({
...results,
[size.name]: {
width: size.width,
height: size.height,
filename: size.filename,
mimeType: size.mimeType,
filesize: size.filesize,
},
}),
{},
);
}
function createImageName(
outputImage: OutputImage,
bufferObject: { data: Buffer; info: sharp.OutputInfo },
): string {
return `${outputImage.name}-${bufferObject.info.width}x${bufferObject.info.height}.${outputImage.extension}`;
}
function needsResize(desiredSize: ImageSize, dimensions: ProbedImageSize): boolean {
return (typeof desiredSize.width === 'number' && desiredSize.width <= dimensions.width)
|| (typeof desiredSize.height === 'number' && desiredSize.height <= dimensions.height);
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import express from 'express';
import serveStatic from 'serve-static';
import { Sharp, ResizeOptions } from 'sharp';
export type FileSize = {
filename: string;
@@ -25,36 +26,50 @@ export type FileData = {
sizes: FileSizes;
};
export type ImageSize = {
name: string,
width: number | null,
height: number | null,
crop?: string, // comes from sharp package
/**
* Params sent to the sharp toFormat() function
* @link https://sharp.pixelplumbing.com/api-output#toformat
*/
export type ImageUploadFormatOptions = {
format: Parameters<Sharp['toFormat']>[0]
options?: Parameters<Sharp['toFormat']>[1]
}
export type ImageSize = ResizeOptions & {
name: string
formatOptions?: ImageUploadFormatOptions
/**
* @deprecated prefer position
*/
crop?: string // comes from sharp package
};
export type GetAdminThumbnail = (args: { doc: Record<string, unknown> }) => string
export type IncomingUploadType = {
imageSizes?: ImageSize[];
staticURL?: string;
staticDir?: string;
imageSizes?: ImageSize[]
staticURL?: string
staticDir?: string
disableLocalStorage?: boolean
adminThumbnail?: string | GetAdminThumbnail;
mimeTypes?: string[];
adminThumbnail?: string | GetAdminThumbnail
mimeTypes?: string[]
staticOptions?: serveStatic.ServeStaticOptions<express.Response<any, Record<string, any>>>
handlers?: any[]
resizeOptions?: ResizeOptions
formatOptions?: ImageUploadFormatOptions
}
export type Upload = {
imageSizes?: ImageSize[]
staticURL: string
staticDir: string
disableLocalStorage: boolean
adminThumbnail?: string | GetAdminThumbnail
mimeTypes?: string[];
mimeTypes?: string[]
staticOptions?: serveStatic.ServeStaticOptions<express.Response<any, Record<string, any>>>
handlers?: any[]
resizeOptions?: ResizeOptions;
formatOptions?: ImageUploadFormatOptions
}
export type File = {

View File

@@ -1,6 +1,8 @@
import mkdirp from 'mkdirp';
import path from 'path';
import mime from 'mime';
import sharp, { Sharp } from 'sharp';
import { fromBuffer } from 'file-type';
import sanitize from 'sanitize-filename';
import { SanitizedConfig } from '../config/types';
import { Collection } from '../collections/config/types';
import { FileUploadError, MissingFile } from '../errors';
@@ -37,7 +39,7 @@ const uploadFile = async ({
if (collectionConfig.upload) {
const fileData: Partial<FileData> = {};
const { staticDir, imageSizes, disableLocalStorage } = collectionConfig.upload;
const { staticDir, imageSizes, disableLocalStorage, resizeOptions, formatOptions } = collectionConfig.upload;
const { file } = req.files || {};
@@ -56,16 +58,37 @@ const uploadFile = async ({
}
if (file) {
const fsSafeName = !overwriteExistingFiles ? await getSafeFileName(Model, staticPath, file.name) : file.name;
try {
let fsSafeName: string;
let fileBuffer: Buffer;
let mimeType: string;
let fileSize: number;
if (!disableLocalStorage) {
await saveBufferToFile(file.data, `${staticPath}/${fsSafeName}`);
let resized: Sharp | undefined;
if (resizeOptions) {
resized = sharp(file.data).resize(resizeOptions);
}
if (formatOptions) {
resized = (resized ?? sharp(file.data)).toFormat(formatOptions.format, formatOptions.options);
}
fileBuffer = resized ? (await resized.toBuffer()) : file.data;
const { mime, ext } = await fromBuffer(fileBuffer);
mimeType = mime;
fileSize = fileBuffer.length;
const baseFilename = sanitize(file.name.substring(0, file.name.lastIndexOf('.')) || file.name);
fsSafeName = `${baseFilename}.${ext}`;
if (!overwriteExistingFiles) {
fsSafeName = await getSafeFileName(Model, staticPath, fsSafeName);
}
await saveBufferToFile(fileBuffer, `${staticPath}/${fsSafeName}`);
}
fileData.filename = fsSafeName;
fileData.filesize = file.size;
fileData.mimeType = file.mimetype || mime.getType(fsSafeName);
fileData.filename = fsSafeName || (!overwriteExistingFiles ? await getSafeFileName(Model, staticPath, file.name) : file.name);
fileData.filesize = fileSize || file.size;
fileData.mimeType = mimeType || (await fromBuffer(file.data)).mime;
if (isImage(file.mimetype)) {
const dimensions = await getImageSize(file);

View File

@@ -80,7 +80,7 @@ export default buildConfig({
if (fs.existsSync(uploadsDir)) fs.readdirSync(uploadsDir).forEach((f) => fs.rmSync(`${uploadsDir}/${f}`));
const filePath = path.resolve(__dirname, './collections/Upload/payload.jpg');
const file = getFileByPath(filePath);
const file = await getFileByPath(filePath);
const createdUploadDoc = await payload.create({ collection: 'uploads', data: uploadsDoc, file });

View File

@@ -39,12 +39,23 @@ export default buildConfig({
upload: {
staticURL: '/media',
staticDir: './media',
resizeOptions: {
width: 1280,
height: 720,
position: 'center',
},
formatOptions: {
format: 'png',
options: { quality: 90 },
},
imageSizes: [
{
name: 'maintainedAspectRatio',
width: 1024,
height: null,
crop: 'center',
position: 'center',
formatOptions: { format: 'png', options: { quality: 90 } },
},
{
name: 'tablet',
@@ -89,7 +100,7 @@ export default buildConfig({
});
// Create image
const filePath = path.resolve(__dirname, './image.png');
const file = getFileByPath(filePath);
const file = await getFileByPath(filePath);
const { id: uploadedImage } = await payload.create({
collection: mediaSlug,

BIN
test/uploads/image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -80,6 +80,30 @@ describe('Collections - Uploads', () => {
expect(doc.sizes.icon.filename).toBeDefined();
});
it('creates images from a different format', async () => {
const formData = new FormData();
formData.append('file', fs.createReadStream(path.join(__dirname, './image.jpg')));
const { status, doc } = await client.create({
file: true,
data: formData,
auth: true,
headers: {},
});
expect(status).toBe(201);
// Check for files
expect(await fileExists(path.join(__dirname, './media', doc.filename))).toBe(true);
expect(await fileExists(path.join(__dirname, './media', doc.sizes.tablet.filename))).toBe(true);
// Check api response
expect(doc.filename).toContain('.png');
expect(doc.mimeType).toEqual('image/png');
expect(doc.sizes.maintainedAspectRatio.filename).toContain('.png');
expect(doc.sizes.maintainedAspectRatio.mimeType).toContain('image/png');
});
it('creates media without storing a file', async () => {
const formData = new FormData();
formData.append('file', fs.createReadStream(path.join(__dirname, './small.png')));
@@ -106,7 +130,7 @@ describe('Collections - Uploads', () => {
it('update', async () => {
// Create image
const filePath = path.resolve(__dirname, './image.png');
const file = getFileByPath(filePath);
const file = await getFileByPath(filePath);
file.name = 'renamed.png';
const mediaDoc = await payload.create({
@@ -118,7 +142,7 @@ describe('Collections - Uploads', () => {
const formData = new FormData();
formData.append('file', fs.createReadStream(path.join(__dirname, './small.png')));
const { status, doc } = await client.update({
const { status } = await client.update({
id: mediaDoc.id,
file: true,
data: formData,
@@ -135,7 +159,7 @@ describe('Collections - Uploads', () => {
it('should allow update removing a relationship', async () => {
const filePath = path.resolve(__dirname, './image.png');
const file = getFileByPath(filePath);
const file = await getFileByPath(filePath);
file.name = 'renamed.png';
const { id } = await payload.create({

View File

@@ -1817,6 +1817,11 @@
"@testing-library/dom" "^8.5.0"
"@types/react-dom" "^18.0.0"
"@tokenizer/token@^0.3.0":
version "0.3.0"
resolved "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==
"@tootallnate/once@1":
version "1.1.2"
resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
@@ -5898,6 +5903,15 @@ file-loader@^6.2.0:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
file-type@16.5.4:
version "16.5.4"
resolved "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz#474fb4f704bee427681f98dd390058a172a6c2fd"
integrity sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==
dependencies:
readable-web-to-node-stream "^3.0.0"
strtok3 "^6.2.4"
token-types "^4.1.1"
file-uri-to-path@2:
version "2.0.0"
resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-2.0.0.tgz#7b415aeba227d575851e0a5b0c640d7656403fba"
@@ -6736,7 +6750,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
ieee754@^1.1.13:
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@@ -8511,11 +8525,6 @@ mime@1.6.0:
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.5.0:
version "2.6.0"
resolved "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@@ -9493,6 +9502,11 @@ pause@0.0.1:
resolved "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==
peek-readable@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz#4ece1111bf5c2ad8867c314c81356847e8a62e72"
integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
@@ -10697,6 +10711,13 @@ readable-stream@~1.0.31:
isarray "0.0.1"
string_decoder "~0.10.x"
readable-web-to-node-stream@^3.0.0:
version "3.0.2"
resolved "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb"
integrity sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==
dependencies:
readable-stream "^3.6.0"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
@@ -11825,6 +11846,14 @@ strip-json-comments@~2.0.1:
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==
strtok3@^6.2.4:
version "6.3.0"
resolved "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz#358b80ffe6d5d5620e19a073aa78ce947a90f9a0"
integrity sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==
dependencies:
"@tokenizer/token" "^0.3.0"
peek-readable "^4.1.0"
style-loader@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c"
@@ -12131,6 +12160,14 @@ toidentifier@1.0.1:
resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
token-types@^4.1.1:
version "4.2.1"
resolved "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz#0f897f03665846982806e138977dbe72d44df753"
integrity sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==
dependencies:
"@tokenizer/token" "^0.3.0"
ieee754 "^1.2.1"
totalist@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"