Added files
This commit is contained in:
commit
231ffa472b
|
@ -0,0 +1,2 @@
|
|||
.env
|
||||
node_modules
|
|
@ -0,0 +1,8 @@
|
|||
version: "3.3"
|
||||
services:
|
||||
meilisearch:
|
||||
image: "getmeili/meilisearch:latest"
|
||||
ports:
|
||||
- "61876:7700"
|
||||
volumes:
|
||||
- "./meili_data:/meili_data"
|
|
@ -0,0 +1,20 @@
|
|||
require("dotenv").config();
|
||||
|
||||
module.exports = {
|
||||
overwrite: true,
|
||||
schema: {
|
||||
[process.env.URL_GRAPHQL]: {
|
||||
headers: { Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}` },
|
||||
},
|
||||
},
|
||||
documents: [
|
||||
"src/shared/fragments/*.graphql",
|
||||
"src/core/graphql/operations/*.graphql",
|
||||
"src/core/graphql/fragments/*.graphql",
|
||||
],
|
||||
generates: {
|
||||
"src/core/graphql/generated.ts": {
|
||||
plugins: ["typescript", "typescript-operations", "typescript-graphql-request"],
|
||||
},
|
||||
},
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "search.accords-library.com",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"generate": "graphql-codegen --config graphql-codegen.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@digitak/esrun": "^3.2.15",
|
||||
"dotenv": "^16.0.3",
|
||||
"meilisearch": "^0.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^2.16.1",
|
||||
"@graphql-codegen/typescript": "2.8.5",
|
||||
"@graphql-codegen/typescript-graphql-request": "^4.5.8",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.10",
|
||||
"@types/node": "^18.11.17",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^5.1.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
module.exports = {
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
semi: true,
|
||||
singleQuote: false,
|
||||
quoteProps: "as-needed",
|
||||
jsxSingleQuote: false,
|
||||
trailingComma: "es5",
|
||||
bracketSpacing: true,
|
||||
bracketSameLine: true,
|
||||
arrowParens: "always",
|
||||
rangeStart: 0,
|
||||
rangeEnd: Infinity,
|
||||
requirePragma: false,
|
||||
insertPragma: false,
|
||||
proseWrap: "preserve",
|
||||
htmlWhitespaceSensitivity: "ignore",
|
||||
endOfLine: "lf",
|
||||
singleAttributePerLine: false,
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
npx esrun --tsconfig=./tsconfig.json src/app.ts
|
|
@ -0,0 +1,11 @@
|
|||
import http from "http";
|
||||
import { requestListener } from "./server";
|
||||
import { synchronizeStrapiAndMeili } from "./syncho";
|
||||
import * as dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
await synchronizeStrapiAndMeili();
|
||||
|
||||
http.createServer(requestListener).listen(process.env.PORT, () => {
|
||||
console.log(`Server started on port: ${process.env.PORT}`);
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
fragment contentAttributes on Content {
|
||||
slug
|
||||
updatedAt
|
||||
translations(pagination: { limit: -1 }) {
|
||||
pre_title
|
||||
title
|
||||
subtitle
|
||||
description
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
text_set {
|
||||
text
|
||||
}
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
type {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
fragment libraryItemAttributes on LibraryItem {
|
||||
title
|
||||
subtitle
|
||||
slug
|
||||
root_item
|
||||
primary
|
||||
descriptions(pagination: { limit: -1 }) {
|
||||
description
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
release_date {
|
||||
...datePicker
|
||||
}
|
||||
price {
|
||||
...pricePicker
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata {
|
||||
__typename
|
||||
... on ComponentMetadataBooks {
|
||||
subtype {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGame {
|
||||
platforms(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataVideo {
|
||||
subtype {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataAudio {
|
||||
subtype {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on ComponentMetadataGroup {
|
||||
subtype {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
subitems_type {
|
||||
data {
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
fragment postAttributes on Post {
|
||||
slug
|
||||
hidden
|
||||
date {
|
||||
...datePicker
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
translations(pagination: { limit: -1 }) {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
excerpt
|
||||
body
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
fragment videoAttributes on Video {
|
||||
uid
|
||||
title
|
||||
description
|
||||
published_date {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
views
|
||||
channel {
|
||||
data {
|
||||
attributes {
|
||||
title
|
||||
uid
|
||||
}
|
||||
}
|
||||
}
|
||||
gone
|
||||
duration
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
fragment wikiPageAttributes on WikiPage {
|
||||
slug
|
||||
thumbnail {
|
||||
data {
|
||||
attributes {
|
||||
...uploadImage
|
||||
}
|
||||
}
|
||||
}
|
||||
categories(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
name
|
||||
short
|
||||
}
|
||||
}
|
||||
}
|
||||
definitions(pagination: { limit: -1 }) {
|
||||
translations {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
definition
|
||||
}
|
||||
}
|
||||
tags(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
slug
|
||||
titles(filters: { language: { code: { eq: "en" } } }) {
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
translations(pagination: { limit: -1 }) {
|
||||
title
|
||||
aliases {
|
||||
alias
|
||||
}
|
||||
body {
|
||||
body
|
||||
}
|
||||
summary
|
||||
language {
|
||||
data {
|
||||
attributes {
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,88 @@
|
|||
import {
|
||||
ContentAttributesFragment,
|
||||
GetContentQuery,
|
||||
GetLibraryItemQuery,
|
||||
GetPostQuery,
|
||||
GetVideoQuery,
|
||||
GetWikiPageQuery,
|
||||
LibraryItemAttributesFragment,
|
||||
PostAttributesFragment,
|
||||
VideoAttributesFragment,
|
||||
WikiPageAttributesFragment,
|
||||
} from "./generated";
|
||||
|
||||
export interface MeiliLibraryItem extends LibraryItemAttributesFragment {
|
||||
id: string;
|
||||
sortable_name: string;
|
||||
sortable_price: number | undefined;
|
||||
sortable_date: number | undefined;
|
||||
untangible_group_item: boolean;
|
||||
}
|
||||
|
||||
export interface MeiliContent
|
||||
extends Omit<ContentAttributesFragment, "translations" | "updatedAt"> {
|
||||
id: string;
|
||||
translations: (Omit<
|
||||
NonNullable<NonNullable<ContentAttributesFragment["translations"]>[number]>,
|
||||
"text_set" | "description"
|
||||
> & {
|
||||
displayable_description?: string | null;
|
||||
})[];
|
||||
sortable_updated_date: number;
|
||||
}
|
||||
|
||||
export interface MeiliVideo extends VideoAttributesFragment {
|
||||
id: string;
|
||||
sortable_published_date: number;
|
||||
channel_uid?: string;
|
||||
}
|
||||
|
||||
export interface MeiliPost extends PostAttributesFragment {
|
||||
id: string;
|
||||
sortable_date: number;
|
||||
}
|
||||
|
||||
export interface MeiliWikiPage extends Omit<WikiPageAttributesFragment, "translations"> {
|
||||
id: string;
|
||||
translations: (Omit<
|
||||
NonNullable<NonNullable<WikiPageAttributesFragment["translations"]>[number]>,
|
||||
"body"
|
||||
> & {
|
||||
displayable_description?: string | null;
|
||||
})[];
|
||||
}
|
||||
|
||||
export enum MeiliIndices {
|
||||
LIBRARY_ITEM = "library-item",
|
||||
CONTENT = "content",
|
||||
VIDEOS = "video",
|
||||
POST = "post",
|
||||
WIKI_PAGE = "wiki-page",
|
||||
}
|
||||
|
||||
export type MeiliDocumentsType =
|
||||
| {
|
||||
index: MeiliIndices.LIBRARY_ITEM;
|
||||
documents: MeiliLibraryItem;
|
||||
strapi: GetLibraryItemQuery["libraryItem"];
|
||||
}
|
||||
| {
|
||||
index: MeiliIndices.CONTENT;
|
||||
documents: MeiliContent;
|
||||
strapi: GetContentQuery["content"];
|
||||
}
|
||||
| {
|
||||
index: MeiliIndices.VIDEOS;
|
||||
documents: MeiliVideo;
|
||||
strapi: GetVideoQuery["video"];
|
||||
}
|
||||
| {
|
||||
index: MeiliIndices.POST;
|
||||
documents: MeiliPost;
|
||||
strapi: GetPostQuery["post"];
|
||||
}
|
||||
| {
|
||||
index: MeiliIndices.WIKI_PAGE;
|
||||
documents: MeiliWikiPage;
|
||||
strapi: GetWikiPageQuery["wikiPage"];
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
query getContent($id: ID) {
|
||||
content(id: $id) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...contentAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
query getContents {
|
||||
contents(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...contentAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
query getLibraryItem($id: ID) {
|
||||
libraryItem(id: $id) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...libraryItemAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
query getLibraryItems {
|
||||
libraryItems(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...libraryItemAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
query getPost($id: ID) {
|
||||
post(id: $id) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...postAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
query getPosts {
|
||||
posts(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...postAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
query getVideo($id: ID) {
|
||||
video(id: $id) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...videoAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
query getVideos {
|
||||
videos(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...videoAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
query getWikiPage($id: ID) {
|
||||
wikiPage(id: $id) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...wikiPageAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
query getWikiPages {
|
||||
wikiPages(pagination: { limit: -1 }) {
|
||||
data {
|
||||
id
|
||||
attributes {
|
||||
...wikiPageAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { GraphQLClient } from "graphql-request";
|
||||
import { getSdk } from "./generated";
|
||||
|
||||
export const getReadySdk = (): ReturnType<typeof getSdk> => {
|
||||
const client = new GraphQLClient(process.env.URL_GRAPHQL ?? "", {
|
||||
headers: { Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}` },
|
||||
});
|
||||
return getSdk(client);
|
||||
};
|
|
@ -0,0 +1,79 @@
|
|||
type JoinDot<K extends string, P extends string> = `${K}${"" extends K ? "" : "."}${P}`;
|
||||
|
||||
type PathDot<T, Acc extends string = ""> = T extends object
|
||||
? {
|
||||
[K in keyof T]: K extends string ? JoinDot<Acc, K> | PathDot<T[K], JoinDot<Acc, K>> : never;
|
||||
}[keyof T]
|
||||
: Acc;
|
||||
|
||||
type PathHead<T extends unknown[]> = T extends [infer head]
|
||||
? head
|
||||
: // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
T extends [infer head, ...infer rest]
|
||||
? head
|
||||
: "";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type PathRest<T extends unknown[]> = T extends [infer head, ...infer rest]
|
||||
? rest extends []
|
||||
? never
|
||||
: rest
|
||||
: never;
|
||||
|
||||
type PathLast<T extends unknown[]> = T["length"] extends 1 ? true : false;
|
||||
|
||||
type Recursive<T, Path extends unknown[]> = PathHead<Path> extends keyof T
|
||||
? Omit<T, PathHead<Path>> & {
|
||||
[P in PathHead<Path>]-?: PathLast<Path> extends true
|
||||
? NonNullable<T[P]>
|
||||
: Recursive<NonNullable<T[P]>, PathRest<Path>>;
|
||||
}
|
||||
: T;
|
||||
|
||||
type Split<Str, Acc extends string[] = []> = Str extends `${infer Head}.${infer Rest}`
|
||||
? Split<Rest, [...Acc, Head]>
|
||||
: Str extends `${infer Last}`
|
||||
? [...Acc, Last]
|
||||
: never;
|
||||
|
||||
export type SelectiveNonNullable<T, P extends PathDot<T>> = Recursive<NonNullable<T>, Split<P>>;
|
||||
|
||||
export const isDefined = <T>(t: T): t is NonNullable<T> => t !== null && t !== undefined;
|
||||
|
||||
export const isUndefined = <T>(t: T | null | undefined): t is null | undefined => !isDefined(t);
|
||||
|
||||
export const isDefinedAndNotEmpty = (string: string | null | undefined): string is string =>
|
||||
isDefined(string) && string.length > 0;
|
||||
|
||||
export const filterDefined = <T>(t: T[] | null | undefined): NonNullable<T>[] =>
|
||||
isUndefined(t) ? [] : (t.filter((item) => isDefined(item)) as NonNullable<T>[]);
|
||||
|
||||
export const filterHasAttributes = <T, P extends PathDot<T>>(
|
||||
t: T[] | null | undefined,
|
||||
paths: readonly P[]
|
||||
): SelectiveNonNullable<T, typeof paths[number]>[] =>
|
||||
isDefined(t)
|
||||
? (t.filter((item) => hasAttributes(item, paths)) as unknown as SelectiveNonNullable<
|
||||
T,
|
||||
typeof paths[number]
|
||||
>[])
|
||||
: [];
|
||||
|
||||
const hasAttributes = <T>(item: T, paths: readonly PathDot<T>[]): boolean =>
|
||||
isDefined(item) && paths.every((path) => hasAttribute(item, path));
|
||||
|
||||
const hasAttribute = <T>(item: T, path: string): boolean => {
|
||||
if (isDefined(item)) {
|
||||
const [head, ...rest] = path.split(".");
|
||||
if (isDefined(head) && Object.keys(item).includes(head)) {
|
||||
const attribute = head as keyof T;
|
||||
if (isDefined(item[attribute])) {
|
||||
if (rest.length > 0) {
|
||||
return hasAttribute(item[attribute], rest.join("."));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import { isDefined, isUndefined } from "./asserts";
|
||||
|
||||
export type DatePickerFragment = {
|
||||
year?: number | null;
|
||||
month?: number | null;
|
||||
day?: number | null;
|
||||
};
|
||||
|
||||
export const compareDate = (
|
||||
a: DatePickerFragment | null | undefined,
|
||||
b: DatePickerFragment | null | undefined
|
||||
): number => {
|
||||
if (isUndefined(a) || isUndefined(b)) {
|
||||
return 0;
|
||||
}
|
||||
return dateInDays(a) - dateInDays(b);
|
||||
};
|
||||
|
||||
export const dateInDays = (date: DatePickerFragment | null | undefined): number =>
|
||||
isDefined(date) ? (date.year ?? Infinity) * 365 + (date.month ?? 12) * 31 + (date.day ?? 31) : 0;
|
||||
|
||||
export const datePickerToDate = (date: DatePickerFragment): Date =>
|
||||
new Date(date.year ?? 0, date.month ? date.month - 1 : 0, date.day ?? 1);
|
||||
|
||||
export const getUnixTime = (date: Date): number => Math.floor(date.getTime() / 1000);
|
|
@ -0,0 +1,221 @@
|
|||
import { isDefinedAndNotEmpty } from "./asserts";
|
||||
|
||||
export const prettySlug = (slug?: string, parentSlug?: string): string => {
|
||||
let newSlug = slug;
|
||||
if (newSlug) {
|
||||
if (isDefinedAndNotEmpty(parentSlug) && newSlug.startsWith(parentSlug))
|
||||
newSlug = newSlug.substring(parentSlug.length + 1);
|
||||
newSlug = newSlug.replaceAll("-", " ");
|
||||
return capitalizeString(newSlug);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const prettyInlineTitle = (
|
||||
pretitle: string | null | undefined,
|
||||
title: string | null | undefined,
|
||||
subtitle: string | null | undefined
|
||||
): string => {
|
||||
let result = "";
|
||||
if (pretitle) result += `${pretitle}: `;
|
||||
result += title;
|
||||
if (subtitle) result += ` - ${subtitle}`;
|
||||
return result;
|
||||
};
|
||||
|
||||
/* eslint-disable id-denylist */
|
||||
export const prettyItemSubType = (
|
||||
metadata:
|
||||
| {
|
||||
__typename: "ComponentMetadataAudio";
|
||||
subtype?: {
|
||||
data?: {
|
||||
attributes?: {
|
||||
slug: string;
|
||||
titles?:
|
||||
| ({
|
||||
title: string;
|
||||
} | null)[]
|
||||
| null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
| {
|
||||
__typename: "ComponentMetadataBooks";
|
||||
subtype?: {
|
||||
data?: {
|
||||
attributes?: {
|
||||
slug: string;
|
||||
titles?:
|
||||
| ({
|
||||
title: string;
|
||||
} | null)[]
|
||||
| null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
| {
|
||||
__typename: "ComponentMetadataGame";
|
||||
platforms?: {
|
||||
data: {
|
||||
id?: string | null;
|
||||
attributes?: {
|
||||
short: string;
|
||||
} | null;
|
||||
}[];
|
||||
} | null;
|
||||
}
|
||||
| {
|
||||
__typename: "ComponentMetadataGroup";
|
||||
subtype?: {
|
||||
data?: {
|
||||
attributes?: {
|
||||
slug: string;
|
||||
titles?:
|
||||
| ({
|
||||
title: string;
|
||||
} | null)[]
|
||||
| null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
subitems_type?: {
|
||||
data?: {
|
||||
attributes?: {
|
||||
slug: string;
|
||||
titles?:
|
||||
| ({
|
||||
title: string;
|
||||
} | null)[]
|
||||
| null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
| {
|
||||
__typename: "ComponentMetadataVideo";
|
||||
subtype?: {
|
||||
data?: {
|
||||
attributes?: {
|
||||
slug: string;
|
||||
titles?:
|
||||
| ({
|
||||
title: string;
|
||||
} | null)[]
|
||||
| null;
|
||||
} | null;
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
| { __typename: "ComponentMetadataOther" }
|
||||
| { __typename: "Error" }
|
||||
| null
|
||||
): string => {
|
||||
if (metadata) {
|
||||
switch (metadata.__typename) {
|
||||
case "ComponentMetadataAudio":
|
||||
case "ComponentMetadataBooks":
|
||||
case "ComponentMetadataVideo":
|
||||
return metadata.subtype?.data?.attributes?.titles &&
|
||||
metadata.subtype.data.attributes.titles.length > 0 &&
|
||||
metadata.subtype.data.attributes.titles[0]
|
||||
? metadata.subtype.data.attributes.titles[0].title
|
||||
: prettySlug(metadata.subtype?.data?.attributes?.slug);
|
||||
case "ComponentMetadataGame":
|
||||
return metadata.platforms?.data &&
|
||||
metadata.platforms.data.length > 0 &&
|
||||
metadata.platforms.data[0]?.attributes
|
||||
? metadata.platforms.data[0].attributes.short
|
||||
: "";
|
||||
case "ComponentMetadataGroup": {
|
||||
const firstPart =
|
||||
metadata.subtype?.data?.attributes?.titles &&
|
||||
metadata.subtype.data.attributes.titles.length > 0 &&
|
||||
metadata.subtype.data.attributes.titles[0]
|
||||
? metadata.subtype.data.attributes.titles[0].title
|
||||
: prettySlug(metadata.subtype?.data?.attributes?.slug);
|
||||
|
||||
const secondPart =
|
||||
metadata.subitems_type?.data?.attributes?.titles &&
|
||||
metadata.subitems_type.data.attributes.titles.length > 0 &&
|
||||
metadata.subitems_type.data.attributes.titles[0]
|
||||
? metadata.subitems_type.data.attributes.titles[0].title
|
||||
: prettySlug(metadata.subitems_type?.data?.attributes?.slug);
|
||||
return `${secondPart} ${firstPart}`;
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
/* eslint-enable id-denylist */
|
||||
|
||||
export const prettyShortenNumber = (number: number): string => {
|
||||
if (number > 1_000_000) {
|
||||
return `${(number / 1_000_000).toLocaleString(undefined, {
|
||||
maximumSignificantDigits: 3,
|
||||
})}M`;
|
||||
} else if (number > 1_000) {
|
||||
return `${(number / 1_000).toLocaleString(undefined, {
|
||||
maximumSignificantDigits: 2,
|
||||
})}K`;
|
||||
}
|
||||
return number.toLocaleString();
|
||||
};
|
||||
|
||||
export const prettyDuration = (seconds: number): string => {
|
||||
let hours = 0;
|
||||
let minutes = 0;
|
||||
let remainingSeconds = seconds;
|
||||
while (remainingSeconds > 60) {
|
||||
minutes++;
|
||||
remainingSeconds -= 60;
|
||||
}
|
||||
while (minutes > 60) {
|
||||
hours++;
|
||||
minutes -= 60;
|
||||
}
|
||||
let result = "";
|
||||
if (hours) result += `${hours.toString().padStart(2, "0")}:`;
|
||||
result += `${minutes.toString().padStart(2, "0")}:`;
|
||||
result += remainingSeconds.toString().padStart(2, "0");
|
||||
return result;
|
||||
};
|
||||
|
||||
export const prettyURL = (url: string): string => {
|
||||
const domain = new URL(url);
|
||||
return domain.hostname.replace("www.", "");
|
||||
};
|
||||
|
||||
const capitalizeString = (string: string): string => {
|
||||
const capitalizeWord = (word: string): string => word.charAt(0).toUpperCase() + word.substring(1);
|
||||
|
||||
let words = string.split(" ");
|
||||
words = words.map((word) => capitalizeWord(word));
|
||||
return words.join(" ");
|
||||
};
|
||||
|
||||
export const slugify = (string: string | undefined): string => {
|
||||
if (!string) {
|
||||
return "";
|
||||
}
|
||||
return string
|
||||
.replace(/[ÀÁÂÃÄÅàáâãäåæÆ]/gu, "a")
|
||||
.replace(/[çÇ]/gu, "c")
|
||||
.replace(/[ðÐ]/gu, "d")
|
||||
.replace(/[ÈÉÊËéèêë]/gu, "e")
|
||||
.replace(/[ÏïÎîÍíÌì]/gu, "i")
|
||||
.replace(/[Ññ]/gu, "n")
|
||||
.replace(/[øØœŒÕõÔôÓóÒò]/gu, "o")
|
||||
.replace(/[ÜüÛûÚúÙù]/gu, "u")
|
||||
.replace(/[ŸÿÝý]/gu, "y")
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9- ]/gu, "")
|
||||
.trim()
|
||||
.replace(/ /gu, "-");
|
||||
};
|
||||
|
||||
export const sJoin = (...args: (string | null | undefined)[]): string => args.join("");
|
|
@ -0,0 +1,16 @@
|
|||
import { isDefined } from "./asserts";
|
||||
|
||||
export const isUntangibleGroupItem = (
|
||||
metadata:
|
||||
| {
|
||||
__typename: string;
|
||||
// eslint-disable-next-line id-denylist
|
||||
subtype?: { data?: { attributes?: { slug: string } | null } | null } | null;
|
||||
}
|
||||
| null
|
||||
| undefined
|
||||
): boolean =>
|
||||
isDefined(metadata) &&
|
||||
metadata.__typename === "ComponentMetadataGroup" &&
|
||||
(metadata.subtype?.data?.attributes?.slug === "variant-set" ||
|
||||
metadata.subtype?.data?.attributes?.slug === "relation-set");
|
|
@ -0,0 +1,25 @@
|
|||
import { Currencies } from "./localData";
|
||||
import { PricePickerFragment } from "graphql/generated";
|
||||
|
||||
export const convertPrice = (
|
||||
pricePicker: PricePickerFragment,
|
||||
targetCurrency: Currencies[number]
|
||||
): number => {
|
||||
if (pricePicker.amount && pricePicker.currency?.data?.attributes && targetCurrency.attributes)
|
||||
return (
|
||||
(pricePicker.amount * pricePicker.currency.data.attributes.rate_to_usd) /
|
||||
targetCurrency.attributes.rate_to_usd
|
||||
);
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const convertMmToInch = (mm: number | null | undefined): string =>
|
||||
mm ? (mm * 0.03937008).toPrecision(3) : "";
|
||||
|
||||
export const randomInt = (min: number, max: number): number =>
|
||||
Math.floor(Math.random() * (max - min)) + min;
|
||||
|
||||
export const isInteger = (value: string): boolean => /^[+-]?[0-9]+$/u.test(value);
|
||||
|
||||
export const clamp = (value: number, min: number, max: number): number =>
|
||||
Math.min(Math.max(value, min), max);
|
|
@ -0,0 +1,143 @@
|
|||
import {
|
||||
MeiliContent,
|
||||
MeiliDocumentsType,
|
||||
MeiliIndices,
|
||||
MeiliWikiPage,
|
||||
} from "core/graphql/meiliTypes";
|
||||
import {
|
||||
filterDefined,
|
||||
filterHasAttributes,
|
||||
isDefined,
|
||||
isDefinedAndNotEmpty,
|
||||
} from "core/helpers/asserts";
|
||||
import { datePickerToDate, getUnixTime } from "core/helpers/date";
|
||||
import { prettyInlineTitle } from "core/helpers/formatters";
|
||||
import { isUntangibleGroupItem } from "core/helpers/libraryItem";
|
||||
import { MeiliSearch } from "meilisearch";
|
||||
|
||||
export const getMeili = () =>
|
||||
new MeiliSearch({
|
||||
host: process.env.MEILISEARCH_URL ?? "",
|
||||
apiKey: process.env.MEILISEARCH_MASTER_KEY,
|
||||
});
|
||||
|
||||
type TransformFunction<I extends MeiliDocumentsType["index"]> = (
|
||||
data: NonNullable<Extract<MeiliDocumentsType, { index: I }>["strapi"]>["data"]
|
||||
) => Extract<MeiliDocumentsType, { index: I }>["documents"];
|
||||
|
||||
const transformVideo: TransformFunction<MeiliIndices.VIDEOS> = (data) => {
|
||||
if (!data) throw new Error(`Data is empty ${MeiliIndices.VIDEOS}`);
|
||||
if (!data.attributes || !data.id)
|
||||
throw new Error(`Incorrect data stucture on ${MeiliIndices.VIDEOS}`);
|
||||
const { id, attributes } = data;
|
||||
return {
|
||||
id,
|
||||
...attributes,
|
||||
sortable_published_date: getUnixTime(datePickerToDate(attributes.published_date)),
|
||||
channel_uid: attributes.channel?.data?.attributes?.uid,
|
||||
};
|
||||
};
|
||||
|
||||
const transformContent: TransformFunction<MeiliIndices.CONTENT> = (data) => {
|
||||
if (!data) throw new Error(`Data is empty ${MeiliIndices.CONTENT}`);
|
||||
if (!data.attributes || !data.id)
|
||||
throw new Error(`Incorrect data stucture on ${MeiliIndices.CONTENT}`);
|
||||
const {
|
||||
id,
|
||||
attributes: { translations, updatedAt, ...otherAttributes },
|
||||
} = data;
|
||||
return {
|
||||
id,
|
||||
...otherAttributes,
|
||||
translations: filterDefined(translations).map(
|
||||
({ text_set, description, ...otherTranslatedFields }) => {
|
||||
let displayable_description = "";
|
||||
if (isDefinedAndNotEmpty(description)) displayable_description += description;
|
||||
if (isDefinedAndNotEmpty(text_set?.text))
|
||||
displayable_description += `\n\n${text_set?.text}`;
|
||||
return {
|
||||
...otherTranslatedFields,
|
||||
displayable_description,
|
||||
};
|
||||
}
|
||||
),
|
||||
sortable_updated_date: updatedAt,
|
||||
};
|
||||
};
|
||||
|
||||
const transformLibraryItem: TransformFunction<MeiliIndices.LIBRARY_ITEM> = (data) => {
|
||||
if (!data) throw new Error(`Data is empty ${MeiliIndices.LIBRARY_ITEM}`);
|
||||
if (!data.attributes || !data.id)
|
||||
throw new Error(`Incorrect data stucture on ${MeiliIndices.LIBRARY_ITEM}`);
|
||||
const { id, attributes } = data;
|
||||
return {
|
||||
id,
|
||||
sortable_date: isDefined(attributes.release_date)
|
||||
? getUnixTime(datePickerToDate(attributes.release_date))
|
||||
: undefined,
|
||||
sortable_name: prettyInlineTitle(undefined, attributes.title, attributes.subtitle),
|
||||
sortable_price:
|
||||
attributes.price?.currency?.data?.attributes && isDefined(attributes.price.amount)
|
||||
? attributes.price.amount * attributes.price.currency.data.attributes.rate_to_usd
|
||||
: undefined,
|
||||
untangible_group_item: isUntangibleGroupItem(attributes.metadata?.[0]),
|
||||
...attributes,
|
||||
};
|
||||
};
|
||||
|
||||
const transformPost: TransformFunction<MeiliIndices.POST> = (data) => {
|
||||
if (!data) throw new Error(`Data is empty ${MeiliIndices.POST}`);
|
||||
if (!data.attributes || !data.id)
|
||||
throw new Error(`Incorrect data stucture on ${MeiliIndices.POST}`);
|
||||
const { id, attributes } = data;
|
||||
return {
|
||||
id,
|
||||
...attributes,
|
||||
sortable_date: getUnixTime(datePickerToDate(attributes.date)),
|
||||
};
|
||||
};
|
||||
|
||||
const transformWikiPage: TransformFunction<MeiliIndices.WIKI_PAGE> = (data) => {
|
||||
if (!data) throw new Error(`Data is empty ${MeiliIndices.POST}`);
|
||||
if (!data.attributes || !data.id)
|
||||
throw new Error(`Incorrect data stucture on ${MeiliIndices.POST}`);
|
||||
const {
|
||||
id,
|
||||
attributes: { translations, definitions, ...otherAttributes },
|
||||
} = data;
|
||||
return {
|
||||
id,
|
||||
definitions,
|
||||
translations: filterDefined(translations).map(({ summary, body, ...otherTransAttributes }) => {
|
||||
let displayable_description = "";
|
||||
if (isDefinedAndNotEmpty(summary)) displayable_description += summary;
|
||||
if (isDefined(body) && isDefinedAndNotEmpty(body.body))
|
||||
displayable_description += `\n\n${body.body}`;
|
||||
definitions?.forEach((def) =>
|
||||
def?.translations?.forEach((defTranslation) => {
|
||||
if (
|
||||
defTranslation?.language?.data?.attributes?.code ===
|
||||
otherTransAttributes.language?.data?.attributes?.code &&
|
||||
isDefined(defTranslation?.definition)
|
||||
) {
|
||||
displayable_description += `\n\n${defTranslation?.definition}`;
|
||||
}
|
||||
})
|
||||
);
|
||||
return {
|
||||
summary,
|
||||
...otherTransAttributes,
|
||||
displayable_description,
|
||||
};
|
||||
}),
|
||||
...otherAttributes,
|
||||
};
|
||||
};
|
||||
|
||||
export const strapiToMeiliTransformFunctions = {
|
||||
video: transformVideo,
|
||||
"library-item": transformLibraryItem,
|
||||
content: transformContent,
|
||||
post: transformPost,
|
||||
"wiki-page": transformWikiPage,
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
import http from "http";
|
||||
import { webhookHandler } from "webhook";
|
||||
|
||||
export const requestListener: http.RequestListener = async (req, res) => {
|
||||
if (req.method !== "POST") {
|
||||
res
|
||||
.writeHead(405, { "Content-Type": "application/json" })
|
||||
.end(JSON.stringify({ message: "Method Not Allowed. Use POST." }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url !== "/strapi/") {
|
||||
res
|
||||
.writeHead(404, { "Content-Type": "application/json" })
|
||||
.end(JSON.stringify({ message: "Route not found.", route: req.url }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.headers.authorization !== `Bearer ${process.env.WEBHOOK_TOKEN}`) {
|
||||
res
|
||||
.writeHead(403, { "Content-Type": "application/json" })
|
||||
.end(JSON.stringify({ message: "Invalid auth token." }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve and parse body
|
||||
const buffers: Uint8Array[] = [];
|
||||
for await (const chunk of req) {
|
||||
buffers.push(chunk);
|
||||
}
|
||||
const data = JSON.parse(Buffer.concat(buffers).toString());
|
||||
|
||||
await webhookHandler(data, res);
|
||||
|
||||
res.writeHead(200, { "Content-Type": "application/json" }).end(
|
||||
JSON.stringify({
|
||||
message: "Done.",
|
||||
})
|
||||
);
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
fragment datePicker on ComponentBasicsDatepicker {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
fragment pricePicker on ComponentBasicsPrice {
|
||||
amount
|
||||
currency {
|
||||
data {
|
||||
attributes {
|
||||
symbol
|
||||
code
|
||||
rate_to_usd
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
fragment uploadImage on UploadFile {
|
||||
name
|
||||
alternativeText
|
||||
caption
|
||||
width
|
||||
height
|
||||
url
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import { MeiliDocumentsType, MeiliIndices } from "core/graphql/meiliTypes";
|
||||
import { getReadySdk } from "core/graphql/sdk";
|
||||
import { getMeili, strapiToMeiliTransformFunctions } from "helpers/meili";
|
||||
|
||||
export const synchronizeStrapiAndMeili = async () => {
|
||||
const sdk = getReadySdk();
|
||||
|
||||
// [ LIBRARY ITEMS ]
|
||||
{
|
||||
const libraryItems = await sdk.getLibraryItems();
|
||||
processIndex(
|
||||
MeiliIndices.LIBRARY_ITEM,
|
||||
libraryItems.libraryItems?.data.map((item) =>
|
||||
strapiToMeiliTransformFunctions["library-item"](item)
|
||||
),
|
||||
["title", "subtitle", "descriptions"],
|
||||
["sortable_name", "sortable_date", "sortable_price"],
|
||||
["primary", "root_item", "id", "untangible_group_item"]
|
||||
);
|
||||
}
|
||||
|
||||
// [ CONTENT ]
|
||||
{
|
||||
const content = await sdk.getContents();
|
||||
processIndex(
|
||||
MeiliIndices.CONTENT,
|
||||
content.contents?.data.map((item) => strapiToMeiliTransformFunctions["content"](item)),
|
||||
["translations"],
|
||||
["slug", "sortable_updated_date"]
|
||||
);
|
||||
}
|
||||
|
||||
// [ VIDEOS ]
|
||||
{
|
||||
const videos = await sdk.getVideos();
|
||||
processIndex(
|
||||
MeiliIndices.VIDEOS,
|
||||
videos.videos?.data.map((item) => strapiToMeiliTransformFunctions["video"](item)),
|
||||
["title", "channel", "description", "uid"],
|
||||
["sortable_published_date", "duration", "views"],
|
||||
["gone", "channel_uid"]
|
||||
);
|
||||
}
|
||||
|
||||
// [ POSTS ]
|
||||
{
|
||||
const posts = await sdk.getPosts();
|
||||
processIndex(
|
||||
MeiliIndices.POST,
|
||||
posts.posts?.data.map((item) => strapiToMeiliTransformFunctions["post"](item)),
|
||||
["translations"],
|
||||
["sortable_date"],
|
||||
["hidden"]
|
||||
);
|
||||
}
|
||||
|
||||
// [ WIKI PAGES ]
|
||||
{
|
||||
const wikiPages = await sdk.getWikiPages();
|
||||
processIndex(
|
||||
MeiliIndices.WIKI_PAGE,
|
||||
wikiPages.wikiPages?.data.map((item) => strapiToMeiliTransformFunctions["wiki-page"](item)),
|
||||
["translations", "definitions"]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const processIndex = async <I extends MeiliDocumentsType["index"]>(
|
||||
indexName: I,
|
||||
data?: Extract<MeiliDocumentsType, { index: I }>["documents"][],
|
||||
searchableAttributes?: (keyof NonNullable<typeof data>[number])[],
|
||||
sortableAttributes?: (keyof NonNullable<typeof data>[number])[],
|
||||
filterableAttributes?: (keyof NonNullable<typeof data>[number])[]
|
||||
) => {
|
||||
const meili = getMeili();
|
||||
await meili.deleteIndexIfExists(indexName);
|
||||
|
||||
if (data && data.length > 0) {
|
||||
await meili.createIndex(indexName);
|
||||
const index = meili.index(indexName);
|
||||
index.updateSettings({
|
||||
searchableAttributes: searchableAttributes as string[],
|
||||
sortableAttributes: sortableAttributes as string[],
|
||||
filterableAttributes: filterableAttributes as string[],
|
||||
pagination: { maxTotalHits: 10000 },
|
||||
});
|
||||
index.addDocuments(data);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,97 @@
|
|||
import { MeiliDocumentsType, MeiliIndices } from "core/graphql/meiliTypes";
|
||||
import { getReadySdk } from "core/graphql/sdk";
|
||||
import { getMeili, strapiToMeiliTransformFunctions } from "helpers/meili";
|
||||
import http from "http";
|
||||
|
||||
const allowedEvents = ["entry.create", "entry.update", "entry.delete"] as const;
|
||||
|
||||
type CRUDEvents = typeof allowedEvents[number];
|
||||
|
||||
type StrapiEvent = {
|
||||
event: CRUDEvents;
|
||||
model: MeiliIndices;
|
||||
entry: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const webhookHandler = async (data: StrapiEvent, res: http.ServerResponse) => {
|
||||
console.log(data);
|
||||
|
||||
if (!allowedEvents.includes(data.event)) {
|
||||
res.writeHead(406, { "Content-Type": "application/json" }).end(
|
||||
JSON.stringify({
|
||||
message: `The event given ${data.event} in not allowed.`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = getReadySdk();
|
||||
|
||||
switch (data.model) {
|
||||
case MeiliIndices.LIBRARY_ITEM: {
|
||||
processIndex(
|
||||
data.model,
|
||||
strapiToMeiliTransformFunctions["library-item"](
|
||||
(await sdk.getLibraryItem({ id: data.entry.id })).libraryItem?.data
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case MeiliIndices.CONTENT: {
|
||||
processIndex(
|
||||
data.model,
|
||||
strapiToMeiliTransformFunctions["content"](
|
||||
(await sdk.getContent({ id: data.entry.id })).content?.data
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case MeiliIndices.VIDEOS: {
|
||||
processIndex(
|
||||
data.model,
|
||||
strapiToMeiliTransformFunctions["video"](
|
||||
(await sdk.getVideo({ id: data.entry.id })).video?.data
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case MeiliIndices.POST: {
|
||||
processIndex(
|
||||
data.model,
|
||||
strapiToMeiliTransformFunctions["post"](
|
||||
(await sdk.getPost({ id: data.entry.id })).post?.data
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case MeiliIndices.WIKI_PAGE: {
|
||||
processIndex(
|
||||
data.model,
|
||||
strapiToMeiliTransformFunctions["wiki-page"](
|
||||
(await sdk.getWikiPage({ id: data.entry.id })).wikiPage?.data
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
console.log("Unrecognized data model", data.model);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const processIndex = async <I extends MeiliDocumentsType["index"]>(
|
||||
indexName: I,
|
||||
data: Extract<MeiliDocumentsType, { index: I }>["documents"] | undefined | null
|
||||
) => {
|
||||
const meili = getMeili();
|
||||
const index = meili.index(indexName);
|
||||
await index.addDocuments([data]);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Type Checking
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"strict": true,
|
||||
|
||||
"target": "ESNext",
|
||||
"importHelpers": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue