Fix front compilation issues

This commit is contained in:
Zoe Roux 2023-09-04 00:28:18 +02:00
parent 68b4e71281
commit e8654ca181
No known key found for this signature in database
20 changed files with 347 additions and 82 deletions

View File

@ -51,7 +51,7 @@
"react-native-svg": "13.9.0",
"react-native-uuid": "^2.0.1",
"react-native-video": "^6.0.0-alpha.7",
"yoshiki": "1.2.4"
"yoshiki": "1.2.7"
},
"devDependencies": {
"@babel/core": "^7.22.10",

View File

@ -38,7 +38,7 @@
"srt-webvtt": "^2.0.0",
"superjson": "^1.13.1",
"sweetalert2": "^11.7.20",
"yoshiki": "1.2.4",
"yoshiki": "1.2.7",
"zod": "^3.21.4"
},
"devDependencies": {
@ -46,10 +46,10 @@
"@types/node": "20.4.8",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.0",
"@types/react-native-video": "^5.0.15",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^8.46.0",
"eslint-config-next": "13.4.13",
"react-native": "0.72.3",
"typescript": "^5.1.6",
"webpack": "^5.88.2"
}

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"declaration": true,
"sourceMap": true,

View File

@ -102,7 +102,6 @@ export const Avatar = forwardRef<
<Image
src={src}
quality={quality}
isLoading={isLoading as any}
alt={alt}
layout={{ width: size, height: size }}
/>

View File

@ -29,14 +29,13 @@ export type YoshikiEnhanced<Style> = Style extends any
}
: never;
type WithLoading<T> = (T & { isLoading?: boolean }) | (Partial<T> & { isLoading: true });
export type Props = WithLoading<{
export type Props = {
src?: KyooImage | null;
quality: "low" | "medium" | "high";
alt: string;
alt?: string;
Error?: ReactElement | null;
}>;
forcedLoading?: boolean;
};
export type ImageLayout = YoshikiEnhanced<
| { width: ImageStyle["width"]; height: ImageStyle["height"] }

View File

