mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-10-26 06:56:06 +01:00
allow conversion between animated and still
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -9,3 +9,6 @@ yarn-error.log
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
||||
|
||||
|
||||
temp
|
||||
|
||||
@@ -50,6 +50,8 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.5.6",
|
||||
"sharp": "^0.30.7",
|
||||
"stream-parser": "^0.3.1",
|
||||
"thunks": "^4.9.6",
|
||||
"typeorm": "0.3.7",
|
||||
"zod": "^3.18.0"
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { SharpOptions } from 'sharp';
|
||||
import { SysPreferenceService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { SharpWrapper } from '../../workers/sharp.wrapper';
|
||||
import { ImageResult } from './imageresult';
|
||||
@@ -21,14 +22,10 @@ export class ImageConverterService {
|
||||
targetFiletype: FileType,
|
||||
options: ImageRequestParams,
|
||||
): AsyncFailable<ImageResult> {
|
||||
if (sourceFiletype.category !== sourceFiletype.category) {
|
||||
return Fail(
|
||||
FT.Impossible,
|
||||
"Can't convert from animated to still or vice versa",
|
||||
);
|
||||
}
|
||||
|
||||
if (sourceFiletype.identifier === targetFiletype.identifier) {
|
||||
if (
|
||||
sourceFiletype.identifier === targetFiletype.identifier &&
|
||||
Object.keys(options).length === 0
|
||||
) {
|
||||
return {
|
||||
filetype: targetFiletype.identifier,
|
||||
image,
|
||||
@@ -37,7 +34,9 @@ export class ImageConverterService {
|
||||
|
||||
if (targetFiletype.category === SupportedFileTypeCategory.Image) {
|
||||
return this.convertStill(image, sourceFiletype, targetFiletype, options);
|
||||
} else if (targetFiletype.category === SupportedFileTypeCategory.Animation) {
|
||||
} else if (
|
||||
targetFiletype.category === SupportedFileTypeCategory.Animation
|
||||
) {
|
||||
return this.convertStill(image, sourceFiletype, targetFiletype, options);
|
||||
//return this.convertAnimation(image, targetmime, options);
|
||||
} else {
|
||||
@@ -61,7 +60,14 @@ export class ImageConverterService {
|
||||
const timeLimitMS = ms(timeLimit);
|
||||
|
||||
const sharpWrapper = new SharpWrapper(timeLimitMS, memLimit);
|
||||
const hasStarted = await sharpWrapper.start(image, sourceFiletype);
|
||||
const sharpOptions: SharpOptions = {
|
||||
animated: targetFiletype.category === SupportedFileTypeCategory.Animation,
|
||||
};
|
||||
const hasStarted = await sharpWrapper.start(
|
||||
image,
|
||||
sourceFiletype,
|
||||
sharpOptions,
|
||||
);
|
||||
if (HasFailed(hasStarted)) return hasStarted;
|
||||
|
||||
// Do modifications
|
||||
|
||||
@@ -41,7 +41,7 @@ export class ImageProcessorService {
|
||||
image: Buffer,
|
||||
filetype: FileType,
|
||||
): AsyncFailable<ImageResult> {
|
||||
// Apng and gif are stored as is for now
|
||||
// Webps and gifs are stored as is for now
|
||||
return {
|
||||
image: image,
|
||||
filetype: filetype.identifier,
|
||||
|
||||
@@ -3,14 +3,13 @@ import Crypto from 'crypto';
|
||||
import { fileTypeFromBuffer, FileTypeResult } from 'file-type';
|
||||
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
|
||||
import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
|
||||
import { FileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { AnimFileType, FileType, ImageFileType, Mime2FileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.enum';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { FindResult } from 'picsur-shared/dist/types/find-result';
|
||||
import {
|
||||
ParseFileType,
|
||||
ParseMime2FileType
|
||||
ParseFileType
|
||||
} from 'picsur-shared/dist/util/parse-mime';
|
||||
import { IsQOI } from 'qoi-img';
|
||||
import { ImageDBService } from '../../collections/image-db/image-db.service';
|
||||
@@ -23,6 +22,7 @@ import { EImageBackend } from '../../models/entities/image.entity';
|
||||
import { MutexFallBack } from '../../models/util/mutex-fallback';
|
||||
import { ImageConverterService } from './image-converter.service';
|
||||
import { ImageProcessorService } from './image-processor.service';
|
||||
import { WebPInfo } from './webpinfo/webpinfo';
|
||||
|
||||
@Injectable()
|
||||
export class ImageManagerService {
|
||||
@@ -224,7 +224,21 @@ export class ImageManagerService {
|
||||
mime = filetypeResult.mime;
|
||||
}
|
||||
|
||||
return ParseMime2FileType(mime ?? 'other/unknown');
|
||||
if (mime === undefined) mime = "other/unknown";
|
||||
|
||||
let filetype: string | undefined;
|
||||
if (mime === "image/webp") {
|
||||
const header = await WebPInfo.from(image);
|
||||
if (header.summary.isAnimated) filetype = AnimFileType.WEBP;
|
||||
else filetype = ImageFileType.WEBP;
|
||||
}
|
||||
if (filetype === undefined) {
|
||||
const parsed = Mime2FileType(mime);
|
||||
if (HasFailed(parsed)) return parsed;
|
||||
filetype = parsed;
|
||||
}
|
||||
|
||||
return ParseFileType(filetype);
|
||||
}
|
||||
|
||||
private getConvertHash(options: object) {
|
||||
|
||||
53
backend/src/managers/image/webpinfo/stream-parser.ts
Normal file
53
backend/src/managers/image/webpinfo/stream-parser.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
|
||||
-- SOURCE: https://github.com/mooyoul/node-webpinfo
|
||||
-- LICENSE:
|
||||
|
||||
The MIT License (MIT)
|
||||
Copyright © 2018 MooYeol Prescott Lee, http://debug.so <mooyoul@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the “Software”), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
import * as Stream from 'stream';
|
||||
// @ts-ignore
|
||||
import StreamParser from 'stream-parser';
|
||||
|
||||
export interface IStreamParserWritable {
|
||||
new (...args: any[]): IStreamParserWritableBase;
|
||||
}
|
||||
|
||||
export interface IStreamParserWritableBase {
|
||||
_bytes(n: number, cb: (buf: Buffer) => void): void;
|
||||
_skipBytes(n: number, cb: () => void): void;
|
||||
}
|
||||
|
||||
class StreamParserWritableClass extends Stream.Writable {
|
||||
constructor() {
|
||||
super();
|
||||
StreamParser(this);
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: The "stream-parser" module *patches* prototype of given class on call
|
||||
// So basically original class does not have any definition about stream-parser injected methods.
|
||||
// thus that's why we cast type here
|
||||
export const StreamParserWritable =
|
||||
StreamParserWritableClass as typeof Stream.Writable & IStreamParserWritable;
|
||||
1103
backend/src/managers/image/webpinfo/webpinfo.ts
Normal file
1103
backend/src/managers/image/webpinfo/webpinfo.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ import {
|
||||
FT,
|
||||
HasFailed
|
||||
} from 'picsur-shared/dist/types';
|
||||
import { Sharp } from 'sharp';
|
||||
import { Sharp, SharpOptions } from 'sharp';
|
||||
import {
|
||||
SharpWorkerFinishOptions,
|
||||
SharpWorkerOperation,
|
||||
@@ -41,7 +41,7 @@ export class SharpWrapper {
|
||||
private readonly memory_limit: number,
|
||||
) {}
|
||||
|
||||
public async start(image: Buffer, filetype: FileType): AsyncFailable<true> {
|
||||
public async start(image: Buffer, filetype: FileType, sharpOptions?: SharpOptions): AsyncFailable<true> {
|
||||
this.worker = fork(SharpWrapper.WORKER_PATH, {
|
||||
serialization: 'advanced',
|
||||
timeout: this.instance_timeout,
|
||||
@@ -80,6 +80,7 @@ export class SharpWrapper {
|
||||
type: 'init',
|
||||
image,
|
||||
filetype,
|
||||
options: sharpOptions,
|
||||
});
|
||||
if (HasFailed(hasSent)) {
|
||||
this.purge();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { Sharp } from 'sharp';
|
||||
import { Sharp, SharpOptions } from 'sharp';
|
||||
import { SharpResult } from './universal-sharp';
|
||||
|
||||
type MapSharpFunctions<T extends keyof Sharp> = T extends any
|
||||
@@ -34,6 +34,7 @@ export interface SharpWorkerInitMessage {
|
||||
type: 'init';
|
||||
image: Buffer;
|
||||
filetype: FileType;
|
||||
options?: SharpOptions;
|
||||
}
|
||||
|
||||
export interface SharpWorkerOperationMessage {
|
||||
|
||||
@@ -59,7 +59,7 @@ export class SharpWorker {
|
||||
}
|
||||
|
||||
this.startTime = Date.now();
|
||||
this.sharpi = UniversalSharpIn(message.image, message.filetype);
|
||||
this.sharpi = UniversalSharpIn(message.image, message.filetype, message.options);
|
||||
}
|
||||
|
||||
private operation(message: SharpWorkerOperationMessage): void {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { BMPdecode, BMPencode } from 'bmp-img';
|
||||
import { FileType, ImageFileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import {
|
||||
AnimFileType,
|
||||
FileType,
|
||||
ImageFileType
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { QOIdecode, QOIencode } from 'qoi-img';
|
||||
import sharp, { Sharp, SharpOptions } from 'sharp';
|
||||
|
||||
@@ -20,11 +24,6 @@ export function UniversalSharpIn(
|
||||
return bmpSharpIn(image, options);
|
||||
} else if (filetype.identifier === ImageFileType.QOI) {
|
||||
return qoiSharpIn(image, options);
|
||||
// } else if (filetype.identifier === AnimFileType.GIF) {
|
||||
// return sharp(image, {
|
||||
// ...options,
|
||||
// animated: true,
|
||||
// });
|
||||
} else {
|
||||
return sharp(image, options);
|
||||
}
|
||||
@@ -95,20 +94,21 @@ export async function UniversalSharpOut(
|
||||
.tiff({ quality: options?.quality })
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
break;
|
||||
case ImageFileType.WEBP:
|
||||
result = await image
|
||||
.webp({ quality: options?.quality })
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
break;
|
||||
case ImageFileType.BMP:
|
||||
result = await bmpSharpOut(image);
|
||||
break;
|
||||
case ImageFileType.QOI:
|
||||
result = await qoiSharpOut(image);
|
||||
break;
|
||||
// case AnimFileType.GIF:
|
||||
// result = await image.gif().toBuffer({ resolveWithObject: true });
|
||||
// break;
|
||||
case ImageFileType.WEBP:
|
||||
case AnimFileType.WEBP:
|
||||
result = await image
|
||||
.webp({ quality: options?.quality })
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
break;
|
||||
case AnimFileType.GIF:
|
||||
result = await image.gif().toBuffer({ resolveWithObject: true });
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported mime type');
|
||||
}
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
width="0"
|
||||
#targetcanvas
|
||||
></canvas>
|
||||
<img *ngIf="state === 'image'" [src]="imageURL" loading="lazy" />
|
||||
<img
|
||||
*ngIf="state === 'image' || state === 'loading'"
|
||||
[style.display]="state === 'loading' ? 'none' : 'block'"
|
||||
loading="lazy"
|
||||
#targetimg
|
||||
/>
|
||||
<mat-icon *ngIf="state === 'error'">broken_image</mat-icon>
|
||||
|
||||
<mat-spinner
|
||||
|
||||
@@ -21,3 +21,10 @@ img {
|
||||
ngui-inview {
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
margin: 1rem;
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: xxx-large;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export class PicsurImgComponent implements OnChanges {
|
||||
private readonly logger = new Logger('ZodImgComponent');
|
||||
|
||||
@ViewChild('targetcanvas') private canvas: ElementRef<HTMLCanvasElement>;
|
||||
@ViewChild('targetimg') private img: ElementRef<HTMLImageElement>;
|
||||
|
||||
private isInView = false;
|
||||
|
||||
@@ -63,6 +64,7 @@ export class PicsurImgComponent implements OnChanges {
|
||||
if (HasFailed(result)) {
|
||||
this.state = PicsurImgState.Error;
|
||||
this.logger.error(result.getReason());
|
||||
this.changeDetector.markForCheck();
|
||||
}
|
||||
})
|
||||
.catch((e) => this.logger.error);
|
||||
@@ -83,6 +85,16 @@ export class PicsurImgComponent implements OnChanges {
|
||||
|
||||
this.state = PicsurImgState.Canvas;
|
||||
} else {
|
||||
const result = await this.apiService.getBuffer(url);
|
||||
if (HasFailed(result)) return result;
|
||||
|
||||
|
||||
|
||||
const img = this.img.nativeElement;
|
||||
|
||||
const blob = new Blob([result.buffer]);
|
||||
img.src = URL.createObjectURL(blob);
|
||||
|
||||
this.state = PicsurImgState.Image;
|
||||
}
|
||||
this.changeDetector.markForCheck();
|
||||
|
||||
@@ -5,10 +5,8 @@ import {
|
||||
AnimFileType,
|
||||
FileType,
|
||||
FileType2Ext,
|
||||
ImageFileType,
|
||||
SupportedAnimFileTypes,
|
||||
SupportedFileTypeCategory,
|
||||
SupportedImageFileTypes
|
||||
ImageFileType, SupportedFileTypeCategory,
|
||||
SupportedFileTypes
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
|
||||
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
||||
@@ -160,31 +158,16 @@ export class ViewComponent implements OnInit {
|
||||
key: string;
|
||||
}[] = [];
|
||||
|
||||
if (this.masterFileType.category === SupportedFileTypeCategory.Image) {
|
||||
newOptions.push(
|
||||
...SupportedImageFileTypes.map((mime) => {
|
||||
let ext = FileType2Ext(mime);
|
||||
if (HasFailed(ext)) ext = 'Error';
|
||||
return {
|
||||
value: ext.toUpperCase(),
|
||||
key: mime,
|
||||
};
|
||||
}),
|
||||
);
|
||||
} else if (
|
||||
this.masterFileType.category === SupportedFileTypeCategory.Animation
|
||||
) {
|
||||
newOptions.push(
|
||||
...SupportedAnimFileTypes.map((mime) => {
|
||||
let ext = FileType2Ext(mime);
|
||||
if (HasFailed(ext)) ext = 'Error';
|
||||
return {
|
||||
value: ext.toUpperCase(),
|
||||
key: mime,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
newOptions.push(
|
||||
...SupportedFileTypes.map((mime) => {
|
||||
let ext = FileType2Ext(mime);
|
||||
if (HasFailed(ext)) ext = 'Error';
|
||||
return {
|
||||
value: ext.toUpperCase(),
|
||||
key: mime,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,38 @@ export interface FileType {
|
||||
|
||||
// Converters
|
||||
|
||||
// -- Mime
|
||||
|
||||
const FileType2MimeMap: {
|
||||
[key in ImageFileType | AnimFileType]: string;
|
||||
} = {
|
||||
[AnimFileType.GIF]: 'image/gif',
|
||||
[AnimFileType.WEBP]: 'image/webp',
|
||||
// [AnimFileType.APNG]: 'image/apng',
|
||||
[ImageFileType.QOI]: 'image/x-qoi',
|
||||
[ImageFileType.JPEG]: 'image/jpeg',
|
||||
[ImageFileType.PNG]: 'image/png',
|
||||
[ImageFileType.WEBP]: 'image/webp', // Image webp comes later, so will be default
|
||||
[ImageFileType.TIFF]: 'image/tiff',
|
||||
[ImageFileType.BMP]: 'image/bmp',
|
||||
// [ImageFileType.ICO]: 'image/x-icon',
|
||||
};
|
||||
|
||||
export const Mime2FileType = (mime: string): Failable<string> => {
|
||||
const entries = Object.entries(FileType2MimeMap).filter(
|
||||
([k, v]) => v === mime,
|
||||
);
|
||||
if (entries.length === 0)
|
||||
return Fail(FT.Internal, undefined, `Unsupported mime type: ${mime}`);
|
||||
return entries[0][0];
|
||||
};
|
||||
export const FileType2Mime = (filetype: string): Failable<string> => {
|
||||
const result = FileType2MimeMap[filetype as ImageFileType | AnimFileType];
|
||||
if (result === undefined)
|
||||
return Fail(FT.Internal, undefined, `Unsupported filetype: ${filetype}`);
|
||||
return result;
|
||||
};
|
||||
|
||||
// -- Ext
|
||||
|
||||
const FileType2ExtMap: {
|
||||
@@ -55,9 +87,12 @@ const FileType2ExtMap: {
|
||||
// [ImageFileType.ICO]: 'ico',
|
||||
};
|
||||
|
||||
const Ext2FileTypeMap: {
|
||||
[key: string]: string;
|
||||
} = Object.fromEntries(Object.entries(FileType2ExtMap).map(([k, v]) => [v, k]));
|
||||
export const Ext2FileType = (ext: string): Failable<string> => {
|
||||
const entries = Object.entries(FileType2ExtMap).filter(([k, v]) => v === ext);
|
||||
if (entries.length === 0)
|
||||
return Fail(FT.Internal, undefined, `Unsupported ext: ${ext}`);
|
||||
return entries[0][0];
|
||||
};
|
||||
|
||||
export const FileType2Ext = (mime: string): Failable<string> => {
|
||||
const result = FileType2ExtMap[mime as ImageFileType | AnimFileType];
|
||||
@@ -65,46 +100,3 @@ export const FileType2Ext = (mime: string): Failable<string> => {
|
||||
return Fail(FT.Internal, undefined, `Unsupported mime type: ${mime}`);
|
||||
return result;
|
||||
};
|
||||
|
||||
export const Ext2FileType = (ext: string): Failable<string> => {
|
||||
const result = Ext2FileTypeMap[ext];
|
||||
if (result === undefined)
|
||||
return Fail(FT.Internal, undefined, `Unsupported ext: ${ext}`);
|
||||
return result;
|
||||
};
|
||||
|
||||
// -- Mime
|
||||
|
||||
const FileType2MimeMap: {
|
||||
[key in ImageFileType | AnimFileType]: string;
|
||||
} = {
|
||||
[AnimFileType.GIF]: 'image/gif',
|
||||
[AnimFileType.WEBP]: 'image/webp',
|
||||
// [AnimFileType.APNG]: 'image/apng',
|
||||
[ImageFileType.QOI]: 'image/x-qoi',
|
||||
[ImageFileType.JPEG]: 'image/jpeg',
|
||||
[ImageFileType.PNG]: 'image/png',
|
||||
[ImageFileType.WEBP]: 'image/webp',
|
||||
[ImageFileType.TIFF]: 'image/tiff',
|
||||
[ImageFileType.BMP]: 'image/bmp',
|
||||
// [ImageFileType.ICO]: 'image/x-icon',
|
||||
};
|
||||
|
||||
const Mime2FileTypeMap: {
|
||||
[key: string]: string;
|
||||
} = Object.fromEntries(
|
||||
Object.entries(FileType2MimeMap).map(([k, v]) => [v, k]),
|
||||
);
|
||||
|
||||
export const Mime2FileType = (mime: string): Failable<string> => {
|
||||
const result = Mime2FileTypeMap[mime as ImageFileType | AnimFileType];
|
||||
if (result === undefined)
|
||||
return Fail(FT.Internal, undefined, `Unsupported mime type: ${mime}`);
|
||||
return result;
|
||||
};
|
||||
export const FileType2Mime = (filetype: string): Failable<string> => {
|
||||
const result = FileType2MimeMap[filetype as ImageFileType | AnimFileType];
|
||||
if (result === undefined)
|
||||
return Fail(FT.Internal, undefined, `Unsupported filetype: ${filetype}`);
|
||||
return result;
|
||||
};
|
||||
|
||||
20
yarn.lock
20
yarn.lock
@@ -4895,7 +4895,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:2.6.9":
|
||||
"debug@npm:2, debug@npm:2.6.9":
|
||||
version: 2.6.9
|
||||
resolution: "debug@npm:2.6.9"
|
||||
dependencies:
|
||||
@@ -8929,6 +8929,8 @@ __metadata:
|
||||
rxjs: ^7.5.6
|
||||
sharp: ^0.30.7
|
||||
source-map-support: ^0.5.21
|
||||
stream-parser: ^0.3.1
|
||||
thunks: ^4.9.6
|
||||
ts-loader: ^9.3.1
|
||||
ts-node: ^10.9.1
|
||||
tsconfig-paths: ^4.1.0
|
||||
@@ -10811,6 +10813,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stream-parser@npm:^0.3.1":
|
||||
version: 0.3.1
|
||||
resolution: "stream-parser@npm:0.3.1"
|
||||
dependencies:
|
||||
debug: 2
|
||||
checksum: 4d86ff8cffe7c7587dc91433fff9dce38a93ea7e9f47560055addc81eae6b6befab22b75643ce539faf325fe2b17d371778242566bed086e75f6cffb1e76c06c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stream-wormhole@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "stream-wormhole@npm:1.1.0"
|
||||
@@ -11136,6 +11147,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"thunks@npm:^4.9.6":
|
||||
version: 4.9.6
|
||||
resolution: "thunks@npm:4.9.6"
|
||||
checksum: 116b46dfd9426de6d3832e56486bfea55b35a240d2eeaa411fa765f16f93c0243a8d05a9c4fe5d40018b29c5bb84eec09d60cc6cd213724647a254332fdaaf12
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"thunky@npm:^1.0.2":
|
||||
version: 1.1.0
|
||||
resolution: "thunky@npm:1.1.0"
|
||||
|
||||
Reference in New Issue
Block a user