@ -30,7 +30,7 @@ export const Image = ({
src,
quality,
alt,
isLoading: forcedLoading = false,
forcedLoading = false,
layout,
Error,
...props

View File

@ -40,7 +40,7 @@ export const Image = ({
src,
quality,
alt,
isLoading: forcedLoading = false,
forcedLoading = false,
layout,
Error,
...props

View File

@ -30,19 +30,11 @@ export { type Props as ImageProps, Image };
export const Poster = ({
alt,
isLoading = false,
layout,
...props
}: Props & { style?: ImageStyle } & {
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>;
}) => (
<Image
isLoading={isLoading as any}
alt={alt!}
layout={{ aspectRatio: 2 / 3, ...layout }}
{...props}
/>
);
}) => <Image alt={alt!} layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />;
export const ImageBackground = <AsProps = ViewProps,>({
src,
@ -53,7 +45,7 @@ export const ImageBackground = <AsProps = ViewProps,>({
children,
containerStyle,
imageStyle,
isLoading,
forcedLoading,
...asProps
}: {
as?: ComponentType<AsProps>;
@ -86,10 +78,13 @@ export const ImageBackground = <AsProps = ViewProps,>({
<Image
src={src}
quality={quality}
forcedLoading={forcedLoading}
alt={alt!}
layout={{ width: percent(100), height: percent(100) }}
Error={null}
{...(css([{ borderWidth: 0, borderRadius: 0 }, imageStyle]) as ImageProps)}
{...(css([{ borderWidth: 0, borderRadius: 0 }, imageStyle]) as {
style: ImageStyle;
})}
/>
)}
{gradient && (

View File

@ -106,7 +106,7 @@ export const Skeleton = ({
<MotiView
key={`skeleton_${i}`}
// No clue why it is a number on mobile and a string on web but /shrug
animate={{ opacity: Platform.OS === "web" ? "1" : 1 }}
animate={{ opacity: Platform.OS === "web" ? "1" as any : 1 }}
exit={{ opacity: 0 }}
transition={{ type: "timing" }}
onLayout={(e) => setWidth(e.nativeEvent.layout.width)}

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"declaration": true,
"sourceMap": true,

View File

@ -20,7 +20,7 @@
import { KyooImage } from "@kyoo/models";
import { Link, Skeleton, Poster, ts, focusReset, P, SubP } from "@kyoo/primitives";
import { Platform } from "react-native";
import { ImageStyle, Platform } from "react-native";
import { percent, px, Stylable, useYoshiki } from "yoshiki/native";
import { Layout, WithLoading } from "../fetch";
@ -51,7 +51,7 @@ export const ItemGrid = ({
m: { xs: ts(1), sm: ts(4) },
child: {
poster: {
borderColor: theme => theme.background,
borderColor: (theme) => theme.background,
borderWidth: px(4),
borderStyle: "solid",
},
@ -80,9 +80,9 @@ export const ItemGrid = ({
src={poster}
alt={name}
quality="low"
isLoading={isLoading}
forcedLoading={isLoading}
layout={{ width: percent(100) }}
{...css("poster")}
{...(css("poster") as { style: ImageStyle })}
/>
<Skeleton>
{isLoading || (

View File

@ -104,7 +104,7 @@ export const ItemList = ({
</Skeleton>
)}
</View>
<Poster src={poster} alt="" quality="low" isLoading={isLoading} layout={{ height: percent(80) }} />
<Poster src={poster} alt="" quality="low" forcedLoading={isLoading} layout={{ height: percent(80) }} />
</ImageBackground>
);
};

View File

@ -20,9 +20,10 @@
import { focusReset, H6, Image, ImageProps, Link, P, Skeleton, ts } from "@kyoo/primitives";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { ImageStyle, View } from "react-native";
import { Layout, WithLoading } from "../fetch";
import { percent, px, rem, Stylable, Theme, useYoshiki } from "yoshiki/native";
import { KyooImage } from "@kyoo/models";
export const episodeDisplayNumber = (
episode: {
@ -54,7 +55,12 @@ export const EpisodeBox = ({
return (
<View {...props}>
<Image src={thumbnail} quality="low" alt="" layout={{ width: percent(100), aspectRatio: 16 / 9 }} />
<Image
src={thumbnail}
quality="low"
alt=""
layout={{ width: percent(100), aspectRatio: 16 / 9 }}
/>
<Skeleton>{isLoading || <P>{name ?? t("show.episodeNoMetadata")}</P>}</Skeleton>
<Skeleton>{isLoading || <P>{overview}</P>}</Skeleton>
</View>
@ -79,12 +85,12 @@ export const EpisodeLine = ({
displayNumber: string;
name: string | null;
overview: string | null;
thumbnail?: string | null;
absoluteNumber: number | null,
episodeNumber: number | null,
seasonNumber: number | null,
releaseDate: Date | null,
id: number,
thumbnail?: KyooImage | null;
absoluteNumber: number | null;
episodeNumber: number | null;
seasonNumber: number | null;
releaseDate: Date | null;
id: number;
}> &
Stylable) => {
const { css } = useYoshiki();
@ -129,7 +135,7 @@ export const EpisodeLine = ({
width: percent(18),
aspectRatio: 16 / 9,
}}
{...css(["poster", { flexShrink: 0, m: ts(1) }])}
{...(css(["poster", { flexShrink: 0, m: ts(1) }]) as { style: ImageStyle })}
/>
<View {...css({ flexGrow: 1, flexShrink: 1, m: ts(1) })}>
<Skeleton>

View File

@ -49,7 +49,7 @@ import {
} from "@kyoo/primitives";
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { Platform, View } from "react-native";
import { ImageStyle, Platform, View } from "react-native";
import {
Theme,
md,
@ -113,14 +113,14 @@ const TitleLine = ({
src={poster}
alt={name}
quality="medium"
isLoading={isLoading}
forcedLoading={isLoading}
layout={{
width: { xs: percent(50), md: percent(25) },
}}
{...css({
{...(css({
maxWidth: { xs: px(175), sm: Platform.OS === "web" ? ("unset" as any) : 99999999 },
flexShrink: 0,
})}
}) as { style: ImageStyle })}
/>
<View
{...css({
@ -195,7 +195,7 @@ const TitleLine = ({
<IconFab
icon={PlayArrow}
as={Link}
href={type === "show" ? `/watch/${slug}` : `/movie/${slug}/watch`}
href={type === "show" ? `/watch/${slug}` : `/movie/${slug}/watch`}
color={{ xs: theme.user.colors.black, md: theme.colors.black }}
{...css({
bg: theme.user.accent,
@ -343,7 +343,15 @@ const Description = ({
);
};
export const Header = ({ query, type, slug }: { query: QueryIdentifier<Show | Movie>; type: "movie" | "show", slug: string }) => {
export const Header = ({
query,
type,
slug,
}: {
query: QueryIdentifier<Show | Movie>;
type: "movie" | "show";
slug: string;
}) => {
const { css } = useYoshiki();
return (

View File

@ -35,7 +35,7 @@ import {
} from "@kyoo/primitives";
import { Chapter, KyooImage, Subtitle } from "@kyoo/models";
import { useAtomValue, useSetAtom, useAtom } from "jotai";
import { Platform, Pressable, View, ViewProps } from "react-native";
import { ImageStyle, Platform, Pressable, View, ViewProps } from "react-native";
import { useTranslation } from "react-i18next";
import { percent, rem, useYoshiki } from "yoshiki/native";
import { useRouter } from "solito/router";
@ -76,7 +76,7 @@ export const Hover = ({
show: boolean;
} & ViewProps) => {
// TODO: animate show
const opacity = !show && (Platform.OS === "web" ? { opacity: 0 } : { display: "none" as const});
const opacity = !show && (Platform.OS === "web" ? { opacity: 0 } : { display: "none" as const });
return (
<ContrastArea mode="dark">
{({ css }) => (
@ -85,12 +85,12 @@ export const Hover = ({
<Pressable
focusable={false}
onPointerDown={onPointerDown}
onPress={Platform.OS !== "web" ? () => onPointerDown?.({} as any): undefined}
onPress={Platform.OS !== "web" ? () => onPointerDown?.({} as any) : undefined}
{...css(
[
{
// Fixed is used because firefox android make the hover disapear under the navigation bar in absolute
position: Platform.OS === "web" ? "fixed" as any : "absolute",
position: Platform.OS === "web" ? ("fixed" as any) : "absolute",
bottom: 0,
left: 0,
right: 0,
@ -185,12 +185,14 @@ export const Back = ({
>
<IconButton
icon={ArrowBack}
{...(href ? { as: Link as any, href: href } : { as: PressableFeedback, onPress: router.back })}
{...(href
? { as: Link as any, href: href }
: { as: PressableFeedback, onPress: router.back })}
{...tooltip(t("player.back"))}
/>
<Skeleton>
{isLoading ? (
<Skeleton {...css({ width: rem(5), })} />
<Skeleton {...css({ width: rem(5) })} />
) : (
<H1
{...css({
@ -222,7 +224,7 @@ const VideoPoster = ({ poster }: { poster?: KyooImage | null }) => {
src={poster}
quality="low"
layout={{ width: percent(100) }}
{...css({ position: "absolute", bottom: 0 })}
{...(css({ position: "absolute", bottom: 0 }) as { style: ImageStyle })}
/>
</View>
);

View File

@ -0,0 +1,266 @@
// Type definitions for react-native-video 5.0
// Project: https://github.com/react-native-community/react-native-video, https://github.com/brentvatne/react-native-video
// Definitions by: HuHuanming <https://github.com/huhuanming>
// Nekith <https://github.com/Nekith>
// Philip Frank <https://github.com/bananer>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8
declare module "react-native-video" {
import * as React from "react";
import { ViewProps } from "react-native";
export interface OnLoadData {
canPlayFastForward: boolean;
canPlayReverse: boolean;
canPlaySlowForward: boolean;
canPlaySlowReverse: boolean;
canStepBackward: boolean;
canStepForward: boolean;
currentPosition: number;
currentTime: number;
duration: number;
naturalSize: {
height: number;
width: number;
orientation: "portrait" | "landscape";
};
videoTracks: Array<{
bitrate: number;
codecs: string;
width: number;
height: number;
trackId: string;
}>;
audioTracks: Array<{
index: number;
title: string;
language: string;
type: string;
}>;
textTracks: Array<{
index: number;
title: string;
language: string;
type: string;
}>;
}
export interface OnProgressData {
currentTime: number;
playableDuration: number;
seekableDuration: number;
}
export interface OnBandwidthUpdateData {
bitrate: number;
}
export interface LoadError {
error: {
"": string;
errorString: string;
};
}
export interface OnSeekData {
currentTime: number;
seekTime: number;
target?: number | undefined;
}
export interface OnPlaybackRateData {
playbackRate: number;
}
export interface OnPictureInPictureStatusData {
isActive: boolean;
}
export interface OnExternalPlaybackChangeData {
isExternalPlaybackActive: boolean;
}
export interface OnBufferData {
isBuffering: boolean;
}
export interface DRMSettings {
type: DRMType;
licenseServer?: string | undefined;
headers?: { [key: string]: string } | undefined;
contentId?: string | undefined;
certificateUrl?: string | undefined;
base64Certificate?: boolean | undefined;
getLicense?(spcString: string): Promise<string>;
}
export const TextTrackType: {
SRT: "application/x-subrip";
TTML: "application/ttml+xml";
VTT: "text/vtt";
};
export enum FilterType {
NONE = "",
INVERT = "CIColorInvert",
MONOCHROME = "CIColorMonochrome",
POSTERIZE = "CIColorPosterize",
FALSE = "CIFalseColor",
MAXIMUMCOMPONENT = "CIMaximumComponent",
MINIMUMCOMPONENT = "CIMinimumComponent",
CHROME = "CIPhotoEffectChrome",
FADE = "CIPhotoEffectFade",
INSTANT = "CIPhotoEffectInstant",
MONO = "CIPhotoEffectMono",
NOIR = "CIPhotoEffectNoir",
PROCESS = "CIPhotoEffectProcess",
TONAL = "CIPhotoEffectTonal",
TRANSFER = "CIPhotoEffectTransfer",
SEPIA = "CISepiaTone",
}
export enum DRMType {
WIDEVINE = "widevine",
PLAYREADY = "playready",
CLEARKEY = "clearkey",
FAIRPLAY = "fairplay",
}
export interface VideoProperties extends ViewProps {
filter?: FilterType | undefined;
filterEnabled?: boolean | undefined;
/* Native only */
src?: any;
seek?: number | undefined;
fullscreen?: boolean | undefined;
fullscreenOrientation?: "all" | "landscape" | "portrait" | undefined;
fullscreenAutorotate?: boolean | undefined;
onVideoLoadStart?(): void;
onVideoLoad?(): void;
onVideoBuffer?(): void;
onVideoError?(): void;
onVideoProgress?(): void;
onVideoSeek?(): void;
onVideoEnd?(): void;
onTimedMetadata?(): void;
onVideoFullscreenPlayerWillPresent?(): void;
onVideoFullscreenPlayerDidPresent?(): void;
onVideoFullscreenPlayerWillDismiss?(): void;
onVideoFullscreenPlayerDidDismiss?(): void;
/* Wrapper component */
// Opaque type returned by require('./video.mp4')
source:
| {
uri?: string | undefined;
headers?: { [key: string]: string } | undefined;
type?: string | undefined;
}
| number;
minLoadRetryCount?: number | undefined;
maxBitRate?: number | undefined;
resizeMode?: "stretch" | "contain" | "cover" | "none" | undefined; // via Image#resizeMode
posterResizeMode?: "stretch" | "contain" | "cover" | "none" | undefined; // via Image#resizeMode
poster?: string | undefined;
repeat?: boolean | undefined;
automaticallyWaitsToMinimizeStalling?: boolean | undefined;
paused?: boolean | undefined;
muted?: boolean | undefined;
volume?: number | undefined;
bufferConfig?:
| {
minBufferMs?: number | undefined;
maxBufferMs?: number | undefined;
bufferForPlaybackMs?: number | undefined;
bufferForPlaybackAfterRebufferMs?: number | undefined;
}
| undefined;
stereoPan?: number | undefined;
rate?: number | undefined;
pictureInPicture?: boolean | undefined;
playInBackground?: boolean | undefined;
playWhenInactive?: boolean | undefined;
ignoreSilentSwitch?: "ignore" | "obey" | undefined;
mixWithOthers?: "inherit" | "mix" | "duck" | undefined;
reportBandwidth?: boolean | undefined;
disableFocus?: boolean | undefined;
controls?: boolean | undefined;
currentTime?: number | undefined;
progressUpdateInterval?: number | undefined;
useTextureView?: boolean | undefined;
hideShutterView?: boolean | undefined;
shutterColor?: string;
frameQuality?: number;
onFrameChange?: (base64ImageString: string) => void;
allowsExternalPlayback?: boolean | undefined;
audioOnly?: boolean | undefined;
preventsDisplaySleepDuringVideoPlayback?: boolean | undefined;
drm?: DRMSettings | undefined;
preferredForwardBufferDuration?: number | undefined;
onLoadStart?(): void;
onLoad?(data: OnLoadData): void;
onBuffer?(data: OnBufferData): void;
onError?(error: LoadError): void;
onProgress?(data: OnProgressData): void;
onBandwidthUpdate?(data: OnBandwidthUpdateData): void;
onSeek?(data: OnSeekData): void;
onEnd?(): void;
onFullscreenPlayerWillPresent?(): void;
onFullscreenPlayerDidPresent?(): void;
onFullscreenPlayerWillDismiss?(): void;
onFullscreenPlayerDidDismiss?(): void;
onReadyForDisplay?(): void;
onPlaybackStalled?(): void;
onPlaybackResume?(): void;
onPlaybackRateChange?(data: OnPlaybackRateData): void;
onAudioFocusChanged?(): void;
onAudioBecomingNoisy?(): void;
onPictureInPictureStatusChanged?(data: OnPictureInPictureStatusData): void;
onRestoreUserInterfaceForPictureInPictureStop?(): void;
onExternalPlaybackChange?(data: OnExternalPlaybackChangeData): void;
selectedAudioTrack?:
| {
type: "system" | "disabled" | "title" | "language" | "index";
value?: string | number | undefined;
}
| undefined;
selectedTextTrack?:
| {
type: "system" | "disabled" | "title" | "language" | "index";
value?: string | number | undefined;
}
| undefined;
selectedVideoTrack?:
| {
type: "auto" | "disabled" | "resolution" | "index";
value?: string | number | undefined;
}
| undefined;
textTracks?:
| Array<{
title?: string | undefined;
language?: string | undefined;
type: "application/x-subrip" | "application/ttml+xml" | "text/vtt";
uri: string;
}>
| undefined;
/* Required by react-native */
scaleX?: number | undefined;
scaleY?: number | undefined;
translateX?: number | undefined;
translateY?: number | undefined;
rotation?: number | undefined;
}
export default class Video extends React.Component<VideoProperties> {
presentFullscreenPlayer(): void;
dismissFullscreenPlayer(): void;
restoreUserInterfaceForPictureInPictureStopCompleted(restored: boolean): void;
save(): Promise<void>;
seek(time: number, tolerance?: number): void;
}
}

View File

@ -18,6 +18,8 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import "./react-native-video.d.ts";
declare module "react-native-video" {
interface VideoProperties {
fonts?: string[];

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"declaration": true,
"sourceMap": true,

View File

@ -3564,7 +3564,7 @@ __metadata:
languageName: node
linkType: hard
"@react-native/virtualized-lists@npm:^0.72.4, @react-native/virtualized-lists@npm:^0.72.6":
"@react-native/virtualized-lists@npm:^0.72.6":
version: 0.72.6
resolution: "@react-native/virtualized-lists@npm:0.72.6"
dependencies:
@ -3995,6 +3995,13 @@ __metadata:
languageName: node
linkType: hard
"@types/inline-style-prefixer@npm:^5.0.0":
version: 5.0.0
resolution: "@types/inline-style-prefixer@npm:5.0.0"
checksum: f138ba446d01a32e4612cc626a8fc44be15713560035c73d721c20c744083c488d072d7a11fc9b14c9212582271f04001e9d8d432267ff806dc3e6186959860a
languageName: node
linkType: hard
"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0":
version: 2.0.4
resolution: "@types/istanbul-lib-coverage@npm:2.0.4"
@ -4087,26 +4094,6 @@ __metadata:
languageName: node
linkType: hard
"@types/react-native-video@npm:^5.0.15":
version: 5.0.15
resolution: "@types/react-native-video@npm:5.0.15"
dependencies:
"@types/react": "*"
"@types/react-native": "*"
checksum: 8ed4542fea36b9b919bddba0bf4232dc680315e79e9750fdfff5153977cac3d1f82bcb3bba520b51a58c1e8cc80015fbeb184c0f7131f054809c85428b8fa622
languageName: node
linkType: hard
"@types/react-native@npm:*":
version: 0.72.2
resolution: "@types/react-native@npm:0.72.2"
dependencies:
"@react-native/virtualized-lists": ^0.72.4
"@types/react": "*"
checksum: b2915594381365ea379cb46b9a65219c6b6c7446d97d15f4da9615b8f180972bacba7cae584f4a8f83cce3a180f8a9a08a9c6b57081b37e2543f23f5717693d4
languageName: node
linkType: hard
"@types/react@npm:*, @types/react@npm:18.x.x":
version: 18.2.19
resolution: "@types/react@npm:18.2.19"
@ -10584,7 +10571,7 @@ __metadata:
react-native-uuid: ^2.0.1
react-native-video: ^6.0.0-alpha.7
typescript: ^5.1.6
yoshiki: 1.2.4
yoshiki: 1.2.7
languageName: unknown
linkType: soft
@ -14262,7 +14249,6 @@ __metadata:
"@types/node": 20.4.8
"@types/react": 18.2.0
"@types/react-dom": 18.2.0
"@types/react-native-video": ^5.0.15
copy-webpack-plugin: ^11.0.0
eslint: ^8.46.0
eslint-config-next: 13.4.13
@ -14279,6 +14265,7 @@ __metadata:
react: 18.2.0
react-dom: 18.2.0
react-i18next: ^13.0.3
react-native: 0.72.3
react-native-reanimated: ~3.3.0
react-native-svg: 13.11.0
react-native-video: ^6.0.0-alpha.7
@ -14289,7 +14276,7 @@ __metadata:
sweetalert2: ^11.7.20
typescript: ^5.1.6
webpack: ^5.88.2
yoshiki: 1.2.4
yoshiki: 1.2.7
zod: ^3.21.4
languageName: unknown
linkType: soft
@ -14671,10 +14658,11 @@ __metadata:
languageName: node
linkType: hard
"yoshiki@npm:1.2.4":
version: 1.2.4
resolution: "yoshiki@npm:1.2.4"
"yoshiki@npm:1.2.7":
version: 1.2.7
resolution: "yoshiki@npm:1.2.7"
dependencies:
"@types/inline-style-prefixer": ^5.0.0
"@types/node": 18.x.x
"@types/react": 18.x.x
inline-style-prefixer: ^7.0.0
@ -14687,7 +14675,7 @@ __metadata:
optional: true
react-native-web:
optional: true
checksum: 03b8c1ef41621deb3ae65238516928f78cf58883e1610cd1ea2f19b6c1c9cf8d09f7bf1a5538d8e9e5c7d46c5e9bdd23db38276e4ccd29cd281762afde7513fa
checksum: 2ebe15f481ad347a90fc8d6bc021d17ef03a8f982b31fa00b8700fa65d27e987044faa3d83d69159becb383ad9aa9ea5523eff3888965e25bc457affe147d361
languageName: node
linkType: hard