diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..151e803d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +node_modules +.git +.gitignore +.out +*.md + +docker-compose.* + +/**/**/.DS_Store +/**/**/*.md +/**/**/node_modules +/**/**/dist +/**/**/.next +/**/**/.git +/**/**/.prettierrc +/**/**/.env +/**/**/.env.* +/**/**/.dockerignore +/**/**/.eslintrc +/**/**/.eslintrc.* +/**/**/.eslintignore +/**/**/.prettierrc +/**/**/.prettierignore +/**/**/.vscode +/**/**/.eslint.cjs +/**/**/jest.*.* + +/packages/not-shared \ No newline at end of file diff --git a/.github/workflows/velog-cron-build.yml b/.github/workflows/velog-cron-build.yml index 3e02c614..e8627b50 100644 --- a/.github/workflows/velog-cron-build.yml +++ b/.github/workflows/velog-cron-build.yml @@ -1,4 +1,4 @@ -name: Velog Server build workflow +name: Velog cron build workflow on: pull_request: @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.18.2] + node-version: [20.11.1] defaults: run: working-directory: './packages/velog-cron' diff --git a/.github/workflows/velog-deploy.yml b/.github/workflows/velog-deploy.yml index 13446cf0..7e7effcc 100644 --- a/.github/workflows/velog-deploy.yml +++ b/.github/workflows/velog-deploy.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.16] + node-version: [20.11.1] steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2 diff --git a/.github/workflows/velog-server-build.yml b/.github/workflows/velog-server-build.yml index 7758b657..1051f994 100644 --- a/.github/workflows/velog-server-build.yml +++ b/.github/workflows/velog-server-build.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.18.2] + node-version: [20.11.1] defaults: run: working-directory: './packages/velog-server' diff --git a/.github/workflows/velog-web-build.yml b/.github/workflows/velog-web-build.yml index 86c80c50..b49c92d0 100644 --- a/.github/workflows/velog-web-build.yml +++ b/.github/workflows/velog-web-build.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.18.2] + node-version: [20.11.1] defaults: run: working-directory: './packages/velog-web' diff --git a/.gitignore b/.gitignore index 8c5165ea..2b4a00eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,26 @@ -.DS_Store +# IDE .idea -./dist + +# Else +.DS_Store +**/.DS_Store + +# build **/node_modules/ +**/dist/ +**/build/** +**/.next/** +# env .env .env.* +**/env.* +!**/env.ts +!**/env.mts !**/**/.env.example -.vscode \ No newline at end of file +# turbo +**/.turbo +out +# Turborepo +.turbo \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit index a5a29d9f..1f8287e7 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -pnpm lint-staged +# pnpm lint-staged \ No newline at end of file diff --git a/.lintstagedrc.mjs b/.lintstagedrc.mjs index e734cec3..5caa554b 100644 --- a/.lintstagedrc.mjs +++ b/.lintstagedrc.mjs @@ -1,3 +1,3 @@ export default { - '*.{md,yml,json,js,ts,jsx,tsx,mjs,mts}': 'prettier --write', + '*.{md,yml,json,js,ts,jsx,tsx,mjs,mts}': 'pnpm -r lint', } diff --git a/.npmrc b/.npmrc index 2eb100af..85c75d17 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -loglevel=error \ No newline at end of file +loglevel=error +public-hoist-pattern[]=*prisma* \ No newline at end of file diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 94b08d4f..00000000 --- a/.prettierignore +++ /dev/null @@ -1,11 +0,0 @@ -# .prettierignore -node_modules/ -pnpm-lock.yaml - -# Next ignore patterns -**/.next/ -**/.build/ -**/.coverage/ - -# Server -**/dist \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..ac725fcb --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode", "aaron-bond.better-comments"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..a303ca2a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnType": false, // required + "editor.formatOnPaste": true, // optional + "editor.formatOnSave": true, // optional + "editor.formatOnSaveMode": "file", // required to format on save + "files.autoSave": "afterDelay", // optional but recommended, + "eslint.workingDirectories": [ + { + "pattern": "./apps/*/" + }, + { + "pattern": "./packages/*/" + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f5ffb8bd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Chaf, Inc. + +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. diff --git a/apps/book-server/.gitignore b/apps/book-server/.gitignore new file mode 100644 index 00000000..91e1698b --- /dev/null +++ b/apps/book-server/.gitignore @@ -0,0 +1,8 @@ +# Ignore node_modules +node_modules +# Ignore dist +dist + +# Ignore build process files +books/* +!books/base \ No newline at end of file diff --git a/packages/velog-cron/.prettierrc b/apps/book-server/.prettierrc similarity index 100% rename from packages/velog-cron/.prettierrc rename to apps/book-server/.prettierrc diff --git a/apps/book-server/books/base/next-env.d.ts b/apps/book-server/books/base/next-env.d.ts new file mode 100644 index 00000000..4f11a03d --- /dev/null +++ b/apps/book-server/books/base/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/book-server/books/base/package.json b/apps/book-server/books/base/package.json new file mode 100644 index 00000000..b4d5acb8 --- /dev/null +++ b/apps/book-server/books/base/package.json @@ -0,0 +1,25 @@ +{ + "name": "@packages/nextra", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "14.2.5", + "nextra": "2.13.4", + "nextra-theme-docs": "2.13.4", + "prettier": "3.2.5", + "react": "18", + "react-dom": "18" + }, + "devDependencies": { + "@types/react": "18", + "@types/react-dom": "18", + "typescript": "5" + } +} diff --git a/packages/velog-scripts/data/.gitkeep b/apps/book-server/books/base/pages/.gitkeep similarity index 100% rename from packages/velog-scripts/data/.gitkeep rename to apps/book-server/books/base/pages/.gitkeep diff --git a/apps/book-server/books/base/public/favicon.ico b/apps/book-server/books/base/public/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/apps/book-server/books/base/public/favicon.ico differ diff --git a/apps/book-server/codegen.mts b/apps/book-server/codegen.mts new file mode 100644 index 00000000..19f1383d --- /dev/null +++ b/apps/book-server/codegen.mts @@ -0,0 +1,45 @@ +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + overwrite: true, + schema: 'src/graphql/*.gql', + documents: undefined, + hooks: { + afterOneFileWrite: ['prettier --write'], + }, + emitLegacyCommonJSImports: false, + generates: { + 'src/graphql/generated.ts': { + plugins: [ + 'typescript', + 'typescript-resolvers', + { + add: { + content: `/* eslint-disable @typescript-eslint/ban-types */`, + }, + }, + ], + config: { + enumsAsTypes: true, + skipTypename: true, + contextType: '../common/interfaces/graphql.mjs#GraphQLContext', + mappers: { + Book: '@packages/database/velog-book-mongo#Book as BookModel', + Writer: '@packages/database/velog-book-mongo#Writer as WriterModel', + Page: '@packages/database/velog-book-mongo#Page as PageModel', + }, + inputMaybeValue: 'T | null', + maybeValue: 'T | null', + scalars: { + Date: 'Date', + JSON: 'Record', + ID: 'string', + Void: 'void', + PositiveInt: 'number', + }, + }, + }, + }, +} + +export default config diff --git a/apps/book-server/docker-compose.sh b/apps/book-server/docker-compose.sh new file mode 100644 index 00000000..c8c5724b --- /dev/null +++ b/apps/book-server/docker-compose.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +if [ -z "$AWS_PROFILE" ]; then + echo "AWS_PROFILE이 설정되어 있지 않습니다." + exit 1 +fi + +# AWS 프로필 설정 읽기 +export AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id --profile $AWS_PROFILE) +export AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key --profile $AWS_PROFILE) +export TURBO_TEAM=$(aws configure get turbo_team --profile $AWS_PROFILE) +export TURBO_TOKEN=$(aws configure get turbo_token --profile $AWS_PROFILE) +export TURBO_REMOTE_CACHE_SIGNATURE_KEY=$(aws configure get turbo_remote_cache_signature_key --profile $AWS_PROFILE) + +# echo "AWS 프로필 설정이 완료되었습니다. $AWS_PROFILE 프로필을 사용합니다." +# echo "AWS_ACCESSKEY_ID: $AWS_ACCESS_KEY_ID" +# echo "AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY" +# echo "TURBO_TEAM: $TURBO_TEAM" +# echo "TURBO_TOKEN: $TURBO_TOKEN" +# echo "TURBO_REMOTE_CACHE_SIGNATURE_KEY: $TURBO_REMOTE_CACHE_SIGNATURE_KEY" + +docker-compose up --build \ No newline at end of file diff --git a/apps/book-server/env/.env.example b/apps/book-server/env/.env.example new file mode 100644 index 00000000..f0bb9aec --- /dev/null +++ b/apps/book-server/env/.env.example @@ -0,0 +1,17 @@ +PORT= +MONGO_URL= +REDIS_HOST= +REDIS_PORT= +DISCORD_BOT_TOKEN= +DISCORD_ERROR_CHANNEL= +DISCORD_IMAGE_CHANNEL= +VELOG_BOOK_API_KEY= +BOOK_BUCKET_URL= +BOOK_BUCKET_NAME= +COOKIE_SECRET_KEY= +JWT_SECRET_KEY= +CLOUDFLARE_R2_ACCOUNT_ID= +CLOUDFLARE_R2_TOKEN_NAME= +CLOUDFLARE_R2_ACCESS_KEY_ID= +CLOUDFLARE_R2_SECRET_ACCESS_KEY= +CLOUDFLARE_R2_CDN= \ No newline at end of file diff --git a/apps/book-server/eslint.config.js b/apps/book-server/eslint.config.js new file mode 100644 index 00000000..f98b9e81 --- /dev/null +++ b/apps/book-server/eslint.config.js @@ -0,0 +1,12 @@ +import baseConfig from '@packages/eslint-config/base.mjs' +import { resolve } from 'node:path' + +const projectPath = resolve(process.cwd()) + +/** @type {Linter.Config} */ +export default [ + ...baseConfig(projectPath), + { + ignores: ['node_modules', 'books/*', 'dist'], + }, +] diff --git a/apps/book-server/package.json b/apps/book-server/package.json new file mode 100644 index 00000000..c59c9ac6 --- /dev/null +++ b/apps/book-server/package.json @@ -0,0 +1,87 @@ +{ + "name": "book-server", + "version": "1.0.0", + "description": "", + "author": { + "name": "velopert", + "email": "public.velopert@gmail.com" + }, + "main": "./src/main.mts", + "type": "module", + "scripts": { + "dev": "nodemon --watch './**/*.mts' --exec 'node --import @swc-node/register/esm-register' ./src/main.mts | pino-pretty", + "start": "node --import @swc-node/register/esm-register dist/src/main.mjs", + "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json", + "lint": "eslint --fix", + "codegen": "graphql-codegen-esm --config codegen.mts", + "create-mock": "NODE_ENV=development dotenv -e ./env/.env.development -- tsx ./scripts/createMock.mts", + "create-service": "tsx ./scripts/createService.mts", + "ssm": "tsx ./scripts/ssm.mts" + }, + "keywords": [ + "velog", + "velog-book" + ], + "license": "MIT", + "dependencies": { + "@fastify/cookie": "^9.3.1", + "@fastify/cors": "^8.3.0", + "@fastify/mongodb": "^8.0.0", + "@fastify/websocket": "^10.0.1", + "@graphql-tools/utils": "^10.2.1", + "@packages/commonjs": "workspace:*", + "@packages/database": "workspace:*", + "@packages/library": "workspace:*", + "@prisma/client": "^5.17.0", + "date-fns": "^2.30.0", + "discord.js": "^14.14.1", + "dotenv": "^16.4.5", + "fastify": "^4.26.2", + "fastify-multer": "^2.0.3", + "fastify-plugin": "^4.5.1", + "fs-extra": "^11.2.0", + "graphql": "^16.7.1", + "graphql-scalars": "^1.22.2", + "graphql-tools": "^9.0.1", + "ioredis": "^5.3.2", + "json-schema-to-ts": "^3.0.1", + "mercurius": "^14.0.0", + "mime": "^4.0.1", + "mqemitter-redis": "^5.0.0", + "nanoid": "^5.0.7", + "next": "^14.2.5", + "nextra": "^2.13.4", + "nextra-theme-docs": "^2.13.4", + "prisma": "^5.17.0", + "react": "^18.2.0", + "react-dom": "18.2.0", + "reflect-metadata": "^0.1.13", + "tsc-alias": "^1.8.7", + "tsyringe": "^4.7.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@faker-js/faker": "^8.3.1", + "@graphql-codegen/add": "^5.0.0", + "@graphql-codegen/cli": "^5.0.2", + "@graphql-codegen/typescript": "^4.0.1", + "@graphql-codegen/typescript-operations": "^4.2.0", + "@graphql-codegen/typescript-resolvers": "^4.0.0", + "@packages/eslint-config": "workspace:*", + "@packages/scripts": "workspace:*", + "@packages/tsconfig": "workspace:*", + "@swc-node/register": "^1.9.1", + "@swc/cli": "^0.3.10", + "@swc/core": "^1.5.0", + "@types/fs-extra": "^11.0.4", + "@types/inquirer": "^9.0.7", + "@types/jest": "^29.5.12", + "dotenv-cli": "^7.2.1", + "inquirer": "^9.2.23", + "jest": "^29.6.4", + "nodemon": "^2.0.22", + "pino-pretty": "^10.0.0", + "prettier": "^3.2.5", + "tsx": "^4.6.2" + } +} \ No newline at end of file diff --git a/apps/book-server/scripts/createMock.mts b/apps/book-server/scripts/createMock.mts new file mode 100644 index 00000000..9ce82a1f --- /dev/null +++ b/apps/book-server/scripts/createMock.mts @@ -0,0 +1,180 @@ +import 'reflect-metadata' +import { MongoService } from '@lib/mongo/MongoService.mjs' +import { getMockPages } from 'test/mock/mockBook.mjs' +import { Writer } from '@packages/database/velog-book-mongo' +import { faker } from '@faker-js/faker' +import { UtilsService } from '@lib/utils/UtilsService.mjs' +import { PageService } from '@services/PageService/index.mjs' +import { BookService } from '@services/BookService/index.mjs' +import { RedisService } from '@lib/redis/RedisService.mjs' +import { WriterService } from '@services/WriterService/index.mjs' +import { JwtService } from '@lib/jwt/JwtService.mjs' + +class Seeder { + constructor( + private readonly mongo: MongoService, + private readonly utils: UtilsService, + private readonly pageService: PageService, + ) {} + async createWriter() { + const exists = await this.mongo.writer.findFirst({}) + + if (exists) { + return exists + } + + const username = 'test_carrick' + return this.mongo.writer.create({ + data: { + fk_user_id: '6152b185-2efb-4d79-88f9-b9cfa96cbb9a', + username, + email: 'test@velog.com', + short_bio: 'Hello, I am test writer', + display_name: 'carrick', + thumbnail: faker.image.dataUri(), + }, + }) + } + + public async createBook(writer: Writer) { + const mockPages = getMockPages(100).map((page) => ({ + ...page, + fk_writer_id: writer.id, + })) + + const title = 'Learning bunJS is Fun!' + const escpaedTitle = this.utils.escapeForUrl(title).toLowerCase() + const url_slug = `/@${writer.username}/${escpaedTitle}` + + const book = await this.mongo.book.create({ + data: { + fk_writer_id: writer.id, + title: title, + thumbnail: faker.image.dataUri(), + description: faker.lorem.paragraph(2), + url_slug, + pages: { + createMany: { + data: mockPages, + }, + }, + }, + }) + + const pages = await this.mongo.page.findMany({ + where: { + fk_book_id: book.id, + }, + }) + const topLevel = pages + .slice(0, 10) + .filter((_, index) => index !== 0 && index !== 2 && index !== 6) + + const secondLevel = pages.slice(10, 50) + + for (let i = 11; i < 51; i++) { + const parentId = topLevel[Math.floor(Math.random() * topLevel.length)].id + const selected = pages[i] + + await this.mongo.page.update({ + where: { + id: selected.id, + }, + data: { + parent_id: parentId, + depth: 2, + }, + }) + + await this.mongo.page.update({ + where: { + id: parentId, + }, + data: { + type: 'folder', + }, + }) + } + + for (let i = 51; i < 101; i++) { + const parentId = secondLevel[Math.floor(Math.random() * secondLevel.length)].id + const selected = pages[i] + + if (!selected) { + continue + } + + await this.mongo.page.update({ + where: { + id: selected.id, + }, + data: { + parent_id: parentId, + depth: 3, + }, + }) + + await this.mongo.page.update({ + where: { + id: parentId, + }, + data: { + type: 'folder', + }, + }) + } + + const targetUrlSlugUpdatePage = await this.mongo.page.findMany({ + where: { + parent_id: null, + type: 'folder', + }, + }) + + for (const topLevelPage of targetUrlSlugUpdatePage) { + await this.pageService.updatePageAndChildrenUrlSlug({ + pageId: topLevelPage.id, + signedWriterId: writer.id, + }) + } + } +} + +const main = async () => { + const mongo = new MongoService() + const utils = new UtilsService() + const redis = new RedisService() + const jwt = new JwtService() + + const writerService = new WriterService(jwt, mongo, redis) + const bookService = new BookService(mongo, redis, writerService) + const pageService = new PageService(mongo, utils, bookService) + const seeder = new Seeder(mongo, utils, pageService) + + try { + const writer = await seeder.createWriter() + await seeder.createBook(writer) + } catch (error) { + console.log('create mock error:', error) + } +} + +function checkAppEnv() { + if (process.env.NODE_ENV !== 'development') { + throw Error('Only Allow development environment') + } +} + +function checkDatabaseUrl() { + if (!process.env.MONGO_URL) { + throw new Error('Not found mongo url env') + } + + if (!process.env.MONGO_URL.includes('localhost')) { + throw new Error('Database host must be localhost') + } +} + +checkAppEnv() +checkDatabaseUrl() +await main() diff --git a/apps/book-server/scripts/createService.mts b/apps/book-server/scripts/createService.mts new file mode 100644 index 00000000..8b0f431e --- /dev/null +++ b/apps/book-server/scripts/createService.mts @@ -0,0 +1,6 @@ +import { CreateServiceScript } from '@packages/scripts' +import { URL } from 'url' + +const __dirname = new URL('.', import.meta.url).pathname +const createServiceScript = new CreateServiceScript({ __dirname }) +createServiceScript.excute() diff --git a/apps/book-server/scripts/ssm.mts b/apps/book-server/scripts/ssm.mts new file mode 100644 index 00000000..58c0dcf1 --- /dev/null +++ b/apps/book-server/scripts/ssm.mts @@ -0,0 +1,4 @@ +import { SSMScript } from '@packages/scripts' + +const ssmScript = new SSMScript({ packageName: 'book-server' }) +ssmScript.execute() diff --git a/apps/book-server/src/app.mts b/apps/book-server/src/app.mts new file mode 100644 index 00000000..b113cc3c --- /dev/null +++ b/apps/book-server/src/app.mts @@ -0,0 +1,28 @@ +import cookie from '@fastify/cookie' +import Fastify from 'fastify' +import multer from 'fastify-multer' +import corsPlugin from '@plugins/global/corsPlugin.mjs' +import ipaddrPlugin from '@plugins/global/ipaddrPlugin.mjs' +import mercuriusPlugin from '@plugins/global/mercuriusPlugin.mjs' +import authPlugin from '@plugins/global/authPlugin.mjs' +import errorHandlerPlugin from '@plugins/global/errorHandlerPlugin.mjs' +import routes from '@routes/index.mjs' + +import { ENV } from '@env' + +const app = Fastify({ + logger: true, + trustProxy: true, +}) + +app.register(cookie, { secret: ENV.cookieSecretKey }) +app.register(corsPlugin) +app.register(ipaddrPlugin) +app.register(authPlugin) +app.register(mercuriusPlugin) +app.register(multer.contentParser) +app.register(errorHandlerPlugin) + +app.register(routes) + +export default app diff --git a/packages/velog-server/src/common/constants/HttpStatusConstants.ts b/apps/book-server/src/common/constants/HttpStatusConstants.mts similarity index 100% rename from packages/velog-server/src/common/constants/HttpStatusConstants.ts rename to apps/book-server/src/common/constants/HttpStatusConstants.mts diff --git a/packages/velog-server/src/common/constants/HttpStatusMesageConstants.ts b/apps/book-server/src/common/constants/HttpStatusMesageConstants.mts similarity index 100% rename from packages/velog-server/src/common/constants/HttpStatusMesageConstants.ts rename to apps/book-server/src/common/constants/HttpStatusMesageConstants.mts diff --git a/packages/velog-server/src/common/constants/TimeConstants.ts b/apps/book-server/src/common/constants/TimeConstants.mts similarity index 100% rename from packages/velog-server/src/common/constants/TimeConstants.ts rename to apps/book-server/src/common/constants/TimeConstants.mts diff --git a/packages/velog-server/src/common/errors/BadRequestErrors.ts b/apps/book-server/src/common/errors/BadRequestErrors.mts similarity index 75% rename from packages/velog-server/src/common/errors/BadRequestErrors.ts rename to apps/book-server/src/common/errors/BadRequestErrors.mts index 94fe7b63..80549542 100644 --- a/packages/velog-server/src/common/errors/BadRequestErrors.ts +++ b/apps/book-server/src/common/errors/BadRequestErrors.mts @@ -1,4 +1,4 @@ -import { HttpError } from './HttpError.js' +import { HttpError } from './HttpError.mjs' export class BadRequestError extends HttpError { constructor(message = 'BAD_REQUEST') { diff --git a/packages/velog-server/src/common/errors/ConfilctError.ts b/apps/book-server/src/common/errors/ConfilctError.mts similarity index 74% rename from packages/velog-server/src/common/errors/ConfilctError.ts rename to apps/book-server/src/common/errors/ConfilctError.mts index 44c4533a..abfec997 100644 --- a/packages/velog-server/src/common/errors/ConfilctError.ts +++ b/apps/book-server/src/common/errors/ConfilctError.mts @@ -1,4 +1,4 @@ -import { HttpError } from './HttpError.js' +import { HttpError } from './HttpError.mjs' export class ConfilctError extends HttpError { constructor(message = 'CONFILCT') { diff --git a/packages/velog-server/src/common/errors/ForbiddenError.ts b/apps/book-server/src/common/errors/ForbiddenError.mts similarity index 74% rename from packages/velog-server/src/common/errors/ForbiddenError.ts rename to apps/book-server/src/common/errors/ForbiddenError.mts index f66a7412..a652b8e5 100644 --- a/packages/velog-server/src/common/errors/ForbiddenError.ts +++ b/apps/book-server/src/common/errors/ForbiddenError.mts @@ -1,4 +1,4 @@ -import { HttpError } from './HttpError.js' +import { HttpError } from './HttpError.mjs' export class ForbiddenError extends HttpError { constructor(message = 'FORBIDDEN') { diff --git a/packages/velog-cron/src/common/errors/HttpError.ts b/apps/book-server/src/common/errors/HttpError.mts similarity index 100% rename from packages/velog-cron/src/common/errors/HttpError.ts rename to apps/book-server/src/common/errors/HttpError.mts diff --git a/apps/book-server/src/common/errors/InternalServerError.mts b/apps/book-server/src/common/errors/InternalServerError.mts new file mode 100644 index 00000000..9b91bd9f --- /dev/null +++ b/apps/book-server/src/common/errors/InternalServerError.mts @@ -0,0 +1,7 @@ +import { HttpError } from './HttpError.mjs' + +export class InternalServerError extends HttpError { + constructor(message = 'INTERNAL_SERVER_ERROR') { + super('internal server error', message, 500) + } +} diff --git a/packages/velog-server/src/common/errors/NotfoundError.ts b/apps/book-server/src/common/errors/NotFoundError.mts similarity index 74% rename from packages/velog-server/src/common/errors/NotfoundError.ts rename to apps/book-server/src/common/errors/NotFoundError.mts index 10d1cafd..5ccd7772 100644 --- a/packages/velog-server/src/common/errors/NotfoundError.ts +++ b/apps/book-server/src/common/errors/NotFoundError.mts @@ -1,4 +1,4 @@ -import { HttpError } from './HttpError.js' +import { HttpError } from './HttpError.mjs' export class NotFoundError extends HttpError { constructor(message = 'NOT_FOUND') { diff --git a/packages/velog-server/src/common/errors/UnauthorizedError.ts b/apps/book-server/src/common/errors/UnauthorizedError.mts similarity index 76% rename from packages/velog-server/src/common/errors/UnauthorizedError.ts rename to apps/book-server/src/common/errors/UnauthorizedError.mts index 518df049..e74b181d 100644 --- a/packages/velog-server/src/common/errors/UnauthorizedError.ts +++ b/apps/book-server/src/common/errors/UnauthorizedError.mts @@ -1,4 +1,4 @@ -import { HttpError } from './HttpError.js' +import { HttpError } from './HttpError.mjs' export class UnauthorizedError extends HttpError { constructor(message = 'Unauthorized') { diff --git a/apps/book-server/src/common/errors/index.mts b/apps/book-server/src/common/errors/index.mts new file mode 100644 index 00000000..479d1fa9 --- /dev/null +++ b/apps/book-server/src/common/errors/index.mts @@ -0,0 +1,7 @@ +export { BadRequestError } from './BadRequestErrors.mjs' +export { ConfilctError } from './ConfilctError.mjs' +export { ForbiddenError } from './ForbiddenError.mjs' +export { HttpError, isHttpError } from './HttpError.mjs' +export { NotFoundError } from './NotFoundError.mjs' +export { UnauthorizedError } from './UnauthorizedError.mjs' +export { InternalServerError } from './InternalServerError.mjs' diff --git a/apps/book-server/src/common/interfaces/graphql.mts b/apps/book-server/src/common/interfaces/graphql.mts new file mode 100644 index 00000000..1fc42d14 --- /dev/null +++ b/apps/book-server/src/common/interfaces/graphql.mts @@ -0,0 +1,13 @@ +import type { FastifyRequest, FastifyReply } from 'fastify' +import { PubSub } from 'mercurius' + +export type GraphQLContext = GraphQLContextBase & { + pubsub: PubSub +} + +export type GraphQLContextBase = { + request: FastifyRequest + reply: FastifyReply + ip: string | null + writer: { id: string } | null +} diff --git a/apps/book-server/src/common/plugins/encapsulated/authGuardPlugin.mts b/apps/book-server/src/common/plugins/encapsulated/authGuardPlugin.mts new file mode 100644 index 00000000..5660663b --- /dev/null +++ b/apps/book-server/src/common/plugins/encapsulated/authGuardPlugin.mts @@ -0,0 +1,13 @@ +import { UnauthorizedError } from '@errors/UnauthorizedError.mjs' +import { FastifyPluginAsync } from 'fastify' +import fp from 'fastify-plugin' + +const authGuardPlugin: FastifyPluginAsync = async (fastify) => { + fastify.addHook('preHandler', async (request) => { + if (!request.writer) { + throw new UnauthorizedError('Not logged in') + } + }) +} + +export default fp(authGuardPlugin) diff --git a/apps/book-server/src/common/plugins/global/authPlugin.mts b/apps/book-server/src/common/plugins/global/authPlugin.mts new file mode 100644 index 00000000..f603f6cc --- /dev/null +++ b/apps/book-server/src/common/plugins/global/authPlugin.mts @@ -0,0 +1,60 @@ +import fp from 'fastify-plugin' +import { FastifyPluginAsync } from 'fastify' +import { WriterService } from '@services/WriterService/index.mjs' +import { container } from 'tsyringe' +import { CookieService } from '@lib/cookie/CookieService.mjs' +import { JwtService } from '@lib/jwt/JwtService.mjs' +import type { AccessTokenData } from '@packages/library/jwt' +import { Time } from '@constants/TimeConstants.mjs' + +const authPlugin: FastifyPluginAsync = async (fastify) => { + fastify.decorateRequest('writer', null) + fastify.addHook('onRequest', async (request, reply) => { + if (request.url.includes('/auth/logout')) return + + const cookie = container.resolve(CookieService) + try { + const writerService = container.resolve(WriterService) + const jwt = container.resolve(JwtService) + const cookie = container.resolve(CookieService) + + let accessToken: string | undefined = request.cookies['access_token'] + const refreshToken: string | undefined = request.cookies['refresh_token'] + + const authorization = request.headers['authorization'] + + if (!accessToken && !!authorization && typeof authorization === 'string') { + accessToken = authorization.split('Bearer ')[1] + } + + if (!accessToken && !refreshToken) return + + if (accessToken || refreshToken) { + const accessTokenData = await jwt.decodeToken(accessToken || refreshToken!) + const { exists, writerId } = await writerService.checkExistsWriter(accessTokenData.user_id) + + if (!exists) { + // TODO: automatic register to writer + throw new Error('Writer not found') + } + + request.writer = { id: writerId } + } + + if (!accessToken && refreshToken) { + const { token, writer } = await writerService.restoreAccessToken({ request, reply }) + cookie.setCookie(reply, 'access_token', token, { + maxAge: Time.ONE_DAY_IN_S, + }) + request.writer = { id: writer.id } + return + } + } catch (error) { + console.log('auth Plugin error', error) + cookie.clearCookie(reply, 'access_token') + cookie.clearCookie(reply, 'refresh_token') + } + }) +} + +export default fp(authPlugin) diff --git a/apps/book-server/src/common/plugins/global/corsPlugin.mts b/apps/book-server/src/common/plugins/global/corsPlugin.mts new file mode 100644 index 00000000..d1530dfc --- /dev/null +++ b/apps/book-server/src/common/plugins/global/corsPlugin.mts @@ -0,0 +1,38 @@ +import fp from 'fastify-plugin' +import { FastifyPluginAsync } from 'fastify' +import cors from '@fastify/cors' +import { ForbiddenError } from '@errors/ForbiddenError.mjs' +import { ENV } from '@env' + +export const getWhiteList = (): RegExp[] => { + const whiteList: RegExp[] = [ + /^https:\/\/velog.io$/, + /^https:\/\/(.*).velog.io$/, + /^https:\/\/(.*\.velog\.io)$/, + /https:\/\/(.*)--velog.netlify.com/, + /https:\/\/velog.graphcdn.app/, + ] + + if (ENV.appEnv === 'development') { + whiteList.push(/^http:\/\/localhost/) + } + + return whiteList +} + +const corsPlugin: FastifyPluginAsync = async (fastify) => { + const corsWhiteList = getWhiteList() + + await fastify.register(cors, { + credentials: true, + origin: (origin, callback) => { + if (!origin || corsWhiteList.some((re) => re.test(origin))) { + callback(null, true) + } else { + callback(new ForbiddenError('Not allow origin'), false) + } + }, + }) +} + +export default fp(corsPlugin) diff --git a/packages/velog-server/src/common/plugins/global/errorHandlerPlugin.ts b/apps/book-server/src/common/plugins/global/errorHandlerPlugin.mts similarity index 58% rename from packages/velog-server/src/common/plugins/global/errorHandlerPlugin.ts rename to apps/book-server/src/common/plugins/global/errorHandlerPlugin.mts index 9f2211bc..0c1abb2b 100644 --- a/packages/velog-server/src/common/plugins/global/errorHandlerPlugin.ts +++ b/apps/book-server/src/common/plugins/global/errorHandlerPlugin.mts @@ -1,8 +1,9 @@ import { ENV } from '@env' -import { isHttpError } from '@errors/HttpError.js' -import { DiscordService } from '@lib/discord/DiscordService.js' +import { isHttpError } from '@errors/HttpError.mjs' +import { DiscordService } from '@lib/discord/DiscordService.mjs' import { FastifyPluginCallback } from 'fastify' import { container } from 'tsyringe' +import fp from 'fastify-plugin' const errorHandlerPlugin: FastifyPluginCallback = (fastify, _, done) => { fastify.addHook('preHandler', function (request, reply, done) { @@ -11,22 +12,29 @@ const errorHandlerPlugin: FastifyPluginCallback = (fastify, _, done) => { } done() }) - fastify.addHook('onError', (request, reply, error) => { + fastify.addHook('onError', (request, reply, error, done) => { request.log.error(error, 'fastify onError') const discord = container.resolve(DiscordService) - discord.sendMessage( - 'error', - JSON.stringify({ - type: 'fastify OnError', - requestbody: request?.body || 'none', - query: request?.query || 'none', - error, - user: request?.user, - }), - ) + discord + .sendMessage( + 'error', + JSON.stringify({ + type: 'fastify OnError', + body: request?.body, + query: request?.query, + error, + writer: request?.writer, + ip: request?.ip, + }), + ) + .catch(console.error) + + done() }) fastify.setErrorHandler((error, request, reply) => { + console.log('!! error', error) if (isHttpError(error)) { + console.log('isHttpError', error) reply.status(error.statusCode).send({ message: error.message, name: error.name, @@ -44,19 +52,23 @@ const errorHandlerPlugin: FastifyPluginCallback = (fastify, _, done) => { request.log.error(error, 'fastify handleError') } else { const discord = container.resolve(DiscordService) - discord.sendMessage( - 'error', - JSON.stringify({ - type: 'fastify handleError', - requestbody: request?.body, - error, - user: request?.user, - }), - ) + discord + .sendMessage( + 'error', + JSON.stringify({ + type: 'fastify handleError', + body: request?.body, + query: request?.query, + error, + writer: request?.writer, + ip: request?.ip, + }), + ) + .catch(console.error) } }) done() } -export default errorHandlerPlugin +export default fp(errorHandlerPlugin) diff --git a/packages/velog-server/src/common/plugins/global/ipaddrPlugin.ts b/apps/book-server/src/common/plugins/global/ipaddrPlugin.mts similarity index 91% rename from packages/velog-server/src/common/plugins/global/ipaddrPlugin.ts rename to apps/book-server/src/common/plugins/global/ipaddrPlugin.mts index 2e365bce..baec2f49 100644 --- a/packages/velog-server/src/common/plugins/global/ipaddrPlugin.ts +++ b/apps/book-server/src/common/plugins/global/ipaddrPlugin.mts @@ -1,5 +1,7 @@ +import fp from 'fastify-plugin' import type { FastifyPluginAsync } from 'fastify' + const ipaddrPlugin: FastifyPluginAsync = async (fastify) => { fastify.decorateRequest('ipaddr', null) fastify.addHook('preHandler', (request, reply, done) => { @@ -16,4 +18,4 @@ const ipaddrPlugin: FastifyPluginAsync = async (fastify) => { }) } -export default ipaddrPlugin +export default fp(ipaddrPlugin) diff --git a/apps/book-server/src/common/plugins/global/mercuriusPlugin.mts b/apps/book-server/src/common/plugins/global/mercuriusPlugin.mts new file mode 100644 index 00000000..4e96f593 --- /dev/null +++ b/apps/book-server/src/common/plugins/global/mercuriusPlugin.mts @@ -0,0 +1,110 @@ +import fp from 'fastify-plugin' +import mercurius from 'mercurius' +import { schema, resolvers } from '@graphql/index.mjs' +import type { FastifyPluginAsync } from 'fastify' +import { isHttpError } from '@errors/HttpError.mjs' +import { container } from 'tsyringe' +import { DiscordService } from '@lib/discord/DiscordService.mjs' +import { MqService } from '@lib/mq/MqService.mjs' +import { GraphQLContextBase } from '@interfaces/graphql.mjs' +import { ENV } from '@env' +import { schemaTransforms } from '@graphql/transformer/index.mjs' +import { getWhiteList } from './corsPlugin.mjs' + +const mercuriusPlugin: FastifyPluginAsync = async (fastify) => { + const mqService = container.resolve(MqService) + + fastify.register(mercurius, { + logLevel: 'error', + schema, + resolvers: resolvers, + graphiql: ENV.appEnv !== 'production', + subscription: { + emitter: mqService.emitter, + verifyClient: (info, next) => { + const whiteList = getWhiteList() + if (info.origin && !whiteList.some((re) => re.test(info.origin))) { + return next(false) + } + next(true) // the connection is allowed + }, + }, + context: (request, reply): GraphQLContextBase => { + return { + request, + reply, + ip: request.ipaddr, + writer: request.writer, + } + }, + schemaTransforms, + errorHandler: (error, request) => { + const { name, message, code, stack, errors, statusCode } = error + const result = { + name, + message, + code, + statusCode, + stack, + errors: errors?.map((error) => ({ + name: error.name, + message: error.message, + })), + } + if (ENV.appEnv === 'development') { + console.log('errorHandler') + request.log.error(request, 'errorHandler') + } else { + const discord = container.resolve(DiscordService) + discord.sendMessage( + 'error', + JSON.stringify({ + type: 'errorHandler', + requestbody: request?.body, + result, + ip: request?.ip, + }), + ) + } + }, + errorFormatter: (execution, ctx) => { + const e = execution.errors?.[0]?.originalError + + if (!isHttpError(e)) { + console.log('send!') + ;(ctx as any).request?.log?.error(execution, 'errorFormatter') + const discord = container.resolve(DiscordService) + discord.sendMessage( + 'error', + JSON.stringify({ + type: 'errorFormat', + requestbody: (ctx as any).request?.body, + execution, + user: (ctx as any).request?.user, + ip: (ctx as any).request?.ip, + }), + ) + + return { statusCode: 500, response: execution } + } + + const errors = execution.errors?.map((error) => + Object.assign(error, { + extensions: { + name: e.name, + message: e.message, + }, + }), + ) + + return { + statusCode: e.statusCode, + response: { + errors, + }, + } + }, + }) +} + +export default fp(mercuriusPlugin) diff --git a/apps/book-server/src/env.mts b/apps/book-server/src/env.mts new file mode 100644 index 00000000..a7457c22 --- /dev/null +++ b/apps/book-server/src/env.mts @@ -0,0 +1,85 @@ +import dotenv from 'dotenv' +import { existsSync } from 'fs' +import { dirname, join } from 'path' +import { fileURLToPath } from 'url' +import { z } from 'zod' + +type DockerEnv = 'development' | 'stage' | 'production' +type AppEnvironment = 'development' | 'production' +type EnvFiles = Record + +function resolveDir(dir: string): string { + const __filename = fileURLToPath(import.meta.url) + const splited = dirname(__filename).split('/src') + const cwd = splited.slice(0, -1).join('/src') + return join(cwd, dir) +} + +const envFiles: EnvFiles = { + development: '.env.development', + production: '.env.production', + stage: '.env.stage', +} + +if (!process.env.DOCKER_ENV && process.env.NODE_ENV !== undefined) { + console.error( + 'Development environment was initiated, despite the absence of the Docker environment.', + ) +} + +const dockerEnv = (process.env.DOCKER_ENV as DockerEnv) || 'development' +const appEnv: AppEnvironment = ['stage', 'production'].includes(dockerEnv) + ? 'production' + : 'development' + +const envFile = envFiles[dockerEnv] +const prefix = './env' + +const configPath = resolveDir(`${prefix}/${envFile}`) + +if (!existsSync(configPath)) { + console.log(`Read target: ${configPath}`) + throw new Error('Not found environment file') +} + +dotenv.config({ path: configPath }) + +const env = z.object({ + dockerEnv: z.enum(['development', 'production', 'stage']), + appEnv: z.enum(['development', 'production']), + port: z.number(), + mongoUrl: z.string(), + discordBotToken: z.string(), + discordErrorChannel: z.string(), + discordImageChannel: z.string(), + redisHost: z.string(), + redisPort: z.number(), + bookBucketName: z.string(), + bookBucketUrl: z.string(), + cookieSecretKey: z.string(), + jwtSecretKey: z.string(), + cloudflareR2AccountId: z.string(), + cloudflareR2AccessKeyId: z.string(), + cloudflareR2SecretAccessKey: z.string(), + cloudflareR2CDN: z.string(), +}) + +export const ENV = env.parse({ + dockerEnv, + appEnv, + port: Number(process.env.PORT), + mongoUrl: process.env.MONGO_URL, + discordBotToken: process.env.DISCORD_BOT_TOKEN, + discordErrorChannel: process.env.DISCORD_ERROR_CHANNEL, + discordImageChannel: process.env.DISCORD_IMAGE_CHANNEL, + redisHost: process.env.REDIS_HOST, + redisPort: Number(process.env.REDIS_PORT), + bookBucketName: process.env.BOOK_BUCKET_NAME, + bookBucketUrl: process.env.BOOK_BUCKET_URL, + cookieSecretKey: process.env.COOKIE_SECRET_KEY, + jwtSecretKey: process.env.JWT_SECRET_KEY, + cloudflareR2AccountId: process.env.CLOUDFLARE_R2_ACCOUNT_ID, + cloudflareR2AccessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID, + cloudflareR2SecretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY, + cloudflareR2CDN: process.env.CLOUDFLARE_R2_CDN, +}) diff --git a/apps/book-server/src/graphql/Base.gql b/apps/book-server/src/graphql/Base.gql new file mode 100644 index 00000000..8f4f43f7 --- /dev/null +++ b/apps/book-server/src/graphql/Base.gql @@ -0,0 +1 @@ +directive @auth on FIELD_DEFINITION diff --git a/apps/book-server/src/graphql/Book.gql b/apps/book-server/src/graphql/Book.gql new file mode 100644 index 00000000..dd2f3a8e --- /dev/null +++ b/apps/book-server/src/graphql/Book.gql @@ -0,0 +1,54 @@ +type Book { + id: ID! + thumbnail: String! + fk_writer_id: String! + is_published: Boolean! + pages: [Page!]! +} + +type Query { + book(input: BookIdInput!): Book + isDeploy(input: IsDeployInput!): Boolean! @auth +} + +type Mutation { + deploy(input: BookUrlSlugInput!): DeployResult! @auth + build(input: BookUrlSlugInput!): BuildResult! @auth +} + +type Subscription { + buildInstalled(input: BookUrlSlugInput!): SubScriptionPayload @auth + buildCompleted(input: BookUrlSlugInput!): SubScriptionPayload @auth + deployCompleted(input: BookUrlSlugInput!): DeployCompletedPayload +} + +input BookIdInput { + book_id: ID! +} + +input BookUrlSlugInput { + book_url_slug: String! +} + +type SubScriptionPayload { + message: String! +} + +type DeployCompletedPayload { + message: String! + published_url: String! +} + +type BuildResult { + result: Boolean! + message: String +} + +type DeployResult { + message: String! + published_url: String +} + +input IsDeployInput { + book_url_slug: String! +} diff --git a/apps/book-server/src/graphql/Page.gql b/apps/book-server/src/graphql/Page.gql new file mode 100644 index 00000000..302024cc --- /dev/null +++ b/apps/book-server/src/graphql/Page.gql @@ -0,0 +1,72 @@ +enum PageType { + page + folder + separator +} + +type Page { + id: ID! + fk_writer_id: ID! + fk_book_id: ID! + title: String! + body: String! + type: String! + parent_id: ID + index: Int! + url_slug: String! + code: String! + depth: Int! + is_deleted: Boolean! + created_at: Date! + updated_at: Date! + childrens: [Page!]! +} + +type Query { + pages(input: GetPagesInput!): [Page!]! @auth + page(input: GetPageInput!): Page @auth +} + +type Mutation { + create(input: CreatePageInput!): Page @auth + reorder(input: ReorderInput!): Void @auth + update(input: UpdatePageInput!): Page @auth + delete(input: DeletePageInput!): Void @auth +} + +input GetPagesInput { + book_url_slug: String! +} + +input GetPageInput { + book_url_slug: String! + page_url_slug: String! +} + +input CreatePageInput { + title: String! + index: Int! + parent_url_slug: String! + book_url_slug: String! + type: PageType! +} + +input ReorderInput { + book_url_slug: String! + target_url_slug: String! + parent_url_slug: String + index: Int! +} + +input UpdatePageInput { + title: String + body: String + is_deleted: Boolean + page_url_slug: String! + book_url_slug: String! +} + +input DeletePageInput { + page_url_slug: String! + book_url_slug: String! +} diff --git a/packages/velog-server/src/graphql/Scalar.gql b/apps/book-server/src/graphql/Scalar.gql similarity index 100% rename from packages/velog-server/src/graphql/Scalar.gql rename to apps/book-server/src/graphql/Scalar.gql diff --git a/apps/book-server/src/graphql/Writer.gql b/apps/book-server/src/graphql/Writer.gql new file mode 100644 index 00000000..c530be1b --- /dev/null +++ b/apps/book-server/src/graphql/Writer.gql @@ -0,0 +1,7 @@ +type Writer { + id: ID! + fk_user_id: String! + username: String! + email: String! + short_bio: String +} diff --git a/apps/book-server/src/graphql/generated.ts b/apps/book-server/src/graphql/generated.ts new file mode 100644 index 00000000..aab7c2ff --- /dev/null +++ b/apps/book-server/src/graphql/generated.ts @@ -0,0 +1,584 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql' +import { + Book as BookModel, + Writer as WriterModel, + Page as PageModel, +} from '@packages/database/velog-book-mongo' +import { GraphQLContext } from '../common/interfaces/graphql.mjs' +export type Maybe = T | null +export type InputMaybe = T | null +export type Exact = { [K in keyof T]: T[K] } +export type MakeOptional = Omit & { [SubKey in K]?: Maybe } +export type MakeMaybe = Omit & { [SubKey in K]: Maybe } +export type MakeEmpty = { + [_ in K]?: never +} +export type Incremental = + | T + | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never } +export type RequireFields = Omit & { [P in K]-?: NonNullable } +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string } + String: { input: string; output: string } + Boolean: { input: boolean; output: boolean } + Int: { input: number; output: number } + Float: { input: number; output: number } + Date: { input: Date; output: Date } + JSON: { input: Record; output: Record } + PositiveInt: { input: number; output: number } + Void: { input: void; output: void } +} + +export type Book = { + fk_writer_id: Scalars['String']['output'] + id: Scalars['ID']['output'] + is_published: Scalars['Boolean']['output'] + pages: Array + thumbnail: Scalars['String']['output'] +} + +export type BookIdInput = { + book_id: Scalars['ID']['input'] +} + +export type BookUrlSlugInput = { + book_url_slug: Scalars['String']['input'] +} + +export type BuildResult = { + message?: Maybe + result: Scalars['Boolean']['output'] +} + +export type CreatePageInput = { + book_url_slug: Scalars['String']['input'] + index: Scalars['Int']['input'] + parent_url_slug: Scalars['String']['input'] + title: Scalars['String']['input'] + type: PageType +} + +export type DeletePageInput = { + book_url_slug: Scalars['String']['input'] + page_url_slug: Scalars['String']['input'] +} + +export type DeployCompletedPayload = { + message: Scalars['String']['output'] + published_url: Scalars['String']['output'] +} + +export type DeployResult = { + message?: Maybe + published_url?: Maybe +} + +export type GetPageInput = { + book_url_slug: Scalars['String']['input'] + page_url_slug: Scalars['String']['input'] +} + +export type GetPagesInput = { + book_url_slug: Scalars['String']['input'] +} + +export type IsDeployInput = { + book_url_slug: Scalars['String']['input'] +} + +export type Mutation = { + build: BuildResult + create?: Maybe + delete?: Maybe + deploy: DeployResult + reorder?: Maybe + update?: Maybe +} + +export type MutationBuildArgs = { + input: BookUrlSlugInput +} + +export type MutationCreateArgs = { + input: CreatePageInput +} + +export type MutationDeleteArgs = { + input: DeletePageInput +} + +export type MutationDeployArgs = { + input: BookUrlSlugInput +} + +export type MutationReorderArgs = { + input: ReorderInput +} + +export type MutationUpdateArgs = { + input: UpdatePageInput +} + +export type Page = { + body: Scalars['String']['output'] + childrens: Array + code: Scalars['String']['output'] + created_at: Scalars['Date']['output'] + depth: Scalars['Int']['output'] + fk_book_id: Scalars['ID']['output'] + fk_writer_id: Scalars['ID']['output'] + id: Scalars['ID']['output'] + index: Scalars['Int']['output'] + is_deleted: Scalars['Boolean']['output'] + parent_id?: Maybe + title: Scalars['String']['output'] + type: Scalars['String']['output'] + updated_at: Scalars['Date']['output'] + url_slug: Scalars['String']['output'] +} + +export type PageType = 'folder' | 'page' | 'separator' + +export type Query = { + book?: Maybe + isDeploy: Scalars['Boolean']['output'] + page?: Maybe + pages: Array +} + +export type QueryBookArgs = { + input: BookIdInput +} + +export type QueryIsDeployArgs = { + input: IsDeployInput +} + +export type QueryPageArgs = { + input: GetPageInput +} + +export type QueryPagesArgs = { + input: GetPagesInput +} + +export type ReorderInput = { + book_url_slug: Scalars['String']['input'] + index: Scalars['Int']['input'] + parent_url_slug?: InputMaybe + target_url_slug: Scalars['String']['input'] +} + +export type SubScriptionPayload = { + message: Scalars['String']['output'] +} + +export type Subscription = { + buildCompleted?: Maybe + buildInstalled?: Maybe + deployCompleted?: Maybe +} + +export type SubscriptionBuildCompletedArgs = { + input: BookUrlSlugInput +} + +export type SubscriptionBuildInstalledArgs = { + input: BookUrlSlugInput +} + +export type SubscriptionDeployCompletedArgs = { + input: BookUrlSlugInput +} + +export type UpdatePageInput = { + body?: InputMaybe + book_url_slug: Scalars['String']['input'] + is_deleted?: InputMaybe + page_url_slug: Scalars['String']['input'] + title?: InputMaybe +} + +export type Writer = { + email: Scalars['String']['output'] + fk_user_id: Scalars['String']['output'] + id: Scalars['ID']['output'] + short_bio?: Maybe + username: Scalars['String']['output'] +} + +export type ResolverTypeWrapper = Promise | T + +export type ResolverWithResolve = { + resolve: ResolverFn +} +export type Resolver = + | ResolverFn + | ResolverWithResolve + +export type ResolverFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo, +) => Promise | TResult + +export type SubscriptionSubscribeFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo, +) => AsyncIterable | Promise> + +export type SubscriptionResolveFn = ( + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo, +) => TResult | Promise + +export interface SubscriptionSubscriberObject< + TResult, + TKey extends string, + TParent, + TContext, + TArgs, +> { + subscribe: SubscriptionSubscribeFn<{ [key in TKey]: TResult }, TParent, TContext, TArgs> + resolve?: SubscriptionResolveFn +} + +export interface SubscriptionResolverObject { + subscribe: SubscriptionSubscribeFn + resolve: SubscriptionResolveFn +} + +export type SubscriptionObject = + | SubscriptionSubscriberObject + | SubscriptionResolverObject + +export type SubscriptionResolver< + TResult, + TKey extends string, + TParent = {}, + TContext = {}, + TArgs = {}, +> = + | ((...args: any[]) => SubscriptionObject) + | SubscriptionObject + +export type TypeResolveFn = ( + parent: TParent, + context: TContext, + info: GraphQLResolveInfo, +) => Maybe | Promise> + +export type IsTypeOfResolverFn = ( + obj: T, + context: TContext, + info: GraphQLResolveInfo, +) => boolean | Promise + +export type NextResolverFn = () => Promise + +export type DirectiveResolverFn = ( + next: NextResolverFn, + parent: TParent, + args: TArgs, + context: TContext, + info: GraphQLResolveInfo, +) => TResult | Promise + +/** Mapping between all available schema types and the resolvers types */ +export type ResolversTypes = { + Book: ResolverTypeWrapper + BookIdInput: BookIdInput + BookUrlSlugInput: BookUrlSlugInput + Boolean: ResolverTypeWrapper + BuildResult: ResolverTypeWrapper + CreatePageInput: CreatePageInput + Date: ResolverTypeWrapper + DeletePageInput: DeletePageInput + DeployCompletedPayload: ResolverTypeWrapper + DeployResult: ResolverTypeWrapper + GetPageInput: GetPageInput + GetPagesInput: GetPagesInput + ID: ResolverTypeWrapper + Int: ResolverTypeWrapper + IsDeployInput: IsDeployInput + JSON: ResolverTypeWrapper + Mutation: ResolverTypeWrapper<{}> + Page: ResolverTypeWrapper + PageType: PageType + PositiveInt: ResolverTypeWrapper + Query: ResolverTypeWrapper<{}> + ReorderInput: ReorderInput + String: ResolverTypeWrapper + SubScriptionPayload: ResolverTypeWrapper + Subscription: ResolverTypeWrapper<{}> + UpdatePageInput: UpdatePageInput + Void: ResolverTypeWrapper + Writer: ResolverTypeWrapper +} + +/** Mapping between all available schema types and the resolvers parents */ +export type ResolversParentTypes = { + Book: BookModel + BookIdInput: BookIdInput + BookUrlSlugInput: BookUrlSlugInput + Boolean: Scalars['Boolean']['output'] + BuildResult: BuildResult + CreatePageInput: CreatePageInput + Date: Scalars['Date']['output'] + DeletePageInput: DeletePageInput + DeployCompletedPayload: DeployCompletedPayload + DeployResult: DeployResult + GetPageInput: GetPageInput + GetPagesInput: GetPagesInput + ID: Scalars['ID']['output'] + Int: Scalars['Int']['output'] + IsDeployInput: IsDeployInput + JSON: Scalars['JSON']['output'] + Mutation: {} + Page: PageModel + PositiveInt: Scalars['PositiveInt']['output'] + Query: {} + ReorderInput: ReorderInput + String: Scalars['String']['output'] + SubScriptionPayload: SubScriptionPayload + Subscription: {} + UpdatePageInput: UpdatePageInput + Void: Scalars['Void']['output'] + Writer: WriterModel +} + +export type AuthDirectiveArgs = {} + +export type AuthDirectiveResolver< + Result, + Parent, + ContextType = GraphQLContext, + Args = AuthDirectiveArgs, +> = DirectiveResolverFn + +export type BookResolvers< + ContextType = GraphQLContext, + ParentType extends ResolversParentTypes['Book'] = ResolversParentTypes['Book'], +> = { + fk_writer_id?: Resolver + id?: Resolver + is_published?: Resolver + pages?: Resolver, ParentType, ContextType> + thumbnail?: Resolver + __isTypeOf?: IsTypeOfResolverFn +} + +export type BuildResultResolvers< + ContextType = GraphQLContext, + ParentType extends ResolversParentTypes['BuildResult'] = ResolversParentTypes['BuildResult'], +> = { + message?: Resolver, ParentType, ContextType> + result?: Resolver + __isTypeOf?: IsTypeOfResolverFn +} + +export interface DateScalarConfig extends GraphQLScalarTypeConfig { + name: 'Date' +} + +export type DeployCompletedPayloadResolvers< + ContextType = GraphQLContext, + ParentType extends + ResolversParentTypes['DeployCompletedPayload'] = ResolversParentTypes['DeployCompletedPayload'], +> = { + message?: Resolver + published_url?: Resolver + __isTypeOf?: IsTypeOfResolverFn +} + +export type DeployResultResolvers< + ContextType = GraphQLContext, + ParentType extends ResolversParentTypes['DeployResult'] = ResolversParentTypes['DeployResult'], +> = { + message?: Resolver, ParentType, ContextType> + published_url?: Resolver, ParentType, ContextType> + __isTypeOf?: IsTypeOfResolverFn +} + +export interface JsonScalarConfig extends GraphQLScalarTypeConfig { + name: 'JSON' +} + +export type MutationResolvers< + ContextType = GraphQLContext, + ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation'], +> = { + build?: Resolver< + ResolversTypes['BuildResult'], + ParentType, + ContextType, + RequireFields + > + create?: Resolver< + Maybe, + ParentType, + ContextType, + RequireFields + > + delete?: Resolver< + Maybe, + ParentType, + ContextType, + RequireFields + > + deploy?: Resolver< + ResolversTypes['DeployResult'], + ParentType, + ContextType, + RequireFields + > + reorder?: Resolver< + Maybe, + ParentType, + ContextType, + RequireFields + > + update?: Resolver< + Maybe, + ParentType, + ContextType, + RequireFields + > +} + +export type PageResolvers< + ContextType = GraphQLContext, + ParentType extends ResolversParentTypes['Page'] = ResolversParentTypes['Page'], +> = { + body?: Resolver + childrens?: Resolver, ParentType, ContextType> + code?: Resolver + created_at?: Resolver + depth?: Resolver + fk_book_id?: Resolver + fk_writer_id?: Resolver + id?: Resolver + index?: Resolver + is_deleted?: Resolver + parent_id?: Resolver, ParentType, ContextType> + title?: Resolver + type?: Resolver + updated_at?: Resolver + url_slug?: Resolver + __isTypeOf?: IsTypeOfResolverFn +} + +export interface PositiveIntScalarConfig + extends GraphQLScalarTypeConfig { + name: 'PositiveInt' +} + +export type QueryResolvers< + ContextType = GraphQLContext, + ParentType extends ResolversParentTypes['Query'] = ResolversParentTypes['Query'], +> = { + book?: Resolver< + Maybe, + ParentType, + ContextType, + RequireFields + > + isDeploy?: Resolver< + ResolversTypes['Boolean'], + ParentType, + ContextType, + RequireFields + > + page?: Resolver< + Maybe, + ParentType, + ContextType, + RequireFields + > + pages?: Resolver< + Array, + ParentType, + ContextType, + RequireFields + > +} + +export type SubScriptionPayloadResolvers< + ContextType = GraphQLContext, + ParentType extends + ResolversParentTypes['SubScriptionPayload'] = ResolversParentTypes['SubScriptionPayload'], +> = { + message?: Resolver + __isTypeOf?: IsTypeOfResolverFn +} + +export type SubscriptionResolvers< + ContextType = GraphQLContext, + ParentType extends ResolversParentTypes['Subscription'] = ResolversParentTypes['Subscription'], +> = { + buildCompleted?: SubscriptionResolver< + Maybe, + 'buildCompleted', + ParentType, + ContextType, + RequireFields + > + buildInstalled?: SubscriptionResolver< + Maybe, + 'buildInstalled', + ParentType, + ContextType, + RequireFields + > + deployCompleted?: SubscriptionResolver< + Maybe, + 'deployCompleted', + ParentType, + ContextType, + RequireFields + > +} + +export interface VoidScalarConfig extends GraphQLScalarTypeConfig { + name: 'Void' +} + +export type WriterResolvers< + ContextType = GraphQLContext, + ParentType extends ResolversParentTypes['Writer'] = ResolversParentTypes['Writer'], +> = { + email?: Resolver + fk_user_id?: Resolver + id?: Resolver + short_bio?: Resolver, ParentType, ContextType> + username?: Resolver + __isTypeOf?: IsTypeOfResolverFn +} + +export type Resolvers = { + Book?: BookResolvers + BuildResult?: BuildResultResolvers + Date?: GraphQLScalarType + DeployCompletedPayload?: DeployCompletedPayloadResolvers + DeployResult?: DeployResultResolvers + JSON?: GraphQLScalarType + Mutation?: MutationResolvers + Page?: PageResolvers + PositiveInt?: GraphQLScalarType + Query?: QueryResolvers + SubScriptionPayload?: SubScriptionPayloadResolvers + Subscription?: SubscriptionResolvers + Void?: GraphQLScalarType + Writer?: WriterResolvers +} + +export type DirectiveResolvers = { + auth?: AuthDirectiveResolver +} diff --git a/apps/book-server/src/graphql/index.mts b/apps/book-server/src/graphql/index.mts new file mode 100644 index 00000000..1c50bb07 --- /dev/null +++ b/apps/book-server/src/graphql/index.mts @@ -0,0 +1,37 @@ +import { GraphQLFileLoader, loadSchemaSync, mergeResolvers } from '@packages/commonjs' +import { readdirSync } from 'fs' +import { VoidResolver, PositiveIntResolver } from 'graphql-scalars' +import { IResolvers, MercuriusContext } from 'mercurius' +import { basename, dirname, resolve } from 'path' +import { fileURLToPath } from 'url' +import { ENV } from '@env' + +async function resolverAutoLoader(): Promise { + const __filename = fileURLToPath(import.meta.url) + const __dirname = dirname(__filename) + const resolverFolderPath = resolve(__dirname, 'resolvers') + + const promises = readdirSync(resolverFolderPath).map(async (resolverPath) => { + const suffix = ENV.appEnv === 'development' ? '.mts' : '.mjs' + const resolvername = basename(resolverPath, suffix) + const resolver = await import(`./resolvers/${resolvername}.mjs`) + return resolver.default + }) + return await Promise.all(promises) +} + +export const schema = loadSchemaSync(resolve(process.cwd(), 'src/graphql/*.gql'), { + loaders: [new GraphQLFileLoader()], +}) + +const loadedResolver = await resolverAutoLoader() + +export const resolvers = mergeResolvers( + loadedResolver.concat([ + { + // Date: DateTimeISOResolver, + Void: VoidResolver, + PositiveInt: PositiveIntResolver, + }, + ]), +) as IResolvers diff --git a/apps/book-server/src/graphql/resolvers/bookResolvers.mts b/apps/book-server/src/graphql/resolvers/bookResolvers.mts new file mode 100644 index 00000000..59e63453 --- /dev/null +++ b/apps/book-server/src/graphql/resolvers/bookResolvers.mts @@ -0,0 +1,57 @@ +import { Resolvers } from '@graphql/generated.js' +import { MqService } from '@lib/mq/MqService.mjs' +import { BookBuildService } from 'src/services/BookBuildService/index.mjs' +import { BookDeployService } from 'src/services/BookDeployService/index.mjs' +import { BookService } from 'src/services/BookService/index.mjs' +import { container } from 'tsyringe' + +const bookResolvers: Resolvers = { + Query: { + book: async (_, { input }) => { + const bookService = container.resolve(BookService) + return await bookService.getBook(input.book_id) + }, + isDeploy: async (_, { input }, ctx) => { + const bookService = container.resolve(BookService) + return await bookService.isDeploy(input, ctx.writer?.id) + }, + }, + Mutation: { + build: async (_, { input }, ctx) => { + const bookBuildService = container.resolve(BookBuildService) + return await bookBuildService.build(input.book_url_slug, ctx.writer?.id) + }, + deploy: async (_, { input }, ctx) => { + const bookDeployService = container.resolve(BookDeployService) + return await bookDeployService.deploy(input.book_url_slug, ctx.writer?.id) + }, + }, + Subscription: { + buildInstalled: { + subscribe: async (_, { input }, { pubsub }) => { + const mqService = container.resolve(MqService) + const generator = mqService.topicGenerator('buildInstalled') + const topic = generator(input.book_url_slug) + return pubsub.subscribe(topic) + }, + }, + buildCompleted: { + subscribe: async (_, { input }, { pubsub }) => { + const mqService = container.resolve(MqService) + const generator = mqService.topicGenerator('buildCompleted') + const topic = generator(input.book_url_slug) + return pubsub.subscribe(topic) + }, + }, + deployCompleted: { + subscribe: async (_, { input }, { pubsub }) => { + const mqService = container.resolve(MqService) + const generator = mqService.topicGenerator('deployCompleted') + const topic = generator(input.book_url_slug) + return pubsub.subscribe(topic) + }, + }, + }, +} + +export default bookResolvers diff --git a/apps/book-server/src/graphql/resolvers/pageResolvers.mts b/apps/book-server/src/graphql/resolvers/pageResolvers.mts new file mode 100644 index 00000000..692bef06 --- /dev/null +++ b/apps/book-server/src/graphql/resolvers/pageResolvers.mts @@ -0,0 +1,36 @@ +import { Resolvers } from '@graphql/generated.js' +import { PageService } from '@services/PageService/index.mjs' +import { container } from 'tsyringe' + +const pageResolvers: Resolvers = { + Query: { + pages: async (_, { input }, ctx) => { + const pageService = container.resolve(PageService) + return await pageService.getPages(input.book_url_slug, ctx.writer?.id) + }, + page: async (_, { input }, ctx) => { + const pageService = container.resolve(PageService) + return await pageService.getPage(input, ctx.writer?.id) + }, + }, + Mutation: { + create: async (_, { input }, ctx) => { + const pageService = container.resolve(PageService) + return await pageService.create(input, ctx.writer?.id) + }, + reorder: async (_, { input }, ctx) => { + const pageService = container.resolve(PageService) + await pageService.reorder(input, ctx.writer?.id) + }, + update: async (_, { input }, ctx) => { + const pageService = container.resolve(PageService) + return await pageService.update(input, ctx.writer?.id) + }, + delete: async (_, { input }, ctx) => { + const pageService = container.resolve(PageService) + return await pageService.delete(input, ctx.writer?.id) + }, + }, +} + +export default pageResolvers diff --git a/apps/book-server/src/graphql/transformer/authSchemaTransformer.mts b/apps/book-server/src/graphql/transformer/authSchemaTransformer.mts new file mode 100644 index 00000000..4358bfd9 --- /dev/null +++ b/apps/book-server/src/graphql/transformer/authSchemaTransformer.mts @@ -0,0 +1,22 @@ +import { UnauthorizedError } from '@errors/UnauthorizedError.mjs' +import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils' +import { GraphQLContext } from '@interfaces/graphql.mjs' +import { GraphQLSchema } from 'graphql' + +export const authSchemaTransformer = (schema: GraphQLSchema) => + mapSchema(schema, { + [MapperKind.OBJECT_FIELD]: (fieldConfig) => { + const authDirective = getDirective(schema, fieldConfig, 'auth')?.[0] + if (authDirective) { + const { resolve } = fieldConfig + fieldConfig.resolve = function (source, args, context: GraphQLContext, info) { + if (!context.writer?.id) { + throw new UnauthorizedError('Unauthorized') + } + if (!resolve) return undefined + return resolve(source, args, context, info) + } + return fieldConfig + } + }, + }) diff --git a/apps/book-server/src/graphql/transformer/index.mts b/apps/book-server/src/graphql/transformer/index.mts new file mode 100644 index 00000000..a1c60ceb --- /dev/null +++ b/apps/book-server/src/graphql/transformer/index.mts @@ -0,0 +1,3 @@ +import { authSchemaTransformer } from './authSchemaTransformer.mjs' + +export const schemaTransforms = [authSchemaTransformer] diff --git a/apps/book-server/src/lib/awsS3/AwsS3.test.mts b/apps/book-server/src/lib/awsS3/AwsS3.test.mts new file mode 100644 index 00000000..1cf6c250 --- /dev/null +++ b/apps/book-server/src/lib/awsS3/AwsS3.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { AwsS3Service } from './AwsS3Service.mjs' + +describe('AwsS3Service', () => { + const service = container.resolve(AwsS3Service) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/lib/awsS3/AwsS3Service.mts b/apps/book-server/src/lib/awsS3/AwsS3Service.mts new file mode 100644 index 00000000..e452701f --- /dev/null +++ b/apps/book-server/src/lib/awsS3/AwsS3Service.mts @@ -0,0 +1,12 @@ +import { injectable, singleton } from 'tsyringe' +import { AwsS3Service as S3 } from '@packages/library/awsS3' + +interface Service extends AwsS3Service {} + +@injectable() +@singleton() +export class AwsS3Service extends S3 implements Service { + constructor() { + super({ region: 'ap-northeast-2' }) + } +} diff --git a/apps/book-server/src/lib/cloudflare/R2/R2.test.mts b/apps/book-server/src/lib/cloudflare/R2/R2.test.mts new file mode 100644 index 00000000..435def6b --- /dev/null +++ b/apps/book-server/src/lib/cloudflare/R2/R2.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { R2Service } from './R2Service.mjs' + +describe('R2Service', () => { + const service = container.resolve(R2Service) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/lib/cloudflare/R2/R2Service.mts b/apps/book-server/src/lib/cloudflare/R2/R2Service.mts new file mode 100644 index 00000000..d39a53d7 --- /dev/null +++ b/apps/book-server/src/lib/cloudflare/R2/R2Service.mts @@ -0,0 +1,19 @@ +import { injectable, singleton } from 'tsyringe' +import { R2Service as R2 } from '@packages/library/r2' +import { ENV } from '@env' + +interface Service {} + +@injectable() +@singleton() +export class R2Service extends R2 implements Service { + constructor() { + super({ + accountId: ENV.cloudflareR2AccountId, + region: 'APAC', + accessKeyId: ENV.cloudflareR2AccessKeyId, + secretAccessKey: ENV.cloudflareR2SecretAccessKey, + bucketName: 'velog', + }) + } +} diff --git a/apps/book-server/src/lib/cookie/Cookie.test.mts b/apps/book-server/src/lib/cookie/Cookie.test.mts new file mode 100644 index 00000000..257153d6 --- /dev/null +++ b/apps/book-server/src/lib/cookie/Cookie.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { CookieService } from './CookieService.mjs' + +describe('CookieService', () => { + const service = container.resolve(CookieService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/lib/cookie/CookieService.mts b/apps/book-server/src/lib/cookie/CookieService.mts new file mode 100644 index 00000000..19c51e27 --- /dev/null +++ b/apps/book-server/src/lib/cookie/CookieService.mts @@ -0,0 +1,13 @@ +import { injectable, singleton } from 'tsyringe' +import { FastifyCookieService } from '@packages/library/fastifyCookie' +import { ENV } from '@env' + +interface Service {} + +@injectable() +@singleton() +export class CookieService extends FastifyCookieService implements Service { + constructor() { + super({ appEnv: ENV.appEnv }) + } +} diff --git a/apps/book-server/src/lib/discord/Discord.test.mts b/apps/book-server/src/lib/discord/Discord.test.mts new file mode 100644 index 00000000..adc77e41 --- /dev/null +++ b/apps/book-server/src/lib/discord/Discord.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { DiscordService } from './DiscordService.mjs' + +describe('DiscordService', () => { + const service = container.resolve(DiscordService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/lib/discord/DiscordService.mts b/apps/book-server/src/lib/discord/DiscordService.mts new file mode 100644 index 00000000..6530b051 --- /dev/null +++ b/apps/book-server/src/lib/discord/DiscordService.mts @@ -0,0 +1,22 @@ +import { ENV } from '@env' +import { DiscordService as Discord } from '@packages/library/discord' +import { injectable, singleton } from 'tsyringe' + +type Channel = { + error: string + image: string +} + +@injectable() +@singleton() +export class DiscordService extends Discord { + constructor() { + super({ + discordBotToken: ENV.discordBotToken, + channels: { + error: ENV.discordErrorChannel, + image: ENV.discordImageChannel, + }, + }) + } +} diff --git a/apps/book-server/src/lib/file/File.test.mts b/apps/book-server/src/lib/file/File.test.mts new file mode 100644 index 00000000..ac62ea01 --- /dev/null +++ b/apps/book-server/src/lib/file/File.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { FileService } from './FileService.mjs' + +describe('FileService', () => { + const service = container.resolve(FileService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/lib/file/FileService.mts b/apps/book-server/src/lib/file/FileService.mts new file mode 100644 index 00000000..8e0f2daf --- /dev/null +++ b/apps/book-server/src/lib/file/FileService.mts @@ -0,0 +1,19 @@ +import { injectable, singleton } from 'tsyringe' + +interface Service { + generateUploadPath(parmeter: GenerateUploadPathArgs): string +} + +@injectable() +@singleton() +export class FileService implements Service { + public generateUploadPath = ({ id, type, username }: GenerateUploadPathArgs) => { + return `books/${username}/${type}/${id}` + } +} + +type GenerateUploadPathArgs = { + id: string + type: 'book' + username: string +} diff --git a/apps/book-server/src/lib/jwt/Jwt.test.mts b/apps/book-server/src/lib/jwt/Jwt.test.mts new file mode 100644 index 00000000..06629765 --- /dev/null +++ b/apps/book-server/src/lib/jwt/Jwt.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { JwtService } from './JwtService.mjs' + +describe('JwtService', () => { + const service = container.resolve(JwtService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/lib/jwt/JwtService.mts b/apps/book-server/src/lib/jwt/JwtService.mts new file mode 100644 index 00000000..8a0f57e6 --- /dev/null +++ b/apps/book-server/src/lib/jwt/JwtService.mts @@ -0,0 +1,13 @@ +import { injectable, singleton } from 'tsyringe' +import { JwtService as JWT } from '@packages/library/jwt' +import { ENV } from '@env' + +interface Service {} + +@injectable() +@singleton() +export class JwtService extends JWT implements Service { + constructor() { + super({ secretKey: ENV.jwtSecretKey }) + } +} diff --git a/apps/book-server/src/lib/mongo/Mongo.test.mts b/apps/book-server/src/lib/mongo/Mongo.test.mts new file mode 100644 index 00000000..a7fe2776 --- /dev/null +++ b/apps/book-server/src/lib/mongo/Mongo.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { MongoService } from './MongoService.mjs' + +describe('MongoService', () => { + const service = container.resolve(MongoService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/lib/mongo/MongoService.mts b/apps/book-server/src/lib/mongo/MongoService.mts new file mode 100644 index 00000000..bd86614b --- /dev/null +++ b/apps/book-server/src/lib/mongo/MongoService.mts @@ -0,0 +1,11 @@ +import { injectable, singleton } from 'tsyringe' +import { PrismaClient } from '@packages/database/velog-book-mongo' +import { ENV } from '@env' + +@injectable() +@singleton() +export class MongoService extends PrismaClient { + constructor() { + super({ datasourceUrl: ENV.mongoUrl }) + } +} diff --git a/apps/book-server/src/lib/mq/Mq.test.mts b/apps/book-server/src/lib/mq/Mq.test.mts new file mode 100644 index 00000000..90cdc3c4 --- /dev/null +++ b/apps/book-server/src/lib/mq/Mq.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { MqService } from './MqService.mjs' + +describe('MqService', () => { + const service = container.resolve(MqService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/lib/mq/MqService.mts b/apps/book-server/src/lib/mq/MqService.mts new file mode 100644 index 00000000..bcb6983e --- /dev/null +++ b/apps/book-server/src/lib/mq/MqService.mts @@ -0,0 +1,65 @@ +import { injectable, singleton } from 'tsyringe' +import MQEmitterRedis, { MQEmitterOptions } from 'mqemitter-redis' +import { RedisOptions } from 'ioredis' +import { SubscriptionResolvers } from '@graphql/generated.js' +import { ENV } from '@env' + +export type MqOptions = { + host: string + port: number +} + +export type Emitter = MQEmitterRedis.MQEmitterRedis + +@injectable() +@singleton() +export class MqService { + public emitter!: Emitter + constructor() { + if (!this.emitter) { + this.init() + } + } + private init() { + const options: MQEmitterOptions & RedisOptions = { + port: ENV.redisPort, + host: ENV.redisHost, + separator: ':', + } + const emitter: Emitter = (MQEmitterRedis as any)(options) + this.emitter = emitter + } + + public topicGenerator(type: T): (args: any) => string { + const map: TopicMap = { + buildCompleted: (bookUrlSlug: string) => `BOOK_BUILD:completed:${bookUrlSlug}`, + buildInstalled: (bookUrlSlug: string) => `BOOK_BUILD:installed:${bookUrlSlug}`, + deployCompleted: (bookUrlSlug: string) => `BOOK_DEPLOY:completed:${bookUrlSlug}`, + } + const generator = map[type] + if (!generator) { + throw new Error('No topic generator found for type') + } + return generator + } + + public async publish({ topicParameter, payload }: PublishArgs) { + const key = Object.keys(payload)[0] as keyof SubscriptionResolvers + const generator = this.topicGenerator(key) + const topic = generator(topicParameter) + this.emitter.emit({ topic, payload }) + } +} + +type SubscriptionResolverKey = keyof SubscriptionResolvers +type Payload = { + [K in SubscriptionResolverKey]?: { message: string } & { [key in string]: any } +} +type TopicMap = { + [K in SubscriptionResolverKey]: (args: any) => string +} + +type PublishArgs = { + topicParameter: string + payload: Payload +} diff --git a/packages/velog-server/src/lib/redis/Redis.test.ts b/apps/book-server/src/lib/redis/Redis.test.mts similarity index 79% rename from packages/velog-server/src/lib/redis/Redis.test.ts rename to apps/book-server/src/lib/redis/Redis.test.mts index 9aa21807..3590f280 100644 --- a/packages/velog-server/src/lib/redis/Redis.test.ts +++ b/apps/book-server/src/lib/redis/Redis.test.mts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { RedisService } from './RedisService' +import { RedisService } from './RedisService.mjs' describe('RedisService', () => { const service = container.resolve(RedisService) diff --git a/apps/book-server/src/lib/redis/RedisService.mts b/apps/book-server/src/lib/redis/RedisService.mts new file mode 100644 index 00000000..17ce1de1 --- /dev/null +++ b/apps/book-server/src/lib/redis/RedisService.mts @@ -0,0 +1,13 @@ +import { injectable, singleton } from 'tsyringe' +import { RedisService as Redis } from '@packages/database/velog-redis' +import { ENV } from '@env' + +interface Service {} + +@injectable() +@singleton() +export class RedisService extends Redis implements Service { + constructor() { + super({ port: ENV.redisPort, host: ENV.redisHost }) + } +} diff --git a/packages/velog-cron/src/lib/utils/Utils.test.ts b/apps/book-server/src/lib/utils/Utils.test.mts similarity index 79% rename from packages/velog-cron/src/lib/utils/Utils.test.ts rename to apps/book-server/src/lib/utils/Utils.test.mts index d1b64417..6472e9dd 100644 --- a/packages/velog-cron/src/lib/utils/Utils.test.ts +++ b/apps/book-server/src/lib/utils/Utils.test.mts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { UtilsService } from './UtilsService' +import { UtilsService } from './UtilsService.mjs' describe('UtilsService', () => { const service = container.resolve(UtilsService) diff --git a/apps/book-server/src/lib/utils/UtilsService.mts b/apps/book-server/src/lib/utils/UtilsService.mts new file mode 100644 index 00000000..9b85f32d --- /dev/null +++ b/apps/book-server/src/lib/utils/UtilsService.mts @@ -0,0 +1,24 @@ +import { dirname, join } from 'path' +import { injectable, singleton } from 'tsyringe' +import { fileURLToPath } from 'url' +import { UtilsService as Utils } from '@packages/library/utils' + +interface Service { + escapeForUrl(text: string): string + resolveDir(dir: string): string +} + +@injectable() +@singleton() +export class UtilsService extends Utils implements Service { + public resolveDir(dir: string): string { + const __filename = fileURLToPath(import.meta.url) + const splited = dirname(__filename).split('/src') + const cwd = splited.slice(0, -1).join('/src') + return join(cwd, dir) + } + public removeCodeFromUrlSlug(urlSlug: string): string { + if (urlSlug === '') return urlSlug + return urlSlug.split('-').slice(0, -1).join('-').trim() + } +} diff --git a/apps/book-server/src/main.mts b/apps/book-server/src/main.mts new file mode 100644 index 00000000..01f0ee24 --- /dev/null +++ b/apps/book-server/src/main.mts @@ -0,0 +1,23 @@ +import 'reflect-metadata' +import app from './app.mjs' +import { container } from 'tsyringe' +import { MongoService } from '@lib/mongo/MongoService.mjs' +import { DiscordService } from '@lib/discord/DiscordService.mjs' +import { ENV } from '@env' +import { RedisService } from '@lib/redis/RedisService.mjs' + +async function main() { + const mongo = container.resolve(MongoService) + await mongo.$connect() + console.info(`Mongo database connection established to ${ENV.mongoUrl}`) + + const discord = container.resolve(DiscordService) + discord.connection() + + const redis = container.resolve(RedisService) + await redis.connection() + + app.listen({ port: ENV.port, host: '::' }) +} + +main() diff --git a/packages/velog-server/src/routes/files/index.ts b/apps/book-server/src/routes/files/index.mts similarity index 86% rename from packages/velog-server/src/routes/files/index.ts rename to apps/book-server/src/routes/files/index.mts index 2ba8bc48..3a3e7abc 100644 --- a/packages/velog-server/src/routes/files/index.ts +++ b/apps/book-server/src/routes/files/index.mts @@ -1,5 +1,5 @@ import { FastifyPluginCallback } from 'fastify' -import v3 from './v3/index.js' +import v3 from './v3/index.mjs' const filesRoute: FastifyPluginCallback = (fastify, opts, done) => { fastify.register(v3, { prefix: '/v3' }) diff --git a/apps/book-server/src/routes/files/v3/filesController.mts b/apps/book-server/src/routes/files/v3/filesController.mts new file mode 100644 index 00000000..6bc3a3c2 --- /dev/null +++ b/apps/book-server/src/routes/files/v3/filesController.mts @@ -0,0 +1,131 @@ +import { injectable, singleton } from 'tsyringe' +import { UploadBody } from './schema.mjs' +import { UnauthorizedError } from '@errors/UnauthorizedError.mjs' +import { FileService } from '@lib/file/FileService.mjs' +import { File } from 'fastify-multer/lib/interfaces.js' +import { BadRequestError } from '@errors/BadRequestErrors.mjs' +import { HttpError } from '@errors/HttpError.mjs' +import { ForbiddenError } from '@errors/ForbiddenError.mjs' +import { InternalServerError } from '@errors/InternalServerError.mjs' +import { WriterService } from '@services/WriterService/index.mjs' +import { BookService } from '@services/BookService/index.mjs' +import { ImageService } from '@services/ImageService/index.mjs' +import { MongoService } from '@lib/mongo/MongoService.mjs' +import { R2Service } from '@lib/cloudflare/R2/R2Service.mjs' +import { ENV } from '@env' + +interface Controller { + upload({ body, file, signedWriterId }: UploadArgs): Promise +} + +@singleton() +@injectable() +export class FilesController implements Controller { + constructor( + private readonly file: FileService, + private readonly mongo: MongoService, + private readonly r2: R2Service, + private readonly writerService: WriterService, + private readonly bookService: BookService, + private readonly imageService: ImageService, + ) {} + public async upload({ body, file, signedWriterId }: UploadArgs): Promise { + if (!signedWriterId) { + throw new UnauthorizedError('Not logged in') + } + + if (!file) { + throw new BadRequestError('Not found file') + } + + const { ref_id, bookUrlSlug, type } = body + + if (!['book'].includes(type)) { + throw new BadRequestError('Invalid type') + } + + const user = await this.writerService.findById(signedWriterId) + + if (!user) { + throw new UnauthorizedError('Invalid User') + } + + const isAbuse = await this.imageService.detectAbuse(signedWriterId) + + if (isAbuse) { + throw new HttpError('Too many requests', 'is abused user', 429) + } + + if (type === 'book' && !!ref_id) { + const book = await this.bookService.findByUrlSlug(bookUrlSlug) + if (book?.fk_writer_id !== signedWriterId) { + throw new ForbiddenError("Can't access the post") + } + } + + const exists = await this.r2.exists() + + if (!exists) { + throw new InternalServerError('R2 Velog bucket not exists') + } + + const originalFileName = file.originalname + const extension = originalFileName.split('.').pop() + const filename = `image.${extension}` + + const image = await this.mongo.image.create({ + data: { + fk_writer_id: signedWriterId, + filesize: file.size || 0, + type, + ref_id: ref_id ?? null, + }, + }) + + const filepath = this.file + .generateUploadPath({ + type: type, + id: image.id, + username: user.username, + }) + .concat(`/${encodeURIComponent(decodeURI(filename))}`) + + try { + // TODO: upload file to R2 + const result = await this.r2.upload({ + contents: file.buffer!, + destination: filepath, + mimeType: file.mimetype, + }) + + const resultPath = `${ENV.cloudflareR2CDN}/${result.objectKey}` + + await this.mongo.image.update({ + where: { + id: image.id, + }, + data: { + key: result.objectKey, + path: resultPath, + }, + }) + + return { + path: resultPath, + } + } catch (error) { + console.log('Upload file error', error) + throw new InternalServerError() + } + } +} + +type UploadArgs = { + body: UploadBody + file?: File + signedWriterId?: string +} + +type UploadResult = { + path: string +} diff --git a/apps/book-server/src/routes/files/v3/index.mts b/apps/book-server/src/routes/files/v3/index.mts new file mode 100644 index 00000000..632aebbc --- /dev/null +++ b/apps/book-server/src/routes/files/v3/index.mts @@ -0,0 +1,35 @@ +import { FastifyPluginCallback } from 'fastify' +import { container } from 'tsyringe' +import multer from 'fastify-multer' +import { UploadBody } from './schema.mjs' +import { FilesController } from './filesController.mjs' +import authGuardPlugin from '@plugins/encapsulated/authGuardPlugin.mjs' + +const v3: FastifyPluginCallback = (fastify, opts, done) => { + const controller = container.resolve(FilesController) + + const upload = multer({ + limits: { + fileSize: 1024 * 1024 * 30, + }, + }) + + fastify.register(authGuardPlugin) + fastify.post<{ Body: UploadBody }>( + '/upload', + { + preHandler: upload.single('image'), + }, + async (request) => { + return await controller.upload({ + body: request.body, + file: request.file, + signedWriterId: request.writer?.id, + }) + }, + ) + + done() +} + +export default v3 diff --git a/apps/book-server/src/routes/files/v3/schema.mts b/apps/book-server/src/routes/files/v3/schema.mts new file mode 100644 index 00000000..b9680b85 --- /dev/null +++ b/apps/book-server/src/routes/files/v3/schema.mts @@ -0,0 +1,21 @@ +import { FromSchema, asConst } from 'json-schema-to-ts' + +export const uploadBodySchema = asConst({ + type: 'object', + required: ['bookUrlSlug', 'type'], + properties: { + ref_id: { + type: 'string', + }, + type: { + type: 'string', + enum: ['book'], + }, + bookUrlSlug: { + type: 'string', + }, + }, + additionalProperties: false, +}) + +export type UploadBody = FromSchema diff --git a/apps/book-server/src/routes/index.mts b/apps/book-server/src/routes/index.mts new file mode 100644 index 00000000..e81e0aa1 --- /dev/null +++ b/apps/book-server/src/routes/index.mts @@ -0,0 +1,35 @@ +import type { FastifyPluginCallback } from 'fastify' +import { format } from 'date-fns' +import filesRoute from './files/index.mjs' + +const api: FastifyPluginCallback = (fastify, opts, done) => { + fastify.register(filesRoute, { prefix: '/files' }) + + fastify.get('/ping', (request, reply) => { + const now = new Date() + const serverTime = format(now, 'yyyy-MM-dd HH:mm:ss') + reply.send({ serverTime }) + }) + + fastify.get('/check', (_, reply) => { + reply.status(200).send({ version: 'books-api' }) + }) + + done() +} + +const routes: FastifyPluginCallback = (fastify, opts, done) => { + fastify.get('/', async (request) => { + const ip = request.ipaddr + const writer = request.writer + return { writer, ip } + }) + + fastify.register(api, { + prefix: '/api', + }) + + done() +} + +export default routes diff --git a/apps/book-server/src/services/BookBuildService/BookBuildService.test.mts b/apps/book-server/src/services/BookBuildService/BookBuildService.test.mts new file mode 100644 index 00000000..d3654cc8 --- /dev/null +++ b/apps/book-server/src/services/BookBuildService/BookBuildService.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { BookBuildService } from './index.mjs' + +describe('BookBuildService', () => { + const service = container.resolve(BookBuildService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/services/BookBuildService/index.mts b/apps/book-server/src/services/BookBuildService/index.mts new file mode 100644 index 00000000..1eed4d23 --- /dev/null +++ b/apps/book-server/src/services/BookBuildService/index.mts @@ -0,0 +1,247 @@ +import path from 'node:path' +import fs from 'fs-extra' +import { exec as execCb } from 'node:child_process' +import { promisify } from 'node:util' +import { NotFoundError } from '@errors/NotFoundError.mjs' +import { MongoService } from '@lib/mongo/MongoService.mjs' +import { injectable, singleton } from 'tsyringe' +import { ConfilctError } from '@errors/ConfilctError.mjs' +import { themeConfigTemplate } from '@templates/themeConfigTemplate.mjs' +import { MqService } from '@lib/mq/MqService.mjs' +import { nextConfigTempate } from '@templates/nextConfigTemplate.mjs' +import { UnauthorizedError } from '@errors/UnauthorizedError.mjs' +import type { Page } from '@packages/database/velog-book-mongo' +import { BookService } from '@services/BookService/index.mjs' +import { WriterService } from '@services/WriterService/index.mjs' +import { BuildResult } from '@graphql/generated.js' +import { PageService } from '@services/PageService/index.mjs' +import { UtilsService } from '@lib/utils/UtilsService.mjs' +import { RedisService } from '@lib/redis/RedisService.mjs' +import { Time } from '@constants/TimeConstants.mjs' + +const exec = promisify(execCb) + +interface Service { + build(bookId: string, signedWriterId?: string): Promise +} + +@injectable() +@singleton() +export class BookBuildService implements Service { + constructor( + private readonly mongo: MongoService, + private readonly utils: UtilsService, + private readonly mq: MqService, + private readonly redis: RedisService, + private readonly bookService: BookService, + private readonly writerService: WriterService, + private readonly pageService: PageService, + ) {} + public async build(url_slug: string, signedWriterId?: string): Promise { + if (!signedWriterId) { + throw new UnauthorizedError('Not logged in') + } + + const writer = await this.writerService.findById(signedWriterId) + + if (!writer) { + throw new NotFoundError('Not found writer') + } + + const book = await this.bookService.findByUrlSlug(url_slug) + + if (!book) { + throw new NotFoundError('Book not found') + } + + if (book.fk_writer_id !== writer.id) { + throw new ConfilctError('Not owner of book') + } + + const buildKey = this.redis.generateKey.buildBook(book.id) + const isBuilding = await this.redis.exists(buildKey) + + if (isBuilding) { + return { + result: false, + message: 'Already building', + } + } + + try { + await this.redis.set(buildKey, 'deploying', 'EX', Time.ONE_MINUTE_IN_S * 10) + + // create deploy code + const deployCode = this.utils.randomString(10).toLocaleLowerCase() + await this.mongo.book.update({ + where: { + id: book.id, + }, + data: { + deploy_code: deployCode, + }, + }) + + // create folder + const dest = path.resolve(process.cwd(), 'books', book.id) + + // TODO: ADD handle cache from s3 + const pagesDir = `${dest}/pages` + const baseExists = fs.existsSync(dest) + if (!baseExists) { + // Create folder + fs.mkdirSync(dest) + // COPY base file to target folder + const src = path.resolve(process.cwd(), 'books/base') + await fs.copy(src, dest, { dereference: true }) + + const stdout = await this.installDependencies('npm install', dest) + if (stdout) { + this.mq.publish({ + topicParameter: book.url_slug, + payload: { + buildInstalled: { + message: stdout, + }, + }, + }) + } + } else { + fs.rmSync(pagesDir, { recursive: true, force: true }) + await this.utils.sleep(100) + fs.mkdirSync(`${pagesDir}/.gitkeep`, { recursive: true }) + await this.utils.sleep(100) + } + + // json to files + const pages = await this.pageService.getPages(book.url_slug, writer.id) + + // create meta.json + await this.writeMetaJson({ + pages: pages || [], + baseDest: pagesDir, + isRecursive: false, + }) + + fs.writeFileSync( + `${dest}/next.config.mjs`, + nextConfigTempate({ + deployCode: deployCode, + urlSlug: book.url_slug, + }), + ) + + fs.writeFileSync(`${dest}/theme.config.tsx`, themeConfigTemplate({ title: book.title })) + + await exec('pnpm prettier -w .', { cwd: dest }) + const buildStdout = await this.buildTsToJs(dest) + if (buildStdout) { + this.mq.publish({ + topicParameter: book.url_slug, + payload: { + buildCompleted: { message: buildStdout }, + }, + }) + } + + return { + result: true, + } + } finally { + await this.redis.del(buildKey) + } + } + private async installDependencies(command: string, dest: string): Promise { + try { + const { stdout, stderr } = await exec(command, { cwd: dest }) + if (stderr) { + console.error(`stderr: ${stderr}`) + throw new Error(stderr) + } + return stdout + } catch (error) { + throw new Error(`exec error: ${error}`) + } + } + private async buildTsToJs(dest: string) { + try { + const { stdout, stderr } = await exec('pnpm next build', { cwd: dest }) + if (stderr) { + console.error(`stderr: ${stderr}`) + return stderr + } + return stdout + } catch (error) { + console.error(`exec error: ${error}`) + } + } + private async writeMetaJson({ pages: _pages, baseDest, isRecursive }: WriteMetaJsonArgs) { + const pages = this.insertKey(_pages) + const meta = pages.reduce((acc, page, index) => { + if (!isRecursive && index === 0) { + acc['index'] = page.title + return acc + } + + if (page.type === 'page' || page.type === 'folder') { + const key = page.key + acc[key] = page.title + return acc + } + + if (page.type === 'separator') { + const key = page.key + acc[key] = { + id: page.id, + type: 'separator', + title: page.title, + } + return acc + } + + return acc + }, {} as MetaJson) + + fs.writeFileSync(`${baseDest}/_meta.json`, JSON.stringify(meta, null, 2)) + + const promises = pages.map((page, index) => { + const filename = index === 0 && !isRecursive ? 'index' : page.key + const mdxTarget = path.resolve(baseDest, `${filename}.mdx`) + fs.writeFileSync(mdxTarget, page.body) + + if (page.type !== 'separator' && (page as any)?.childrens?.length > 0) { + const folderPath = page.key + const targetPath = `${baseDest}/${folderPath}` + fs.mkdirSync(targetPath) + return this.writeMetaJson({ + pages: (page as any)?.childrens || [], + baseDest: targetPath, + isRecursive: true, + }) + } + }) + + await Promise.all(promises) + return pages + } + private insertKey = (pages: Page[]) => { + return pages.map((page) => ({ + key: `${this.utils.escapeForUrl(page.title)}-${page.code}`.toLocaleLowerCase(), + ...page, + })) + } +} + +type MetaJsonSerpator = { id?: string; type: 'separator' | 'page'; title?: string } +type MetaJsonValue = string | MetaJsonSerpator +type MetaJson = Record + +type WriteMetaJsonArgs = { + pages: PageData[] + baseDest: string + isRecursive: boolean +} + +type PageData = { + childrens: PageData[] +} & Page diff --git a/apps/book-server/src/services/BookDeployService/BookDeployService.test.mts b/apps/book-server/src/services/BookDeployService/BookDeployService.test.mts new file mode 100644 index 00000000..eecd2342 --- /dev/null +++ b/apps/book-server/src/services/BookDeployService/BookDeployService.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { BookDeployService } from './index.mjs' + +describe('BookDeployService', () => { + const service = container.resolve(BookDeployService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/services/BookDeployService/index.mts b/apps/book-server/src/services/BookDeployService/index.mts new file mode 100644 index 00000000..7195ae54 --- /dev/null +++ b/apps/book-server/src/services/BookDeployService/index.mts @@ -0,0 +1,141 @@ +import path from 'node:path' +import fs from 'fs-extra' +import { injectable, singleton } from 'tsyringe' +import { BookService } from '@services/BookService/index.mjs' +import { NotFoundError } from '@errors/NotFoundError.mjs' +import { WriterService } from '@services/WriterService/index.mjs' +import { AwsS3Service } from '@lib/awsS3/AwsS3Service.mjs' +import mime from 'mime' +import { ENV } from '@env' +import { UnauthorizedError } from '@errors/UnauthorizedError.mjs' +import { ConfilctError } from '@errors/ConfilctError.mjs' +import { DeployResult } from '@graphql/generated.js' +import { MongoService } from '@lib/mongo/MongoService.mjs' +import { RedisService } from '@lib/redis/RedisService.mjs' +import { MqService } from '@lib/mq/MqService.mjs' + +interface Service { + deploy: (bookId: string, signedWriterId?: string) => Promise +} +@injectable() +@singleton() +export class BookDeployService implements Service { + constructor( + private readonly mongo: MongoService, + private readonly awsS3: AwsS3Service, + private readonly redis: RedisService, + private readonly mq: MqService, + private readonly writerService: WriterService, + private readonly bookService: BookService, + ) {} + async deploy(url_slug: string, signedWriterId?: string): Promise { + if (!signedWriterId) { + throw new UnauthorizedError('Not logged in') + } + + const writer = await this.writerService.findById(signedWriterId) + + if (!writer) { + console.log('Not found writer') + throw new NotFoundError('Not found writer') + } + + const book = await this.bookService.findByUrlSlug(url_slug) + + if (!book) { + console.log('Not found book') + throw new NotFoundError('Not found book') + } + + if (book.fk_writer_id !== writer.id) { + throw new ConfilctError('Not owner of book') + } + + const deployKey = this.redis.generateKey.deployBook(book.id) + const isDeploying = await this.redis.exists(deployKey) + + if (isDeploying) { + return { + published_url: null, + message: 'Already deploying', + } + } + + try { + const output = path.resolve(process.cwd(), 'books', book.id, 'out') + + // find output + const exists = fs.existsSync(output) + if (!exists) { + console.log('Not found book output') + throw new NotFoundError('Not found book output') + } + + const files = await fs.readdir(output, { recursive: true, encoding: 'utf8' }) + const targetFiles: string[] = files + .map((file) => { + const filePath = path.join(output, file) + const stat = fs.statSync(filePath) + return stat.isFile() ? filePath : '' + }) + .filter(Boolean) + + // upload to S3 + const baseUrl = `${book.url_slug}`.replace('/', '') + + const promises = targetFiles.map(async (filePath) => { + const body = fs.readFileSync(filePath) + let relativePath = filePath.replace(output, '') + const ext = path.extname(relativePath) + if (ext === '.html' && !relativePath.includes('index.html')) { + relativePath = relativePath.replace('.html', '') + } + const key = `${baseUrl}/${book.deploy_code}${relativePath}` + const contentType = mime.getType(filePath) + await this.awsS3.uploadFile({ + bucketName: ENV.bookBucketName, + key, + body: body, + ContentType: contentType ?? 'application/octet-stream', + ACL: 'public-read', + }) + }) + + await Promise.all(promises) + const published_url = `https://books.velog.io/${baseUrl}/${book.deploy_code}` + + setImmediate(async () => { + await this.mongo.book.update({ + where: { + id: book.id, + }, + data: { + published_url, + }, + }) + }) + + console.log('published_urlpublished_url', published_url) + this.mq.publish({ + topicParameter: book.url_slug, + payload: { + deployCompleted: { + message: 'Deploy completed', + published_url, + }, + }, + }) + + return { + published_url, + } + } catch (error) { + console.error(error) + return { + published_url: null, + } + } finally { + await this.redis.del(deployKey) + } + } +} diff --git a/apps/book-server/src/services/BookService/BookService.test.mts b/apps/book-server/src/services/BookService/BookService.test.mts new file mode 100644 index 00000000..9066d828 --- /dev/null +++ b/apps/book-server/src/services/BookService/BookService.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { BookService } from './index.mjs' + +describe('BookService', () => { + const service = container.resolve(BookService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/services/BookService/index.mts b/apps/book-server/src/services/BookService/index.mts new file mode 100644 index 00000000..5e6464fd --- /dev/null +++ b/apps/book-server/src/services/BookService/index.mts @@ -0,0 +1,95 @@ +import { NotFoundError } from '@errors/NotFoundError.mjs' +import { MongoService } from '@lib/mongo/MongoService.mjs' +import { injectable, singleton } from 'tsyringe' +import { BadRequestError } from '@errors/BadRequestErrors.mjs' +import { ConfilctError } from '@errors/ConfilctError.mjs' +import { Book } from '@packages/database/velog-book-mongo' +import { IsDeployInput } from '@graphql/generated.js' +import { UnauthorizedError } from '@errors/UnauthorizedError.mjs' +import { RedisService } from '@lib/redis/RedisService.mjs' +import { WriterService } from '@services/WriterService/index.mjs' + +interface Service { + findById(bookId: string): Promise + findByUrlSlug(urlSlug: string): Promise + getBook(bookId: string, signedUserId?: string): Promise + isDeploy(input: IsDeployInput, signedUserId?: string): Promise +} + +@injectable() +@singleton() +export class BookService implements Service { + constructor( + private readonly mongo: MongoService, + private readonly redis: RedisService, + private readonly writerService: WriterService, + ) {} + public async findById(bookId: string): Promise { + return await this.mongo.book.findUnique({ + where: { + id: bookId, + }, + }) + } + public async findByUrlSlug(urlSlug: string): Promise { + return await this.mongo.book.findUnique({ + where: { + url_slug: urlSlug, + }, + }) + } + public async getBook(bookId: string, signedUserId?: string): Promise { + if (!bookId) { + throw new BadRequestError('Not found book') + } + + const book = await this.mongo.book.findUnique({ + where: { + id: bookId, + }, + }) + + if (!book) { + throw new NotFoundError('Not found book') + } + + if (!book.is_published && book.fk_writer_id !== signedUserId) { + throw new ConfilctError('Not owner of book') + } + + return book + } + + public async isDeploy(input: IsDeployInput, signedWriterId?: string): Promise { + if (!signedWriterId) { + throw new UnauthorizedError('Not logged in') + } + + const writer = await this.writerService.findById(signedWriterId) + + if (!writer) { + throw new NotFoundError('Not found writer') + } + + const { book_url_slug } = input + + const book = await this.findByUrlSlug(book_url_slug) + + if (!book) { + throw new NotFoundError('Not found book') + } + + if (book.fk_writer_id !== writer.id) { + throw new ConfilctError('Not owner of book') + } + + const buildKey = this.redis.generateKey.buildBook(book.id) + const deployKey = this.redis.generateKey.deployBook(book.id) + + const buildResult = await this.redis.exists(buildKey) + const deployResult = await this.redis.exists(deployKey) + + const isDeploy = [buildResult, deployResult].some((v) => v === 1) + return isDeploy + } +} diff --git a/apps/book-server/src/services/ImageService/ImageService.test.mts b/apps/book-server/src/services/ImageService/ImageService.test.mts new file mode 100644 index 00000000..0ff0ad96 --- /dev/null +++ b/apps/book-server/src/services/ImageService/ImageService.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { ImageService } from './index.mjs' + +describe('ImageService', () => { + const service = container.resolve(ImageService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/services/ImageService/index.mts b/apps/book-server/src/services/ImageService/index.mts new file mode 100644 index 00000000..bb671484 --- /dev/null +++ b/apps/book-server/src/services/ImageService/index.mts @@ -0,0 +1,64 @@ +import { Time } from '@constants/TimeConstants.mjs' +import { DiscordService } from '@lib/discord/DiscordService.mjs' +import { MongoService } from '@lib/mongo/MongoService.mjs' +import { WriterService } from '@services/WriterService/index.mjs' +import { injectable, singleton } from 'tsyringe' + +interface Service { + detectAbuse(signedWriterId: string): Promise +} + +@injectable() +@singleton() +export class ImageService implements Service { + constructor( + private readonly mongo: MongoService, + private readonly discord: DiscordService, + private readonly writerService: WriterService, + ) {} + async detectAbuse(writerId: string): Promise { + const oneHourAgo = new Date(Date.now() - Time.ONE_HOUR_IN_MS) + const oneMinuteAgo = new Date(Date.now() - Time.ONE_MINUTE_IN_MS) + + const imageCountLastHour = await this.mongo.image.count({ + where: { + fk_writer_id: writerId, + created_at: { + gt: oneHourAgo, + }, + }, + }) + + const writer = await this.writerService.findById(writerId) + if (!writer) return true + + if (imageCountLastHour > 150) { + const message = `User ${writer.username} (${writerId}) is blocked due to upload abuse.` + this.discord.sendMessage('image', message) + return true + } + + if (imageCountLastHour > 100) { + const message = `User ${writer.username} (${writerId}) has uploaded ${imageCountLastHour} images in the last hour.` + this.discord.sendMessage('image', message) + return true + } + + const imageCountLastMinute = await this.mongo.image.count({ + where: { + fk_writer_id: writerId, + created_at: { + gt: oneMinuteAgo, + }, + }, + }) + + if (imageCountLastMinute >= 20) { + const message = `User ${writer.username} (${writerId}) is blocked due to uploading ${imageCountLastMinute} images in a minute.` + this.discord.sendMessage('image', message) + return true + } + + return false + } +} diff --git a/apps/book-server/src/services/PageService/PageService.test.mts b/apps/book-server/src/services/PageService/PageService.test.mts new file mode 100644 index 00000000..15a6e8f1 --- /dev/null +++ b/apps/book-server/src/services/PageService/PageService.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { PageService } from './index.mjs' + +describe('PageService', () => { + const service = container.resolve(PageService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/book-server/src/services/PageService/index.mts b/apps/book-server/src/services/PageService/index.mts new file mode 100644 index 00000000..0cc944da --- /dev/null +++ b/apps/book-server/src/services/PageService/index.mts @@ -0,0 +1,477 @@ +import { BadRequestError } from '@errors/BadRequestErrors.mjs' +import { ConfilctError } from '@errors/ConfilctError.mjs' +import { NotFoundError } from '@errors/NotFoundError.mjs' +import { UnauthorizedError } from '@errors/UnauthorizedError.mjs' +import type { + CreatePageInput, + DeletePageInput, + GetPageInput, + ReorderInput, + UpdatePageInput, +} from '@graphql/generated.js' +import { MongoService } from '@lib/mongo/MongoService.mjs' +import { UtilsService } from '@lib/utils/UtilsService.mjs' +import { Page, Prisma } from '@packages/database/velog-book-mongo' +import { BookService } from '@services/BookService/index.mjs' +import { injectable, singleton } from 'tsyringe' + +interface Service { + updatePageAndChildrenUrlSlug(args: UpdatePageAndChildrenUrlSlugArgs): Promise + getPages(bookUrlSlug: string, signedWriterId?: string): Promise + getPage(input: GetPageInput, signedWriterId?: string): Promise + create(input: CreatePageInput, signedWriterId?: string): Promise + update(input: UpdatePageInput, signedWriterId?: string): Promise + reorder(input: ReorderInput, signedWriterId?: string): Promise + delete(input: DeletePageInput, signedWriterId?: string): Promise +} + +@injectable() +@singleton() +export class PageService implements Service { + constructor( + private readonly mongo: MongoService, + private readonly utils: UtilsService, + private readonly bookSerivce: BookService, + ) {} + public async updatePageAndChildrenUrlSlug({ + pageId, + signedWriterId, + urlPrefix = '', + }: UpdatePageAndChildrenUrlSlugArgs) { + if (!signedWriterId) { + throw new UnauthorizedError('Unauthorized') + } + + const page = await this.mongo.page.findUnique({ + where: { + id: pageId, + }, + }) + + if (!page) { + throw new NotFoundError('Page not found') + } + + if (urlPrefix === '') { + const book = await this.mongo.book.findUnique({ + where: { + id: page.fk_book_id, + }, + }) + + if (!book) { + throw new NotFoundError('Book not found') + } + + if (book.fk_writer_id !== signedWriterId) { + throw new UnauthorizedError('Book is not yours') + } + } + + const escapedTitle = `${urlPrefix}/${this.utils.escapeForUrl(page.title).toLowerCase()}` + const newUrlSlug = `${escapedTitle}-${page.code}` + await this.mongo.page.update({ + where: { + id: page.id, + }, + data: { + url_slug: newUrlSlug, + }, + }) + + const childrens = await this.mongo.page.findMany({ + where: { + parent_id: pageId, + }, + }) + + for (const child of childrens) { + await this.updatePageAndChildrenUrlSlug({ + pageId: child.id, + signedWriterId, + urlPrefix: `${escapedTitle}`, + }) + } + } + + public async getPages(bookUrlSlug: string, signedWriterId?: string): Promise { + if (!signedWriterId) { + throw new UnauthorizedError('Not authorized') + } + + const book = await this.bookSerivce.findByUrlSlug(bookUrlSlug) + + if (!book) { + throw new NotFoundError('Not found book') + } + + if (book.fk_writer_id !== signedWriterId) { + throw new ConfilctError('Not owner of book') + } + + const orderBy: Prisma.PageOrderByWithRelationInput[] = [{ index: 'asc' }] + + const fetchChildren = (depth: number): Prisma.PageInclude => { + if (depth === 0) return {} + + return { + childrens: { + where: { + is_deleted: false, + }, + orderBy, + include: fetchChildren(depth - 1), + }, + } + } + + const pages = await this.mongo.page.findMany({ + where: { + fk_book_id: book.id, + parent_id: null, + is_deleted: false, + }, + orderBy, + include: fetchChildren(3), + }) + + return pages as any + } + + public async getPage(input: GetPageInput, signedWriterId?: string): Promise { + if (!signedWriterId) { + throw new UnauthorizedError('Not authorized') + } + + const { book_url_slug, page_url_slug } = input + + const book = await this.bookSerivce.findByUrlSlug(book_url_slug) + + if (!book) { + throw new NotFoundError('Not found book') + } + + if (book.fk_writer_id !== signedWriterId) { + throw new ConfilctError('Not owner of book') + } + + const whereQuery: Prisma.PageWhereInput = { fk_book_id: book.id, fk_writer_id: signedWriterId } + + if (page_url_slug === '/') { + Object.assign(whereQuery, { parent_id: null, index: 0 }) + } else { + Object.assign(whereQuery, { url_slug: page_url_slug }) + } + + const page = await this.mongo.page.findFirst({ + where: whereQuery, + }) + + return page + } + + public async create(input: CreatePageInput, signedWriterId?: string): Promise { + if (!signedWriterId) { + throw new UnauthorizedError('Not authorized') + } + + const { book_url_slug, index, parent_url_slug, title, type } = input + + const book = await this.bookSerivce.findByUrlSlug(book_url_slug) + + if (!book) { + throw new NotFoundError('Not found book') + } + + if (book.fk_writer_id !== signedWriterId) { + throw new ConfilctError('Not owner of book') + } + + let parentPage: Page | null = null + + if (parent_url_slug !== '') { + parentPage = await this.mongo.page.findUnique({ + where: { + url_slug: parent_url_slug, + }, + }) + + if (!parentPage) { + throw new NotFoundError('Not found parent page') + } + } + + const code = this.utils.randomString(8) + const urlSlug = this.generateUrlSlug({ parent_url_slug, title, code }) + + const page = await this.mongo.page.create({ + data: { + title, + url_slug: urlSlug, + index, + code, + body: '', + fk_writer_id: signedWriterId, + fk_book_id: book.id, + type, + depth: Math.min(3, parentPage ? parentPage.depth + 1 : 1), // max level 3 + parent_id: parentPage ? parentPage!.id : null, + }, + }) + + return page + } + private generateUrlSlug({ parent_url_slug, title, code }: Record) { + return `${this.utils.removeCodeFromUrlSlug(parent_url_slug)}/${this.utils.escapeForUrl(title).toLowerCase()}-${code}` + } + + public async update(input: UpdatePageInput, signedWriterId?: string): Promise { + if (!signedWriterId) { + throw new UnauthorizedError('Not authorized') + } + + const { book_url_slug, page_url_slug, ...rest_input } = input + + if (Object.values(rest_input).every((value) => typeof value === 'undefined')) { + throw new BadRequestError('No input') + } + + const book = await this.bookSerivce.findByUrlSlug(book_url_slug) + + if (!book) { + throw new NotFoundError('Not found book') + } + + if (book.fk_writer_id !== signedWriterId) { + throw new ConfilctError('Not owner of book') + } + + let pageUrlSlug = page_url_slug + if (page_url_slug === '/' || !page_url_slug) { + const initPage = await this.mongo.page.findFirst({ + where: { + parent_id: null, + index: 0, + }, + }) + pageUrlSlug = initPage?.url_slug ?? page_url_slug + } + + const page = await this.mongo.page.findFirst({ + where: { + url_slug: pageUrlSlug, + fk_writer_id: signedWriterId, + }, + }) + + if (!page) { + throw new NotFoundError('Not found page') + } + + const updateInput: Prisma.PageUpdateInput = {} + + if (typeof rest_input.body === 'string') { + Object.assign(updateInput, { body: rest_input.body }) + } + + if (typeof rest_input.title === 'string') { + const newUrlSlug = `/${this.utils.escapeForUrl(rest_input.title)}-${page.code}` + Object.assign(updateInput, { title: rest_input.title, url_slug: newUrlSlug }) + } + + if (typeof rest_input.is_deleted === 'boolean') { + Object.assign(updateInput, { is_deleted: rest_input.is_deleted }) + } + + const updatedPage = await this.mongo.page.update({ + where: { + id: page.id, + }, + data: { + ...updateInput, + updated_at: new Date(), + }, + }) + + return updatedPage + } + + public async reorder(input: ReorderInput, signedWriterId?: string): Promise { + if (!signedWriterId) { + throw new UnauthorizedError('Not authorized') + } + + const { book_url_slug, target_url_slug, parent_url_slug, index } = input + + const book = await this.bookSerivce.findByUrlSlug(book_url_slug) + + if (!book) { + throw new NotFoundError('Not found book') + } + + if (book.fk_writer_id !== signedWriterId) { + throw new ConfilctError('Not owner of book') + } + + const page = await this.mongo.page.findUnique({ + where: { + url_slug: target_url_slug, + }, + }) + + if (!page) { + throw new NotFoundError('Not found page') + } + + if (page.fk_book_id !== book.id) { + throw new ConfilctError('Not related page') + } + + const parentPage = parent_url_slug + ? await this.mongo.page.findUnique({ + where: { + url_slug: parent_url_slug, + }, + }) + : null + + if (parentPage && parentPage?.fk_book_id !== book.id) { + throw new ConfilctError('Not related parent page') + } + + if (parentPage && parentPage.type !== 'folder') { + await this.mongo.page.update({ + where: { + id: parentPage.id, + }, + data: { + type: 'folder', + }, + }) + } + + const targetPage = await this.mongo.page.update({ + where: { + id: page.id, + }, + data: { + parent_id: parentPage?.id || null, + index, + updated_at: new Date(), + }, + }) + + const siblings = await this.mongo.page.findMany({ + where: { + parent_id: parentPage?.id ?? null, + is_deleted: false, + id: { + not: targetPage.id, + }, + }, + orderBy: [{ index: 'asc' }, { updated_at: 'desc' }], + }) + + const updatedSiblings = siblings.map((sibling, i) => { + return this.mongo.page.update({ + where: { + id: sibling.id, + }, + data: { + index: i < index ? i : i + 1, + }, + }) + }) + + await Promise.all(updatedSiblings) + } + + public async delete(input: DeletePageInput, signedWriterId?: string): Promise { + if (!signedWriterId) { + throw new UnauthorizedError('Not authorized') + } + + const { book_url_slug, page_url_slug } = input + + const book = await this.bookSerivce.findByUrlSlug(book_url_slug) + + if (!book) { + throw new NotFoundError('Not found book') + } + + if (book.fk_writer_id !== signedWriterId) { + throw new ConfilctError('Not owner of book') + } + + const page = await this.mongo.page.findFirst({ + where: { + fk_book_id: book.id, + url_slug: page_url_slug, + fk_writer_id: signedWriterId, + }, + }) + + if (!page) { + throw new NotFoundError('Not found page') + } + + if (page.is_deleted) { + throw new BadRequestError('Already deleted') + } + + const deletedPage = await this.mongo.page.update({ + where: { + id: page.id, + }, + data: { + is_deleted: true, + index: null, + updated_at: new Date(), + }, + }) + + const siblings = await this.mongo.page.findMany({ + where: { + parent_id: page.parent_id, + is_deleted: false, + id: { + not: deletedPage.id, + }, + }, + orderBy: [{ index: 'asc' }, { updated_at: 'desc' }], + }) + + const updateSiblings = siblings.map((sibling, index) => { + return this.mongo.page.update({ + where: { + id: sibling.id, + }, + data: { + index, + }, + }) + }) + + await Promise.all(updateSiblings) + } +} + +type UpdatePageAndChildrenUrlSlugArgs = { + pageId: string + signedWriterId?: string + urlPrefix?: string +} + +export type PageWithChildren = Prisma.PageGetPayload<{ + include: { + childrens: { + include: { + childrens: { + include: { + childrens: true + } + } + } + } + } +}> diff --git a/packages/velog-server/src/services/WriterService/WriterService.test.ts b/apps/book-server/src/services/WriterService/WriterService.test.mts similarity index 81% rename from packages/velog-server/src/services/WriterService/WriterService.test.ts rename to apps/book-server/src/services/WriterService/WriterService.test.mts index 6644ce70..cceb7685 100644 --- a/packages/velog-server/src/services/WriterService/WriterService.test.ts +++ b/apps/book-server/src/services/WriterService/WriterService.test.mts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { WriterService } from './index.js' +import { WriterService } from './index.mjs' describe('WriterService', () => { const service = container.resolve(WriterService) diff --git a/apps/book-server/src/services/WriterService/index.mts b/apps/book-server/src/services/WriterService/index.mts new file mode 100644 index 00000000..a20de944 --- /dev/null +++ b/apps/book-server/src/services/WriterService/index.mts @@ -0,0 +1,142 @@ +import { Time } from '@constants/TimeConstants.mjs' +import { NotFoundError } from '@errors/NotFoundError.mjs' +import { Writer } from '@graphql/generated.js' +import { GraphQLContext } from '@interfaces/graphql.mjs' +import { JwtService } from '@lib/jwt/JwtService.mjs' +import { MongoService } from '@lib/mongo/MongoService.mjs' +import { RedisService } from '@lib/redis/RedisService.mjs' +import { RefreshTokenData } from '@packages/library/jwt' +import { injectable, singleton } from 'tsyringe' + +interface Service { + findById(writerId: string): Promise + findByFkUserId(fkUserId: string): Promise + create(args: CreateArgs): Promise + checkExistsWriter(fkUserId?: string): Promise +} + +@injectable() +@singleton() +export class WriterService implements Service { + constructor( + private readonly jwt: JwtService, + private readonly mongo: MongoService, + private readonly redis: RedisService, + ) {} + public async findById(writerId: string): Promise { + return await this.mongo.writer.findUnique({ + where: { + id: writerId, + }, + }) + } + public async findByFkUserId(userId: string): Promise { + return await this.mongo.writer.findUnique({ + where: { + fk_user_id: userId, + }, + }) + } + public async create({ + fk_user_id, + username, + email, + short_bio, + display_name, + thumbnail, + }: CreateArgs): Promise { + const exists = await this.mongo.writer.findUnique({ + where: { + fk_user_id, + }, + }) + + if (exists) { + return exists + } + + const writer = await this.mongo.writer.create({ + data: { + fk_user_id, + username, + email, + short_bio, + display_name, + thumbnail, + }, + }) + + return writer + } + public async checkExistsWriter(fkUserId?: string): Promise { + if (!fkUserId) return { exists: false } + + const key = this.redis.generateKey.existsWriter(fkUserId) + const [value, writer] = await Promise.all([this.redis.get(key), this.findByFkUserId(fkUserId)]) + + if (value) { + return { + exists: true, + writerId: value, + } + } + + if (!writer) return { exists: false } + + await this.redis.set(key, writer.id, 'EX', Time.ONE_MINUTE_IN_S * 10) + + return { + exists: true, + writerId: writer.id, + } + } + public async restoreAccessToken( + ctx: Pick, + ): Promise { + const refreshToken: string | undefined = ctx.request.cookies['refresh_token'] + + if (!refreshToken) { + throw new NotFoundError('Refresh token not found') + } + + const decoded = await this.jwt.decodeToken(refreshToken) + const writer = await this.findByFkUserId(decoded.user_id) + + if (!writer) { + throw new NotFoundError('Writer not found') + } + + const token = await this.jwt.generateToken( + { + user_id: decoded.user_id, + }, + { + subject: 'access_token', + expiresIn: '24h', + }, + ) + + return { token, writer } + } +} + +type CreateArgs = { + fk_user_id: string + username: string + email: string + short_bio: string | null + display_name: string + thumbnail: string +} + +type CheckExistsWriterResult = + | { + exists: false + writerId?: undefined + } + | { exists: true; writerId: string } + +type RestoreAccessTokenResult = { + token: string + writer: Writer +} diff --git a/apps/book-server/src/templates/indexJSXTemplate.mts b/apps/book-server/src/templates/indexJSXTemplate.mts new file mode 100644 index 00000000..2eeef900 --- /dev/null +++ b/apps/book-server/src/templates/indexJSXTemplate.mts @@ -0,0 +1,18 @@ +export const indexJSXTemplate = (name: string) => { + return ` + 'use client' + + import { useRouter } from 'next/navigation' + import { useEffect } from 'react' + + const Page = () => { + const router = useRouter() + useEffect(() => { + router.push('${name}') + }, []) + return <> + } + + export default Page + ` +} diff --git a/apps/book-server/src/templates/nextConfigTemplate.mts b/apps/book-server/src/templates/nextConfigTemplate.mts new file mode 100644 index 00000000..baff653a --- /dev/null +++ b/apps/book-server/src/templates/nextConfigTemplate.mts @@ -0,0 +1,33 @@ +type Props = { + deployCode: string + urlSlug: string +} + +export const nextConfigTempate = ({ deployCode, urlSlug }: Props) => { + return ` + import nextra from 'nextra' + + const withNextra = nextra({ + theme: 'nextra-theme-docs', + themeConfig: './theme.config.tsx', + latex: true, + flexsearch: { + codeblocks: true, + }, + defaultShowCopyCode: true, + }) + + const nextConfig = { + output: 'export', + images: { + unoptimized: true, + }, + assetPrefix: process.env.NODE_ENV === 'production' ? 'https://books.velog.io${urlslug}/$%7BdeployCode%7D' : undefined, + basePath: '${urlSlug}/${deployCode}', + trailingSlash: false + } + + export default withNextra(nextConfig) + + ` +} diff --git a/apps/book-server/src/templates/themeConfigTemplate.mts b/apps/book-server/src/templates/themeConfigTemplate.mts new file mode 100644 index 00000000..2e436ab9 --- /dev/null +++ b/apps/book-server/src/templates/themeConfigTemplate.mts @@ -0,0 +1,95 @@ +type Props = { + title: string +} + +export const themeConfigTemplate = ({ title }: Props) => { + return ` + import { useRouter } from 'next/router' + import type { DocsThemeConfig } from 'nextra-theme-docs' + import { + useConfig, + Callout, + Bleed, + Card, + Cards, + FileTree, + Tabs, + Tab, + Steps, +} from 'nextra-theme-docs' + + const components = { Callout, Bleed, Card, Cards, FileTree, Tabs, Tab, Steps } as any + + const config: DocsThemeConfig = { + logo: ${title}, + editLink: { + text: '', + }, + feedback: { + content: '', + }, + footer: { + component: null, + text: '', + }, + sidebar: { + titleComponent({ title, type }) { + if (type === 'separator') { + return {title} + } + return <>{title} + }, + defaultMenuCollapseLevel: 1, + toggleButton: true, + }, + useNextSeoProps() { + const { asPath } = useRouter() + if (asPath !== '/') { + return { + titleTemplate: '%s – ${title}' + } + } + }, + head: function useHead() { + const { title } = useConfig() + const { route } = useRouter() + + return ( + <> + + + + + + + + + + + + + + + + + + ) + }, + components: { + ...components, + }, + } + + export default config + ` +} diff --git a/apps/book-server/src/types/fastify.d.ts b/apps/book-server/src/types/fastify.d.ts new file mode 100644 index 00000000..4898c86b --- /dev/null +++ b/apps/book-server/src/types/fastify.d.ts @@ -0,0 +1,10 @@ +import 'fastify' +import { File } from 'fastify-multer/lib/interfaces.js' + +declare module 'fastify' { + interface FastifyRequest { + ipaddr: string | null + writer: null | { id: string } + file?: File + } +} diff --git a/apps/book-server/test/mock/mockBook.mts b/apps/book-server/test/mock/mockBook.mts new file mode 100644 index 00000000..e98ca5e8 --- /dev/null +++ b/apps/book-server/test/mock/mockBook.mts @@ -0,0 +1,113 @@ +import { faker } from '@faker-js/faker' +import { UtilsService } from '@lib/utils/UtilsService.mjs' +import { container } from 'tsyringe' + +const utils = container.resolve(UtilsService) + +export const getMockPages = (count: number) => + Array(count) + .fill(0) + .map((_, index) => { + if (index === 0) { + const code = utils.randomString(8) + return { + title: 'Introduction', + index: index, + type: 'page', + code, + url_slug: `/${utils.escapeForUrl('Introduction')}-${code}`, + parent_id: null, + body: `# Introduction_${index} + - ${faker.lorem.paragraph(2)} + - ${faker.lorem.paragraph(1)} + - [${faker.lorem.words(2)}](${faker.internet.url()}) + \`\`\`js filename="demo.js" {4} copy + import { useState } from 'react' + + export const Counter = () => { + const [count, setCount] = useState(0) + return ( +
+ +
+ ) + } + \`\`\` + ![](${faker.image.url()}) + `.replaceAll(' ', ''), + } + } + + if (index === 2) { + const code = utils.randomString(8) + return { + title: 'Getting Started', + index: index, + body: '', + type: 'separator', + parent_id: null, + code, + url_slug: `/${utils.escapeForUrl('Getting Started')}-${code}`, + } + } + + if (index === 6) { + const code = utils.randomString(8) + return { + title: 'API Reference', + index: index, + body: '', + type: 'separator', + parent_id: null, + code: code, + url_slug: `/${utils.escapeForUrl('API Reference')}-${code}`, + } + } + + const title = faker.lorem.sentence({ min: 1, max: 3 }) + const code = utils.randomString(8) + return { + title, + index: index, + type: index > 9 ? 'page' : 'folder', + code: code, + url_slug: `/${utils.escapeForUrl(title).toLocaleLowerCase()}-${code}`, + parent_id: null, + body: `# ${title}_${index} + - ${faker.lorem.paragraph(2)} + - ${faker.lorem.paragraph(1)} + - [${faker.lorem.words(2)}](${faker.internet.url()}) + + ## ${faker.company.catchPhrase()} + - ${faker.lorem.paragraph(1)} + - ${faker.lorem.paragraph(2)} + + ## ${faker.company.catchPhrase()} + - ${faker.lorem.paragraph(3)} + - ${faker.lorem.paragraph(1)} + + ### ${faker.company.catchPhrase()} + - ${faker.lorem.paragraph(3)} + - ${faker.lorem.paragraph(1)} + + # ${faker.company.catchPhrase()} + - ${faker.lorem.paragraph(1)} + - ${faker.lorem.paragraph(2)} + + ## ${faker.company.catchPhrase()} + - ${faker.lorem.paragraph(3)} + - ${faker.lorem.paragraph(1)} + + ### ${faker.company.catchPhrase()} + - ${faker.lorem.paragraph(3)} + - ${faker.lorem.paragraph(1)} + + ${faker.lorem.paragraphs(2)} + + `.replaceAll(' ', ''), + } + }) + +export const getMockBooks = () => Array(100).fill(0).map(getMockPages) diff --git a/apps/book-server/tsconfig.build.json b/apps/book-server/tsconfig.build.json new file mode 100644 index 00000000..fd196ce0 --- /dev/null +++ b/apps/book-server/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.mts"], + "exclude": ["src/**/*.test.mts"] +} diff --git a/apps/book-server/tsconfig.json b/apps/book-server/tsconfig.json new file mode 100644 index 00000000..701fb3a9 --- /dev/null +++ b/apps/book-server/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "@packages/tsconfig/base.json", + "compilerOptions": { + "rootDir": "./", + "baseUrl": "./", + "outDir": "./dist", + "paths": { + "@lib/*": ["src/lib/*"], + "@errors/*": ["src/common/errors/*"], + "@graphql/*": ["src/graphql/*"], + "@plugins/*": ["src/common/plugins/*"], + "@interfaces/*": ["src/common/interfaces/*"], + "@templates/*": ["src/templates/*"], + "@services/*": ["src/services/*"], + "@routes/*": ["src/routes/*"], + "@env": ["src/env.mts"], + "@constants/*": ["src/common/constants/*"] + }, + "typeRoots": ["./node_modules/@types", "./src/types", "./node_modules/@packages/**/*.d.ts"] + }, + "include": ["./src", "./scripts", "./codegen.mts", "test/**/*.mts", "eslint.config.js"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/book-web/.gitignore b/apps/book-web/.gitignore new file mode 100644 index 00000000..fd3dbb57 --- /dev/null +++ b/apps/book-web/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/book-web/README.md b/apps/book-web/README.md new file mode 100644 index 00000000..a75ac524 --- /dev/null +++ b/apps/book-web/README.md @@ -0,0 +1,40 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/apps/book-web/codegen.ts b/apps/book-web/codegen.ts new file mode 100644 index 00000000..c1f547b9 --- /dev/null +++ b/apps/book-web/codegen.ts @@ -0,0 +1,68 @@ +import { ENV } from './src/env.js' +import type { CodegenConfig } from '@graphql-codegen/cli' +import type { Types } from '@graphql-codegen/plugin-helpers' + +const commonGenerateOptions: Types.ConfiguredOutput = { + config: { + enumsAsTypes: true, + reactQueryVersion: 5, + addSuspenseQuery: true, + exposeQueryKeys: true, + exposeFetcher: true, + exposeMutationKeys: true, + skipTypename: true, + inputMaybeValue: 'T | null | undefined', + maybeValue: 'T | null', + avoidOptionals: { + field: true, + inputValue: false, + object: true, + defaultValue: true, + }, + scalars: { + Date: 'Date', + JSON: 'Record', + ID: 'string', + Void: 'void', + PositiveInt: 'number', + }, + }, + plugins: [ + 'typescript', + '@graphql-codegen/typescript-operations', + '@graphql-codegen/typescript-react-query', + ], +} + +const config: CodegenConfig = { + overwrite: true, + hooks: { + afterOneFileWrite: ['prettier --write .'], + }, + generates: { + // 'src/graphql/server/generated/server.ts': { + // schema: `${ENV.apiV3Host}/graphql`, + // documents: './src/graphql/server/*.gql', + // config: { + // ...commonGenerateOptions.config, + // fetcher: { + // func: '../helpers/serverFetcher#fetcher', + // }, + // }, + // plugins: commonGenerateOptions.plugins, + // }, + 'src/graphql/bookServer/generated/bookServer.ts': { + schema: `${ENV.graphqlBookServerHost}/graphql`, + documents: './src/graphql/bookServer/*.gql', + config: { + ...commonGenerateOptions.config, + fetcher: { + func: '../helpers/bookServerFetcher#fetcher', + }, + }, + plugins: commonGenerateOptions.plugins, + }, + }, +} + +export default config diff --git a/apps/book-web/env/.env.example b/apps/book-web/env/.env.example new file mode 100644 index 00000000..04a59e20 --- /dev/null +++ b/apps/book-web/env/.env.example @@ -0,0 +1,5 @@ +NEXT_PUBLIC_PUBLIC_URL= +NEXT_PUBLIC_CLIENT_HOST= + +NEXT_PUBLIC_GRAPHQL_SERVER_HOST= +NEXT_PUBLIC_GRAPHQL_BOOK_SERVER_HOST= \ No newline at end of file diff --git a/apps/book-web/eslint.config.js b/apps/book-web/eslint.config.js new file mode 100644 index 00000000..a82c5b0e --- /dev/null +++ b/apps/book-web/eslint.config.js @@ -0,0 +1,7 @@ +import nextConfig from '@packages/eslint-config/next.mjs' +import { resolve } from 'node:path' + +const projectPath = resolve(process.cwd()) + +/** @type {import("eslint").Linter.Config} */ +export default [...nextConfig(projectPath)] diff --git a/apps/book-web/next.config.mjs b/apps/book-web/next.config.mjs new file mode 100644 index 00000000..8241ca9b --- /dev/null +++ b/apps/book-web/next.config.mjs @@ -0,0 +1,10 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ['next-mdx-remote'], + reactStrictMode: true, + experimental: { + serverComponentsExternalPackages: ['shiki', 'vscode-oniguruma'], + }, +} + +export default nextConfig diff --git a/apps/book-web/package.json b/apps/book-web/package.json new file mode 100644 index 00000000..40d7847c --- /dev/null +++ b/apps/book-web/package.json @@ -0,0 +1,64 @@ +{ + "name": "book-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "pnpm env:copy -e development && next dev -p 3002 --turbo", + "build": "next build", + "start": "next start", + "lint": "next lint", + "codegen": "pnpm env:copy -e development && graphql-codegen-esm --config codegen.ts -r dotenv/config", + "env:copy": "tsx ./scripts/copyEnv.mts", + "create-component": "tsx ./scripts/createComponent.mts", + "ssm": "tsx ./scripts/ssm.mts" + }, + "dependencies": { + "@mdx-js/mdx": "2.3.0", + "@next/mdx": "^14.2.3", + "@packages/markdown-editor": "workspace:*", + "@shikijs/rehype": "^1.4.0", + "@tanstack/react-query": "^5.29.0", + "@theguild/remark-mermaid": "^0.0.5", + "@theguild/remark-npm2yarn": "0.2.0", + "@types/mdx": "^2.0.13", + "graphql": "^16.7.1", + "graphql-request": "^6.1.0", + "graphql-tag": "^2.12.6", + "graphql-ws": "^5.16.0", + "gray-matter": "^4.0.3", + "next": "14.2.5", + "react": "^18", + "react-dom": "^18", + "rehype-katex": "^7.0.0", + "rehype-pretty-code": "0.13.1", + "rehype-raw": "^7.0.0", + "remark-gfm": "^3.0.1", + "remark-math": "^5.1.1", + "remark-reading-time": "^2.0.1", + "shiki": "^0.14.3", + "unified": "^10.1.2", + "urql": "^4.1.0", + "vscode-oniguruma": "^2.0.1", + "wonka": "^6.3.4", + "zod": "^3.23.7" + }, + "devDependencies": { + "@graphql-codegen/cli": "^5.0.2", + "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/typescript-operations": "^4.2.0", + "@graphql-codegen/typescript-react-query": "^6.1.0", + "@packages/eslint-config": "workspace:*", + "@packages/scripts": "workspace:*", + "@packages/tsconfig": "workspace:*", + "@testing-library/react": "^14.0.0", + "@types/inquirer": "^9.0.7", + "@types/react": "^18", + "@types/react-dom": "^18", + "inquirer": "^9.2.23", + "postcss": "^8", + "prettier": "^3.2.5", + "tailwindcss": "^3.4.1", + "tsx": "^4.7.3" + } +} diff --git a/apps/book-web/postcss.config.mjs b/apps/book-web/postcss.config.mjs new file mode 100644 index 00000000..0dc456ad --- /dev/null +++ b/apps/book-web/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +} + +export default config diff --git a/apps/book-web/public/favicon.ico b/apps/book-web/public/favicon.ico new file mode 100644 index 00000000..718d6fea Binary files /dev/null and b/apps/book-web/public/favicon.ico differ diff --git a/apps/book-web/public/next.svg b/apps/book-web/public/next.svg new file mode 100644 index 00000000..5174b28c --- /dev/null +++ b/apps/book-web/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/book-web/public/vercel.svg b/apps/book-web/public/vercel.svg new file mode 100644 index 00000000..d2f84222 --- /dev/null +++ b/apps/book-web/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/book-web/public/wasm/onig.wasm b/apps/book-web/public/wasm/onig.wasm new file mode 100644 index 00000000..6edbc886 Binary files /dev/null and b/apps/book-web/public/wasm/onig.wasm differ diff --git a/apps/book-web/scripts/copyEnv.mts b/apps/book-web/scripts/copyEnv.mts new file mode 100644 index 00000000..0d46eaff --- /dev/null +++ b/apps/book-web/scripts/copyEnv.mts @@ -0,0 +1,4 @@ +import { CopyEnvScript } from '@packages/scripts' + +const copyEnvScript = new CopyEnvScript() +copyEnvScript.execute() diff --git a/apps/book-web/scripts/createComponent.mts b/apps/book-web/scripts/createComponent.mts new file mode 100644 index 00000000..129eca93 --- /dev/null +++ b/apps/book-web/scripts/createComponent.mts @@ -0,0 +1,88 @@ +import { URL } from 'url' +import path from 'path' +import fs from 'fs' +import inquirer from 'inquirer' + +const __dirname = new URL('.', import.meta.url).pathname +const templateDir = path.resolve(__dirname, './templates/component') +const files = fs.readdirSync(templateDir) +const filesData = new Map() +const cacheDir = path.resolve(__dirname, './.cache') + +files.forEach((file) => { + const filePath = path.resolve(templateDir, file) + const fileData = fs.readFileSync(filePath, 'utf8') + filesData.set(file, fileData) +}) + +function replaceMyComponent(text: string, name: string) { + return text.replace(/MyComponent/g, name) +} + +function createComponent(directory: string, componentName: string) { + const componentDir = path.resolve(directory, componentName) + // create directory if not exist + if (!fs.existsSync(componentDir)) { + fs.mkdirSync(componentDir, { recursive: true }) + } + files.forEach((file) => { + const fileData = filesData.get(file) + if (fileData === undefined) return + const code = replaceMyComponent(fileData, componentName) + const newFilePath = path.resolve(componentDir, replaceMyComponent(file, componentName)) + fs.writeFileSync(newFilePath, code) + }) +} + +async function main() { + const { type } = await inquirer.prompt([ + { + type: 'list', + name: 'type', + message: 'Do you want to save in components directory or features directory?', + choices: ['features', 'components'], + default: 'features', + }, + ]) + + let feature = 'sample' + if (type === 'features') { + try { + feature = fs.readFileSync(cacheDir, 'utf8') + } catch (e) {} + } + + const answers = await inquirer.prompt( + [ + type === 'features' + ? { + name: 'feature', + message: 'Enter feature name', + default: feature, + } + : null, + { + name: 'names', + message: 'Enter component name \x1b[2m(Separate multiple names with space)\x1b[0m', + }, + ].filter((q) => q !== null), + ) + + const dir = + type === 'components' + ? path.resolve(__dirname, '../src/components') + : path.resolve(__dirname, `../src/features/${answers.feature}/components`) + + const names = answers.names.split(' ') as string[] + + names.forEach((name) => { + if (name.trim() === '') throw new Error('Invalid component name') + createComponent(dir, name.trim()) + }) + + if (type === 'features') { + fs.writeFileSync(path.resolve(__dirname, './templates/.cache'), answers.feature, 'utf8') + } +} + +main() diff --git a/apps/book-web/scripts/ssm.mts b/apps/book-web/scripts/ssm.mts new file mode 100644 index 00000000..2389b0cb --- /dev/null +++ b/apps/book-web/scripts/ssm.mts @@ -0,0 +1,4 @@ +import { SSMScript } from '@packages/scripts' + +const script = new SSMScript({ packageName: 'book-web' }) +script.execute() diff --git a/packages/velog-web/scripts/.gitignore b/apps/book-web/scripts/templates/.gitignore similarity index 100% rename from packages/velog-web/scripts/.gitignore rename to apps/book-web/scripts/templates/.gitignore diff --git a/packages/velog-web/scripts/templates/component/MyComponent.module.css b/apps/book-web/scripts/templates/component/MyComponent.module.css similarity index 100% rename from packages/velog-web/scripts/templates/component/MyComponent.module.css rename to apps/book-web/scripts/templates/component/MyComponent.module.css diff --git a/packages/velog-web/scripts/templates/component/MyComponent.test.tsx b/apps/book-web/scripts/templates/component/MyComponent.test.tsx similarity index 100% rename from packages/velog-web/scripts/templates/component/MyComponent.test.tsx rename to apps/book-web/scripts/templates/component/MyComponent.test.tsx diff --git a/apps/book-web/scripts/templates/component/MyComponent.tsx b/apps/book-web/scripts/templates/component/MyComponent.tsx new file mode 100644 index 00000000..a2d8720c --- /dev/null +++ b/apps/book-web/scripts/templates/component/MyComponent.tsx @@ -0,0 +1,7 @@ +'use client' + +function MyComponent() { + return
+} + +export default MyComponent diff --git a/apps/book-web/scripts/templates/component/index.tsx b/apps/book-web/scripts/templates/component/index.tsx new file mode 100644 index 00000000..730f151b --- /dev/null +++ b/apps/book-web/scripts/templates/component/index.tsx @@ -0,0 +1 @@ +export { default } from './MyComponent.jsx' diff --git a/packages/velog-web/src/components/Error/CrashErrorScreen/CrashErrorScreen.module.css b/apps/book-web/src/components/ErrorBoundary/ErrorBoundary.module.css similarity index 100% rename from packages/velog-web/src/components/Error/CrashErrorScreen/CrashErrorScreen.module.css rename to apps/book-web/src/components/ErrorBoundary/ErrorBoundary.module.css diff --git a/apps/book-web/src/components/ErrorBoundary/ErrorBoundary.test.tsx b/apps/book-web/src/components/ErrorBoundary/ErrorBoundary.test.tsx new file mode 100644 index 00000000..adba0e77 --- /dev/null +++ b/apps/book-web/src/components/ErrorBoundary/ErrorBoundary.test.tsx @@ -0,0 +1,8 @@ +import ErrorBoundary from './ErrorBoundary' +import { render } from '@testing-library/react' + +describe('ErrorBoundary', () => { + it('renders successfully', () => { + render() + }) +}) diff --git a/apps/book-web/src/components/ErrorBoundary/ErrorBoundary.tsx b/apps/book-web/src/components/ErrorBoundary/ErrorBoundary.tsx new file mode 100644 index 00000000..f1cfc05b --- /dev/null +++ b/apps/book-web/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -0,0 +1,41 @@ +import { ErrorBoundaryProps } from 'next/dist/client/components/error-boundary' +import React from 'react' + +type State = { hasError: boolean } + +class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props) + + // Define a state variable to track whether is an error or not + this.state = { hasError: false } + } + static getDerivedStateFromError(error: any) { + // Update state so the next render will show the fallback UI + + return { hasError: true } + } + componentDidCatch(error: any, errorInfo: any) { + // You can use your own error logging service here + console.log({ error, errorInfo }) + } + render() { + // Check if the error is thrown + if (this.state.hasError) { + // You can render any custom fallback UI + // return ( + //
+ //

Oops, there is an error!

+ // + //
+ // ) + } + + // Return children components in case of no error + return this.props.children + } +} + +export default ErrorBoundary diff --git a/apps/book-web/src/components/ErrorBoundary/index.tsx b/apps/book-web/src/components/ErrorBoundary/index.tsx new file mode 100644 index 00000000..0f76c913 --- /dev/null +++ b/apps/book-web/src/components/ErrorBoundary/index.tsx @@ -0,0 +1 @@ +export { default } from './ErrorBoundary.jsx' diff --git a/packages/velog-web/src/components/Layouts/BasicLayout/BasicLayout.module.css b/apps/book-web/src/components/MarkdownRender/MarkdownRender.module.css similarity index 100% rename from packages/velog-web/src/components/Layouts/BasicLayout/BasicLayout.module.css rename to apps/book-web/src/components/MarkdownRender/MarkdownRender.module.css diff --git a/packages/velog-web/src/components/MarkdownRender/MarkdownRender.test.tsx b/apps/book-web/src/components/MarkdownRender/MarkdownRender.test.tsx similarity index 100% rename from packages/velog-web/src/components/MarkdownRender/MarkdownRender.test.tsx rename to apps/book-web/src/components/MarkdownRender/MarkdownRender.test.tsx diff --git a/apps/book-web/src/components/MarkdownRender/MarkdownRender.tsx b/apps/book-web/src/components/MarkdownRender/MarkdownRender.tsx new file mode 100644 index 00000000..f75d6fb8 --- /dev/null +++ b/apps/book-web/src/components/MarkdownRender/MarkdownRender.tsx @@ -0,0 +1,7 @@ +'use client' + +function MarkdownRender() { + return
+} + +export default MarkdownRender diff --git a/apps/book-web/src/components/MarkdownRender/index.tsx b/apps/book-web/src/components/MarkdownRender/index.tsx new file mode 100644 index 00000000..bcc4470a --- /dev/null +++ b/apps/book-web/src/components/MarkdownRender/index.tsx @@ -0,0 +1 @@ +export { default } from './MarkdownRender.jsx' diff --git a/apps/book-web/src/env.ts b/apps/book-web/src/env.ts new file mode 100644 index 00000000..13fa725e --- /dev/null +++ b/apps/book-web/src/env.ts @@ -0,0 +1,28 @@ +import { z } from 'zod' + +type DockerEnv = 'development' | 'stage' | 'production' +type AppEnvironment = 'development' | 'production' + +const dockerEnv = (process.env.DOCKER_ENV as DockerEnv) || 'development' +const appEnv: AppEnvironment = ['stage', 'production'].includes(dockerEnv) + ? 'production' + : 'development' + +const env = z.object({ + dockerEnv: z.enum(['development', 'production', 'stage']), + appEnv: z.enum(['development', 'production']), + publicUrl: z.string(), + clientHost: z.string(), + graphqlServerHost: z.string(), + graphqlBookServerHost: z.string(), +}) + +export const ENV = env.parse({ + dockerEnv, + // allowed standard env names, https://nextjs.org/docs/messages/non-standard-node-env + appEnv, + publicUrl: process.env.NEXT_PUBLIC_PUBLIC_URL, + clientHost: process.env.NEXT_PUBLIC_CLIENT_HOST, + graphqlServerHost: process.env.NEXT_PUBLIC_GRAPHQL_SERVER_HOST, + graphqlBookServerHost: process.env.NEXT_PUBLIC_GRAPHQL_BOOK_SERVER_HOST, +}) diff --git a/packages/velog-web/src/components/MarkdownEditor/MarkdownEditor.module.css b/apps/book-web/src/features/edit/components/Body/Body.module.css similarity index 100% rename from packages/velog-web/src/components/MarkdownEditor/MarkdownEditor.module.css rename to apps/book-web/src/features/edit/components/Body/Body.module.css diff --git a/apps/book-web/src/features/edit/components/Body/Body.test.tsx b/apps/book-web/src/features/edit/components/Body/Body.test.tsx new file mode 100644 index 00000000..506eb549 --- /dev/null +++ b/apps/book-web/src/features/edit/components/Body/Body.test.tsx @@ -0,0 +1,8 @@ +import Body from './Body' +import { render } from '@testing-library/react' + +describe('Body', () => { + it('renders successfully', () => { + render() + }) +}) diff --git a/apps/book-web/src/features/edit/components/Body/Body.tsx b/apps/book-web/src/features/edit/components/Body/Body.tsx new file mode 100644 index 00000000..5771c3dd --- /dev/null +++ b/apps/book-web/src/features/edit/components/Body/Body.tsx @@ -0,0 +1,7 @@ +'use client' + +function Body() { + return
body
+} + +export default Body diff --git a/apps/book-web/src/features/edit/components/Body/index.tsx b/apps/book-web/src/features/edit/components/Body/index.tsx new file mode 100644 index 00000000..9caea274 --- /dev/null +++ b/apps/book-web/src/features/edit/components/Body/index.tsx @@ -0,0 +1 @@ +export { default } from './Body' diff --git a/apps/book-web/src/graphql/bookServer/book.gql b/apps/book-web/src/graphql/bookServer/book.gql new file mode 100644 index 00000000..6bf32c11 --- /dev/null +++ b/apps/book-web/src/graphql/bookServer/book.gql @@ -0,0 +1,22 @@ +mutation deploy($input: BookUrlSlugInput!) { + deploy(input: $input) { + published_url + } +} + +mutation build($input: BookUrlSlugInput!) { + build(input: $input) { + result + } +} + +query isDeploy($input: IsDeployInput!) { + isDeploy(input: $input) +} + +subscription deployCompleted($input: BookUrlSlugInput!) { + deployCompleted(input: $input) { + message + published_url + } +} diff --git a/apps/book-web/src/graphql/bookServer/generated/bookServer.ts b/apps/book-web/src/graphql/bookServer/generated/bookServer.ts new file mode 100644 index 00000000..a6d9eaae --- /dev/null +++ b/apps/book-web/src/graphql/bookServer/generated/bookServer.ts @@ -0,0 +1,631 @@ +import { + useMutation, + useQuery, + useSuspenseQuery, + UseMutationOptions, + UseQueryOptions, + UseSuspenseQueryOptions, +} from '@tanstack/react-query' +import { fetcher } from '../helpers/bookServerFetcher' +export type Maybe = T | null +export type InputMaybe = T | null | undefined +export type Exact = { [K in keyof T]: T[K] } +export type MakeOptional = Omit & { [SubKey in K]?: Maybe } +export type MakeMaybe = Omit & { [SubKey in K]: Maybe } +export type MakeEmpty = { + [_ in K]?: never +} +export type Incremental = + | T + | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never } +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string } + String: { input: string; output: string } + Boolean: { input: boolean; output: boolean } + Int: { input: number; output: number } + Float: { input: number; output: number } + Date: { input: Date; output: Date } + JSON: { input: Record; output: Record } + PositiveInt: { input: number; output: number } + Void: { input: void; output: void } +} + +export type Book = { + fk_writer_id: Scalars['String']['output'] + id: Scalars['ID']['output'] + is_published: Scalars['Boolean']['output'] + pages: Array + thumbnail: Scalars['String']['output'] +} + +export type BookIdInput = { + book_id: Scalars['ID']['input'] +} + +export type BookUrlSlugInput = { + book_url_slug: Scalars['String']['input'] +} + +export type BuildResult = { + message: Maybe + result: Scalars['Boolean']['output'] +} + +export type CreatePageInput = { + book_url_slug: Scalars['String']['input'] + index: Scalars['Int']['input'] + parent_url_slug: Scalars['String']['input'] + title: Scalars['String']['input'] + type: PageType +} + +export type DeletePageInput = { + book_url_slug: Scalars['String']['input'] + page_url_slug: Scalars['String']['input'] +} + +export type DeployCompletedPayload = { + message: Scalars['String']['output'] + published_url: Scalars['String']['output'] +} + +export type DeployResult = { + message: Maybe + published_url: Maybe +} + +export type GetPageInput = { + book_url_slug: Scalars['String']['input'] + page_url_slug: Scalars['String']['input'] +} + +export type GetPagesInput = { + book_url_slug: Scalars['String']['input'] +} + +export type IsDeployInput = { + book_url_slug: Scalars['String']['input'] +} + +export type Mutation = { + build: BuildResult + create: Maybe + delete: Maybe + deploy: DeployResult + reorder: Maybe + update: Maybe +} + +export type MutationBuildArgs = { + input: BookUrlSlugInput +} + +export type MutationCreateArgs = { + input: CreatePageInput +} + +export type MutationDeleteArgs = { + input: DeletePageInput +} + +export type MutationDeployArgs = { + input: BookUrlSlugInput +} + +export type MutationReorderArgs = { + input: ReorderInput +} + +export type MutationUpdateArgs = { + input: UpdatePageInput +} + +export type Page = { + body: Scalars['String']['output'] + childrens: Array + code: Scalars['String']['output'] + created_at: Scalars['Date']['output'] + depth: Scalars['Int']['output'] + fk_book_id: Scalars['ID']['output'] + fk_writer_id: Scalars['ID']['output'] + id: Scalars['ID']['output'] + index: Scalars['Int']['output'] + is_deleted: Scalars['Boolean']['output'] + parent_id: Maybe + title: Scalars['String']['output'] + type: Scalars['String']['output'] + updated_at: Scalars['Date']['output'] + url_slug: Scalars['String']['output'] +} + +export type PageType = 'folder' | 'page' | 'separator' + +export type Query = { + book: Maybe + isDeploy: Scalars['Boolean']['output'] + page: Maybe + pages: Array +} + +export type QueryBookArgs = { + input: BookIdInput +} + +export type QueryIsDeployArgs = { + input: IsDeployInput +} + +export type QueryPageArgs = { + input: GetPageInput +} + +export type QueryPagesArgs = { + input: GetPagesInput +} + +export type ReorderInput = { + book_url_slug: Scalars['String']['input'] + index: Scalars['Int']['input'] + parent_url_slug?: InputMaybe + target_url_slug: Scalars['String']['input'] +} + +export type SubScriptionPayload = { + message: Scalars['String']['output'] +} + +export type Subscription = { + buildCompleted: Maybe + buildInstalled: Maybe + deployCompleted: Maybe +} + +export type SubscriptionBuildCompletedArgs = { + input: BookUrlSlugInput +} + +export type SubscriptionBuildInstalledArgs = { + input: BookUrlSlugInput +} + +export type SubscriptionDeployCompletedArgs = { + input: BookUrlSlugInput +} + +export type UpdatePageInput = { + body?: InputMaybe + book_url_slug: Scalars['String']['input'] + is_deleted?: InputMaybe + page_url_slug: Scalars['String']['input'] + title?: InputMaybe +} + +export type Writer = { + email: Scalars['String']['output'] + fk_user_id: Scalars['String']['output'] + id: Scalars['ID']['output'] + short_bio: Maybe + username: Scalars['String']['output'] +} + +export type DeployMutationVariables = Exact<{ + input: BookUrlSlugInput +}> + +export type DeployMutation = { deploy: { published_url: string | null } } + +export type BuildMutationVariables = Exact<{ + input: BookUrlSlugInput +}> + +export type BuildMutation = { build: { result: boolean } } + +export type IsDeployQueryVariables = Exact<{ + input: IsDeployInput +}> + +export type IsDeployQuery = { isDeploy: boolean } + +export type DeployCompletedSubscriptionVariables = Exact<{ + input: BookUrlSlugInput +}> + +export type DeployCompletedSubscription = { + deployCompleted: { message: string; published_url: string } | null +} + +export type GetPagesQueryVariables = Exact<{ + input: GetPagesInput +}> + +export type GetPagesQuery = { + pages: Array<{ + id: string + title: string + url_slug: string + type: string + parent_id: string | null + code: string + childrens: Array<{ + id: string + title: string + url_slug: string + type: string + parent_id: string | null + code: string + childrens: Array<{ + id: string + title: string + url_slug: string + type: string + parent_id: string | null + code: string + }> + }> + }> +} + +export type GetPageQueryVariables = Exact<{ + input: GetPageInput +}> + +export type GetPageQuery = { + page: { + id: string + title: string + url_slug: string + parent_id: string | null + body: string + } | null +} + +export type CreatePageMutationVariables = Exact<{ + input: CreatePageInput +}> + +export type CreatePageMutation = { create: { id: string } | null } + +export type ReorderPageMutationVariables = Exact<{ + input: ReorderInput +}> + +export type ReorderPageMutation = { reorder: void | null } + +export type UpdatePageMutationVariables = Exact<{ + input: UpdatePageInput +}> + +export type UpdatePageMutation = { + update: { id: string; title: string; body: string; is_deleted: boolean } | null +} + +export type DeletePageMutationVariables = Exact<{ + input: DeletePageInput +}> + +export type DeletePageMutation = { delete: void | null } + +export const DeployDocument = ` + mutation deploy($input: BookUrlSlugInput!) { + deploy(input: $input) { + published_url + } +} + ` + +export const useDeployMutation = ( + options?: UseMutationOptions, +) => { + return useMutation({ + mutationKey: ['deploy'], + mutationFn: (variables?: DeployMutationVariables) => + fetcher(DeployDocument, variables)(), + ...options, + }) +} + +useDeployMutation.getKey = () => ['deploy'] + +useDeployMutation.fetcher = ( + variables: DeployMutationVariables, + options?: RequestInit['headers'], +) => fetcher(DeployDocument, variables, options) + +export const BuildDocument = ` + mutation build($input: BookUrlSlugInput!) { + build(input: $input) { + result + } +} + ` + +export const useBuildMutation = ( + options?: UseMutationOptions, +) => { + return useMutation({ + mutationKey: ['build'], + mutationFn: (variables?: BuildMutationVariables) => + fetcher(BuildDocument, variables)(), + ...options, + }) +} + +useBuildMutation.getKey = () => ['build'] + +useBuildMutation.fetcher = (variables: BuildMutationVariables, options?: RequestInit['headers']) => + fetcher(BuildDocument, variables, options) + +export const IsDeployDocument = ` + query isDeploy($input: IsDeployInput!) { + isDeploy(input: $input) +} + ` + +export const useIsDeployQuery = ( + variables: IsDeployQueryVariables, + options?: Omit, 'queryKey'> & { + queryKey?: UseQueryOptions['queryKey'] + }, +) => { + return useQuery({ + queryKey: ['isDeploy', variables], + queryFn: fetcher(IsDeployDocument, variables), + ...options, + }) +} + +useIsDeployQuery.getKey = (variables: IsDeployQueryVariables) => ['isDeploy', variables] + +export const useSuspenseIsDeployQuery = ( + variables: IsDeployQueryVariables, + options?: Omit, 'queryKey'> & { + queryKey?: UseSuspenseQueryOptions['queryKey'] + }, +) => { + return useSuspenseQuery({ + queryKey: ['isDeploySuspense', variables], + queryFn: fetcher(IsDeployDocument, variables), + ...options, + }) +} + +useSuspenseIsDeployQuery.getKey = (variables: IsDeployQueryVariables) => [ + 'isDeploySuspense', + variables, +] + +useIsDeployQuery.fetcher = (variables: IsDeployQueryVariables, options?: RequestInit['headers']) => + fetcher(IsDeployDocument, variables, options) + +export const DeployCompletedDocument = ` + subscription deployCompleted($input: BookUrlSlugInput!) { + deployCompleted(input: $input) { + message + published_url + } +} + ` +export const GetPagesDocument = ` + query getPages($input: GetPagesInput!) { + pages(input: $input) { + id + title + url_slug + type + parent_id + code + childrens { + id + title + url_slug + type + parent_id + code + childrens { + id + title + url_slug + type + parent_id + code + } + } + } +} + ` + +export const useGetPagesQuery = ( + variables: GetPagesQueryVariables, + options?: Omit, 'queryKey'> & { + queryKey?: UseQueryOptions['queryKey'] + }, +) => { + return useQuery({ + queryKey: ['getPages', variables], + queryFn: fetcher(GetPagesDocument, variables), + ...options, + }) +} + +useGetPagesQuery.getKey = (variables: GetPagesQueryVariables) => ['getPages', variables] + +export const useSuspenseGetPagesQuery = ( + variables: GetPagesQueryVariables, + options?: Omit, 'queryKey'> & { + queryKey?: UseSuspenseQueryOptions['queryKey'] + }, +) => { + return useSuspenseQuery({ + queryKey: ['getPagesSuspense', variables], + queryFn: fetcher(GetPagesDocument, variables), + ...options, + }) +} + +useSuspenseGetPagesQuery.getKey = (variables: GetPagesQueryVariables) => [ + 'getPagesSuspense', + variables, +] + +useGetPagesQuery.fetcher = (variables: GetPagesQueryVariables, options?: RequestInit['headers']) => + fetcher(GetPagesDocument, variables, options) + +export const GetPageDocument = ` + query getPage($input: GetPageInput!) { + page(input: $input) { + id + title + url_slug + parent_id + body + } +} + ` + +export const useGetPageQuery = ( + variables: GetPageQueryVariables, + options?: Omit, 'queryKey'> & { + queryKey?: UseQueryOptions['queryKey'] + }, +) => { + return useQuery({ + queryKey: ['getPage', variables], + queryFn: fetcher(GetPageDocument, variables), + ...options, + }) +} + +useGetPageQuery.getKey = (variables: GetPageQueryVariables) => ['getPage', variables] + +export const useSuspenseGetPageQuery = ( + variables: GetPageQueryVariables, + options?: Omit, 'queryKey'> & { + queryKey?: UseSuspenseQueryOptions['queryKey'] + }, +) => { + return useSuspenseQuery({ + queryKey: ['getPageSuspense', variables], + queryFn: fetcher(GetPageDocument, variables), + ...options, + }) +} + +useSuspenseGetPageQuery.getKey = (variables: GetPageQueryVariables) => [ + 'getPageSuspense', + variables, +] + +useGetPageQuery.fetcher = (variables: GetPageQueryVariables, options?: RequestInit['headers']) => + fetcher(GetPageDocument, variables, options) + +export const CreatePageDocument = ` + mutation createPage($input: CreatePageInput!) { + create(input: $input) { + id + } +} + ` + +export const useCreatePageMutation = ( + options?: UseMutationOptions, +) => { + return useMutation({ + mutationKey: ['createPage'], + mutationFn: (variables?: CreatePageMutationVariables) => + fetcher(CreatePageDocument, variables)(), + ...options, + }) +} + +useCreatePageMutation.getKey = () => ['createPage'] + +useCreatePageMutation.fetcher = ( + variables: CreatePageMutationVariables, + options?: RequestInit['headers'], +) => + fetcher(CreatePageDocument, variables, options) + +export const ReorderPageDocument = ` + mutation reorderPage($input: ReorderInput!) { + reorder(input: $input) +} + ` + +export const useReorderPageMutation = ( + options?: UseMutationOptions, +) => { + return useMutation({ + mutationKey: ['reorderPage'], + mutationFn: (variables?: ReorderPageMutationVariables) => + fetcher(ReorderPageDocument, variables)(), + ...options, + }) +} + +useReorderPageMutation.getKey = () => ['reorderPage'] + +useReorderPageMutation.fetcher = ( + variables: ReorderPageMutationVariables, + options?: RequestInit['headers'], +) => + fetcher( + ReorderPageDocument, + variables, + options, + ) + +export const UpdatePageDocument = ` + mutation updatePage($input: UpdatePageInput!) { + update(input: $input) { + id + title + body + is_deleted + } +} + ` + +export const useUpdatePageMutation = ( + options?: UseMutationOptions, +) => { + return useMutation({ + mutationKey: ['updatePage'], + mutationFn: (variables?: UpdatePageMutationVariables) => + fetcher(UpdatePageDocument, variables)(), + ...options, + }) +} + +useUpdatePageMutation.getKey = () => ['updatePage'] + +useUpdatePageMutation.fetcher = ( + variables: UpdatePageMutationVariables, + options?: RequestInit['headers'], +) => + fetcher(UpdatePageDocument, variables, options) + +export const DeletePageDocument = ` + mutation deletePage($input: DeletePageInput!) { + delete(input: $input) +} + ` + +export const useDeletePageMutation = ( + options?: UseMutationOptions, +) => { + return useMutation({ + mutationKey: ['deletePage'], + mutationFn: (variables?: DeletePageMutationVariables) => + fetcher(DeletePageDocument, variables)(), + ...options, + }) +} + +useDeletePageMutation.getKey = () => ['deletePage'] + +useDeletePageMutation.fetcher = ( + variables: DeletePageMutationVariables, + options?: RequestInit['headers'], +) => + fetcher(DeletePageDocument, variables, options) diff --git a/apps/book-web/src/graphql/bookServer/helpers/bookServerFetcher.ts b/apps/book-web/src/graphql/bookServer/helpers/bookServerFetcher.ts new file mode 100644 index 00000000..240dbca8 --- /dev/null +++ b/apps/book-web/src/graphql/bookServer/helpers/bookServerFetcher.ts @@ -0,0 +1,41 @@ +import { ENV } from '@/env' +import graphqlFetch from '@/lib/graphqlFetch' +import { pipe, subscribe } from 'wonka' +import { urqlClient } from './bookServerUrlql' +import { AnyVariables } from 'urql' + +export function fetcher>( + query: string, + variables?: TVariables, + headers?: RequestInit['headers'], +) { + return async (): Promise> => { + const data = await graphqlFetch({ + url: `${ENV.graphqlBookServerHost}/graphql`, + method: 'POST', + body: { query, variables: variables ?? {} }, + headers: { + ...headers, + }, + }) + + return data as Awaited + } +} +export function subscriptionFetcher( + query: string, + variables: TVariables, +) { + return new Promise((resolve) => { + const subscription = pipe( + urqlClient.subscription(query, variables), + subscribe((result) => { + if (result.data) { + resolve(result.data) + } + }), + ) + + return () => subscription.unsubscribe() + }) +} diff --git a/apps/book-web/src/graphql/bookServer/helpers/bookServerUrlql.ts b/apps/book-web/src/graphql/bookServer/helpers/bookServerUrlql.ts new file mode 100644 index 00000000..645024bc --- /dev/null +++ b/apps/book-web/src/graphql/bookServer/helpers/bookServerUrlql.ts @@ -0,0 +1,22 @@ +import { createClient, subscriptionExchange, cacheExchange, fetchExchange } from 'urql' +import { createClient as createWSClient, SubscribePayload } from 'graphql-ws' +import { ENV } from '@/env' + +const wsClient = createWSClient({ + url: `ws://${ENV.graphqlBookServerHost.replace(/^http(s)?:\/\//, '')}/graphql`, +}) + +export const urqlClient = createClient({ + url: `${ENV.graphqlBookServerHost}/graphql`, + exchanges: [ + cacheExchange, + fetchExchange, + subscriptionExchange({ + forwardSubscription: (operation) => ({ + subscribe: (sink) => ({ + unsubscribe: wsClient.subscribe(operation as SubscribePayload, sink), + }), + }), + }), + ], +}) diff --git a/apps/book-web/src/graphql/bookServer/page.gql b/apps/book-web/src/graphql/bookServer/page.gql new file mode 100644 index 00000000..689e681d --- /dev/null +++ b/apps/book-web/src/graphql/bookServer/page.gql @@ -0,0 +1,59 @@ +query getPages($input: GetPagesInput!) { + pages(input: $input) { + id + title + url_slug + type + parent_id + code + childrens { + id + title + url_slug + type + parent_id + code + childrens { + id + title + url_slug + type + parent_id + code + } + } + } +} + +query getPage($input: GetPageInput!) { + page(input: $input) { + id + title + url_slug + parent_id + body + } +} + +mutation createPage($input: CreatePageInput!) { + create(input: $input) { + id + } +} + +mutation reorderPage($input: ReorderInput!) { + reorder(input: $input) +} + +mutation updatePage($input: UpdatePageInput!) { + update(input: $input) { + id + title + body + is_deleted + } +} + +mutation deletePage($input: DeletePageInput!) { + delete(input: $input) +} diff --git a/apps/book-web/src/hooks/useUrlSlug.ts b/apps/book-web/src/hooks/useUrlSlug.ts new file mode 100644 index 00000000..c7587ac9 --- /dev/null +++ b/apps/book-web/src/hooks/useUrlSlug.ts @@ -0,0 +1,14 @@ +import { useRouter } from 'next/router' + +export const useUrlSlug = () => { + const router = useRouter() + + const { query } = router + + const pageUrlSlug = Array.isArray(query.pageUrlSlug) ? `/${query.pageUrlSlug.join('/')}` : '/' + const bookUrlSlug = `/${query.username}/${query.bookTitle}` + const username = `/${query.username}` + const fullUrlSlug = pageUrlSlug === '/' ? `${bookUrlSlug}` : `${bookUrlSlug}/${pageUrlSlug}` + + return { bookUrlSlug, pageUrlSlug, fullUrlSlug, username } +} diff --git a/apps/book-web/src/layouts/MarkdownEditorLayout/MarkdownEditorLayout.tsx b/apps/book-web/src/layouts/MarkdownEditorLayout/MarkdownEditorLayout.tsx new file mode 100644 index 00000000..d31011a1 --- /dev/null +++ b/apps/book-web/src/layouts/MarkdownEditorLayout/MarkdownEditorLayout.tsx @@ -0,0 +1,272 @@ +import { + MarkdownEditor, + CustomEventDetail, + markdownCustomEventName, +} from '@packages/markdown-editor' +import { themeConfig } from './context' +import { useEffect, useState } from 'react' +import { BookMetadata, generateBookMetadata, Pages } from '@/lib/generateBookMetadata' +import { + DeployCompletedDocument, + DeployCompletedPayload, + useBuildMutation, + useCreatePageMutation, + useDeletePageMutation, + useDeployMutation, + useGetPageQuery, + useGetPagesQuery, + useIsDeployQuery, + useReorderPageMutation, + useUpdatePageMutation, +} from '@/graphql/bookServer/generated/bookServer' +import { useUrlSlug } from '@/hooks/useUrlSlug' +import { useSubscription } from 'urql' + +type Props = { + children?: React.ReactNode + mdxText: string +} + +function MarkdownEditorLayout({ children, mdxText }: Props) { + const { bookUrlSlug, pageUrlSlug } = useUrlSlug() + const [bookMetadata, setBookMetadata] = useState(null) + const [mdx, setMdx] = useState(mdxText) + + const { mutateAsync: createPageAsyncMutate, isPending: isCreatePending } = useCreatePageMutation() + const { mutateAsync: reorderAsyncMutate, isPending: isReorderPending } = useReorderPageMutation() + const { mutateAsync: updatePageAsyncMutate, isPending: isUpdatePending } = useUpdatePageMutation() + const { mutateAsync: buildAsyncMutate } = useBuildMutation() + const { mutateAsync: deployAsyncMutate } = useDeployMutation() + const { mutateAsync: deletePageAsyncMutate } = useDeletePageMutation() + + const { + data: getPagesData, + refetch: getPagesRefetch, + isLoading: isGetPagesLoading, + } = useGetPagesQuery({ input: { book_url_slug: bookUrlSlug } }) + + const { + data: getPageData, + refetch: getPageRefetch, + isLoading: isGetPageLoading, + } = useGetPageQuery({ + input: { + book_url_slug: bookUrlSlug, + page_url_slug: pageUrlSlug, + }, + }) + + const [deployCompleted] = useSubscription({ + query: DeployCompletedDocument, + variables: { input: { book_url_slug: bookUrlSlug } }, + }) + + // 재접속시에 deploy 결과 확인 + useEffect(() => { + if (!deployCompleted.data) return + console.log(deployCompleted) + const { published_url } = deployCompleted.data + if (published_url) { + console.log('published_url in client', published_url) + const event = new CustomEvent(markdownCustomEventName.deployEndEvent, { + detail: { publishedUrl: published_url }, + }) + window.dispatchEvent(event) + } + }, [deployCompleted]) + + const { data: isDeployData } = useIsDeployQuery({ + input: { + book_url_slug: bookUrlSlug, + }, + }) + + useEffect(() => { + if (!getPagesData?.pages) return + const metadata = generateBookMetadata({ + pages: getPagesData.pages as Pages, + bookUrl: bookUrlSlug, + }) + setBookMetadata(metadata) + }, [getPagesData?.pages]) + + // 페이지 변경시 item 데이터 불러오기 + useEffect(() => { + getPageRefetch() + }, [pageUrlSlug]) + + // new Page data + useEffect(() => { + if (isGetPageLoading) return + if (!getPageData?.page) return + if (typeof getPageData.page?.body !== 'string') return + setMdx(getPageData.page.body) + }, [getPageData, isGetPageLoading]) + + // create item + useEffect(() => { + if (isCreatePending) return + const create = async (e: CustomEventInit) => { + if (!e.detail) return + const { title, parentUrlSlug, index, bookUrlSlug, type } = e.detail + await createPageAsyncMutate({ + input: { + title, + parent_url_slug: parentUrlSlug, + book_url_slug: bookUrlSlug, + index, + type, + }, + }) + getPagesRefetch() + } + window.addEventListener(markdownCustomEventName.createItemEvent, create) + return () => { + window.removeEventListener(markdownCustomEventName.createItemEvent, create) + } + }, []) + + // update item + useEffect(() => { + if (isUpdatePending) return + const update = async (e: CustomEventInit) => { + if (!e.detail) return + const bodyInput = { + book_url_slug: bookUrlSlug, + page_url_slug: pageUrlSlug, + body: e.detail?.body, + } + + const titleInput = { + book_url_slug: bookUrlSlug, + page_url_slug: e.detail.pageUrlSlug ?? '', + title: e.detail.title, + } + + const isUpdateTitle = !!e.detail.title + + try { + await updatePageAsyncMutate({ + input: isUpdateTitle ? titleInput : bodyInput, + }) + + const event = new CustomEvent(markdownCustomEventName.updateItemResultEvent, { + detail: { result: 'success' }, + }) + dispatchEvent(event) + } catch (error) { + const event = new CustomEvent(markdownCustomEventName.updateItemResultEvent, { + detail: { result: 'error' }, + }) + dispatchEvent(event) + } + + getPageRefetch() + + if (isUpdateTitle) { + getPagesRefetch() + } + } + + window.addEventListener(markdownCustomEventName.updateItemEvent, update) + return () => { + window.removeEventListener(markdownCustomEventName.updateItemEvent, update) + } + }, [pageUrlSlug, bookUrlSlug]) + + // change Item order + useEffect(() => { + const changeItem = async (e: CustomEventInit) => { + if (!e.detail) return + if (isReorderPending) return + + const { bookUrlSlug, targetUrlSlug, parentUrlSlug, index } = e.detail + await reorderAsyncMutate({ + input: { + book_url_slug: bookUrlSlug, + target_url_slug: targetUrlSlug, + parent_url_slug: parentUrlSlug, + index, + }, + }) + getPagesRefetch() + } + + window.addEventListener(markdownCustomEventName.changeItemOrderEvent, changeItem) + return () => { + window.removeEventListener(markdownCustomEventName.changeItemOrderEvent, changeItem) + } + }, []) + + // deploy start and dispatch deploy end event + useEffect(() => { + const deployStart = async () => { + const { build } = await buildAsyncMutate({ input: { book_url_slug: bookUrlSlug } }) + + if (!build.result) { + //TODO: show error + return + } + const { deploy } = await deployAsyncMutate({ input: { book_url_slug: bookUrlSlug } }) + const event = new CustomEvent(markdownCustomEventName.deployEndEvent, { + detail: { publishedUrl: deploy.published_url }, + }) + window.dispatchEvent(event) + } + + window.addEventListener(markdownCustomEventName.deployStartEvent, deployStart) + return () => { + window.removeEventListener(markdownCustomEventName.deployStartEvent, deployStart) + } + }, [bookUrlSlug]) + + // deleteItem + useEffect(() => { + const deleteItemStart = async (e: CustomEventInit) => { + if (!e.detail) return + await deletePageAsyncMutate({ + input: { + page_url_slug: e.detail.pageUrlSlug, + book_url_slug: bookUrlSlug, + }, + }) + getPagesRefetch() + } + window.addEventListener(markdownCustomEventName.deleteItemEvent, deleteItemStart) + return () => { + window.removeEventListener(markdownCustomEventName.deleteItemEvent, deleteItemStart) + } + }, [bookUrlSlug]) + + // check isDeploying... + useEffect(() => { + if (!isDeployData) return + const dispatchDeployEvent = () => { + const event = new CustomEvent(markdownCustomEventName.checkIsDeployEvent, { + detail: { + isDeploy: isDeployData.isDeploy, + }, + }) + window.dispatchEvent(event) + } + + const timeoutId = setTimeout(dispatchDeployEvent, 200) + return () => { + clearTimeout(timeoutId) + } + }, [bookUrlSlug, isDeployData]) + + if (isGetPagesLoading || !bookMetadata) return
loading...
+ return ( + + {children} + + ) +} + +export default MarkdownEditorLayout diff --git a/apps/book-web/src/layouts/MarkdownEditorLayout/context.tsx b/apps/book-web/src/layouts/MarkdownEditorLayout/context.tsx new file mode 100644 index 00000000..4f9de7b3 --- /dev/null +++ b/apps/book-web/src/layouts/MarkdownEditorLayout/context.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import { useRouter } from 'next/router' +import type { DocsThemeConfig } from '@packages/markdown-editor' + +const bookUrl = `/@test_carrick/learning-bunjs-is-fun` + +export const themeConfig: DocsThemeConfig = { + toc: {}, + logo: Learning bunJS is Fun!, + logoLink: `${bookUrl}`, + editLink: { + text: '', + }, + feedback: { + content: '', + }, + footer: { + component: null, + text: '', + }, + sidebar: { + defaultMenuCollapseLevel: 1, + toggleButton: true, + }, + useNextSeoProps() { + const { asPath } = useRouter() + if (asPath !== '/') { + return { + titleTemplate: '%s – Learning bunJS is Fun!', + } + } + }, + head: function useHead() { + const title = 'hello world' + return ( + <> + + + + + + + + + + + + + + + + + + ) + }, +} diff --git a/apps/book-web/src/layouts/MarkdownEditorLayout/index.tsx b/apps/book-web/src/layouts/MarkdownEditorLayout/index.tsx new file mode 100644 index 00000000..b9d218ef --- /dev/null +++ b/apps/book-web/src/layouts/MarkdownEditorLayout/index.tsx @@ -0,0 +1 @@ +export { default } from './MarkdownEditorLayout' diff --git a/apps/book-web/src/lib/.gitignore b/apps/book-web/src/lib/.gitignore new file mode 100644 index 00000000..f6caf378 --- /dev/null +++ b/apps/book-web/src/lib/.gitignore @@ -0,0 +1 @@ +context.json diff --git a/apps/book-web/src/lib/generateBookMetadata.ts b/apps/book-web/src/lib/generateBookMetadata.ts new file mode 100644 index 00000000..9e9f46b8 --- /dev/null +++ b/apps/book-web/src/lib/generateBookMetadata.ts @@ -0,0 +1,152 @@ +import type { DocsThemeConfig, PageMapItem, PageOpts } from '@packages/markdown-editor' +import { escapeForUrl } from './utils' +// import fs from 'node:fs' +// import path from 'node:path' + +export type Pages = Page[] +type Page = { + id: string + title: string + url_slug: string + type: string + parent_id: string | null + code: string + childrens?: Page[] +} + +type Args = { + pages: Pages + bookUrl: string +} + +export type BookMetadata = { + pageOpts: PageOpts + themeConfig: DocsThemeConfig +} + +type Data = { [key: string]: { title: string; type?: string } } + +const removeCode = (page: Page) => { + let route = page.parent_id === null ? '/' : page.url_slug.split('/').slice(0, -1).join('/') + if (route === '') { + route = page.url_slug.split('-')[0] + if (!route) { + console.log('route is empty', route) + } + } + return route +} + +const generatePageMap = (pages: Pages, bookUrl: string) => { + let init = false + const map = new Map() + + const createMeta = (pages: Page[], route: string) => { + return [ + { + id: route, + kind: 'Meta', + route, + data: pages.reduce((acc, page, index) => { + // create unique key + const key = + index === 0 && !init ? 'index' : `${escapeForUrl(`${page.title}-${page.code}`)}` + const value = { title: page.title } + // save key + map.set(page.url_slug, key) + if (page.type === 'separator') { + Object.assign(value, { id: page.url_slug, type: 'separator' }) + } + acc[key] = value + return acc + }, {} as Data), + }, + ] + } + + const createMdxPage = (page: Page) => { + if (init) { + const key = map.get(page.url_slug) + return [ + { + id: `${bookUrl}${page.url_slug}`, + kind: 'MdxPage', + name: key, + route: `${bookUrl}${page.url_slug}`, + code: page.code, + urlSlug: page.url_slug, + }, + ] + } + init = true + return [ + { + id: `${bookUrl}${page.url_slug}`, + kind: 'MdxPage', + name: 'index', + route: `${bookUrl}`, + code: page.code, + urlSlug: page.url_slug, + }, + ] + } + + const createFolder = (page: Page) => { + const key = map.get(page.url_slug) + const result = { + id: `${bookUrl}${page.url_slug}`, + kind: 'Folder', + name: key, + route: `${bookUrl}${page.url_slug}`, + code: page.code, + urlSlug: page.url_slug, + children: [], + } + + if (page.childrens && page.childrens.length > 0) { + const children = page.childrens.reduce(recursive, []).flat() + Object.assign(result, { children }) + return result + } + + if (page.childrens && page.childrens.length === 0) { + const route = page.url_slug.split('-').slice(0, -1).join('-') + const metaJson = createMeta([], route)[0] as PageMapItem + ;(result.children as PageMapItem[]).push(metaJson) + } + return result + } + + const recursive = (result: any[], page: Page, index: number, origin: Page[]) => { + if (index === 0) { + const route = removeCode(page) + result.push(createMeta(origin, route)) + } + if (page.type !== 'separator') { + result.push(createMdxPage(page)) + } + if (page.childrens && (page.childrens.length > 0 || page.type === 'folder')) { + result.push(createFolder(page)) + } + return result + } + + const pageMap = pages.reduce(recursive, []).flat() + return pageMap as PageMapItem[] +} + +export const generateBookMetadata = ({ pages, bookUrl }: Args): BookMetadata => { + const pageMap = generatePageMap(pages, bookUrl) + + return { + pageOpts: { + frontMatter: {}, + filePath: '', + route: '', + pageMap, + title: 'Welcome to Nextra', + headings: [], + }, + themeConfig: {}, + } +} diff --git a/apps/book-web/src/lib/graphqlFetch.ts b/apps/book-web/src/lib/graphqlFetch.ts new file mode 100644 index 00000000..ccd780c8 --- /dev/null +++ b/apps/book-web/src/lib/graphqlFetch.ts @@ -0,0 +1,99 @@ +import { ENV } from '@/env' + +export default async function graphqlFetch({ + url = `${ENV.graphqlBookServerHost}/graphql`, + method = 'POST', + body, + headers = {}, + next, + cache, + ...init +}: Parameter): Promise { + let targetUrl = url + + if (method === 'GET' && body) { + const queryString = convertToQueryString(body) + targetUrl = `${url}?${queryString}` + } + + const defaultHeaders = { + 'Content-Type': 'application/json', + } + + const combinedHeaders = new Headers({ + ...defaultHeaders, + ...headers, + }) + + const res = await fetch(targetUrl, { + method, + body: method.toUpperCase() === 'POST' ? JSON.stringify(body) : undefined, + headers: combinedHeaders, + credentials: 'include', + next, + cache, + ...init, + }) + + if (!res.ok) { + const errors = await res.json() + const allowOperationNameList = ['userTags', 'velogConfig'] + const allowStatusCode = [404] + if ( + !allowOperationNameList.includes(body?.operationName || '') && + allowStatusCode.includes(res.status) + ) { + console.log('graphqlFetch errors', errors) + console.log('body', body) + } + + if (process.env.NODE_ENV === 'development') { + console.log('fetch failed res:', res) + } + + const message = { + operationName: body?.operationName, + status: res.status, + statusText: res.statusText, + } + + throw new Error(message as any) + } + + const json = await res?.json() + return json.data as T +} + +function convertToQueryString(body: GraphqlRequestBody): string { + if (!body.query) { + throw new Error('The query field is required!') + } + const query = encodeURIComponent(body.query) + + const operationPart = body.operationName + ? `operationName=${encodeURIComponent(body.operationName)}&` + : '' + + // variables가 제공되지 않으면 빈 문자열을 반환 + const variablesPart = body?.variables + ? `&variables=${encodeURIComponent(JSON.stringify(body.variables))}` + : '' + + return `${operationPart}query=${query}${variablesPart}` +} + +type Parameter = { + url?: string + body?: GraphqlRequestBody + headers?: HeadersInit + init?: Omit + next?: NextFetchRequestConfig + cache?: RequestCache + method?: 'GET' | 'POST' +} + +export type GraphqlRequestBody> = { + operationName?: string + query: string + variables: T +} diff --git a/apps/book-web/src/lib/mdx/theme.ts b/apps/book-web/src/lib/mdx/theme.ts new file mode 100644 index 00000000..cb96525b --- /dev/null +++ b/apps/book-web/src/lib/mdx/theme.ts @@ -0,0 +1,200 @@ +const theme = { + name: 'css-variables', + type: 'css', + colors: { + // 'editor.foreground': '#000001', + // 'editor.background': '#000002', + // 'terminal.ansiBlack': '#A00000', + // 'terminal.ansiRed': '#A00001', + // 'terminal.ansiGreen': '#A00002', + // 'terminal.ansiYellow': '#A00003', + // 'terminal.ansiBlue': '#A00004', + // 'terminal.ansiMagenta': '#A00005', + // 'terminal.ansiCyan': '#A00006', + // 'terminal.ansiWhite': '#A00007', + // 'terminal.ansiBrightBlack': '#A00008', + // 'terminal.ansiBrightRed': '#A00009', + // 'terminal.ansiBrightGreen': '#A00010', + // 'terminal.ansiBrightYellow': '#A00011', + // 'terminal.ansiBrightBlue': '#A00012', + // 'terminal.ansiBrightMagenta': '#A00013', + // 'terminal.ansiBrightCyan': '#A00014', + // 'terminal.ansiBrightWhite': '#A00015', + }, + tokenColors: [ + { + settings: { + foreground: '#000001', + }, + }, + { + scope: ['markup.deleted', 'meta.diff.header.from-file', 'punctuation.definition.deleted'], + settings: { + foreground: '#ef6270', + }, + }, + { + scope: ['markup.inserted', 'meta.diff.header.to-file', 'punctuation.definition.inserted'], + settings: { + foreground: '#4bb74a', + }, + }, + { + scope: [ + 'keyword.operator.accessor', + 'meta.group.braces.round.function.arguments', + 'meta.template.expression', + 'markup.fenced_code meta.embedded.block', + ], + settings: { + foreground: '#000001', + }, + }, + { + scope: 'emphasis', + settings: { + fontStyle: 'italic', + }, + }, + { + scope: ['strong', 'markup.heading.markdown', 'markup.bold.markdown'], + settings: { + fontStyle: 'bold', + }, + }, + { + scope: ['markup.italic.markdown'], + settings: { + fontStyle: 'italic', + }, + }, + { + scope: 'meta.link.inline.markdown', + settings: { + fontStyle: 'underline', + foreground: '#000004', + }, + }, + { + scope: ['string', 'markup.fenced_code', 'markup.inline'], + settings: { + foreground: '#000005', + }, + }, + { + scope: ['comment', 'string.quoted.docstring.multi'], + settings: { + foreground: '#000006', + }, + }, + { + scope: [ + 'constant.numeric', + 'constant.language', + 'constant.other.placeholder', + 'constant.character.format.placeholder', + 'variable.language.this', + 'variable.other.object', + 'variable.other.class', + 'variable.other.constant', + 'meta.property-name', + 'meta.property-value', + 'support', + ], + settings: { + foreground: '#000004', + }, + }, + { + scope: [ + 'keyword', + 'storage.modifier', + 'storage.type', + 'storage.control.clojure', + 'entity.name.function.clojure', + 'entity.name.tag.yaml', + 'support.function.node', + 'support.type.property-name.json', + 'punctuation.separator.key-value', + 'punctuation.definition.template-expression', + ], + settings: { + foreground: '#000007', + }, + }, + { + scope: 'variable.parameter.function', + settings: { + foreground: '#000008', + }, + }, + { + scope: [ + 'support.function', + 'entity.name.type', + 'entity.other.inherited-class', + 'meta.function-call', + 'meta.instance.constructor', + 'entity.other.attribute-name', + 'entity.name.function', + 'constant.keyword.clojure', + ], + settings: { + foreground: '#000009', + }, + }, + { + scope: [ + 'entity.name.tag', + 'string.quoted', + 'string.regexp', + 'string.interpolated', + 'string.template', + 'string.unquoted.plain.out.yaml', + 'keyword.other.template', + ], + settings: { + foreground: '#000010', + }, + }, + { + scope: [ + 'punctuation.definition.arguments', + 'punctuation.definition.dict', + 'punctuation.separator', + 'meta.function-call.arguments', + ], + settings: { + foreground: '#000011', + }, + }, + { + name: '[Custom] Markdown links', + scope: ['markup.underline.link', 'punctuation.definition.metadata.markdown'], + settings: { + foreground: '#000012', + }, + }, + { + name: '[Custom] Markdown list', + scope: ['beginning.punctuation.definition.list.markdown'], + settings: { + foreground: '#000005', + }, + }, + { + name: '[Custom] Markdown punctuation definition brackets', + scope: [ + 'punctuation.definition.string.begin.markdown', + 'punctuation.definition.string.end.markdown', + 'string.other.link.title.markdown', + 'string.other.link.description.markdown', + ], + settings: { + foreground: '#000007', + }, + }, + ], +} + +export default theme diff --git a/apps/book-web/src/lib/mdx/utils.ts b/apps/book-web/src/lib/mdx/utils.ts new file mode 100644 index 00000000..2924cd1a --- /dev/null +++ b/apps/book-web/src/lib/mdx/utils.ts @@ -0,0 +1,5 @@ +type Truthy = T extends false | '' | 0 | null | undefined ? never : T // from lodash + +export function truthy(value: T): value is Truthy { + return !!value +} diff --git a/apps/book-web/src/lib/utils.ts b/apps/book-web/src/lib/utils.ts new file mode 100644 index 00000000..188cca0e --- /dev/null +++ b/apps/book-web/src/lib/utils.ts @@ -0,0 +1,11 @@ +export const escapeForUrl = (text: string): string => { + return text + .replace( + /[^0-9a-zA-Zㄱ-힣.\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf -]/g, + '', + ) + .trim() + .replace(/ /g, '-') + .replace(/--+/g, '-') + .replace(/\.+$/, '') +} diff --git a/apps/book-web/src/pages/404.tsx b/apps/book-web/src/pages/404.tsx new file mode 100644 index 00000000..dd2b0f5c --- /dev/null +++ b/apps/book-web/src/pages/404.tsx @@ -0,0 +1,8 @@ +export default function Custom404() { + return ( +
+

404 - Page Not Found

+

Sorry, the page you are looking for does not exist.

+
+ ) +} diff --git a/apps/book-web/src/pages/[username]/[bookTitle]/[[...pageUrlSlug]].tsx b/apps/book-web/src/pages/[username]/[bookTitle]/[[...pageUrlSlug]].tsx new file mode 100644 index 00000000..76049d92 --- /dev/null +++ b/apps/book-web/src/pages/[username]/[bookTitle]/[[...pageUrlSlug]].tsx @@ -0,0 +1,141 @@ +import MarkdownEditorLayout from '@/layouts/MarkdownEditorLayout' +import getPage from '@/prefetch/getPage' +import getPages from '@/prefetch/getPages' + +import { GetServerSideProps } from 'next' + +import { useRouter } from 'next/router' + +type Props = { + mdxText: string +} + +const Home = ({ mdxText }: Props) => { + const router = useRouter() + + if (router.isFallback) { + return
isFallback Loading...
+ } + + return +} + +export default Home + +export const getServerSideProps = (async (ctx) => { + const { params, req } = ctx + const bookUrlSlug = `/${params?.username}/${params?.bookTitle}` + + let pageUrlSlug = params?.pageUrlSlug ? `/${params?.pageUrlSlug?.join('/')}` : '' + + if (!pageUrlSlug) { + const pages = await getPages(bookUrlSlug, req.cookies) + + if (!pages) { + throw new Error('failed get book metadata') + } + pageUrlSlug = pages[0].url_slug + } + + const page = await getPage(bookUrlSlug, pageUrlSlug, req.cookies) + const mdxText = page?.body ?? '' + // const mdxText = ` + // ${bookUrlSlug} hello! + + // {/* wrapped with {} to mark it as javascript so mdx will not put it under a p tag */} + // {

SWR

} + + // El nombre “SWR” es derivado de \`stale-while-revalidate\`, una estrategia de + // invalidación de caché HTTP popularizada por + // [HTTP RFC 5861](https://tools.ietf.org/html/rfc5861). SWR es una estrategia para + // devolver primero los datos en caché (obsoletos), luego envíe la solicitud de + // recuperación (revalidación), y finalmente entrege los datos actualizados. + + //
+ // Con SWR, el componente obtendrá constante y + // automáticamente el último flujo de datos. + //
Y la interfaz de usuario será siempre rápida y + // reactiva + // . + //
+ + //
+ // [Get Started](/docs/getting-started) · [Examples](/examples) · [Blog](/blog) · + // [GitHub Repository](https://github.com/vercel/swr) + //
+ + // ## Resumen + + // \`\`\`jsx filename="hello.js" {4-4} + // import useSWR from 'swr' + + // function Profile() { + // const { data, error } = useSWR('/api/user', fetcher) + + // if (error) return
failed to load
+ // if (!data) return
loading...
+ // return
hello {data.name}!
+ // } + // \`\`\` + + // En este ejemplo, el hook \`useSWR\` acepta una \`key\` que es un cadena y una + // función \`fetcher\`. \`key\` es un indentificador único de los datos (normalmente la + // URL de la API) y pasará al \`fetcher\`. El \`fetcher\` puede ser cualquier función + // asíncrona que devuelve datos, puedes utilizar el fetch nativo o herramientas + // como Axios. + + // El hook devuelve 2 valores: \`data\` y \`error\`, basados en el estado de la + // solicitud. + + // ## Características + + // Con una sola línea de código, puede simplificar la obtención de datos en su + // proyecto, y tambien tiene todas estas increíbles características fuera de caja: + + // - Obtención de datos **rápida**, **ligera** y **reutilizable** + // - Caché integrada y deduplicación de solicitudes + // - Experiencia en **tiempo real** + // - Agnóstico al transporte y al protocolo + // - SSR / ISR / SSG support + // - TypeScript ready + // - React Native + + // SWR le cubre en todos los aspectos de velocidad, correción, y estabilidad para + // ayudarle a construir mejores experiencias: + + // - Navegación rápida por la página + // - Polling on interval + // - Dependencia de los datos + // - Revalidation on focus + // - Revalidation on network recovery + // - Mutación local (Optimistic UI) + // - Smart error retry + // - Pagination and scroll position recovery + // - React Suspense + + // And lot [more](/docs/getting-started). + + // ## Comunidad + + //

+ // stars + // downloads + // license + //

+ + // SWR es creado por el mismo equipo que esta detrás de + // [Next.js](https://nextjs.org), el framework de React. Sigan + // [@vercel](https://twitter.com/vercel) en Twitter para futuras actualizaciones + // del proyecto. + + // Sientase libre de unirse a + // [discusiones en GitHub](https://github.com/vercel/swr/discussions)! + // ` + + return { props: { mdxText } } +}) satisfies GetServerSideProps< + { + mdxText: string + }, + { username: string; bookTitle: string; pageUrlSlug: string[] } +> diff --git a/apps/book-web/src/pages/_app.tsx b/apps/book-web/src/pages/_app.tsx new file mode 100644 index 00000000..83f8f6c8 --- /dev/null +++ b/apps/book-web/src/pages/_app.tsx @@ -0,0 +1,25 @@ +import '@packages/markdown-editor/style.css' +import '../styles/global.css' +import { ErrorBoundary } from 'next/dist/client/components/error-boundary' + +import type { AppProps } from 'next/app' +import CoreProvider from '@/provider/CoreProvider' +import { HydrationBoundary } from '@tanstack/react-query' + +type Props = { + mdxSource: string +} & AppProps + +const App = ({ Component, pageProps }: Props) => { + return ( + + + + + + + + ) +} + +export default App diff --git a/apps/book-web/src/pages/_document.tsx b/apps/book-web/src/pages/_document.tsx new file mode 100644 index 00000000..54e8bf3e --- /dev/null +++ b/apps/book-web/src/pages/_document.tsx @@ -0,0 +1,13 @@ +import { Html, Head, Main, NextScript } from 'next/document' + +export default function Document() { + return ( + + + +
+ + + + ) +} diff --git a/apps/book-web/src/prefetch/getPage.ts b/apps/book-web/src/prefetch/getPage.ts new file mode 100644 index 00000000..5a4f6426 --- /dev/null +++ b/apps/book-web/src/prefetch/getPage.ts @@ -0,0 +1,45 @@ +import { + GetPageDocument, + type GetPageQuery, + type GetPageQueryVariables, +} from '@/graphql/bookServer/generated/bookServer' +import type { GraphqlRequestBody } from '@/lib/graphqlFetch' +import type { NextApiRequestCookies } from 'next/dist/server/api-utils' +import graphqlFetch from '@/lib/graphqlFetch' + +export default async function getPage( + bookUrlSlug: string, + pageUrlSlug: string, + cookies: NextApiRequestCookies, +) { + try { + const headers = {} + const token = cookies.access_token + + if (token) { + Object.assign(headers, { authorization: `Bearer ${token}` }) + } + + const body: GraphqlRequestBody = { + operationName: 'getPage', + query: GetPageDocument, + variables: { + input: { + book_url_slug: bookUrlSlug, + page_url_slug: pageUrlSlug, + }, + }, + } + + const data = await graphqlFetch({ + method: 'GET', + body, + headers, + }) + + return data?.page + } catch (error) { + console.error('Get page error:', error) + return null + } +} diff --git a/apps/book-web/src/prefetch/getPages.ts b/apps/book-web/src/prefetch/getPages.ts new file mode 100644 index 00000000..23ed250a --- /dev/null +++ b/apps/book-web/src/prefetch/getPages.ts @@ -0,0 +1,35 @@ +import { GetPagesDocument, type GetPagesQuery } from '@/graphql/bookServer/generated/bookServer' +import graphqlFetch, { type GraphqlRequestBody } from '@/lib/graphqlFetch' +import { NextApiRequestCookies } from 'next/dist/server/api-utils' + +export default async function getPages(bookUrlSlug: string, cookies: NextApiRequestCookies) { + try { + const headers = {} + const token = cookies.access_token + + if (token) { + Object.assign(headers, { authorization: `Bearer ${token}` }) + } + + const body: GraphqlRequestBody = { + operationName: 'getPages', + query: GetPagesDocument, + variables: { + input: { + book_url_slug: bookUrlSlug, + }, + }, + } + + const data = await graphqlFetch({ + method: 'GET', + body, + headers, + }) + + return data?.pages || [] + } catch (error) { + console.error('Get pages error:', error) + return null + } +} diff --git a/apps/book-web/src/provider/CoreProvider.tsx b/apps/book-web/src/provider/CoreProvider.tsx new file mode 100644 index 00000000..41b6f5b4 --- /dev/null +++ b/apps/book-web/src/provider/CoreProvider.tsx @@ -0,0 +1,18 @@ +import ReactQueryProvider from './ReactQueryProvider' +import UrqlProvider from './UrqlProvider' + +type Props = { + children: React.ReactNode +} + +function CoreProvider({ children }: Props) { + return ( + <> + + {children} + + + ) +} + +export default CoreProvider diff --git a/apps/book-web/src/provider/ReactQueryProvider.tsx b/apps/book-web/src/provider/ReactQueryProvider.tsx new file mode 100644 index 00000000..86d977ac --- /dev/null +++ b/apps/book-web/src/provider/ReactQueryProvider.tsx @@ -0,0 +1,12 @@ +import { QueryClientProvider, QueryClient } from '@tanstack/react-query' + +type Props = { + children: React.ReactNode +} + +function ReactQueryProvider({ children }: Props) { + const queryClient = new QueryClient() + return {children} +} + +export default ReactQueryProvider diff --git a/apps/book-web/src/provider/UrqlProvider.tsx b/apps/book-web/src/provider/UrqlProvider.tsx new file mode 100644 index 00000000..8a6f7c33 --- /dev/null +++ b/apps/book-web/src/provider/UrqlProvider.tsx @@ -0,0 +1,35 @@ +import { createClient, subscriptionExchange, cacheExchange, fetchExchange, Provider } from 'urql' +import { createClient as createWSClient } from 'graphql-ws' +import { ENV } from '@/env' + +type Props = { + children: React.ReactNode +} + +const wsClient = createWSClient({ + url: `ws://${ENV.graphqlBookServerHost.replace(/^http(s)?:\/\//, '')}/graphql`, +}) + +const urqlClient = createClient({ + url: `${ENV.graphqlBookServerHost}/graphql`, + exchanges: [ + cacheExchange, + fetchExchange, + subscriptionExchange({ + forwardSubscription: (request) => ({ + subscribe: (sink) => { + const input = { ...request, query: request.query || '' } + return { + unsubscribe: wsClient.subscribe(input, sink), + } + }, + }), + }), + ], +}) + +function UrqlProvider({ children }: Props) { + return {children} +} + +export default UrqlProvider diff --git a/apps/book-web/src/styles/global.css b/apps/book-web/src/styles/global.css new file mode 100644 index 00000000..022306f6 --- /dev/null +++ b/apps/book-web/src/styles/global.css @@ -0,0 +1,3 @@ +body { + overflow: hidden; +} diff --git a/apps/book-web/tailwind.config.ts b/apps/book-web/tailwind.config.ts new file mode 100644 index 00000000..8149869f --- /dev/null +++ b/apps/book-web/tailwind.config.ts @@ -0,0 +1,19 @@ +import type { Config } from 'tailwindcss' + +const config: Config = { + content: [ + './src/pages/**/*.{js,ts,jsx,tsx,mdx}', + './src/components/**/*.{js,ts,jsx,tsx,mdx}', + './src/app/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + backgroundImage: { + 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', + 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', + }, + }, + }, + plugins: [], +} +export default config diff --git a/apps/book-web/tsconfig.json b/apps/book-web/tsconfig.json new file mode 100644 index 00000000..6c36c7ea --- /dev/null +++ b/apps/book-web/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "@packages/tsconfig/next.json", + "compilerOptions": { + "baseUrl": "./", + "rootDir": "./", + "typeRoots": ["node_modules/@types/", "./src/types/", "./node_modules/@packages/**/**/*.d.ts"], + "paths": { + "@/*": ["./src/*"], + "@/public/*": ["./public/*"] + }, + "strictNullChecks": true + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "public/*", + "src/assets/**/svg", + "scripts/*", + "next.config.mjs", + "codegen.ts", + "eslint.config.mjs", + ".next/types/**/*.ts", + "src/env.ts" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/velog-cron/.dockerignore b/apps/cron/.dockerignore similarity index 100% rename from packages/velog-cron/.dockerignore rename to apps/cron/.dockerignore diff --git a/apps/cron/.gitignore b/apps/cron/.gitignore new file mode 100644 index 00000000..d8c314ed --- /dev/null +++ b/apps/cron/.gitignore @@ -0,0 +1,9 @@ +env/* +!env/.env.example + +prisma + +dist + +# spam test data +spam_post.json \ No newline at end of file diff --git a/packages/velog-server/.prettierrc b/apps/cron/.prettierrc similarity index 100% rename from packages/velog-server/.prettierrc rename to apps/cron/.prettierrc diff --git a/apps/cron/Dockerfile b/apps/cron/Dockerfile new file mode 100644 index 00000000..17d8c413 --- /dev/null +++ b/apps/cron/Dockerfile @@ -0,0 +1,70 @@ +FROM node:20.16.0-alpine AS base +RUN apk update +RUN apk add --no-cache libc6-compat + +# RUN corepack enable +RUN npm install --global corepack@latest + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN pnpm add -g turbo prisma +RUN turbo telemetry disable + +ARG DOCKER_ENV +ENV DOCKER_ENV=${DOCKER_ENV} +RUN echo "DOCKER_ENV value is: ${DOCKER_ENV}" + +ARG AWS_ACCESS_KEY_ID +ENV AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} + +ARG AWS_SECRET_ACCESS_KEY +ENV AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} + +ARG TURBO_TEAM +ENV TURBO_TEAM=${TURBO_TEAM} + +ARG TURBO_TOKEN +ENV TURBO_TOKEN=${TURBO_TOKEN} + +ARG TURBO_REMOTE_CACHE_SIGNATURE_KEY +ENV TURBO_REMOTE_CACHE_SIGNATURE_KEY=$TURBO_REMOTE_CACHE_SIGNATURE_KEY + +ENV APP_NAME="cron" +ENV APP_DIR="apps/${APP_NAME}" +RUN echo "APP_DIR value is: ${APP_DIR}" + +WORKDIR /app + +FROM base AS pruner +COPY . . +RUN turbo prune ${APP_NAME} --docker + +FROM base As builder +COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml +COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml +COPY --from=pruner /app/out/json/ ./ +COPY --from=pruner /app/out/full/ ./ + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prefer-frozen-lockfile + +RUN pnpm --filter @packages/database prisma:generate +RUN pnpm --filter ${APP_NAME} ssm pull -e ${DOCKER_ENV} +RUN turbo build --filter=${APP_NAME} --remote-only + +FROM base As runner +COPY --from=builder /app/package*.json . +COPY --from=builder /app/pnpm-*.yaml . +COPY --from=builder /app/packages/ /app/packages + +WORKDIR /app/${APP_DIR} + +COPY --from=builder /app/${APP_DIR}/dist /app/${APP_DIR}/dist +COPY --from=builder /app/${APP_DIR}/*.json . +COPY --from=builder /app/${APP_DIR}/env /app/${APP_DIR}/env + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile + +EXPOSE 5003 + +CMD [ "pnpm", "start" ] diff --git a/apps/cron/docker-compose.sh b/apps/cron/docker-compose.sh new file mode 100755 index 00000000..c8c5724b --- /dev/null +++ b/apps/cron/docker-compose.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +if [ -z "$AWS_PROFILE" ]; then + echo "AWS_PROFILE이 설정되어 있지 않습니다." + exit 1 +fi + +# AWS 프로필 설정 읽기 +export AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id --profile $AWS_PROFILE) +export AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key --profile $AWS_PROFILE) +export TURBO_TEAM=$(aws configure get turbo_team --profile $AWS_PROFILE) +export TURBO_TOKEN=$(aws configure get turbo_token --profile $AWS_PROFILE) +export TURBO_REMOTE_CACHE_SIGNATURE_KEY=$(aws configure get turbo_remote_cache_signature_key --profile $AWS_PROFILE) + +# echo "AWS 프로필 설정이 완료되었습니다. $AWS_PROFILE 프로필을 사용합니다." +# echo "AWS_ACCESSKEY_ID: $AWS_ACCESS_KEY_ID" +# echo "AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY" +# echo "TURBO_TEAM: $TURBO_TEAM" +# echo "TURBO_TOKEN: $TURBO_TOKEN" +# echo "TURBO_REMOTE_CACHE_SIGNATURE_KEY: $TURBO_REMOTE_CACHE_SIGNATURE_KEY" + +docker-compose up --build \ No newline at end of file diff --git a/apps/cron/docker-compose.yml b/apps/cron/docker-compose.yml new file mode 100755 index 00000000..a11bef37 --- /dev/null +++ b/apps/cron/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3.9' +services: + cron: + container_name: velog-cron + build: + context: ../../ + dockerfile: ./apps/cron/Dockerfile + no_cache: true + args: + DOCKER_ENV: stage + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + TURBO_TEAM: ${TURBO_TEAM} + TURBO_TOKEN: ${TURBO_TOKEN} + TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${TURBO_REMOTE_CACHE_SIGNATURE_KEY} + tty: true + stdin_open: true + ports: + - 5004:5004 + deploy: + resources: + limits: + cpus: '1' # range 0.01 to 10.00 + memory: '2G' + reservations: + cpus: '0.25' + memory: '256M' diff --git a/apps/cron/env/.env.example b/apps/cron/env/.env.example new file mode 100644 index 00000000..593566d8 --- /dev/null +++ b/apps/cron/env/.env.example @@ -0,0 +1,8 @@ +PORT= +DATABASE_URL= +CRON_API_KEY= +REDIS_HOST= +REDIS_PORT= +DISCORD_BOT_TOKEN= +DISCORD_STATS_CHANNEL= +DISCORD_SPAM_CHANNEL= \ No newline at end of file diff --git a/apps/cron/eslint.config.js b/apps/cron/eslint.config.js new file mode 100644 index 00000000..3871f86d --- /dev/null +++ b/apps/cron/eslint.config.js @@ -0,0 +1,12 @@ +import baseConfig from '@packages/eslint-config/base.mjs' +import { resolve } from 'node:path' + +const projectPath = resolve(process.cwd()) + +/** @type {Linter.Config} */ +export default [ + ...baseConfig(projectPath), + { + ignores: ['node_modules', 'dist'], + }, +] diff --git a/apps/cron/package.json b/apps/cron/package.json new file mode 100644 index 00000000..25acd143 --- /dev/null +++ b/apps/cron/package.json @@ -0,0 +1,60 @@ +{ + "name": "cron", + "version": "1.0.0", + "author": { + "name": "velopert", + "email": "public.velopert@gmail.com" + }, + "description": "", + "keywords": [ + "velog", + "cron" + ], + "license": "MIT", + "engines": { + "node": ">=20.11.1" + }, + "main": "/src/main.ts", + "type": "module", + "scripts": { + "dev": "nodemon --watch './**/*.mts' --exec 'node --import @swc-node/register/esm-register' ./src/main.mts | pino-pretty", + "start": "NODE_ENV=production node dist/main.mjs", + "build": "tsc --project tsconfig.build.json && tsc-alias -p tsconfig.build.json", + "lint": "eslint --fix", + "create-service": "tsx ./scripts/createService.ts", + "ssm": "tsx ./scripts/ssm.mts" + }, + "dependencies": { + "@fastify/cors": "^8.3.0", + "@packages/database": "workspace:*", + "@packages/scripts": "workspace:*", + "@prisma/client": "^5.17.0", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.1.3", + "discord.js": "^14.14.1", + "dotenv": "^16.4.5", + "fastify": "^4.26.2", + "fastify-cron": "^1.3.1", + "fastify-plugin": "^4.5.1", + "geoip-country": "^4.2.68", + "inquirer": "^9.2.23", + "ioredis": "^5.3.2", + "pino-pretty": "^10.0.0", + "prisma": "^5.17.0", + "reflect-metadata": "^0.1.13", + "tsyringe": "^4.7.0", + "zod": "^3.21.4" + }, + "devDependencies": { + "@packages/eslint-config": "workspace:*", + "@packages/tsconfig": "workspace:*", + "@swc-node/register": "^1.9.1", + "@types/geoip-country": "^4.0.2", + "@types/inquirer": "^9.0.7", + "@types/jest": "^29.5.2", + "nodemon": "^2.0.22", + "ts-paths-esm-loader": "^1.4.3", + "tsc-alias": "^1.8.7", + "tsx": "^4.7.2" + } +} diff --git a/packages/velog-common/scripts/createService.ts b/apps/cron/scripts/createService.ts similarity index 100% rename from packages/velog-common/scripts/createService.ts rename to apps/cron/scripts/createService.ts diff --git a/apps/cron/scripts/ssm.mts b/apps/cron/scripts/ssm.mts new file mode 100644 index 00000000..57226666 --- /dev/null +++ b/apps/cron/scripts/ssm.mts @@ -0,0 +1,4 @@ +import { SSMScript } from '@packages/scripts' + +const ssmScript = new SSMScript({ packageName: 'cron' }) +ssmScript.execute() diff --git a/packages/velog-common/scripts/templates/lib/My.test.ts b/apps/cron/scripts/templates/lib/My.test.ts similarity index 81% rename from packages/velog-common/scripts/templates/lib/My.test.ts rename to apps/cron/scripts/templates/lib/My.test.ts index 2b4ab086..2fe326e1 100644 --- a/packages/velog-common/scripts/templates/lib/My.test.ts +++ b/apps/cron/scripts/templates/lib/My.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { MyService } from './MyService' +import { MyService } from './MyService.js' describe('MyService', () => { const service = container.resolve(MyService) diff --git a/packages/velog-common/scripts/templates/lib/MyService.ts b/apps/cron/scripts/templates/lib/MyService.ts similarity index 100% rename from packages/velog-common/scripts/templates/lib/MyService.ts rename to apps/cron/scripts/templates/lib/MyService.ts diff --git a/packages/velog-common/scripts/templates/services/My.test.ts b/apps/cron/scripts/templates/services/My.test.ts similarity index 100% rename from packages/velog-common/scripts/templates/services/My.test.ts rename to apps/cron/scripts/templates/services/My.test.ts diff --git a/packages/velog-common/scripts/templates/services/index.ts b/apps/cron/scripts/templates/services/index.ts similarity index 100% rename from packages/velog-common/scripts/templates/services/index.ts rename to apps/cron/scripts/templates/services/index.ts diff --git a/apps/cron/src/app.mts b/apps/cron/src/app.mts new file mode 100644 index 00000000..ec52fa87 --- /dev/null +++ b/apps/cron/src/app.mts @@ -0,0 +1,25 @@ +import Fastify from 'fastify' +import fastifyCron from 'fastify-cron' +import routes from '@routes/index.mjs' +import closePlugin from '@plugins/encapsulated/closePlugin.mjs' +import checkApiKeyPlugin from '@plugins/global/checkApiKeyPlugin.mjs' +import corsPlugin from '@plugins/global/corsPlugin.mjs' +import errorHandlerPlugin from '@plugins/global/errorHandlerPlugin.mjs' +import cronPlugin from '@plugins/global/cronPlugin.mjs' + +const app = Fastify({ + logger: true, + pluginTimeout: 60000, +}) + +app.register(checkApiKeyPlugin) +app.register(corsPlugin) +app.register(errorHandlerPlugin) +app.register(closePlugin) +await app.register(fastifyCron.default) + +await app.register(cronPlugin) + +app.register(routes) + +export default app diff --git a/packages/velog-cron/src/common/constants/HttpStatusConstants.ts b/apps/cron/src/common/constants/HttpStatusConstants.mts similarity index 100% rename from packages/velog-cron/src/common/constants/HttpStatusConstants.ts rename to apps/cron/src/common/constants/HttpStatusConstants.mts diff --git a/packages/velog-cron/src/common/constants/HttpStatusMessageConstants.ts b/apps/cron/src/common/constants/HttpStatusMessageConstants.mts similarity index 100% rename from packages/velog-cron/src/common/constants/HttpStatusMessageConstants.ts rename to apps/cron/src/common/constants/HttpStatusMessageConstants.mts diff --git a/apps/cron/src/common/errors/BadRequestErrors.mts b/apps/cron/src/common/errors/BadRequestErrors.mts new file mode 100644 index 00000000..80549542 --- /dev/null +++ b/apps/cron/src/common/errors/BadRequestErrors.mts @@ -0,0 +1,7 @@ +import { HttpError } from './HttpError.mjs' + +export class BadRequestError extends HttpError { + constructor(message = 'BAD_REQUEST') { + super('bad request', message, 400) + } +} diff --git a/apps/cron/src/common/errors/ConfilctError.mts b/apps/cron/src/common/errors/ConfilctError.mts new file mode 100644 index 00000000..abfec997 --- /dev/null +++ b/apps/cron/src/common/errors/ConfilctError.mts @@ -0,0 +1,7 @@ +import { HttpError } from './HttpError.mjs' + +export class ConfilctError extends HttpError { + constructor(message = 'CONFILCT') { + super('confilct', message, 409) + } +} diff --git a/apps/cron/src/common/errors/ForbiddenError.mts b/apps/cron/src/common/errors/ForbiddenError.mts new file mode 100644 index 00000000..a652b8e5 --- /dev/null +++ b/apps/cron/src/common/errors/ForbiddenError.mts @@ -0,0 +1,7 @@ +import { HttpError } from './HttpError.mjs' + +export class ForbiddenError extends HttpError { + constructor(message = 'FORBIDDEN') { + super('forbidden', message, 403) + } +} diff --git a/packages/velog-server/src/common/errors/HttpError.ts b/apps/cron/src/common/errors/HttpError.mts similarity index 100% rename from packages/velog-server/src/common/errors/HttpError.ts rename to apps/cron/src/common/errors/HttpError.mts diff --git a/apps/cron/src/common/errors/NotfoundError.mts b/apps/cron/src/common/errors/NotfoundError.mts new file mode 100644 index 00000000..5ccd7772 --- /dev/null +++ b/apps/cron/src/common/errors/NotfoundError.mts @@ -0,0 +1,7 @@ +import { HttpError } from './HttpError.mjs' + +export class NotFoundError extends HttpError { + constructor(message = 'NOT_FOUND') { + super('not found', message, 404) + } +} diff --git a/apps/cron/src/common/errors/UnauthorizedError.mts b/apps/cron/src/common/errors/UnauthorizedError.mts new file mode 100644 index 00000000..e74b181d --- /dev/null +++ b/apps/cron/src/common/errors/UnauthorizedError.mts @@ -0,0 +1,7 @@ +import { HttpError } from './HttpError.mjs' + +export class UnauthorizedError extends HttpError { + constructor(message = 'Unauthorized') { + super('unauthorized', message, 401) + } +} diff --git a/apps/cron/src/common/errors/index.mts b/apps/cron/src/common/errors/index.mts new file mode 100644 index 00000000..8fc42736 --- /dev/null +++ b/apps/cron/src/common/errors/index.mts @@ -0,0 +1,6 @@ +export { BadRequestError } from './BadRequestErrors.mjs' +export { ConfilctError } from './ConfilctError.mjs' +export { ForbiddenError } from './ForbiddenError.mjs' +export { HttpError, isHttpError } from './HttpError.mjs' +export { NotFoundError } from './NotfoundError.mjs' +export { UnauthorizedError } from './UnauthorizedError.mjs' diff --git a/packages/velog-cron/src/common/plugins/encapsulated/closePlugin.ts b/apps/cron/src/common/plugins/encapsulated/closePlugin.mts similarity index 100% rename from packages/velog-cron/src/common/plugins/encapsulated/closePlugin.ts rename to apps/cron/src/common/plugins/encapsulated/closePlugin.mts diff --git a/packages/velog-cron/src/common/plugins/globals/checkApiKeyPlugin.ts b/apps/cron/src/common/plugins/global/checkApiKeyPlugin.mts similarity index 77% rename from packages/velog-cron/src/common/plugins/globals/checkApiKeyPlugin.ts rename to apps/cron/src/common/plugins/global/checkApiKeyPlugin.mts index 8c5113dd..ac7c3b2a 100644 --- a/packages/velog-cron/src/common/plugins/globals/checkApiKeyPlugin.ts +++ b/apps/cron/src/common/plugins/global/checkApiKeyPlugin.mts @@ -1,6 +1,7 @@ import { ENV } from '@env' -import { UnauthorizedError } from '@errors/UnauthorizedError.js' +import { UnauthorizedError } from '@errors/UnauthorizedError.mjs' import { FastifyPluginCallback } from 'fastify' +import fp from 'fastify-plugin' const checkApiKeyPlugin: FastifyPluginCallback = (fastify, opts, done) => { fastify.addHook('preHandler', (request, reply, done) => { @@ -17,4 +18,4 @@ const checkApiKeyPlugin: FastifyPluginCallback = (fastify, opts, done) => { done() } -export default checkApiKeyPlugin +export default fp(checkApiKeyPlugin) diff --git a/packages/velog-cron/src/common/plugins/globals/corsPlugin.ts b/apps/cron/src/common/plugins/global/corsPlugin.mts similarity index 86% rename from packages/velog-cron/src/common/plugins/globals/corsPlugin.ts rename to apps/cron/src/common/plugins/global/corsPlugin.mts index 561c45f8..996bb1c3 100644 --- a/packages/velog-cron/src/common/plugins/globals/corsPlugin.ts +++ b/apps/cron/src/common/plugins/global/corsPlugin.mts @@ -1,7 +1,8 @@ import { FastifyPluginCallback } from 'fastify' import { ENV } from '@env' import cors from '@fastify/cors' -import { ForbiddenError } from '@errors/ForbiddenError.js' +import { ForbiddenError } from '@errors/ForbiddenError.mjs' +import fp from 'fastify-plugin' const corsPlugin: FastifyPluginCallback = (fastify, opts, done) => { const corsWhitelist: RegExp[] = [ @@ -30,4 +31,4 @@ const corsPlugin: FastifyPluginCallback = (fastify, opts, done) => { done() } -export default corsPlugin +export default fp(corsPlugin) diff --git a/apps/cron/src/common/plugins/global/cronPlugin.mts b/apps/cron/src/common/plugins/global/cronPlugin.mts new file mode 100644 index 00000000..0eed8390 --- /dev/null +++ b/apps/cron/src/common/plugins/global/cronPlugin.mts @@ -0,0 +1,186 @@ +import fp from 'fastify-plugin' +import { StatsDaily, StatsWeekly, StatsMonthly } from '@jobs/stats/index.js' +import { GenerateFeedJob } from '@jobs/GenerateFeedJob.mjs' +import { CalculatePostScoreJob } from '@jobs/CalculatePostScoreJob.mjs' +import { GenerateTrendingWritersJob } from '@jobs/GenerateTrendingWritersJob.mjs' +import { DeleteFeedJob } from '@jobs/DeleteFeedJob.mjs' +import type { FastifyPluginAsync } from 'fastify' +import { container } from 'tsyringe' +import { ENV } from '@env' +import { CheckPostSpamJob } from '@jobs/CheckPostSpamJob.mjs' +import { DeletePostReadJob } from '@jobs/DeletePostReadJob.mjs' +import { ScorePostJob } from '@jobs/ScorePostJob.mjs' + +const cronPlugin: FastifyPluginAsync = async (fastfiy) => { + const calculatePostScoreJob = container.resolve(CalculatePostScoreJob) + const generateFeedJob = container.resolve(GenerateFeedJob) + const generateTrendingWritersJob = container.resolve(GenerateTrendingWritersJob) + const deleteFeedJob = container.resolve(DeleteFeedJob) + const statsDailyJob = container.resolve(StatsDaily) + const statsWeeklyJob = container.resolve(StatsWeekly) + const statsMonthlyJob = container.resolve(StatsMonthly) + const checkPostSpamJob = container.resolve(CheckPostSpamJob) + const deleteReadPostJob = container.resolve(DeletePostReadJob) + const scorePostJob = container.resolve(ScorePostJob) + + // 덜 실행하면서, 실행되는 순서로 정렬 + // crontime은 UTC 기준으로 작성되기 때문에 KST에서 9시간을 빼줘야함 + const jobDescription: JobDescription[] = [ + { + name: 'generate trending writers every day', + cronTime: '0 5 * * *', // every day at 05:00 (5:00 AM) + jobService: generateTrendingWritersJob, + }, + { + name: 'posts score calculation in every day', + cronTime: '0 21 * * *', // every day at 06:00 (6:00 AM) + jobService: calculatePostScoreJob, + param: 0.1, + }, + { + name: 'delete feed in every hour', + cronTime: '10 * * * *', // every hour at 10 minutes + jobService: deleteFeedJob, + }, + { + name: 'generate feeds in every 1 minute', + cronTime: '*/1 * * * *', // every 1 minute + jobService: generateFeedJob, + }, + { + name: 'posts score calculation in every 5 minutes', + cronTime: '*/5 * * * *', // every 5 minutes + jobService: calculatePostScoreJob, + param: 0.5, + }, + { + name: 'score post in every 1 minutes', + cronTime: '*/1 * * * *', // every 1 minutes + jobService: scorePostJob, + }, + { + name: 'check post spam in every 2 minutes', + cronTime: '*/2 * * * *', // every 2 minutes + jobService: checkPostSpamJob, + }, + { + name: 'delete post read in every 2 minutes', + cronTime: '*/2 * * * *', // every 2 minutes + jobService: deleteReadPostJob, + }, + // Stats Start + { + name: 'providing a count of new users and posts from the past 1 day', + cronTime: '0 0 * * *', // every day at 9:00 AM + jobService: statsDailyJob, + }, + { + name: 'providing a count of new users and posts from the past 1 week', + cronTime: '59 23 * * 0', + jobService: statsWeeklyJob, + }, + { + name: 'providing a count of new users and posts from the past 1 month', + cronTime: '0 0 1 * *', // every 1st day of month at 8:58 AM + jobService: statsMonthlyJob, + }, + // Stats end + ] + + const createJob = (description: JobDescription) => { + const { name, cronTime } = description + return fastfiy.cron.createJob({ + name, + cronTime, + onTick: async () => await createTick(description), + }) + } + + const initializeJobs = async () => { + console.log('Initializing cron jobs...') + if (ENV.appEnv !== 'production') { + const immediateRunJobs = jobDescription.filter((job) => job.isImmediateExecute) + const crons = await Promise.all(immediateRunJobs.map(createJob)) + for (const cron of crons) { + cron.start() + console.log(`${cron.name} is registered`) + } + } + + if (['production', 'stage'].includes(ENV.dockerEnv)) { + const crons = await Promise.all(jobDescription.map(createJob)) + for (const cron of crons) { + cron.start() + console.log(`${cron.name} is registered`) + } + } + } + + try { + await initializeJobs() + } catch (error) { + console.error('Error initializing cron jobs:', error) + } +} + +function isNeedParamJobService(arg: any): arg is NeedParamJobService { + return arg.jobService instanceof CalculatePostScoreJob +} + +function isNotNeedParamJobService(arg: any): arg is NotNeedParamJobService { + return arg.jobService instanceof CalculatePostScoreJob === false +} + +async function createTick(description: JobDescription) { + const { jobService } = description + + if (jobService.isProgressing) return + jobService.start() + + if (isNeedParamJobService(description)) { + await description.jobService.runner(description.param) + } + + if (isNotNeedParamJobService(description)) { + await description.jobService.runner() + } + + jobService.stop() +} + +type JobDescription = NeedParamJobService | NotNeedParamJobService + +type JobService = + | CalculatePostScoreJob + | GenerateFeedJob + | GenerateTrendingWritersJob + | DeleteFeedJob + | StatsDaily + | StatsWeekly + | StatsMonthly + | CheckPostSpamJob + | DeletePostReadJob + | ScorePostJob + +type BaseJobService = { + name: string + cronTime: string + isImmediateExecute?: boolean +} + +type NeedParamJobService = BaseJobService & { + param: any + jobService: CalculatePostScoreJob +} + +type NotNeedParamJobService = Omit & { + jobService: Exclude +} + +export default fp(cronPlugin, { + name: 'cronPlugin', + fastify: '4.x', + decorators: { + fastify: ['cron'], + }, +}) diff --git a/packages/velog-cron/src/common/plugins/globals/errorHandlerPlugin.ts b/apps/cron/src/common/plugins/global/errorHandlerPlugin.mts similarity index 83% rename from packages/velog-cron/src/common/plugins/globals/errorHandlerPlugin.ts rename to apps/cron/src/common/plugins/global/errorHandlerPlugin.mts index e4a91009..e35252c5 100644 --- a/packages/velog-cron/src/common/plugins/globals/errorHandlerPlugin.ts +++ b/apps/cron/src/common/plugins/global/errorHandlerPlugin.mts @@ -1,11 +1,12 @@ import { ENV } from '@env' -import { isHttpError } from '@errors/HttpError.js' +import { isHttpError } from '@errors/HttpError.mjs' import { FastifyPluginCallback } from 'fastify' import fp from 'fastify-plugin' const pluginFn: FastifyPluginCallback = (fastify, _, done) => { - fastify.addHook('onError', (request, reply, error) => { - console.log('fastify hook error:', error) + fastify.addHook('onError', (request, reply, error, done) => { + request.log.error(error, 'fastify onError') + done() }) fastify.setErrorHandler((error, _, reply) => { if (isHttpError(error)) { diff --git a/packages/velog-cron/src/env.ts b/apps/cron/src/env.mts similarity index 72% rename from packages/velog-cron/src/env.ts rename to apps/cron/src/env.mts index 1bdf72ef..09c90de6 100644 --- a/packages/velog-cron/src/env.ts +++ b/apps/cron/src/env.mts @@ -1,6 +1,6 @@ import dotenv from 'dotenv' import { container } from 'tsyringe' -import { UtilsService } from '@lib/utils/UtilsService.js' +import { UtilsService } from '@lib/utils/UtilsService.mjs' import { existsSync } from 'fs' import { z } from 'zod' @@ -20,7 +20,7 @@ const appEnv: AppEnvironment = ['stage', 'production'].includes(dockerEnv) : 'development' const envFile = envFiles[dockerEnv] -const prefix = dockerEnv === 'development' ? './env' : '../env' +const prefix = './env' const utils = container.resolve(UtilsService) const configPath = utils.resolveDir(`${prefix}/${envFile}`) @@ -39,6 +39,11 @@ const env = z.object({ databaseUrl: z.string(), cronApiKey: z.string(), redisHost: z.string(), + redisPort: z.number(), + discordBotToken: z.string(), + discordStatsChannel: z.string(), + discordSpamChannel: z.string(), + discordErrorChannel: z.string(), }) export const ENV = env.parse({ @@ -48,4 +53,9 @@ export const ENV = env.parse({ databaseUrl: process.env.DATABASE_URL, cronApiKey: process.env.CRON_API_KEY, redisHost: process.env.REDIS_HOST, + redisPort: Number(process.env.REDIS_PORT), + discordBotToken: process.env.DISCORD_BOT_TOKEN, + discordStatsChannel: process.env.DISCORD_STATS_CHANNEL, + discordSpamChannel: process.env.DISCORD_SPAM_CHANNEL, + discordErrorChannel: process.env.DISCORD_ERROR_CHANNEL, }) diff --git a/packages/velog-cron/src/jobs/CalculatePostScoreJob.ts b/apps/cron/src/jobs/CalculatePostScoreJob.mts similarity index 73% rename from packages/velog-cron/src/jobs/CalculatePostScoreJob.ts rename to apps/cron/src/jobs/CalculatePostScoreJob.mts index 1a22a595..79f1965f 100644 --- a/packages/velog-cron/src/jobs/CalculatePostScoreJob.ts +++ b/apps/cron/src/jobs/CalculatePostScoreJob.mts @@ -1,15 +1,12 @@ -import { DbService } from '@lib/db/DbService.js' -import { PostService } from '@services/PostService/index.js' +import { DbService } from '@lib/db/DbService.mjs' +import { PostService } from '@services/PostService/index.mjs' import { injectable, singleton } from 'tsyringe' -import { Job, JobProgress } from '@jobs/JobProgress.js' +import { Job, JobProgress } from '@jobs/JobProgress.mjs' @singleton() @injectable() export class CalculatePostScoreJob extends JobProgress implements Job { - constructor( - private readonly postService: PostService, - private readonly db: DbService, - ) { + constructor(private readonly postService: PostService, private readonly db: DbService) { super() } public async runner(score: number) { diff --git a/apps/cron/src/jobs/CheckPostSpamJob.mts b/apps/cron/src/jobs/CheckPostSpamJob.mts new file mode 100644 index 00000000..87f12a8b --- /dev/null +++ b/apps/cron/src/jobs/CheckPostSpamJob.mts @@ -0,0 +1,42 @@ +import { injectable, singleton } from 'tsyringe' +import { Job, JobProgress } from './JobProgress.mjs' +import { PostService } from '@services/PostService/index.mjs' +import { type CheckPostSpamQueueData, RedisService } from '@lib/redis/RedisService.mjs' +import { DiscordService } from '@lib/discord/DiscordService.mjs' + +@injectable() +@singleton() +export class CheckPostSpamJob extends JobProgress implements Job { + constructor( + private readonly redis: RedisService, + private readonly discord: DiscordService, + private readonly postService: PostService, + ) { + super() + } + public async runner(): Promise { + console.log('PostSpamCheckJob start...') + console.time('PostSpamCheckJob') + + const spamQueueName = this.redis.queueName.checkPostSpam + let handledQueueCount = 0 + while (true) { + const item = await this.redis.lindex(spamQueueName, 0) + if (!item) break + const data: CheckPostSpamQueueData = JSON.parse(item) + try { + await this.postService.checkSpam(data) + } catch (error) { + console.log('PostSpamCheckJob error', error) + const message = { message: 'PostSpamCheckJob error', payload: item, error: error } + this.discord.sendMessage('error', JSON.stringify(message)) + } finally { + await this.redis.lpop(spamQueueName) + handledQueueCount++ + } + } + + console.log(`handled Queue count: ${handledQueueCount}`) + console.timeEnd('PostSpamCheckJob') + } +} diff --git a/packages/velog-cron/src/jobs/DeleteFeedJob.ts b/apps/cron/src/jobs/DeleteFeedJob.mts similarity index 80% rename from packages/velog-cron/src/jobs/DeleteFeedJob.ts rename to apps/cron/src/jobs/DeleteFeedJob.mts index c0fa7c4d..f6ed84d9 100644 --- a/packages/velog-cron/src/jobs/DeleteFeedJob.ts +++ b/apps/cron/src/jobs/DeleteFeedJob.mts @@ -1,6 +1,6 @@ import { injectable, singleton } from 'tsyringe' -import { Job, JobProgress } from './JobProgress.js' -import { FeedService } from '@services/FeedService/index.js' +import { Job, JobProgress } from './JobProgress.mjs' +import { FeedService } from '@services/FeedService/index.mjs' @singleton() @injectable() diff --git a/apps/cron/src/jobs/DeletePostReadJob.mts b/apps/cron/src/jobs/DeletePostReadJob.mts new file mode 100644 index 00000000..6a56a63f --- /dev/null +++ b/apps/cron/src/jobs/DeletePostReadJob.mts @@ -0,0 +1,27 @@ +import { injectable, singleton } from 'tsyringe' +import { Job, JobProgress } from './JobProgress.mjs' +import { PostReadService } from '@services/PostReadService/index.mjs' + +@injectable() +@singleton() +export class DeletePostReadJob extends JobProgress implements Job { + constructor(private readonly postReadService: PostReadService) { + super() + } + public async runner() { + console.log('Delete post read job start...') + console.time('delete post read') + + const count = 1000 + const postReads = await this.postReadService.findMany(count) + + const postReadIds = postReads.map((postRead) => postRead.id) + + for (const postReadId of postReadIds) { + await this.postReadService.deleteById(postReadId) + } + + console.log(`Deleted PostRead count: ${count}`) + console.timeEnd('delete post read') + } +} diff --git a/packages/velog-cron/src/jobs/GenerateFeedJob.ts b/apps/cron/src/jobs/GenerateFeedJob.mts similarity index 50% rename from packages/velog-cron/src/jobs/GenerateFeedJob.ts rename to apps/cron/src/jobs/GenerateFeedJob.mts index 5ce676f6..ff8985fc 100644 --- a/packages/velog-cron/src/jobs/GenerateFeedJob.ts +++ b/apps/cron/src/jobs/GenerateFeedJob.mts @@ -1,31 +1,34 @@ -import { Job, JobProgress } from '@jobs/JobProgress.js' -import { RedisService } from '@lib/redis/RedisService.js' -import { FeedService } from '@services/FeedService/index.js' +import { Job, JobProgress } from '@jobs/JobProgress.mjs' +import { RedisService } from '@lib/redis/RedisService.mjs' +import { FeedService } from '@services/FeedService/index.mjs' import { injectable, singleton } from 'tsyringe' @singleton() @injectable() export class GenerateFeedJob extends JobProgress implements Job { - constructor( - private readonly redis: RedisService, - private readonly feedService: FeedService, - ) { + constructor(private readonly redis: RedisService, private readonly feedService: FeedService) { super() } public async runner() { console.log('Create feed job start...') console.time('create feed') - const feedQueueName = this.redis.queueName.feed + const feedQueueName = this.redis.queueName.createFeed let handledQueueCount = 0 while (true) { const item = await this.redis.lindex(feedQueueName, 0) if (!item) break const data: FeedQueueData = JSON.parse(item) const { fk_following_id, fk_post_id } = data - await this.feedService.createFeed({ followingId: fk_following_id, postId: fk_post_id }) - await this.redis.lpop(feedQueueName) - handledQueueCount++ + try { + await this.feedService.createFeed({ followingId: fk_following_id, postId: fk_post_id }) + } catch (error) { + console.log('Error occurred while creating feed', error) + console.log('data', data) + } finally { + await this.redis.lpop(feedQueueName) + handledQueueCount++ + } } console.log(`Created Feed Count: ${handledQueueCount}`) console.timeEnd('create feed') diff --git a/packages/velog-cron/src/jobs/GenerateTrendingWritersJob.ts b/apps/cron/src/jobs/GenerateTrendingWritersJob.mts similarity index 68% rename from packages/velog-cron/src/jobs/GenerateTrendingWritersJob.ts rename to apps/cron/src/jobs/GenerateTrendingWritersJob.mts index ba353b70..089ec867 100644 --- a/packages/velog-cron/src/jobs/GenerateTrendingWritersJob.ts +++ b/apps/cron/src/jobs/GenerateTrendingWritersJob.mts @@ -1,15 +1,12 @@ import { injectable, singleton } from 'tsyringe' -import { Job, JobProgress } from './JobProgress.js' -import { RedisService } from '@lib/redis/RedisService.js' -import { WriterService } from '@services/WriterService/index.js' +import { Job, JobProgress } from './JobProgress.mjs' +import { RedisService } from '@lib/redis/RedisService.mjs' +import { WriterService } from '@services/WriterService/index.mjs' @singleton() @injectable() export class GenerateTrendingWritersJob extends JobProgress implements Job { - constructor( - private readonly redis: RedisService, - private readonly writerService: WriterService, - ) { + constructor(private readonly redis: RedisService, private readonly writerService: WriterService) { super() } public async runner(): Promise { diff --git a/packages/velog-cron/src/jobs/JobProgress.ts b/apps/cron/src/jobs/JobProgress.mts similarity index 84% rename from packages/velog-cron/src/jobs/JobProgress.ts rename to apps/cron/src/jobs/JobProgress.mts index b0a65335..9a0a05f7 100644 --- a/packages/velog-cron/src/jobs/JobProgress.ts +++ b/apps/cron/src/jobs/JobProgress.mts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ - export abstract class Job { public async runner(param?: any): Promise {} } diff --git a/apps/cron/src/jobs/ScorePostJob.mts b/apps/cron/src/jobs/ScorePostJob.mts new file mode 100644 index 00000000..8d1edc63 --- /dev/null +++ b/apps/cron/src/jobs/ScorePostJob.mts @@ -0,0 +1,44 @@ +import { injectable, singleton } from 'tsyringe' +import { Job, JobProgress } from './JobProgress.mjs' +import { RedisService } from '@lib/redis/RedisService.mjs' +import { PostService } from '@services/PostService/index.mjs' +import { ScorePostQueueData } from '@packages/database/velog-redis' +import { DiscordService } from '@lib/discord/DiscordService.mjs' + +@singleton() +@injectable() +export class ScorePostJob extends JobProgress implements Job { + constructor( + private readonly postService: PostService, + private readonly redis: RedisService, + private readonly discord: DiscordService, + ) { + super() + } + public async runner(): Promise { + console.log('ScorePostJob start...') + console.time('ScorePostJob') + + const scorePostQueueName = this.redis.queueName.scorePost + let handledQueueCount = 0 + + while (true) { + const item = await this.redis.lindex(scorePostQueueName, 0) + if (!item) break + const data: ScorePostQueueData = JSON.parse(item) + try { + await this.postService.scoreCalculator(data.post_id) + } catch (error: any) { + console.log('ScorePostJob error', error) + const message = { message: 'ScorePostJob error', payload: item, error: error?.message } + this.discord.sendMessage('error', JSON.stringify(message)) + } finally { + await this.redis.lpop(scorePostQueueName) + handledQueueCount++ + } + } + + console.log(`handled Score Post Queue count: ${handledQueueCount}`) + console.timeEnd('ScorePostJob') + } +} diff --git a/apps/cron/src/jobs/stats/StatsDaily.ts b/apps/cron/src/jobs/stats/StatsDaily.ts new file mode 100644 index 00000000..fa8354fd --- /dev/null +++ b/apps/cron/src/jobs/stats/StatsDaily.ts @@ -0,0 +1,16 @@ +import { injectable, singleton } from 'tsyringe' +import { Job, JobProgress } from '../JobProgress.mjs' +import { StatsService } from '@services/StatsService/index.mjs' + +@singleton() +@injectable() +export class StatsDaily extends JobProgress implements Job { + constructor(private readonly statsService: StatsService) { + super() + } + + public async runner(): Promise { + console.log('StatsDaily start...') + await this.statsService.daily() + } +} diff --git a/apps/cron/src/jobs/stats/StatsMonthly.ts b/apps/cron/src/jobs/stats/StatsMonthly.ts new file mode 100644 index 00000000..d9cf24f2 --- /dev/null +++ b/apps/cron/src/jobs/stats/StatsMonthly.ts @@ -0,0 +1,16 @@ +import { injectable, singleton } from 'tsyringe' +import { Job, JobProgress } from '../JobProgress.mjs' +import { StatsService } from '@services/StatsService/index.mjs' + +@singleton() +@injectable() +export class StatsMonthly extends JobProgress implements Job { + constructor(private readonly statsService: StatsService) { + super() + } + + public async runner(): Promise { + console.log('StatsMontly start...') + await this.statsService.monthly() + } +} diff --git a/apps/cron/src/jobs/stats/StatsWeekly.ts b/apps/cron/src/jobs/stats/StatsWeekly.ts new file mode 100644 index 00000000..167eb1f9 --- /dev/null +++ b/apps/cron/src/jobs/stats/StatsWeekly.ts @@ -0,0 +1,16 @@ +import { injectable, singleton } from 'tsyringe' +import { Job, JobProgress } from '../JobProgress.mjs' +import { StatsService } from '@services/StatsService/index.mjs' + +@singleton() +@injectable() +export class StatsWeekly extends JobProgress implements Job { + constructor(private readonly statsService: StatsService) { + super() + } + + public async runner(): Promise { + console.log('StatsWeekly start...') + await this.statsService.weekly() + } +} diff --git a/apps/cron/src/jobs/stats/index.ts b/apps/cron/src/jobs/stats/index.ts new file mode 100644 index 00000000..cdc6014b --- /dev/null +++ b/apps/cron/src/jobs/stats/index.ts @@ -0,0 +1,3 @@ +export { StatsDaily } from './StatsDaily.js' +export { StatsWeekly } from './StatsWeekly.js' +export { StatsMonthly } from './StatsMonthly.js' diff --git a/apps/cron/src/lib/db/DbService.mts b/apps/cron/src/lib/db/DbService.mts new file mode 100644 index 00000000..fcc9f4b4 --- /dev/null +++ b/apps/cron/src/lib/db/DbService.mts @@ -0,0 +1,13 @@ +import { ENV } from '@env' +import { PrismaClient } from '@packages/database/velog-rds' +import { injectable, singleton } from 'tsyringe' + +@injectable() +@singleton() +export class DbService extends PrismaClient { + constructor() { + super({ + datasourceUrl: ENV.databaseUrl, + }) + } +} diff --git a/apps/cron/src/lib/discord/Discord.test.mts b/apps/cron/src/lib/discord/Discord.test.mts new file mode 100644 index 00000000..adc77e41 --- /dev/null +++ b/apps/cron/src/lib/discord/Discord.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { DiscordService } from './DiscordService.mjs' + +describe('DiscordService', () => { + const service = container.resolve(DiscordService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/cron/src/lib/discord/DiscordService.mts b/apps/cron/src/lib/discord/DiscordService.mts new file mode 100644 index 00000000..15aec3af --- /dev/null +++ b/apps/cron/src/lib/discord/DiscordService.mts @@ -0,0 +1,75 @@ +import { injectable, singleton } from 'tsyringe' +import { Client, GatewayIntentBits } from 'discord.js' +import { ENV } from '@env' + +@injectable() +@singleton() +export class DiscordService { + private client!: Client + public isSending: boolean = false + construct() {} + public connection(): Promise { + return new Promise((resolve) => { + this.client = new Client({ + intents: [GatewayIntentBits.MessageContent], + }) + + this.client.on('ready', () => { + console.log('Discord Client ready') + resolve(this.client) + }) + + this.client.login(ENV.discordBotToken) + }) + } + public async sendMessage(type: MessageType, message: string) { + this.isSending = true + + const frequentWord: string[] = [] + const isFrequentWordIncluded = frequentWord.some((word) => message.includes(word)) + + if (isFrequentWordIncluded) { + this.isSending = false + console.log('Frequent word included skip sending message') + return + } + + try { + const isReady = this.client.isReady() + + if (!isReady) { + throw new Error('Discord bot is not ready') + } + + const channelMapper: Record = { + stats: ENV.discordStatsChannel, + spam: ENV.discordSpamChannel, + error: ENV.discordErrorChannel, + } + + const channelId = channelMapper[type] + + if (!channelId) { + throw new Error('Not found discord channel id') + } + + const channel = await this.client.channels.fetch(channelId) + + if (channel?.isTextBased()) { + const chunkSize = 2000 + for (let i = 0; i < message.length; i += chunkSize) { + await channel.send(message.slice(i, i + chunkSize)) + } + } else { + throw new Error('Wrong channel type') + } + } catch (error: any) { + console.log('error', error) + throw new Error('Failed to send meesage to discord channel') + } finally { + this.isSending = false + } + } +} + +type MessageType = 'stats' | 'spam' | 'error' diff --git a/packages/velog-common/src/lib/redis/Redis.test.ts b/apps/cron/src/lib/redis/Redis.test.mts similarity index 79% rename from packages/velog-common/src/lib/redis/Redis.test.ts rename to apps/cron/src/lib/redis/Redis.test.mts index 9aa21807..3590f280 100644 --- a/packages/velog-common/src/lib/redis/Redis.test.ts +++ b/apps/cron/src/lib/redis/Redis.test.mts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { RedisService } from './RedisService' +import { RedisService } from './RedisService.mjs' describe('RedisService', () => { const service = container.resolve(RedisService) diff --git a/apps/cron/src/lib/redis/RedisService.mts b/apps/cron/src/lib/redis/RedisService.mts new file mode 100644 index 00000000..2524c503 --- /dev/null +++ b/apps/cron/src/lib/redis/RedisService.mts @@ -0,0 +1,16 @@ +import { injectable, singleton } from 'tsyringe' +import { ENV } from '@env' +import { RedisService as Redis } from '@packages/database/velog-redis' +export type { + ChangeEmailArgs, + CheckPostSpamQueueData, + CreateFeedQueueData, +} from '@packages/database/velog-redis' + +@injectable() +@singleton() +export class RedisService extends Redis { + constructor() { + super({ port: ENV.redisPort, host: ENV.redisHost }) + } +} diff --git a/apps/cron/src/lib/utils/Utils.test.mts b/apps/cron/src/lib/utils/Utils.test.mts new file mode 100644 index 00000000..6472e9dd --- /dev/null +++ b/apps/cron/src/lib/utils/Utils.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { UtilsService } from './UtilsService.mjs' + +describe('UtilsService', () => { + const service = container.resolve(UtilsService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/packages/velog-cron/src/lib/utils/UtilsService.ts b/apps/cron/src/lib/utils/UtilsService.mts similarity index 75% rename from packages/velog-cron/src/lib/utils/UtilsService.ts rename to apps/cron/src/lib/utils/UtilsService.mts index cd0895bb..c73dbc9d 100644 --- a/packages/velog-cron/src/lib/utils/UtilsService.ts +++ b/apps/cron/src/lib/utils/UtilsService.mts @@ -1,7 +1,7 @@ -import { dirname, join } from 'path' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' import { injectable, singleton } from 'tsyringe' -import { fileURLToPath } from 'url' -import { utcToZonedTime } from 'date-fns-tz' +import { toZonedTime } from 'date-fns-tz' interface Service { resolveDir(dir: string): string @@ -20,6 +20,6 @@ export class UtilsService implements Service { public get now() { const utcTime = new Date() const timezone = 'Asia/Seoul' - return utcToZonedTime(utcTime, timezone) + return toZonedTime(utcTime, timezone) } } diff --git a/packages/velog-cron/src/main.ts b/apps/cron/src/main.mts similarity index 61% rename from packages/velog-cron/src/main.ts rename to apps/cron/src/main.mts index 44db32d3..84497d16 100644 --- a/packages/velog-cron/src/main.ts +++ b/apps/cron/src/main.mts @@ -1,26 +1,29 @@ import 'reflect-metadata' - import { ENV } from '@env' -import { disableKeepAlive } from '@plugins/encapsulated/closePlugin.js' -import app from './app.js' +import { disableKeepAlive } from '@plugins/encapsulated/closePlugin.mjs' +import app from './app.mjs' import { container } from 'tsyringe' -import { DbService } from '@lib/db/DbService.js' -import { RedisService } from '@lib/redis/RedisService.js' +import { DbService } from '@lib/db/DbService.mjs' +import { RedisService } from '@lib/redis/RedisService.mjs' +import { DiscordService } from '@lib/discord/DiscordService.mjs' async function main() { - app.listen({ port: ENV.port, host: '::' }) + const discord = container.resolve(DiscordService) + await discord.connection() const dbService = container.resolve(DbService) await dbService.$connect() const redis = container.resolve(RedisService) - await redis.connection().then((message) => console.log(message)) + await redis.connection() console.info(`INFO: Database connected to "${ENV.databaseUrl.split('@')[1]}"`) process.on('SIGINT', async () => { disableKeepAlive() process.exit(0) }) + + app.listen({ port: ENV.port, host: '::' }) } -main() +await main() diff --git a/packages/velog-cron/src/routes/index.ts b/apps/cron/src/routes/index.mts similarity index 92% rename from packages/velog-cron/src/routes/index.ts rename to apps/cron/src/routes/index.mts index 133e99b0..54827bca 100644 --- a/packages/velog-cron/src/routes/index.ts +++ b/apps/cron/src/routes/index.mts @@ -1,7 +1,7 @@ import { format } from 'date-fns' import { FastifyPluginCallback } from 'fastify' -import post from '@routes/posts/index.js' -import { HttpStatusMessage } from '@constants/HttpStatusMessageConstants.js' +import post from '@routes/posts/index.mjs' +import { HttpStatusMessage } from '@constants/HttpStatusMessageConstants.mjs' const api: FastifyPluginCallback = (fastify, opts, done) => { fastify.register(post, { prefix: '/posts' }) diff --git a/packages/velog-cron/src/routes/posts/index.ts b/apps/cron/src/routes/posts/index.mts similarity index 85% rename from packages/velog-cron/src/routes/posts/index.ts rename to apps/cron/src/routes/posts/index.mts index ae6fee52..1a334b77 100644 --- a/packages/velog-cron/src/routes/posts/index.ts +++ b/apps/cron/src/routes/posts/index.mts @@ -1,5 +1,5 @@ import { FastifyPluginCallback } from 'fastify' -import v1 from './v1/index.js' +import v1 from './v1/index.mjs' const postRoute: FastifyPluginCallback = (fastify, opts, done) => { fastify.register(v1, { prefix: '/v1' }) diff --git a/apps/cron/src/routes/posts/v1/PostController.mts b/apps/cron/src/routes/posts/v1/PostController.mts new file mode 100644 index 00000000..16a46d71 --- /dev/null +++ b/apps/cron/src/routes/posts/v1/PostController.mts @@ -0,0 +1,131 @@ +import { BadRequestError } from '@errors/BadRequestErrors.mjs' +import { NotFoundError } from '@errors/NotfoundError.mjs' +import { DbService } from '@lib/db/DbService.mjs' +import { PostService } from '@services/PostService/index.mjs' +import { container, injectable, singleton } from 'tsyringe' +import { startOfDay, subMonths } from 'date-fns' +import { ENV } from '@env' +import fs from 'fs' +import path from 'path' +import { UtilsService } from '@lib/utils/UtilsService.mjs' + +interface Controller { + updatePostScore(postId: string): Promise + calculateRecentPostScore(): Promise +} + +@singleton() +@injectable() +export class PostController implements Controller { + constructor( + private readonly db: DbService, + private readonly utils: UtilsService, + private readonly postService: PostService, + ) {} + async updatePostScore(postId: string): Promise { + const post = await this.postService.findById(postId) + + if (!post) { + throw new NotFoundError('Not found Post') + } + + await this.postService.scoreCalculator(post.id) + } + async calculateRecentPostScore(): Promise { + if (ENV.appEnv !== 'development') { + throw new BadRequestError('This operation is only allowed in development environment.') + } + + const now = this.utils.now + const startOfToday = startOfDay(now) + const threeMonthsAgo = subMonths(startOfToday, 3) + + const posts = await this.db.post.findMany({ + where: { + is_private: false, + likes: { + gte: 1, + }, + released_at: { + gte: threeMonthsAgo, + }, + }, + select: { + id: true, + }, + }) + + for (let i = 0; i < posts.length; i++) { + console.log(`${i} / ${posts.length}`) + const postId = posts[i].id + await this.postService.scoreCalculator(postId) + } + + return posts.length + } + async spamFilterTestRunner() { + if (ENV.appEnv !== 'development') return + + const utils = container.resolve(UtilsService) + const postService = container.resolve(PostService) + try { + const filePath = path.resolve(utils.resolveDir('./src/routes/posts/v1/spam_post.json')) + + const fileExits = fs.existsSync(filePath) + if (!fileExits) { + throw new NotFoundError('Not found spam_post.json') + } + + const readFileResult = fs.readFileSync(filePath, { encoding: 'utf-8' }) + const data = JSON.parse(readFileResult) + const key = Object.keys(data)[0] + const posts: PostData[] = data[key] + .filter((post: any) => post.title) + .map((post: any, index: number) => ({ id: index, ...post })) + + const maxPostLength = 5000 + const set = new Set() + const bannedUesrnames: string[] = [] + + for (const post of posts.slice(0, maxPostLength)) { + const { id, title, body, username } = post + if (bannedUesrnames.includes(username)) { + set.add(id) + } + + const isForeignSpam = await postService.checkIsSpam(title, body, username, '', 'US') + if (isForeignSpam) { + set.add(id) + continue + } + + const isInternalSpam = await postService.checkIsSpam(title, body, username, '', 'KR') + if (isInternalSpam) { + set.add(id) + } + } + + const isSpamCount = set.size + console.log('isSpamCount: ', isSpamCount) + console.log('ratio: ', isSpamCount / maxPostLength) + + const allowIds: number[] = [] + for (const id of allowIds) { + set.add(id) + } + + const notFilteredPosts = posts.filter((post) => !set.has(post.id)) + console.log('notFilteredPosts', notFilteredPosts[0]) + } catch (error) { + throw error + } + } +} + +type PostData = { + id: number + title: string + body: string + tags: string + username: string +} diff --git a/packages/velog-cron/src/routes/posts/v1/index.ts b/apps/cron/src/routes/posts/v1/index.mts similarity index 72% rename from packages/velog-cron/src/routes/posts/v1/index.ts rename to apps/cron/src/routes/posts/v1/index.mts index 40bdf34d..0d7cf861 100644 --- a/packages/velog-cron/src/routes/posts/v1/index.ts +++ b/apps/cron/src/routes/posts/v1/index.mts @@ -1,9 +1,9 @@ import { FastifyPluginCallback } from 'fastify' import { container } from 'tsyringe' -import { PostScoreParams, PostScoreSchema } from '@routes/posts/v1/schema.js' -import { HttpStatus } from '@constants/HttpStatusConstants.js' -import { HttpStatusMessage } from '@constants/HttpStatusMessageConstants.js' -import { PostController } from '@routes/posts/v1/PostController.js' +import { PostScoreParams, PostScoreSchema } from '@routes/posts/v1/schema.mjs' +import { HttpStatus } from '@constants/HttpStatusConstants.mjs' +import { HttpStatusMessage } from '@constants/HttpStatusMessageConstants.mjs' +import { PostController } from '@routes/posts/v1/PostController.mjs' const v1: FastifyPluginCallback = (fastify, opts, done) => { const postController = container.resolve(PostController) @@ -19,11 +19,18 @@ const v1: FastifyPluginCallback = (fastify, opts, done) => { }, ) + // dev 환경에서만 사용 가능 fastify.patch('/score', async (_, reply) => { const processedPostsCount = await postController.calculateRecentPostScore() reply.status(HttpStatus.OK).send({ processedPostsCount }) }) + // dev 환경에서만 사용 가능 + fastify.post('/test/spam-filter', async (_, reply) => { + await postController.spamFilterTestRunner() + reply.status(HttpStatus.OK).send(HttpStatusMessage.Ok) + }) + done() } diff --git a/packages/velog-cron/src/routes/posts/v1/schema.ts b/apps/cron/src/routes/posts/v1/schema.mts similarity index 100% rename from packages/velog-cron/src/routes/posts/v1/schema.ts rename to apps/cron/src/routes/posts/v1/schema.mts diff --git a/packages/velog-server/src/services/FeedService/FeedService.test.ts b/apps/cron/src/services/FeedService/FeedService.test.ts similarity index 82% rename from packages/velog-server/src/services/FeedService/FeedService.test.ts rename to apps/cron/src/services/FeedService/FeedService.test.ts index 3d9cf155..eef87dfb 100644 --- a/packages/velog-server/src/services/FeedService/FeedService.test.ts +++ b/apps/cron/src/services/FeedService/FeedService.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { FeedService } from './index.js' +import { FeedService } from './index.mjs' describe('FeedService', () => { const service = container.resolve(FeedService) diff --git a/packages/velog-cron/src/services/FeedService/index.ts b/apps/cron/src/services/FeedService/index.mts similarity index 85% rename from packages/velog-cron/src/services/FeedService/index.ts rename to apps/cron/src/services/FeedService/index.mts index 4fa6e81e..847f2413 100644 --- a/packages/velog-cron/src/services/FeedService/index.ts +++ b/apps/cron/src/services/FeedService/index.mts @@ -1,6 +1,6 @@ -import { DbService } from '@lib/db/DbService.js' -import { UtilsService } from '@lib/utils/UtilsService.js' -import { FollowUserService } from '@services/FollowUserService/index.js' +import { DbService } from '@lib/db/DbService.mjs' +import { UtilsService } from '@lib/utils/UtilsService.mjs' +import { FollowUserService } from '@services/FollowUserService/index.mjs' import { subHours } from 'date-fns' import { injectable, singleton } from 'tsyringe' @@ -20,11 +20,11 @@ export class FeedService implements Service { public async createFeed({ followingId, postId }: CreateFeedArgs): Promise { const followers = await this.followUserService.getFollowers(followingId) const followerIds = followers.map((user) => user.id) - for (const userId of followerIds) { + for (const followeId of followerIds) { try { await this.db.feed.create({ data: { - fk_user_id: userId, + fk_user_id: followeId, fk_post_id: postId, }, }) diff --git a/packages/velog-cron/src/services/FollowUserService/FollowUserService.test.ts b/apps/cron/src/services/FollowUserService/FollowUserService.test.mts similarity index 81% rename from packages/velog-cron/src/services/FollowUserService/FollowUserService.test.ts rename to apps/cron/src/services/FollowUserService/FollowUserService.test.mts index 3f32e2a9..d0def466 100644 --- a/packages/velog-cron/src/services/FollowUserService/FollowUserService.test.ts +++ b/apps/cron/src/services/FollowUserService/FollowUserService.test.mts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { FollowUserService } from './index.js' +import { FollowUserService } from './index.mjs' describe('FollowUserService', () => { const service = container.resolve(FollowUserService) diff --git a/packages/velog-cron/src/services/FollowUserService/index.ts b/apps/cron/src/services/FollowUserService/index.mts similarity index 86% rename from packages/velog-cron/src/services/FollowUserService/index.ts rename to apps/cron/src/services/FollowUserService/index.mts index effe49df..59f1361b 100644 --- a/packages/velog-cron/src/services/FollowUserService/index.ts +++ b/apps/cron/src/services/FollowUserService/index.mts @@ -1,8 +1,8 @@ -import { UserService } from '@services/UserService/index.js' -import { DbService } from '@lib/db/DbService.js' -import { Prisma } from '@prisma/client' +import { UserService } from '@services/UserService/index.mjs' +import { DbService } from '@lib/db/DbService.mjs' +import { Prisma } from '@packages/database/velog-rds' import { injectable, singleton } from 'tsyringe' -import { NotFoundError } from '@errors/NotfoundError.js' +import { NotFoundError } from '@errors/NotfoundError.mjs' interface Service { getFollowings(fk_follower_id: string): Promise @@ -12,10 +12,7 @@ interface Service { @injectable() @singleton() export class FollowUserService implements Service { - constructor( - private readonly db: DbService, - private readonly userService: UserService, - ) {} + constructor(private readonly db: DbService, private readonly userService: UserService) {} public async getFollowings(fk_follower_id: string): Promise { const follower = await this.userService.findByUserId(fk_follower_id) diff --git a/apps/cron/src/services/PostReadService/PostReadService.test.mts b/apps/cron/src/services/PostReadService/PostReadService.test.mts new file mode 100644 index 00000000..01f74d6a --- /dev/null +++ b/apps/cron/src/services/PostReadService/PostReadService.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { PostReadService } from './index.mjs' + +describe('PostReadService', () => { + const service = container.resolve(PostReadService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/cron/src/services/PostReadService/index.mts b/apps/cron/src/services/PostReadService/index.mts new file mode 100644 index 00000000..7315dc8c --- /dev/null +++ b/apps/cron/src/services/PostReadService/index.mts @@ -0,0 +1,53 @@ +import { DbService } from '@lib/db/DbService.mjs' +import { PostRead } from '@packages/database/velog-rds' +import { injectable, singleton } from 'tsyringe' + +interface Service { + findMany(take: number): Promise + findById(postReadId: string): Promise + deleteById(postReadId: string): Promise +} + +@injectable() +@singleton() +export class PostReadService implements Service { + constructor(private readonly db: DbService) {} + public async findMany(take: number): Promise { + const startDate = new Date('2024-01-01') + const postReads = await this.db.postRead.findMany({ + where: { + created_at: { + lt: startDate, + }, + }, + orderBy: { + created_at: 'asc', + }, + take, + }) + return postReads + } + public async findById(postReadId: string): Promise { + const postRead = await this.db.postRead.findUnique({ + where: { + id: postReadId, + }, + }) + return postRead + } + public async deleteById(postReadId: string): Promise { + const postRead = await this.findById(postReadId) + + if (!postRead) { + console.log(`Not found postReadId: ${postReadId}`) + return + } + try { + await this.db.postRead.deleteMany({ + where: { + id: postReadId, + }, + }) + } catch (_) {} + } +} diff --git a/packages/velog-cron/src/services/PostService/PostService.test.ts b/apps/cron/src/services/PostService/PostService.test.mts similarity index 82% rename from packages/velog-cron/src/services/PostService/PostService.test.ts rename to apps/cron/src/services/PostService/PostService.test.mts index 4d86311d..881690b3 100644 --- a/packages/velog-cron/src/services/PostService/PostService.test.ts +++ b/apps/cron/src/services/PostService/PostService.test.mts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { PostService } from './index.js' +import { PostService } from './index.mjs' describe('PostService', () => { const service = container.resolve(PostService) diff --git a/apps/cron/src/services/PostService/index.mts b/apps/cron/src/services/PostService/index.mts new file mode 100644 index 00000000..7c5e8f01 --- /dev/null +++ b/apps/cron/src/services/PostService/index.mts @@ -0,0 +1,337 @@ +import { DbService } from '@lib/db/DbService.mjs' +import { Post, Prisma } from '@packages/database/velog-rds' +import { injectable, singleton } from 'tsyringe' +import geoip from 'geoip-country' +import { subMonths } from 'date-fns' +import { DiscordService } from '@lib/discord/DiscordService.mjs' +import type { CheckPostSpamQueueData } from '@lib/redis/RedisService.mjs' + +interface Service { + findById(postId: string): Promise + scoreCalculator(postId: string): Promise + checkSpam(data: CheckPostSpamQueueData): Promise +} + +@singleton() +@injectable() +export class PostService implements Service { + constructor(private readonly db: DbService, private readonly discord: DiscordService) {} + + public async findById(postId: string): Promise { + const post = await this.db.post.findUnique({ + where: { + id: postId, + }, + }) + return post + } + + public async findByUserId({ userId, ...queries }: FindByUserIdParams): Promise { + const { where, ...query } = queries + const posts = await this.db.post.findMany({ + where: { + ...where, + fk_user_id: userId, + }, + ...query, + }) + return posts + } + + public async scoreCalculator(postId: string): Promise { + const post = await this.findById(postId) + + if (!post) { + throw new Error('Not found Post') + } + + const postLikes = await this.db.postLike.count({ + where: { + fk_post_id: postId, + }, + }) + + const ONE_HOUR = 1000 * 60 * 60 + const itemHourAge = (Date.now() - post.released_at!.getTime()) / ONE_HOUR + const gravity = 0.35 + const votes = postLikes + (post.views ?? 0) * 0.04 + + const score = votes / Math.pow(itemHourAge + 2, gravity) + + await this.db.post.update({ + where: { + id: post.id, + }, + data: { + score, + }, + }) + } + + public async checkSpam({ post_id, user_id, ip }: CheckPostSpamQueueData): Promise { + const post = await this.db.post.findUnique({ + where: { + id: post_id, + }, + include: { + postTags: { + include: { + tag: true, + }, + }, + }, + }) + + if (!post) { + console.log('Not found Post when spam check') + return + } + + const user = await this.db.user.findUnique({ + where: { + id: user_id, + }, + include: { + profile: true, + }, + }) + + if (!user) return + + const country = geoip.lookup(ip)?.country ?? '' + + const extraText = post.postTags + .flatMap((postTag) => postTag.tag) + .map((tag) => tag?.name ?? '') + .join('') + .concat(user.profile?.short_bio ?? '', user.profile?.display_name ?? '') + + const { + isSpam, + reason, + targetType = '', + } = await this.checkIsSpam(post.title ?? '', post.body ?? '', user.username, extraText, country) + + if (!isSpam) return + + await this.db.post.update({ + where: { + id: post.id, + }, + data: { + is_private: true, + }, + }) + + setTimeout(() => { + const message = { + text: `[Captured By Cron], *userId*: ${user_id}\ntitle: ${post.title}, ip: ${ip}, country: ${country}, reason: ${reason}, type: spam`, + } + + if (targetType) { + message.text = message.text.concat(`, targetType: ${targetType}`) + } + this.discord.sendMessage('spam', JSON.stringify(message)) + }, 0) + } + + public async checkIsSpam( + title: string, + body: string, + username: string, + extraText: string, + country: string, + ): Promise<{ isSpam: boolean; reason: string; targetType?: string }> { + const allowList = ['KR', 'GB', ''] + const blockList = ['IN', 'PK', 'CN', 'VN', 'TH', 'PH', 'UG'] + const isForeign = !allowList.includes(country) + + if (blockList.includes(country)) { + return { isSpam: true, reason: 'blocked country' } + } + + const { isSpam: isTitleSpam, reason: titleSpamReason } = await this.spamFilter( + title!, + username, + isForeign, + true, + ) + if (isTitleSpam) { + return { isSpam: isTitleSpam, reason: titleSpamReason, targetType: 'title' } + } + + const { isSpam: isBodySpam, reason: bodySpamReason } = await this.spamFilter( + body!.concat(extraText), + username, + isForeign, + ) + if (isBodySpam) { + return { isSpam: isBodySpam, reason: bodySpamReason, targetType: 'body' } + } + + return { isSpam: false, reason: '' } + } + private async spamFilter( + text: string, + username: string, + isForeign: boolean, + isTitle = false, + ): Promise<{ isSpam: boolean; reason: string }> { + const includesCN = /[\u4e00-\u9fa5]/.test(text) + const includesKR = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(text) + const includesVN = /[àáâãèéêìíòóôõùúýăđĩũơưạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ]/.test( + text, + ) + + if (includesCN && !includesKR) { + return { isSpam: true, reason: 'includesCN' } + } + + if (includesVN && !includesKR) { + return { isSpam: true, reason: 'includesVN' } + } + + let replaced = text.replace(/```([\s\S]*?)```/g, '') // remove code blocks + // replace image markdown + replaced = replaced.replace(/!\[([\s\S]*?)\]\(([\s\S]*?)\)/g, '') + + if (isTitle) { + replaced = replaced.replace(/\s/g, '') + } + + const hasLink = /http/.test(replaced) + + const phoneRegex = [/\+\d{13}/, /\+\d{11}/] + + const containsPhoneNumber = phoneRegex.some((regex) => regex.test(replaced)) + + if (containsPhoneNumber && isForeign) { + return { isSpam: true, reason: 'containsPhoneNumber' } + } + + if (!isTitle && isForeign && hasLink) { + const lines = replaced.split('\n').filter((line) => line.trim().length > 1) + const koreanLinesCount = lines.filter((line) => this.hasKorean(line)).length + const confidence = koreanLinesCount / lines.length + return { isSpam: confidence < 0.3, reason: 'foreignWithLink' } + } + + const removeDuplicatedWords = Array.from( + new Set(replaced.toLocaleLowerCase().replace(/\s/g, '').split(/\n| /)), + ).join(' ') + + const oneMonthAgo = subMonths(new Date(), 1) + const bannedKeywords = await this.db.dynamicConfigItem.findMany({ + where: { + type: 'bannedKeyword', + last_used_at: { + gte: oneMonthAgo, + }, + // usage_count: { + // gte: 5, + // }, + }, + orderBy: [ + { + usage_count: 'desc', + }, + { + last_used_at: 'desc', + }, + ], + }) + + const usedBannedKeywords: string[] = [] + const checkKeyword = bannedKeywords + .map((keyword) => keyword.value) + .some((keyword) => { + if (removeDuplicatedWords.includes(keyword)) { + usedBannedKeywords.push(keyword) + this.updateDynmicConfigItem(keyword) + return true + } else { + return false + } + }) + + if (checkKeyword) { + return { isSpam: true, reason: `bannedKeyword: `.concat(...usedBannedKeywords.join(',')) } + } + + const bannedAltKeywords = await this.db.dynamicConfigItem.findMany({ + where: { + type: 'bannedAltKeyword', + }, + }) + + let score = 0 + + if (hasLink) { + score++ + } + + const isOnlyNumbers = /^\d+$/.test(username) + if (isOnlyNumbers) { + score++ + } + + const notAlphanumbericKorean = replaced.replace(/[a-zA-Zㄱ-힣0-9]/g, '') // remove korean + if (notAlphanumbericKorean.length / replaced.length > 0.3) { + score++ + } + + if (!isForeign) { + score-- + } + + const initScore = score + const usedBannedAltKeywords: string[] = [] + for (const { value: keyword } of bannedAltKeywords) { + if (removeDuplicatedWords.includes(keyword)) { + usedBannedAltKeywords.push(keyword) + this.updateDynmicConfigItem(keyword) + score++ + } + + if (score >= 2 && isForeign) { + return { + isSpam: true, + reason: `initScore: ${initScore}, foreign, ${'bannedAltKeywords: '.concat( + usedBannedAltKeywords.join(','), + )}`, + } + } + + if (score >= 4) { + return { + isSpam: true, + reason: `initScore: ${initScore}, ${'bannedAltKeywords: '.concat( + usedBannedAltKeywords.join(','), + )}`, + } + } + } + + return { isSpam: false, reason: '' } + } + private hasKorean(text: string) { + return /[ㄱ-힣]/g.test(text) + } + private async updateDynmicConfigItem(value: string) { + await this.db.dynamicConfigItem.updateMany({ + where: { + value, + }, + data: { + last_used_at: new Date(), + usage_count: { + increment: 1, + }, + }, + }) + } +} + +type FindByUserIdParams = { + userId: string +} & Prisma.PostFindManyArgs diff --git a/apps/cron/src/services/StatsService/StatsService.test.mts b/apps/cron/src/services/StatsService/StatsService.test.mts new file mode 100644 index 00000000..fa35f6e7 --- /dev/null +++ b/apps/cron/src/services/StatsService/StatsService.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { StatsService } from './index.mjs' + +describe('StatsService', () => { + const service = container.resolve(StatsService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/cron/src/services/StatsService/index.mts b/apps/cron/src/services/StatsService/index.mts new file mode 100644 index 00000000..faf33026 --- /dev/null +++ b/apps/cron/src/services/StatsService/index.mts @@ -0,0 +1,97 @@ +import { DbService } from '@lib/db/DbService.mjs' +import { DiscordService } from '@lib/discord/DiscordService.mjs' +import { injectable, singleton } from 'tsyringe' +import { + endOfDay, + startOfDay, + subDays, + format, + startOfWeek, + subWeeks, + endOfWeek, + startOfMonth, + endOfMonth, + subMonths, +} from 'date-fns' + +interface Service { + daily(): Promise + weekly(): Promise + monthly(): Promise +} + +@injectable() +@singleton() +export class StatsService implements Service { + constructor(private readonly db: DbService, private readonly discord: DiscordService) {} + public async daily(): Promise { + const start = startOfDay(subDays(new Date(), 1)) + const end = endOfDay(subDays(new Date(), 1)) + + const usersCount = await this.getUsersCount(start, end) + const postCount = await this.getPostsCount(start, end) + + await this.discord.sendMessage( + 'stats', + `[Daily]\n${format( + start, + 'yyyy-MM-dd', + )} 동안\n${usersCount}명의 사용자가 가입했습니다.\n${postCount}개의 공개 포스트가 작성되었습니다.`, + ) + } + public async weekly() { + const oneWeekAgo = subWeeks(new Date(), 1) + const start = startOfWeek(oneWeekAgo, { weekStartsOn: 1 }) // 월요일부터 시작 + const end = endOfWeek(oneWeekAgo, { weekStartsOn: 1 }) + + const usersCount = await this.getUsersCount(start, end) + const postCount = await this.getPostsCount(start, end) + + const timeFormat = 'yyyy-MM-dd HH:mm:ss' + await this.discord.sendMessage( + 'stats', + `[Weekly]\n${format(start, timeFormat)}-${format( + end, + timeFormat, + )} 동안\n${usersCount}명의 사용자가 가입했습니다.\n${postCount}개의 공개 포스트가 작성되었습니다.`, + ) + } + public async monthly() { + const start = startOfMonth(subMonths(new Date(), 1)) + const end = endOfMonth(subMonths(new Date(), 1)) + + const usersCount = await this.getUsersCount(start, end) + const postCount = await this.getPostsCount(start, end) + + const timeFormat = 'yyyy-MM-dd' + await this.discord.sendMessage( + 'stats', + `[Monthly]\n기간: ${format(start, timeFormat)} ~ ${format( + end, + timeFormat, + )}\n${usersCount}명의 사용자가 가입했습니다.\n${postCount}개의 공개 포스트가 작성되었습니다.`, + ) + } + private async getUsersCount(start: Date, end: Date) { + return await this.db.user.count({ + where: { + created_at: { + gte: start, + lte: end, + }, + }, + }) + } + private async getPostsCount(start: Date, end: Date) { + return await this.db.post.count({ + where: { + is_temp: false, + is_private: false, + created_at: { + gte: start, + lte: end, + }, + }, + }) + } +} diff --git a/packages/velog-cron/src/services/UserService/UserService.test.ts b/apps/cron/src/services/UserService/UserService.test.mts similarity index 82% rename from packages/velog-cron/src/services/UserService/UserService.test.ts rename to apps/cron/src/services/UserService/UserService.test.mts index 36320f15..c4ca80ee 100644 --- a/packages/velog-cron/src/services/UserService/UserService.test.ts +++ b/apps/cron/src/services/UserService/UserService.test.mts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { UserService } from './index.js' +import { UserService } from './index.mjs' describe('UserService', () => { const service = container.resolve(UserService) diff --git a/packages/velog-cron/src/services/UserService/index.ts b/apps/cron/src/services/UserService/index.mts similarity index 80% rename from packages/velog-cron/src/services/UserService/index.ts rename to apps/cron/src/services/UserService/index.mts index b9cd6b4e..1ccf9dd4 100644 --- a/packages/velog-cron/src/services/UserService/index.ts +++ b/apps/cron/src/services/UserService/index.mts @@ -1,5 +1,5 @@ -import { DbService } from '@lib/db/DbService.js' -import { User } from '@prisma/client' +import { DbService } from '@lib/db/DbService.mjs' +import { User } from '@packages/database/velog-rds' import { injectable, singleton } from 'tsyringe' interface Service { diff --git a/apps/cron/src/services/WriterService/WriterService.test.mts b/apps/cron/src/services/WriterService/WriterService.test.mts new file mode 100644 index 00000000..cceb7685 --- /dev/null +++ b/apps/cron/src/services/WriterService/WriterService.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { WriterService } from './index.mjs' + +describe('WriterService', () => { + const service = container.resolve(WriterService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/packages/velog-cron/src/services/WriterService/index.ts b/apps/cron/src/services/WriterService/index.mts similarity index 93% rename from packages/velog-cron/src/services/WriterService/index.ts rename to apps/cron/src/services/WriterService/index.mts index 8c185c03..1422dec5 100644 --- a/packages/velog-cron/src/services/WriterService/index.ts +++ b/apps/cron/src/services/WriterService/index.mts @@ -1,6 +1,6 @@ -import { DbService } from '@lib/db/DbService.js' -import { UtilsService } from '@lib/utils/UtilsService.js' -import { Prisma } from '@prisma/client' +import { DbService } from '@lib/db/DbService.mjs' +import { UtilsService } from '@lib/utils/UtilsService.mjs' +import { Prisma } from '@packages/database/velog-rds' import { injectable, singleton } from 'tsyringe' import { subMonths } from 'date-fns' @@ -11,10 +11,7 @@ interface Service { @injectable() @singleton() export class WriterService implements Service { - constructor( - private readonly db: DbService, - private readonly utils: UtilsService, - ) {} + constructor(private readonly db: DbService, private readonly utils: UtilsService) {} public async generateTrendingWriters(): Promise { const threeMonthAgo = subMonths(this.utils.now, 3) const sixMonthAgo = subMonths(this.utils.now, 6) diff --git a/apps/cron/tsconfig.build.json b/apps/cron/tsconfig.build.json new file mode 100644 index 00000000..6d4c51eb --- /dev/null +++ b/apps/cron/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["./src"], + "exclude": ["node_modules", "**/*.test.ts", "./test/**/*.ts", "./scripts/**/*.ts"] +} diff --git a/apps/cron/tsconfig.json b/apps/cron/tsconfig.json new file mode 100644 index 00000000..dd6cfd72 --- /dev/null +++ b/apps/cron/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@packages/tsconfig/base.json", + "compilerOptions": { + "rootDir": "./", + "baseUrl": "./", + "outDir": "./dist", + "typeRoots": ["./node_modules/@types", "./src/types", "./node_modules/@packages/**/*.d.ts"], + "paths": { + "@test/*": ["src/test/*"], + "@lib/*": ["src/lib/*"], + "@jobs/*": ["src/jobs/*"], + "@routes/*": ["src/routes/*"], + "@services/*": ["src/services/*"], + "@errors/*": ["src/common/errors/*"], + "@plugins/*": ["src/common/plugins/*"], + "@constants/*": ["src/common/constants/*"], + "@env": ["src/env.mts"] + } + }, + "include": ["./src", "eslint.config.js", "./scripts", "test/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/velog-server/.dockerignore b/apps/server/.dockerignore similarity index 71% rename from packages/velog-server/.dockerignore rename to apps/server/.dockerignore index 74635cc7..a1f9e9ae 100644 --- a/packages/velog-server/.dockerignore +++ b/apps/server/.dockerignore @@ -1,4 +1,4 @@ dockers/ node_modules dist -env/.env.* \ No newline at end of file +env/.env.* diff --git a/packages/velog-server/.gitignore b/apps/server/.gitignore similarity index 100% rename from packages/velog-server/.gitignore rename to apps/server/.gitignore diff --git a/apps/server/.prettierignore b/apps/server/.prettierignore new file mode 100644 index 00000000..2c138ab1 --- /dev/null +++ b/apps/server/.prettierignore @@ -0,0 +1 @@ +/src/**/TagService/index.tsx \ No newline at end of file diff --git a/packages/velog-web/.prettierrc b/apps/server/.prettierrc similarity index 100% rename from packages/velog-web/.prettierrc rename to apps/server/.prettierrc diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile new file mode 100644 index 00000000..6ed8f1b5 --- /dev/null +++ b/apps/server/Dockerfile @@ -0,0 +1,70 @@ +FROM node:20.16.0-alpine AS base +RUN apk update +RUN apk add --no-cache libc6-compat + +# RUN corepack enable +RUN npm install --global corepack@latest + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN pnpm add -g turbo +RUN turbo telemetry disable + +ARG DOCKER_ENV +ENV DOCKER_ENV=${DOCKER_ENV} +RUN echo "DOCKER_ENV value is: ${DOCKER_ENV}" + +ARG AWS_ACCESS_KEY_ID +ENV AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} + +ARG AWS_SECRET_ACCESS_KEY +ENV AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} + +ARG TURBO_TEAM +ENV TURBO_TEAM=${TURBO_TEAM} + +ARG TURBO_TOKEN +ENV TURBO_TOKEN=${TURBO_TOKEN} + +ARG TURBO_REMOTE_CACHE_SIGNATURE_KEY +ENV TURBO_REMOTE_CACHE_SIGNATURE_KEY=$TURBO_REMOTE_CACHE_SIGNATURE_KEY + +ENV APP_NAME="server" +ENV APP_DIR="apps/${APP_NAME}" +RUN echo "APP_DIR value is: ${APP_DIR}" + +WORKDIR /app + +FROM base AS pruner +COPY . . +RUN turbo prune ${APP_NAME} --docker + +FROM base As builder +COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml +COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml +COPY --from=pruner /app/out/json/ ./ +COPY --from=pruner /app/out/full/ ./ + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prefer-frozen-lockfile + +RUN pnpm --filter @packages/database prisma:generate +RUN pnpm --filter ${APP_NAME} ssm pull -e ${DOCKER_ENV} +RUN turbo build --filter=${APP_NAME} --remote-only + +FROM base As runner +COPY --from=builder /app/package*.json /app/ +COPY --from=builder /app/pnpm-*.yaml /app/ +COPY --from=builder /app/packages/ /app/packages + +WORKDIR /app/${APP_DIR} +COPY --from=builder /app/${APP_DIR}/dist /app/${APP_DIR}/dist +COPY --from=builder /app/${APP_DIR}/src/graphql /app/${APP_DIR}/src/graphql +COPY --from=builder /app/${APP_DIR}/*.json . +COPY --from=builder /app/${APP_DIR}/env /app/${APP_DIR}/env + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile + +EXPOSE 5003 + +CMD [ "pnpm", "start" ] \ No newline at end of file diff --git a/packages/velog-server/README.md b/apps/server/README.md similarity index 100% rename from packages/velog-server/README.md rename to apps/server/README.md diff --git a/packages/velog-server/codegen.ts b/apps/server/codegen.ts similarity index 66% rename from packages/velog-server/codegen.ts rename to apps/server/codegen.ts index 683fad14..af2348cf 100644 --- a/packages/velog-server/codegen.ts +++ b/apps/server/codegen.ts @@ -5,7 +5,7 @@ const config: CodegenConfig = { schema: 'src/graphql/**/*.gql', documents: undefined, hooks: { - afterOneFileWrite: ['prettier --write'], + afterOneFileWrite: ['prettier --write .'], }, generates: { 'src/graphql/helpers/generated.ts': { @@ -21,15 +21,15 @@ const config: CodegenConfig = { ], config: { skipTypename: true, - contextType: './../../common/interfaces/graphql#GraphQLContext', + contextType: './../../common/interfaces/graphql.js#GraphQLContext', enumValues: { - NotificationType: './enums#NotificationType', + NotificationType: './enums.js#NotificationType', }, mappers: { - User: '@prisma/client#User as UserModel', - UserProfile: '@prisma/client#UserProfile as UserProfileModel', - Post: '@prisma/client#Post as PostModel', - Comment: '@prisma/client#Comment as CommentModel', + User: '@packages/database/velog-rds#User as UserModel', + UserProfile: '@packages/database/velog-rds#UserProfile as UserProfileModel', + Post: '@packages/database/velog-rds#Post as PostModel', + Comment: '@packages/database/velog-rds#Comment as CommentModel', }, inputMaybeValue: 'T | undefined', maybeValue: 'T | null | undefined', diff --git a/apps/server/docker-compose.sh b/apps/server/docker-compose.sh new file mode 100755 index 00000000..c8c5724b --- /dev/null +++ b/apps/server/docker-compose.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +if [ -z "$AWS_PROFILE" ]; then + echo "AWS_PROFILE이 설정되어 있지 않습니다." + exit 1 +fi + +# AWS 프로필 설정 읽기 +export AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id --profile $AWS_PROFILE) +export AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key --profile $AWS_PROFILE) +export TURBO_TEAM=$(aws configure get turbo_team --profile $AWS_PROFILE) +export TURBO_TOKEN=$(aws configure get turbo_token --profile $AWS_PROFILE) +export TURBO_REMOTE_CACHE_SIGNATURE_KEY=$(aws configure get turbo_remote_cache_signature_key --profile $AWS_PROFILE) + +# echo "AWS 프로필 설정이 완료되었습니다. $AWS_PROFILE 프로필을 사용합니다." +# echo "AWS_ACCESSKEY_ID: $AWS_ACCESS_KEY_ID" +# echo "AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY" +# echo "TURBO_TEAM: $TURBO_TEAM" +# echo "TURBO_TOKEN: $TURBO_TOKEN" +# echo "TURBO_REMOTE_CACHE_SIGNATURE_KEY: $TURBO_REMOTE_CACHE_SIGNATURE_KEY" + +docker-compose up --build \ No newline at end of file diff --git a/apps/server/docker-compose.yml b/apps/server/docker-compose.yml new file mode 100644 index 00000000..0522c66a --- /dev/null +++ b/apps/server/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.9' +services: + server: + container_name: velog-server + build: + context: ../../ + dockerfile: ./apps/server/Dockerfile + no_cache: true + args: + DOCKER_ENV: stage + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + TURBO_TEAM: ${TURBO_TEAM} + TURBO_TOKEN: ${TURBO_TOKEN} + TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${TURBO_REMOTE_CACHE_SIGNATURE_KEY} + + tty: true + stdin_open: true + ports: + - 5003:5003 + deploy: + resources: + limits: + cpus: '1' # range 0.01 to 10.00 + memory: '2G' + reservations: + cpus: '0.25' + memory: '256M' diff --git a/packages/velog-server/dockers/.gitignore b/apps/server/dockers/.gitignore similarity index 100% rename from packages/velog-server/dockers/.gitignore rename to apps/server/dockers/.gitignore diff --git a/packages/velog-server/dockers/README.md b/apps/server/dockers/README.md similarity index 100% rename from packages/velog-server/dockers/README.md rename to apps/server/dockers/README.md diff --git a/packages/velog-server/dockers/docker-compose.yml b/apps/server/dockers/docker-compose.yml similarity index 100% rename from packages/velog-server/dockers/docker-compose.yml rename to apps/server/dockers/docker-compose.yml diff --git a/packages/velog-server/env/.env.example b/apps/server/env/.env.example similarity index 97% rename from packages/velog-server/env/.env.example rename to apps/server/env/.env.example index f61dd575..282f95f0 100644 --- a/packages/velog-server/env/.env.example +++ b/apps/server/env/.env.example @@ -34,7 +34,7 @@ REDIS_HOST= #SLACK SLACK_TOKEN= SLACK_IMAGE= -# BLAKC_LIST +# BLACK_LIST BLACKLIST_USERNAME= BLACKLIST_IP= # AWS diff --git a/apps/server/eslint.config.js b/apps/server/eslint.config.js new file mode 100644 index 00000000..3871f86d --- /dev/null +++ b/apps/server/eslint.config.js @@ -0,0 +1,12 @@ +import baseConfig from '@packages/eslint-config/base.mjs' +import { resolve } from 'node:path' + +const projectPath = resolve(process.cwd()) + +/** @type {Linter.Config} */ +export default [ + ...baseConfig(projectPath), + { + ignores: ['node_modules', 'dist'], + }, +] diff --git a/packages/velog-server/jest.config.ts b/apps/server/jest.config.ts similarity index 100% rename from packages/velog-server/jest.config.ts rename to apps/server/jest.config.ts diff --git a/packages/velog-server/jest.setup.ts b/apps/server/jest.setup.ts similarity index 100% rename from packages/velog-server/jest.setup.ts rename to apps/server/jest.setup.ts diff --git a/packages/velog-server/package.json b/apps/server/package.json similarity index 62% rename from packages/velog-server/package.json rename to apps/server/package.json index a34df46e..35f25468 100644 --- a/packages/velog-server/package.json +++ b/apps/server/package.json @@ -1,5 +1,5 @@ { - "name": "velog-server", + "name": "server", "version": "1.0.0", "author": { "name": "velopert", @@ -10,40 +10,36 @@ "velog" ], "engines": { - "node": ">=18.16" + "node": ">=20.11.1" }, - "main": "main.ts", + "main": "/src/main.mts", "type": "module", "scripts": { - "dev": "nodemon --watch './**/*.ts' --exec 'node --loader ts-paths-esm-loader/transpile-only' src/main.ts | pino-pretty", - "stage": "pnpm ssm pull -e stage && NODE_ENV=production pnpm start", - "prod": "pnpm ssm pull -e production && NODE_ENV=production pnpm start", - "build": "tsc --project tsconfig.json && tsc-alias -p tsconfig.json", - "start": "node dist/src/main.js", - "prisma:copy": "tsx ./scripts/copyPrisma.ts", - "prisma:rm": "rm -rf ./prisma", + "dev": "nodemon --watch './**/*.mts' --exec 'node --import @swc-node/register/esm-register' ./src/main.mts | pino-pretty", + "start": "NODE_ENV=production node dist/main.mjs", + "build": "tsc --project tsconfig.build.json && tsc-alias -p tsconfig.build.json", + "lint": "eslint --fix", "codegen": "graphql-codegen --config codegen.ts", "test": "pnpm jest --detectOpenHandles", "create-service": "tsx ./scripts/createService.ts", "create-mock": "NODE_ENV=development tsx ./scripts/createMock.ts", - "ssm": "tsx ./scripts/ssm/index.ts" + "ssm": "tsx ./scripts/ssm.mts", + "turbo": "turbo build" }, "dependencies": { "@aws-sdk/client-s3": "^3.473.0", "@aws-sdk/client-ses": "^3.405.0", - "@aws-sdk/client-ssm": "^3.379.1", "@aws-sdk/s3-request-presigner": "^3.473.0", "@elastic/elasticsearch": "7.3.0", - "@fastify/autoload": "^5.7.1", "@fastify/cookie": "^9.3.1", "@fastify/cors": "^8.3.0", "@fastify/formbody": "^7.4.0", - "@graphql-tools/graphql-file-loader": "^8.0.0", - "@graphql-tools/load": "^8.0.0", - "@graphql-tools/merge": "^9.0.0", - "@graphql-tools/schema": "^10.0.0", "@octokit/rest": "^20.0.1", - "@prisma/client": "^5.8.1", + "@packages/commonjs": "workspace:*", + "@packages/database": "workspace:*", + "@packages/library": "workspace:*", + "@packages/scripts": "workspace:*", + "@prisma/client": "^5.17.0", "ajv": "^8.12.0", "axios": "^1.4.0", "backblaze-b2": "^1.7.0", @@ -51,45 +47,45 @@ "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "discord.js": "^14.14.1", - "dotenv": "^16.1.4", - "fastify": "^4.18.0", + "dotenv": "^16.4.5", + "fastify": "^4.26.2", "fastify-multer": "^2.0.3", "fastify-plugin": "^4.5.1", "geoip-country": "^4.2.68", "googleapis": "^124.0.0", "graphql": "^16.7.1", "graphql-scalars": "^1.22.2", - "inquirer": "^9.2.12", - "ioredis": "^5.3.2", + "inquirer": "^9.2.23", + "json-schema-to-ts": "^3.0.1", "jsonwebtoken": "^9.0.2", "lru-cache": "^10.0.0", "marked": "^8.0.0", - "mercurius": "^13.0.0", + "mercurius": "^14.0.0", "mime-types": "^2.1.35", - "nanoid": "^4.0.2", + "nanoid": "^5.0.7", "nanoid-dictionary": "^4.3.0", "pino-pretty": "^10.0.0", - "prisma": "^5.8.1", + "prisma": "^5.17.0", "qs": "^6.11.2", "rambda": "^8.3.0", "reflect-metadata": "^0.1.13", "remove-markdown": "^0.5.0", "sanitize-html": "^2.11.0", "tmp": "^0.2.1", - "tsc-alias": "^1.8.7", - "tsx": "^4.6.2", "tsyringe": "^4.7.0", "uuid": "^9.0.1", - "velog-server": "link:", "zod": "^3.21.4" }, "devDependencies": { "@faker-js/faker": "^8.3.1", "@graphql-codegen/add": "^5.0.0", - "@graphql-codegen/cli": "^5.0.0", + "@graphql-codegen/cli": "^5.0.2", "@graphql-codegen/typescript": "^4.0.0", "@graphql-codegen/typescript-operations": "^4.0.0", "@graphql-codegen/typescript-resolvers": "^4.0.0", + "@packages/eslint-config": "workspace:*", + "@packages/tsconfig": "workspace:*", + "@swc-node/register": "^1.9.0", "@types/backblaze-b2": "^1.5.2", "@types/geoip-country": "^4.0.2", "@types/inquirer": "^9.0.7", @@ -97,27 +93,19 @@ "@types/jsonwebtoken": "^9.0.2", "@types/mime-types": "^2.1.1", "@types/nanoid-dictionary": "^4.2.3", - "@types/node": "^20.3.1", "@types/qs": "^6.9.7", "@types/remove-markdown": "^0.3.1", "@types/sanitize-html": "^2.9.0", "@types/tmp": "^0.2.3", "@types/uuid": "^9.0.7", - "@typescript-eslint/eslint-plugin": "^6.6.0", - "@typescript-eslint/parser": "^6.4.1", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", "jest": "^29.6.4", "jest-mock-axios": "^4.7.3", "jest-mock-extended": "^3.0.5", - "json-schema-to-ts": "^2.9.2", "nodemon": "^2.0.22", - "prettier": "3.0.2", "ts-jest": "^29.1.1", "ts-jest-mock-import-meta": "^1.0.0", - "ts-node": "^10.9.1", "ts-paths-esm-loader": "^1.4.3", - "typescript": "^5.3.3" + "tsc-alias": "^1.8.7", + "tsx": "^4.7.2" } } diff --git a/packages/velog-server/public/images/.gitkeep b/apps/server/public/images/.gitkeep similarity index 100% rename from packages/velog-server/public/images/.gitkeep rename to apps/server/public/images/.gitkeep diff --git a/packages/velog-server/scripts/createMock.ts b/apps/server/scripts/createMock.mts similarity index 95% rename from packages/velog-server/scripts/createMock.ts rename to apps/server/scripts/createMock.mts index cb022db2..5a354fc9 100644 --- a/packages/velog-server/scripts/createMock.ts +++ b/apps/server/scripts/createMock.mts @@ -1,11 +1,11 @@ import 'reflect-metadata' import { faker } from '@faker-js/faker' -import { getMockUserWithProfile, MockUserWithProfileType } from 'test/mock/mockUser' +import { getMockUserWithProfile, MockUserWithProfileType } from 'test/mock/mockUser.js' import { DbService } from '@lib/db/DbService.js' import { UtilsService } from '@lib/utils/UtilsService.js' -import { Post, Prisma, User } from '@prisma/client' -import { mockComment } from 'test/mock/mockComment' -import { MockPostsType, mockPosts } from 'test/mock/mockPost' +import { Post, Prisma, User } from '@packages/database/velog-rds' +import { mockComment } from 'test/mock/mockComment.js' +import { MockPostsType, mockPosts } from 'test/mock/mockPost.js' import { v4 as uuidv4 } from 'uuid' import { ENV } from '@env' import { @@ -13,15 +13,12 @@ import { CommentReplyNotifictionActionInput, FollowNotificationActionInput, PostLikeNotificationActionInput, -} from '@graphql/helpers/generated' +} from '@graphql/helpers/generated.js' const MAX_COMMENTS_PER_POST = 5 class Seeder { - constructor( - private readonly db: DbService, - private readonly utils: UtilsService, - ) {} + constructor(private readonly db: DbService, private readonly utils: UtilsService) {} public createUser(mockUser: MockUserWithProfileType[]) { return mockUser.map((user) => { const { profile, username, email, is_certified } = user diff --git a/packages/velog-cron/scripts/createService.ts b/apps/server/scripts/createService.mts similarity index 100% rename from packages/velog-cron/scripts/createService.ts rename to apps/server/scripts/createService.mts diff --git a/apps/server/scripts/ssm.mts b/apps/server/scripts/ssm.mts new file mode 100644 index 00000000..e818a90b --- /dev/null +++ b/apps/server/scripts/ssm.mts @@ -0,0 +1,4 @@ +import { SSMScript } from '@packages/scripts' + +const ssmScript = new SSMScript({ packageName: 'server' }) +ssmScript.execute() diff --git a/packages/velog-server/scripts/templates/services/My.test.ts b/apps/server/scripts/templates/lib/My.test.mts similarity index 81% rename from packages/velog-server/scripts/templates/services/My.test.ts rename to apps/server/scripts/templates/lib/My.test.mts index 02723a69..d303def3 100644 --- a/packages/velog-server/scripts/templates/services/My.test.ts +++ b/apps/server/scripts/templates/lib/My.test.mts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { MyService } from './index.js' +import { MyService } from './MyService.mjs' describe('MyService', () => { const service = container.resolve(MyService) diff --git a/packages/velog-cron/scripts/templates/lib/MyService.ts b/apps/server/scripts/templates/lib/MyService.mts similarity index 100% rename from packages/velog-cron/scripts/templates/lib/MyService.ts rename to apps/server/scripts/templates/lib/MyService.mts diff --git a/packages/velog-cron/scripts/templates/services/My.test.ts b/apps/server/scripts/templates/services/My.test.ts similarity index 100% rename from packages/velog-cron/scripts/templates/services/My.test.ts rename to apps/server/scripts/templates/services/My.test.ts diff --git a/packages/velog-cron/scripts/templates/services/index.ts b/apps/server/scripts/templates/services/index.ts similarity index 100% rename from packages/velog-cron/scripts/templates/services/index.ts rename to apps/server/scripts/templates/services/index.ts diff --git a/apps/server/src/app.mts b/apps/server/src/app.mts new file mode 100644 index 00000000..1c26c0eb --- /dev/null +++ b/apps/server/src/app.mts @@ -0,0 +1,34 @@ +import Fastify from 'fastify' +import formbody from '@fastify/formbody' +import cookie from '@fastify/cookie' +import { ENV } from '@env' +import routes from '@routes/index.mjs' +import multer from 'fastify-multer' +import validatorCompilerPlugin from '@plugins/global/validatorCompilerPlugin.mjs' +import authPlugin from '@plugins/global/authPlugin.mjs' +import corsPlugin from '@plugins/global/corsPlugin.mjs' +import errorHandlerPlugin from '@plugins/global/errorHandlerPlugin.mjs' +import ipaddrPlugin from '@plugins/global/ipaddrPlugin.mjs' +import keepAlivePlugin from '@plugins/global/keepAlivePlugin.mjs' +import mercuriusPlugin from '@plugins/global/mercuriusPlugin.mjs' + +const app = Fastify({ + logger: true, + trustProxy: true, +}) + +app.register(cookie, { secret: ENV.cookieSecretKey }) +app.register(formbody) + +await app.register(corsPlugin) +app.register(authPlugin) +app.register(ipaddrPlugin) +app.register(mercuriusPlugin) +app.register(multer.contentParser) +app.register(validatorCompilerPlugin) +app.register(errorHandlerPlugin) +app.register(keepAlivePlugin) + +app.register(routes) + +export default app diff --git a/apps/server/src/common/constants/HttpStatusConstants.ts b/apps/server/src/common/constants/HttpStatusConstants.ts new file mode 100644 index 00000000..35157997 --- /dev/null +++ b/apps/server/src/common/constants/HttpStatusConstants.ts @@ -0,0 +1,19 @@ +const OK = 200 +const CREATED = 201 +const BAD_REQUEST = 400 +const UNAUTHORIZED = 401 +const FORBIDDEN = 403 +const NOT_FOUND = 404 +const CONFLICT = 409 +const INTERNAL_SERVER_ERROR = 500 + +export const HttpStatus = { + OK, + CREATED, + BAD_REQUEST, + FORBIDDEN, + UNAUTHORIZED, + NOT_FOUND, + CONFLICT, + INTERNAL_SERVER_ERROR, +} diff --git a/apps/server/src/common/constants/HttpStatusMesageConstants.ts b/apps/server/src/common/constants/HttpStatusMesageConstants.ts new file mode 100644 index 00000000..42bba212 --- /dev/null +++ b/apps/server/src/common/constants/HttpStatusMesageConstants.ts @@ -0,0 +1,19 @@ +const OK = 'Ok' +const CREATED = 'CREATED' +const BAD_REQUEST = 'BAD_REQUEST' +const UNAUTHORIZED = 'UNAUTHORIZED' +const FORBIDDEN = 'FORBIDDEN' +const NOT_FOUND = 'NOT_FOUND' +const CONFLICT = 'CONFLICT' +const INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR' + +export const HttpStatusMessage = { + OK, + CREATED, + BAD_REQUEST, + FORBIDDEN, + UNAUTHORIZED, + NOT_FOUND, + CONFLICT, + INTERNAL_SERVER_ERROR, +} diff --git a/apps/server/src/common/constants/TimeConstants.ts b/apps/server/src/common/constants/TimeConstants.ts new file mode 100644 index 00000000..bfb1e598 --- /dev/null +++ b/apps/server/src/common/constants/TimeConstants.ts @@ -0,0 +1,17 @@ +const ONE_MINUTE_IN_MS = 1000 * 60 +const ONE_HOUR_IN_MS = ONE_MINUTE_IN_MS * 60 +const ONE_DAY_IN_MS = ONE_HOUR_IN_MS * 24 + +const ONE_MINUTE_IN_S = 60 +const ONE_HOUR_IN_S = ONE_MINUTE_IN_S * 60 +const ONE_DAY_IN_S = ONE_HOUR_IN_S * 24 + +export const Time = { + ONE_MINUTE_IN_MS, + ONE_HOUR_IN_MS, + ONE_DAY_IN_MS, + + ONE_MINUTE_IN_S, + ONE_HOUR_IN_S, + ONE_DAY_IN_S, +} diff --git a/packages/velog-cron/src/common/errors/BadRequestErrors.ts b/apps/server/src/common/errors/BadRequestErrors.ts similarity index 100% rename from packages/velog-cron/src/common/errors/BadRequestErrors.ts rename to apps/server/src/common/errors/BadRequestErrors.ts diff --git a/packages/velog-cron/src/common/errors/ConfilctError.ts b/apps/server/src/common/errors/ConfilctError.ts similarity index 100% rename from packages/velog-cron/src/common/errors/ConfilctError.ts rename to apps/server/src/common/errors/ConfilctError.ts diff --git a/packages/velog-cron/src/common/errors/ForbiddenError.ts b/apps/server/src/common/errors/ForbiddenError.ts similarity index 100% rename from packages/velog-cron/src/common/errors/ForbiddenError.ts rename to apps/server/src/common/errors/ForbiddenError.ts diff --git a/apps/server/src/common/errors/HttpError.ts b/apps/server/src/common/errors/HttpError.ts new file mode 100644 index 00000000..7e1191c6 --- /dev/null +++ b/apps/server/src/common/errors/HttpError.ts @@ -0,0 +1,17 @@ +export class HttpError extends Error { + message + statusCode + name + constructor(name: string, message: string, statusCode: number) { + super() + this.message = message + this.statusCode = statusCode + this.name = name + // Ensure the stack trace is captured + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor) + } + } +} + +export const isHttpError = (e: unknown): e is HttpError => e instanceof HttpError diff --git a/packages/velog-server/src/common/errors/InternalServerError.ts b/apps/server/src/common/errors/InternalServerError.ts similarity index 100% rename from packages/velog-server/src/common/errors/InternalServerError.ts rename to apps/server/src/common/errors/InternalServerError.ts diff --git a/packages/velog-cron/src/common/errors/NotfoundError.ts b/apps/server/src/common/errors/NotfoundError.ts similarity index 100% rename from packages/velog-cron/src/common/errors/NotfoundError.ts rename to apps/server/src/common/errors/NotfoundError.ts diff --git a/packages/velog-cron/src/common/errors/UnauthorizedError.ts b/apps/server/src/common/errors/UnauthorizedError.ts similarity index 100% rename from packages/velog-cron/src/common/errors/UnauthorizedError.ts rename to apps/server/src/common/errors/UnauthorizedError.ts diff --git a/packages/velog-server/src/common/errors/index.ts b/apps/server/src/common/errors/index.ts similarity index 100% rename from packages/velog-server/src/common/errors/index.ts rename to apps/server/src/common/errors/index.ts diff --git a/packages/velog-server/src/common/interfaces/comment.ts b/apps/server/src/common/interfaces/comment.ts similarity index 100% rename from packages/velog-server/src/common/interfaces/comment.ts rename to apps/server/src/common/interfaces/comment.ts diff --git a/packages/velog-server/src/common/interfaces/graphql.ts b/apps/server/src/common/interfaces/graphql.ts similarity index 100% rename from packages/velog-server/src/common/interfaces/graphql.ts rename to apps/server/src/common/interfaces/graphql.ts diff --git a/packages/velog-server/src/common/interfaces/user.ts b/apps/server/src/common/interfaces/user.ts similarity index 62% rename from packages/velog-server/src/common/interfaces/user.ts rename to apps/server/src/common/interfaces/user.ts index 0a9e69b1..7b4d5fa4 100644 --- a/packages/velog-server/src/common/interfaces/user.ts +++ b/apps/server/src/common/interfaces/user.ts @@ -1,4 +1,4 @@ -import { Prisma } from '@prisma/client' +import { Prisma } from '@packages/database/velog-rds' export type CurrentUser = Prisma.UserGetPayload<{ include: { diff --git a/packages/velog-server/src/common/plugins/encapsulated/authGuardPlugin.ts b/apps/server/src/common/plugins/encapsulated/authGuardPlugin.ts similarity index 100% rename from packages/velog-server/src/common/plugins/encapsulated/authGuardPlugin.ts rename to apps/server/src/common/plugins/encapsulated/authGuardPlugin.ts diff --git a/packages/velog-server/src/common/plugins/global/authPlugin.ts b/apps/server/src/common/plugins/global/authPlugin.mts similarity index 69% rename from packages/velog-server/src/common/plugins/global/authPlugin.ts rename to apps/server/src/common/plugins/global/authPlugin.mts index b9e89064..04b001cd 100644 --- a/packages/velog-server/src/common/plugins/global/authPlugin.ts +++ b/apps/server/src/common/plugins/global/authPlugin.mts @@ -1,11 +1,13 @@ import { JwtService } from '@lib/jwt/JwtService.js' -import { AccessTokenData } from '@lib/jwt/JwtInterface.js' +import type { AccessTokenData } from '@packages/library/jwt' import { FastifyPluginAsync } from 'fastify' import { container } from 'tsyringe' import { UserService } from '@services/UserService/index.js' import { CookieService } from '@lib/cookie/CookieService.js' import { Time } from '@constants/TimeConstants.js' +import fp from 'fastify-plugin' +// TODO: apply fastify-plugin const authPlugin: FastifyPluginAsync = async (fastify) => { fastify.decorateRequest('user', null) fastify.addHook('preHandler', async (request, reply) => { @@ -13,19 +15,20 @@ const authPlugin: FastifyPluginAsync = async (fastify) => { const userService = container.resolve(UserService) const jwt = container.resolve(JwtService) + const cookie = container.resolve(CookieService) let accessToken: string | undefined = request.cookies['access_token'] const refreshToken: string | undefined = request.cookies['refresh_token'] const authorization = request.headers['authorization'] try { - if (!accessToken && authorization) { + if (!accessToken && !!authorization && typeof authorization === 'string') { accessToken = authorization.split('Bearer ')[1] } if (!accessToken && !refreshToken) return - if (accessToken && refreshToken) { + if (accessToken) { const accessTokenData = await jwt.decodeToken(accessToken) const diff = accessTokenData.exp * 1000 - new Date().getTime() @@ -33,17 +36,28 @@ const authPlugin: FastifyPluginAsync = async (fastify) => { if (diff < Time.ONE_MINUTE_IN_MS * 30 && refreshToken) { await userService.restoreToken({ request, reply }) } + + const exists = await userService.checkExistsUser(accessTokenData.user_id) + if (!exists) { + cookie.clearCookie(reply, 'access_token') + cookie.clearCookie(reply, 'refresh_token') + throw new Error('User not found') + } + + request.user = { id: accessTokenData.user_id } + return } if (!accessToken && refreshToken) { const tokens = await userService.restoreToken({ request, reply }) accessToken = tokens.accessToken - } - if (!accessToken) return + const accessTokenData = await jwt.decodeToken(accessToken) + request.user = { id: accessTokenData.user_id } + return + } - const accessTokenData = await jwt.decodeToken(accessToken) - request.user = { id: accessTokenData.user_id } + request.user = null } catch (e) { console.log('accessToken', accessToken) console.log('authPlugin error', e) @@ -55,11 +69,9 @@ const authPlugin: FastifyPluginAsync = async (fastify) => { const accessTokenData = await jwt.decodeToken(accessToken) request.user = { id: accessTokenData.user_id } - } else { - throw new Error() } } catch (error) { - const cookie = container.resolve(CookieService) + console.log('refresh token error', error) cookie.clearCookie(reply, 'access_token') cookie.clearCookie(reply, 'refresh_token') } @@ -67,4 +79,4 @@ const authPlugin: FastifyPluginAsync = async (fastify) => { }) } -export default authPlugin +export default fp(authPlugin) diff --git a/packages/velog-server/src/common/plugins/global/corsPlugin.ts b/apps/server/src/common/plugins/global/corsPlugin.mts similarity index 86% rename from packages/velog-server/src/common/plugins/global/corsPlugin.ts rename to apps/server/src/common/plugins/global/corsPlugin.mts index 1beee8c5..781f40c1 100644 --- a/packages/velog-server/src/common/plugins/global/corsPlugin.ts +++ b/apps/server/src/common/plugins/global/corsPlugin.mts @@ -2,7 +2,9 @@ import { FastifyPluginAsync } from 'fastify' import cors from '@fastify/cors' import { ForbiddenError } from '@errors/ForbiddenError.js' import { ENV } from '@env' +import fp from 'fastify-plugin' +// TODO: apply fastify-plugin const corsPlugin: FastifyPluginAsync = async (fastify) => { const corsWhitelist: RegExp[] = [ /^https:\/\/velog.io$/, @@ -16,7 +18,7 @@ const corsPlugin: FastifyPluginAsync = async (fastify) => { corsWhitelist.push(/^http:\/\/localhost/) } - fastify.register(cors, { + await fastify.register(cors, { credentials: true, origin: (origin, callback) => { if (!origin || corsWhitelist.some((re) => re.test(origin))) { @@ -28,4 +30,4 @@ const corsPlugin: FastifyPluginAsync = async (fastify) => { }) } -export default corsPlugin +export default fp(corsPlugin) diff --git a/apps/server/src/common/plugins/global/errorHandlerPlugin.mts b/apps/server/src/common/plugins/global/errorHandlerPlugin.mts new file mode 100644 index 00000000..0cd75ffb --- /dev/null +++ b/apps/server/src/common/plugins/global/errorHandlerPlugin.mts @@ -0,0 +1,74 @@ +import { ENV } from '@env' +import type { FastifyPluginCallback, FastifyRequest } from 'fastify' +import { container } from 'tsyringe' +import fp from 'fastify-plugin' +import { isHttpError } from '@errors/HttpError.js' +import { + DiscordService, + type MessagePayload, + type MessageType, +} from '@lib/discord/DiscordService.js' +import { DynamicConfigService } from '@services/DynamicConfigService/index.js' + +const errorHandlerPlugin: FastifyPluginCallback = fp(async (fastify) => { + const discord = container.resolve(DiscordService) + const dynamicConfig = container.resolve(DynamicConfigService) + + fastify.addHook('preHandler', (request, _, done) => { + if (request.body) { + request.log.info({ body: request.body }, 'parsed body') + } + done() + }) + + const sendErrorToDiscord = async ( + type: MessageType, + payload: MessagePayload, + request: FastifyRequest, + ) => { + const isBlocked = await dynamicConfig.isBlockedUser(request.user?.username) + if (isBlocked) { + fastify.log.info('Blocked user, skipping error message') + return + } + + try { + await discord.sendMessage(type, { + ...payload, + body: payload.body ?? request.body ?? 'none', + query: payload.query ?? request.query ?? 'none', + user: request.user, + ip: request.ip, + }) + } catch (discordError) { + fastify.log.error(discordError, 'Failed to send error message to Discord') + } + } + + fastify.addHook('onError', async (request, _, error) => { + request.log.error(error, 'fastify onError') + await sendErrorToDiscord('error', { type: 'fastify OnError', error }, request) + }) + + fastify.setErrorHandler(async (error, request, reply) => { + const errorResponse = { + message: error.message || 'Internal Server Error', + name: error.name || 'Error', + stack: ENV.appEnv === 'development' ? error.stack : undefined, + } + + if (isHttpError(error)) { + reply.status(error.statusCode).send(errorResponse) + } else { + reply.status(500).send(errorResponse) + } + + if (ENV.appEnv === 'development') { + request.log.error(error, 'fastify handleError') + } else { + await sendErrorToDiscord('error', { type: 'fastify handleError', error }, request) + } + }) +}) + +export default errorHandlerPlugin diff --git a/apps/server/src/common/plugins/global/ipaddrPlugin.mts b/apps/server/src/common/plugins/global/ipaddrPlugin.mts new file mode 100644 index 00000000..69c1d54d --- /dev/null +++ b/apps/server/src/common/plugins/global/ipaddrPlugin.mts @@ -0,0 +1,21 @@ +import type { FastifyPluginAsync } from 'fastify' +import fp from 'fastify-plugin' + +// TODO: apply fastify-plugin +const ipaddrPlugin: FastifyPluginAsync = async (fastify) => { + fastify.decorateRequest('ipaddr', null) + fastify.addHook('preHandler', (request, reply, done) => { + const fromCdnIp = request.headers['gcdn-client-ip'] + const xForwardedIp = request.headers['X-Forwarded-For'] + + const graphCdnAddress = Array.isArray(fromCdnIp) ? fromCdnIp[0] : fromCdnIp + const xForwardedForAddress = Array.isArray(xForwardedIp) ? xForwardedIp[0] : xForwardedIp + + const ipaddr = + xForwardedForAddress ?? graphCdnAddress ?? request.ips?.slice(-1)[0] ?? request.ip + request.ipaddr = ipaddr + done() + }) +} + +export default fp(ipaddrPlugin) diff --git a/packages/velog-server/src/common/plugins/global/keepAlivePlugin.ts b/apps/server/src/common/plugins/global/keepAlivePlugin.mts similarity index 78% rename from packages/velog-server/src/common/plugins/global/keepAlivePlugin.ts rename to apps/server/src/common/plugins/global/keepAlivePlugin.mts index 67b1fba1..4860ce25 100644 --- a/packages/velog-server/src/common/plugins/global/keepAlivePlugin.ts +++ b/apps/server/src/common/plugins/global/keepAlivePlugin.mts @@ -1,10 +1,12 @@ import { FastifyPluginAsync } from 'fastify' +import fp from 'fastify-plugin' let isClosing = false export const startClosing = () => { isClosing = true } +// TODO: apply fastify-plugin const keepAlivePlugin: FastifyPluginAsync = async (fastify) => { fastify.addHook('onRequest', (_, reply, done) => { if (isClosing) { @@ -15,4 +17,4 @@ const keepAlivePlugin: FastifyPluginAsync = async (fastify) => { }) } -export default keepAlivePlugin +export default fp(keepAlivePlugin) diff --git a/packages/velog-server/src/common/plugins/global/mercuriusPlugin.ts b/apps/server/src/common/plugins/global/mercuriusPlugin.mts similarity index 65% rename from packages/velog-server/src/common/plugins/global/mercuriusPlugin.ts rename to apps/server/src/common/plugins/global/mercuriusPlugin.mts index d3ba88ce..b3095efa 100644 --- a/packages/velog-server/src/common/plugins/global/mercuriusPlugin.ts +++ b/apps/server/src/common/plugins/global/mercuriusPlugin.mts @@ -6,13 +6,15 @@ import { ENV } from '@env' import { isHttpError } from '@errors/HttpError.js' import { container } from 'tsyringe' import { DiscordService } from '@lib/discord/DiscordService.js' +import fp from 'fastify-plugin' +// TODO: apply fastify-plugin' const mercuriusPlugin: FastifyPluginAsync = async (fastify) => { fastify.register(mercurius, { logLevel: 'error', schema, resolvers: resolvers, - graphiql: ENV.appEnv !== 'production', + graphiql: ENV.dockerEnv !== 'production', context: (request, reply): GraphQLContext => { return { request, @@ -23,7 +25,7 @@ const mercuriusPlugin: FastifyPluginAsync = async (fastify) => { }, errorHandler: (error, request) => { const { name, message, code, stack, errors, statusCode } = error - const result = { + const errorData = { name, message, code, @@ -36,38 +38,40 @@ const mercuriusPlugin: FastifyPluginAsync = async (fastify) => { request.log.error(request, 'errorHandler') } else { const discord = container.resolve(DiscordService) - discord.sendMessage( - 'error', - JSON.stringify({ + + discord + .sendMessage('error', { type: 'errorHandler', - requestbody: request?.body, - result, + body: request?.body, user: request?.user, - }), - ) + ip: request?.ip, + originError: error, + error: errorData, + }) + .catch(console.error) } }, - errorFormatter: (execution, ctx) => { - const e = execution.errors?.[0]?.originalError + errorFormatter: (error, ctx) => { + const e = error.errors?.[0]?.originalError if (!isHttpError(e)) { - console.log('send!') - ;(ctx as any).request?.log?.error(execution, 'errorFormatter') + console.log('mecurius errorFormatter') + ;(ctx as any).request?.log?.error(error, 'errorFormatter') const discord = container.resolve(DiscordService) - discord.sendMessage( - 'error', - JSON.stringify({ + discord + .sendMessage('error', { type: 'errorFormat', - requestbody: (ctx as any).request?.body, - execution, + body: (ctx as any).request?.body, + error, user: (ctx as any).request?.user, - }), - ) + ip: (ctx as any).request?.ip, + }) + .catch(console.error) - return { statusCode: 500, response: execution } + return { statusCode: 500, response: error } } - const errors = execution.errors?.map((error) => + const errors = error.errors?.map((error) => Object.assign(error, { extensions: { name: e.name, @@ -86,4 +90,4 @@ const mercuriusPlugin: FastifyPluginAsync = async (fastify) => { }) } -export default mercuriusPlugin +export default fp(mercuriusPlugin) diff --git a/packages/velog-server/src/common/plugins/global/validatorCompilerPlugin.ts b/apps/server/src/common/plugins/global/validatorCompilerPlugin.mts similarity index 77% rename from packages/velog-server/src/common/plugins/global/validatorCompilerPlugin.ts rename to apps/server/src/common/plugins/global/validatorCompilerPlugin.mts index 6ea2ef00..5ec73aa0 100644 --- a/packages/velog-server/src/common/plugins/global/validatorCompilerPlugin.ts +++ b/apps/server/src/common/plugins/global/validatorCompilerPlugin.mts @@ -1,29 +1,32 @@ +// const Ajv = require('ajv') import Ajv from 'ajv' import { FastifyPluginAsync } from 'fastify' +import fp from 'fastify-plugin' const schemaCompilers = { - body: new Ajv({ + body: new Ajv.default({ removeAdditional: true, coerceTypes: false, allErrors: true, }), - params: new Ajv({ + params: new Ajv.default({ removeAdditional: true, coerceTypes: true, allErrors: true, }), - querystring: new Ajv({ + querystring: new Ajv.default({ removeAdditional: true, coerceTypes: true, allErrors: true, }), - headers: new Ajv({ + headers: new Ajv.default({ removeAdditional: true, coerceTypes: true, allErrors: true, }), } +// TODO: apply fastify-plugin const validatorCompiler: FastifyPluginAsync = async (fastify) => { fastify.setValidatorCompiler((request) => { if (!request.httpPart) { @@ -37,4 +40,4 @@ const validatorCompiler: FastifyPluginAsync = async (fastify) => { }) } -export default validatorCompiler +export default fp(validatorCompiler) diff --git a/packages/velog-server/src/env.ts b/apps/server/src/env.mts similarity index 85% rename from packages/velog-server/src/env.ts rename to apps/server/src/env.mts index 13dfe977..02cfa35c 100644 --- a/packages/velog-server/src/env.ts +++ b/apps/server/src/env.mts @@ -3,8 +3,6 @@ import { existsSync } from 'fs' import { z } from 'zod' import { fileURLToPath } from 'url' import { dirname, join } from 'path' -import { container } from 'tsyringe' -import { DbService } from '@lib/db/DbService.js' type DockerEnv = 'development' | 'stage' | 'production' type AppEnvironment = 'development' | 'production' @@ -28,7 +26,7 @@ const appEnv: AppEnvironment = ['stage', 'production'].includes(dockerEnv) : 'development' const envFile = envFiles[dockerEnv] -const prefix = dockerEnv === 'development' ? './env' : '../env' +const prefix = './env' function resolveDir(dir: string): string { const __filename = fileURLToPath(import.meta.url) @@ -85,13 +83,9 @@ const env = z.object({ discordErrorChannel: z.string(), discordSpamChannel: z.string(), turnstileSecretKey: z.string(), - bannedKeywords: z.array(z.string()), - bannedAltKeywords: z.array(z.string()), graphcdnToken: z.string(), }) -const { bannedKeywords, bannedAltKeywords } = await readEnvFromDatabase() - export const ENV = env.parse({ dockerEnv, appEnv, @@ -131,17 +125,5 @@ export const ENV = env.parse({ discordErrorChannel: process.env.DISCORD_ERROR_CHANNEL, discordSpamChannel: process.env.DISCORD_SPAM_CHANNEL, turnstileSecretKey: process.env.TURNSTILE_SECRET_KEY, - bannedKeywords: bannedKeywords, - bannedAltKeywords: bannedAltKeywords, graphcdnToken: process.env.GRAPHCDN_TOKEN, }) - -async function readEnvFromDatabase() { - const db = container.resolve(DbService) - const items = await db.dynamicConfigItem.findMany() - - return { - bannedKeywords: items.filter((item) => item.type === 'banned').map((item) => item.value), - bannedAltKeywords: items.filter((item) => item.type === 'bannedAlt').map((item) => item.value), - } -} diff --git a/packages/velog-server/src/graphql/Ad.gql b/apps/server/src/graphql/Ad.gql similarity index 100% rename from packages/velog-server/src/graphql/Ad.gql rename to apps/server/src/graphql/Ad.gql diff --git a/packages/velog-server/src/graphql/Auth.gql b/apps/server/src/graphql/Auth.gql similarity index 100% rename from packages/velog-server/src/graphql/Auth.gql rename to apps/server/src/graphql/Auth.gql diff --git a/packages/velog-server/src/graphql/Comment.gql b/apps/server/src/graphql/Comment.gql similarity index 100% rename from packages/velog-server/src/graphql/Comment.gql rename to apps/server/src/graphql/Comment.gql diff --git a/packages/velog-server/src/graphql/Follow.gql b/apps/server/src/graphql/Follow.gql similarity index 100% rename from packages/velog-server/src/graphql/Follow.gql rename to apps/server/src/graphql/Follow.gql diff --git a/packages/velog-server/src/graphql/Notification.gql b/apps/server/src/graphql/Notification.gql similarity index 100% rename from packages/velog-server/src/graphql/Notification.gql rename to apps/server/src/graphql/Notification.gql diff --git a/packages/velog-server/src/graphql/Post.gql b/apps/server/src/graphql/Post.gql similarity index 100% rename from packages/velog-server/src/graphql/Post.gql rename to apps/server/src/graphql/Post.gql diff --git a/apps/server/src/graphql/Scalar.gql b/apps/server/src/graphql/Scalar.gql new file mode 100644 index 00000000..23e9e51d --- /dev/null +++ b/apps/server/src/graphql/Scalar.gql @@ -0,0 +1,4 @@ +scalar Date +scalar JSON +scalar Void +scalar PositiveInt diff --git a/packages/velog-server/src/graphql/Series.gql b/apps/server/src/graphql/Series.gql similarity index 100% rename from packages/velog-server/src/graphql/Series.gql rename to apps/server/src/graphql/Series.gql diff --git a/packages/velog-server/src/graphql/Tag.gql b/apps/server/src/graphql/Tag.gql similarity index 100% rename from packages/velog-server/src/graphql/Tag.gql rename to apps/server/src/graphql/Tag.gql diff --git a/packages/velog-server/src/graphql/User.gql b/apps/server/src/graphql/User.gql similarity index 100% rename from packages/velog-server/src/graphql/User.gql rename to apps/server/src/graphql/User.gql diff --git a/packages/velog-server/src/graphql/Writer.gql b/apps/server/src/graphql/Writer.gql similarity index 100% rename from packages/velog-server/src/graphql/Writer.gql rename to apps/server/src/graphql/Writer.gql diff --git a/packages/velog-server/src/graphql/helpers/enums.ts b/apps/server/src/graphql/helpers/enums.ts similarity index 100% rename from packages/velog-server/src/graphql/helpers/enums.ts rename to apps/server/src/graphql/helpers/enums.ts diff --git a/packages/velog-server/src/graphql/helpers/generated.ts b/apps/server/src/graphql/helpers/generated.ts similarity index 98% rename from packages/velog-server/src/graphql/helpers/generated.ts rename to apps/server/src/graphql/helpers/generated.ts index c98deb9f..7abab73d 100644 --- a/packages/velog-server/src/graphql/helpers/generated.ts +++ b/apps/server/src/graphql/helpers/generated.ts @@ -1,14 +1,14 @@ /* eslint-disable @typescript-eslint/ban-types */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { NotificationType } from './enums' + +import { NotificationType } from './enums.js' import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql' import { User as UserModel, UserProfile as UserProfileModel, Post as PostModel, Comment as CommentModel, -} from '@prisma/client' -import { GraphQLContext } from './../../common/interfaces/graphql' +} from '@packages/database/velog-rds' +import { GraphQLContext } from './../../common/interfaces/graphql.js' export type Maybe = T | null | undefined export type InputMaybe = T | undefined export type Exact = { [K in keyof T]: T[K] } @@ -1333,8 +1333,7 @@ export type QueryResolvers< export type ReadCountByDayResolvers< ContextType = GraphQLContext, - ParentType extends - ResolversParentTypes['ReadCountByDay'] = ResolversParentTypes['ReadCountByDay'], + ParentType extends ResolversParentTypes['ReadCountByDay'] = ResolversParentTypes['ReadCountByDay'], > = { count?: Resolver, ParentType, ContextType> day?: Resolver, ParentType, ContextType> @@ -1352,8 +1351,7 @@ export type SearchResultResolvers< export type SendMailResponseResolvers< ContextType = GraphQLContext, - ParentType extends - ResolversParentTypes['SendMailResponse'] = ResolversParentTypes['SendMailResponse'], + ParentType extends ResolversParentTypes['SendMailResponse'] = ResolversParentTypes['SendMailResponse'], > = { registered?: Resolver, ParentType, ContextType> __isTypeOf?: IsTypeOfResolverFn @@ -1415,8 +1413,7 @@ export type TagResolvers< export type TrendingWriterResolvers< ContextType = GraphQLContext, - ParentType extends - ResolversParentTypes['TrendingWriter'] = ResolversParentTypes['TrendingWriter'], + ParentType extends ResolversParentTypes['TrendingWriter'] = ResolversParentTypes['TrendingWriter'], > = { id?: Resolver index?: Resolver @@ -1427,8 +1424,7 @@ export type TrendingWriterResolvers< export type TrendingWriterPostsResolvers< ContextType = GraphQLContext, - ParentType extends - ResolversParentTypes['TrendingWriterPosts'] = ResolversParentTypes['TrendingWriterPosts'], + ParentType extends ResolversParentTypes['TrendingWriterPosts'] = ResolversParentTypes['TrendingWriterPosts'], > = { id?: Resolver thumbnail?: Resolver @@ -1439,8 +1435,7 @@ export type TrendingWriterPostsResolvers< export type TrendingWriterProfileResolvers< ContextType = GraphQLContext, - ParentType extends - ResolversParentTypes['TrendingWriterProfile'] = ResolversParentTypes['TrendingWriterProfile'], + ParentType extends ResolversParentTypes['TrendingWriterProfile'] = ResolversParentTypes['TrendingWriterProfile'], > = { display_name?: Resolver short_bio?: Resolver @@ -1450,8 +1445,7 @@ export type TrendingWriterProfileResolvers< export type TrendingWriterUserResolvers< ContextType = GraphQLContext, - ParentType extends - ResolversParentTypes['TrendingWriterUser'] = ResolversParentTypes['TrendingWriterUser'], + ParentType extends ResolversParentTypes['TrendingWriterUser'] = ResolversParentTypes['TrendingWriterUser'], > = { id?: Resolver profile?: Resolver diff --git a/packages/velog-server/src/graphql/index.ts b/apps/server/src/graphql/index.ts similarity index 84% rename from packages/velog-server/src/graphql/index.ts rename to apps/server/src/graphql/index.ts index 3d7ba7ff..74e00da0 100644 --- a/packages/velog-server/src/graphql/index.ts +++ b/apps/server/src/graphql/index.ts @@ -1,13 +1,11 @@ -import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader' -import { loadSchemaSync } from '@graphql-tools/load' -import { mergeResolvers } from '@graphql-tools/merge' -import { Resolvers } from '@graphql/helpers/generated' +import { Resolvers } from '@graphql/helpers/generated.js' import { readdirSync } from 'fs' import { DateTimeISOResolver, VoidResolver, PositiveIntResolver } from 'graphql-scalars' import { IResolvers, MercuriusContext } from 'mercurius' import { basename, dirname, resolve } from 'path' import { ENV } from '@env' import { fileURLToPath } from 'url' +import { GraphQLFileLoader, loadSchemaSync, mergeResolvers } from '@packages/commonjs' async function resolverAutoLoader(): Promise { const __filename = fileURLToPath(import.meta.url) diff --git a/packages/velog-server/src/graphql/resolvers/adResolvers.ts b/apps/server/src/graphql/resolvers/adResolvers.ts similarity index 84% rename from packages/velog-server/src/graphql/resolvers/adResolvers.ts rename to apps/server/src/graphql/resolvers/adResolvers.ts index 54626def..bef0f993 100644 --- a/packages/velog-server/src/graphql/resolvers/adResolvers.ts +++ b/apps/server/src/graphql/resolvers/adResolvers.ts @@ -1,4 +1,4 @@ -import { Resolvers } from '@graphql/helpers/generated' +import { Resolvers } from '@graphql/helpers/generated.js' import { AdService } from '@services/AdService/index.js' import { container } from 'tsyringe' diff --git a/packages/velog-server/src/graphql/resolvers/authResolvers.ts b/apps/server/src/graphql/resolvers/authResolvers.ts similarity index 87% rename from packages/velog-server/src/graphql/resolvers/authResolvers.ts rename to apps/server/src/graphql/resolvers/authResolvers.ts index 51893ac7..04498599 100644 --- a/packages/velog-server/src/graphql/resolvers/authResolvers.ts +++ b/apps/server/src/graphql/resolvers/authResolvers.ts @@ -1,4 +1,4 @@ -import { Resolvers } from '@graphql/helpers/generated' +import { Resolvers } from '@graphql/helpers/generated.js' import { AuthService } from '@services/AuthService/index.js' import { container } from 'tsyringe' diff --git a/packages/velog-server/src/graphql/resolvers/followResolvers.ts b/apps/server/src/graphql/resolvers/followResolvers.ts similarity index 95% rename from packages/velog-server/src/graphql/resolvers/followResolvers.ts rename to apps/server/src/graphql/resolvers/followResolvers.ts index 5bfeb217..b851fcd2 100644 --- a/packages/velog-server/src/graphql/resolvers/followResolvers.ts +++ b/apps/server/src/graphql/resolvers/followResolvers.ts @@ -1,4 +1,4 @@ -import { Resolvers } from '@graphql/helpers/generated' +import { Resolvers } from '@graphql/helpers/generated.js' import { FollowUserService } from '@services/FollowUser/index.js' import { container } from 'tsyringe' diff --git a/packages/velog-server/src/graphql/resolvers/notificationResolvers.ts b/apps/server/src/graphql/resolvers/notificationResolvers.ts similarity index 97% rename from packages/velog-server/src/graphql/resolvers/notificationResolvers.ts rename to apps/server/src/graphql/resolvers/notificationResolvers.ts index 1ec70724..6f3bd78b 100644 --- a/packages/velog-server/src/graphql/resolvers/notificationResolvers.ts +++ b/apps/server/src/graphql/resolvers/notificationResolvers.ts @@ -1,4 +1,4 @@ -import { Resolvers } from '@graphql/helpers/generated' +import { Resolvers } from '@graphql/helpers/generated.js' import { AuthService } from '@services/AuthService/index.js' import { NotificationService } from '@services/NotificationService/index.js' import { container } from 'tsyringe' diff --git a/packages/velog-server/src/graphql/resolvers/postResolvers.ts b/apps/server/src/graphql/resolvers/postResolvers.ts similarity index 86% rename from packages/velog-server/src/graphql/resolvers/postResolvers.ts rename to apps/server/src/graphql/resolvers/postResolvers.ts index e2a391bf..5b041320 100644 --- a/packages/velog-server/src/graphql/resolvers/postResolvers.ts +++ b/apps/server/src/graphql/resolvers/postResolvers.ts @@ -1,25 +1,36 @@ -import { Resolvers } from '@graphql/helpers/generated' +import { Resolvers } from '@graphql/helpers/generated.js' import { container } from 'tsyringe' import { PostService } from '@services/PostService/index.js' import { UserService } from '@services/UserService/index.js' import { PostIncludeComment, PostIncludeUser } from '@services/PostService/PostServiceInterface.js' import { CommentService } from '@services/CommentService/index.js' -import { Post, Tag } from '@prisma/client' +import { Post, Tag } from '@packages/database/velog-rds' import { PostLikeService } from '@services/PostLikeService/index.js' import { DbService } from '@lib/db/DbService.js' -import { SeriesService } from '@services/SeriesService/index.js' +import { SeriesService } from '@services/SeriesService/index.mjs' import { TagService } from '@services/TagService/index.js' import { FollowUserService } from '@services/FollowUser/index.js' import { FeedService } from '@services/FeedService/index.js' -import { PostApiService } from '@services/PostApiService/index.js' +import { PostApiService } from '@services/PostApiService/index.mjs' const postResolvers: Resolvers = { Post: { user: async (parent: PostIncludeUser) => { if (!parent.user) { - const userService = container.resolve(UserService) - return await userService.getCurrentUser(parent.fk_user_id) + if (parent?.fk_user_id) { + const userService = container.resolve(UserService) + return await userService.getCurrentUser(parent.fk_user_id) + } + + if (parent.id) { + const postService = container.resolve(PostService) + const userService = container.resolve(UserService) + const post = await postService.findById(parent.id) + return await userService.getCurrentUser(post?.fk_user_id) + } + + return null } return parent?.user }, diff --git a/packages/velog-server/src/graphql/resolvers/seriesResolvers.ts b/apps/server/src/graphql/resolvers/seriesResolvers.ts similarity index 91% rename from packages/velog-server/src/graphql/resolvers/seriesResolvers.ts rename to apps/server/src/graphql/resolvers/seriesResolvers.ts index 346abebe..4044e009 100644 --- a/packages/velog-server/src/graphql/resolvers/seriesResolvers.ts +++ b/apps/server/src/graphql/resolvers/seriesResolvers.ts @@ -1,5 +1,5 @@ -import { Resolvers } from '@graphql/helpers/generated' -import { SeriesService } from '@services/SeriesService/index.js' +import { Resolvers } from '@graphql/helpers/generated.js' +import { SeriesService } from '@services/SeriesService/index.mjs' import { UserService } from '@services/UserService/index.js' import { container } from 'tsyringe' diff --git a/packages/velog-server/src/graphql/resolvers/tagResolvers.ts b/apps/server/src/graphql/resolvers/tagResolvers.ts similarity index 86% rename from packages/velog-server/src/graphql/resolvers/tagResolvers.ts rename to apps/server/src/graphql/resolvers/tagResolvers.ts index 136734d7..a535b094 100644 --- a/packages/velog-server/src/graphql/resolvers/tagResolvers.ts +++ b/apps/server/src/graphql/resolvers/tagResolvers.ts @@ -1,4 +1,4 @@ -import { Resolvers } from '@graphql/helpers/generated' +import { Resolvers } from '@graphql/helpers/generated.js' import { TagService } from '@services/TagService/index.js' import { container } from 'tsyringe' diff --git a/packages/velog-server/src/graphql/resolvers/userResolvers.ts b/apps/server/src/graphql/resolvers/userResolvers.ts similarity index 97% rename from packages/velog-server/src/graphql/resolvers/userResolvers.ts rename to apps/server/src/graphql/resolvers/userResolvers.ts index 1c256fae..e0b54828 100644 --- a/packages/velog-server/src/graphql/resolvers/userResolvers.ts +++ b/apps/server/src/graphql/resolvers/userResolvers.ts @@ -1,10 +1,10 @@ -import { Resolvers } from '@graphql/helpers/generated' +import { Resolvers } from '@graphql/helpers/generated.js' import { AuthService } from '@services/AuthService/index.js' import { FollowUserService } from '@services/FollowUser/index.js' -import { SeriesService } from '@services/SeriesService/index.js' +import { SeriesService } from '@services/SeriesService/index.mjs' import { UserMetaService } from '@services/UserMetaService/index.js' import { UserProfileService } from '@services/UserProfileService/index.js' -import {} from '@prisma/client' + import { UserService } from '@services/UserService/index.js' import { VelogConfigService } from '@services/VelogConfigService/index.js' import { container } from 'tsyringe' diff --git a/packages/velog-server/src/graphql/resolvers/writerResolvers.ts b/apps/server/src/graphql/resolvers/writerResolvers.ts similarity index 87% rename from packages/velog-server/src/graphql/resolvers/writerResolvers.ts rename to apps/server/src/graphql/resolvers/writerResolvers.ts index 4f17ba91..78636cef 100644 --- a/packages/velog-server/src/graphql/resolvers/writerResolvers.ts +++ b/apps/server/src/graphql/resolvers/writerResolvers.ts @@ -1,4 +1,4 @@ -import { Resolvers } from '@graphql/helpers/generated' +import { Resolvers } from '@graphql/helpers/generated.js' import { WriterService } from '@services/WriterService/index.js' import { container } from 'tsyringe' diff --git a/packages/velog-server/src/lib/aws/Aws.test.ts b/apps/server/src/lib/aws/Aws.test.ts similarity index 100% rename from packages/velog-server/src/lib/aws/Aws.test.ts rename to apps/server/src/lib/aws/Aws.test.ts diff --git a/packages/velog-server/src/lib/aws/AwsService.ts b/apps/server/src/lib/aws/AwsService.ts similarity index 100% rename from packages/velog-server/src/lib/aws/AwsService.ts rename to apps/server/src/lib/aws/AwsService.ts diff --git a/packages/velog-server/src/lib/b2Manager/B2Manager.test.ts b/apps/server/src/lib/b2Manager/B2Manager.test.ts similarity index 78% rename from packages/velog-server/src/lib/b2Manager/B2Manager.test.ts rename to apps/server/src/lib/b2Manager/B2Manager.test.ts index 43500054..dcdd4c37 100644 --- a/packages/velog-server/src/lib/b2Manager/B2Manager.test.ts +++ b/apps/server/src/lib/b2Manager/B2Manager.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { B2ManagerService } from './B2ManagerService' +import { B2ManagerService } from './B2ManagerService.js' describe('B2ManagerService', () => { const service = container.resolve(B2ManagerService) diff --git a/packages/velog-server/src/lib/b2Manager/B2ManagerService.ts b/apps/server/src/lib/b2Manager/B2ManagerService.ts similarity index 91% rename from packages/velog-server/src/lib/b2Manager/B2ManagerService.ts rename to apps/server/src/lib/b2Manager/B2ManagerService.ts index 3608d6c9..87cd79be 100644 --- a/packages/velog-server/src/lib/b2Manager/B2ManagerService.ts +++ b/apps/server/src/lib/b2Manager/B2ManagerService.ts @@ -3,6 +3,7 @@ import B2 from 'backblaze-b2' import { ENV } from '@env' import { finished } from 'stream/promises' import { Readable } from 'stream' +import { ReadableStream } from 'stream/web' interface Service { authorize(): Promise @@ -53,9 +54,9 @@ export class B2ManagerService implements Service { return { authorizationToken, uploadUrl } } - public async streamUpload(stream: Readable, fileName: string, contentLength?: number) { + public async streamUpload(stream: any, fileName: string, contentLength?: number) { const chunks: any[] = [] - stream.on('data', (chunk) => chunks.push(chunk)) + stream.on('data', (chunk: string) => chunks.push(chunk)) await finished(stream) const buffer = Buffer.concat(chunks) diff --git a/packages/velog-server/src/lib/cache/Cache.test.ts b/apps/server/src/lib/cache/Cache.test.ts similarity index 80% rename from packages/velog-server/src/lib/cache/Cache.test.ts rename to apps/server/src/lib/cache/Cache.test.ts index 389faec2..525e2bb8 100644 --- a/packages/velog-server/src/lib/cache/Cache.test.ts +++ b/apps/server/src/lib/cache/Cache.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { CacheService } from './CacheService' +import { CacheService } from './CacheService.js' describe('CacheService', () => { const service = container.resolve(CacheService) diff --git a/packages/velog-server/src/lib/cache/CacheService.ts b/apps/server/src/lib/cache/CacheService.ts similarity index 100% rename from packages/velog-server/src/lib/cache/CacheService.ts rename to apps/server/src/lib/cache/CacheService.ts diff --git a/packages/velog-server/src/lib/cloudflare/turnstile/Turnstile.test.ts b/apps/server/src/lib/cloudflare/turnstile/Turnstile.test.ts similarity index 78% rename from packages/velog-server/src/lib/cloudflare/turnstile/Turnstile.test.ts rename to apps/server/src/lib/cloudflare/turnstile/Turnstile.test.ts index 2e2cf772..d306fd80 100644 --- a/packages/velog-server/src/lib/cloudflare/turnstile/Turnstile.test.ts +++ b/apps/server/src/lib/cloudflare/turnstile/Turnstile.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { TurnstileService } from './TurnstileService' +import { TurnstileService } from './TurnstileService.js' describe('TurnstileService', () => { const service = container.resolve(TurnstileService) diff --git a/packages/velog-server/src/lib/cloudflare/turnstile/TurnstileService.ts b/apps/server/src/lib/cloudflare/turnstile/TurnstileService.ts similarity index 93% rename from packages/velog-server/src/lib/cloudflare/turnstile/TurnstileService.ts rename to apps/server/src/lib/cloudflare/turnstile/TurnstileService.ts index 6eaa052b..7bf18ba4 100644 --- a/packages/velog-server/src/lib/cloudflare/turnstile/TurnstileService.ts +++ b/apps/server/src/lib/cloudflare/turnstile/TurnstileService.ts @@ -1,6 +1,6 @@ import { ENV } from '@env' +import { axios } from '@packages/commonjs' import { injectable, singleton } from 'tsyringe' -import axios from 'axios' interface Service { verifyToken(token: string): Promise diff --git a/apps/server/src/lib/cookie/CookieService.ts b/apps/server/src/lib/cookie/CookieService.ts new file mode 100644 index 00000000..bb820ff9 --- /dev/null +++ b/apps/server/src/lib/cookie/CookieService.ts @@ -0,0 +1,11 @@ +import { ENV } from '@env' +import { FastifyCookieService } from '@packages/library/fastifyCookie' +import { injectable, singleton } from 'tsyringe' + +@injectable() +@singleton() +export class CookieService extends FastifyCookieService { + constructor() { + super({ appEnv: ENV.appEnv }) + } +} diff --git a/apps/server/src/lib/db/Db.test.ts b/apps/server/src/lib/db/Db.test.ts new file mode 100644 index 00000000..9c87dbcc --- /dev/null +++ b/apps/server/src/lib/db/Db.test.ts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { DbService } from './DbService.js' + +describe('DbService', () => { + const service = container.resolve(DbService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/server/src/lib/db/DbService.ts b/apps/server/src/lib/db/DbService.ts new file mode 100644 index 00000000..fcc9f4b4 --- /dev/null +++ b/apps/server/src/lib/db/DbService.ts @@ -0,0 +1,13 @@ +import { ENV } from '@env' +import { PrismaClient } from '@packages/database/velog-rds' +import { injectable, singleton } from 'tsyringe' + +@injectable() +@singleton() +export class DbService extends PrismaClient { + constructor() { + super({ + datasourceUrl: ENV.databaseUrl, + }) + } +} diff --git a/packages/velog-server/src/lib/discord/Discord.test.ts b/apps/server/src/lib/discord/Discord.test.ts similarity index 79% rename from packages/velog-server/src/lib/discord/Discord.test.ts rename to apps/server/src/lib/discord/Discord.test.ts index 3665c784..8eb18d29 100644 --- a/packages/velog-server/src/lib/discord/Discord.test.ts +++ b/apps/server/src/lib/discord/Discord.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { DiscordService } from './DiscordService' +import { DiscordService } from './DiscordService.js' describe('DiscordService', () => { const service = container.resolve(DiscordService) diff --git a/apps/server/src/lib/discord/DiscordService.ts b/apps/server/src/lib/discord/DiscordService.ts new file mode 100644 index 00000000..1ec3f292 --- /dev/null +++ b/apps/server/src/lib/discord/DiscordService.ts @@ -0,0 +1,112 @@ +import { injectable, singleton } from 'tsyringe' +import { Client, GatewayIntentBits } from 'discord.js' +import { ENV } from '@env' +import { Time } from '@constants/TimeConstants.js' +import { RedisService } from '@lib/redis/RedisService.js' + +@injectable() +@singleton() +export class DiscordService { + private client!: Client + public isSending: boolean = false + constructor(private readonly redis: RedisService) {} + public connection(): Promise { + return new Promise((resolve) => { + this.client = new Client({ + intents: [GatewayIntentBits.MessageContent], + }) + + this.client.on('ready', () => { + console.log('Discord Client ready') + resolve(this.client) + }) + this.client.login(ENV.discordBotToken) + }) + } + public async sendMessage(type: MessageType, payload: MessagePayload | string) { + this.isSending = true + + let message: string = '' + if (typeof payload === 'string') { + message = payload + } else { + const metaData = Object.assign(payload, { + body: payload.body ?? 'none', + query: payload.query ?? 'none', + }) + message = JSON.stringify(metaData) + } + + const frequentWord = [ + 'connection pool', + 'canceling statement', + 'Not allow origin', + 'Unknown query', + 'ECONNRESET', + 'https://oauth2.googleapis.com/', + '/api/posts/v1/score', + 'Code is required', + `notificationCount`, + ] + const isFrequentWordIncluded = frequentWord.some((word) => message.includes(word)) + + if (isFrequentWordIncluded) { + this.isSending = false + console.log('Frequent word included skip sending message') + return + } + + if (typeof payload === 'object' && message.includes('WritePost') && payload?.user?.id) { + const key = this.redis.generateKey.errorMessageCache(payload.type, payload?.user?.id) + const exists = await this.redis.exists(key) + if (exists === 1) return + await this.redis.setex(key, Time.ONE_MINUTE_IN_S * 10, 'true') + } + + try { + const isReady = this.client.isReady() + + if (!isReady) { + throw new Error('Discord bot is not ready') + } + + const channelMapper: Record = { + error: ENV.discordErrorChannel, + spam: ENV.discordSpamChannel, + } + + const channelId = channelMapper[type] + + if (!channelId) { + throw new Error('Not found discord channel id') + } + + const channel = await this.client.channels.fetch(channelId) + + if (channel?.isTextBased()) { + const chunkSize = 2000 + for (let i = 0; i < message.length; i += chunkSize) { + await channel.send(message.slice(i, i + chunkSize)) + } + } else { + throw new Error('Wrong channel type') + } + } catch (error: any) { + console.log(error) + throw new Error('Failed to send meesage to discord channel') + } finally { + this.isSending = false + } + } +} + +export type MessageType = 'error' | 'spam' +export type MessagePayload = { + type: string + body?: any + query?: any + user?: { id: string } + ip?: string + error?: any + originError?: any +} diff --git a/packages/velog-server/src/lib/elasticSearch/BuildQueryService.ts b/apps/server/src/lib/elasticSearch/BuildQueryService.ts similarity index 99% rename from packages/velog-server/src/lib/elasticSearch/BuildQueryService.ts rename to apps/server/src/lib/elasticSearch/BuildQueryService.ts index a4ed77a8..ee338d9b 100644 --- a/packages/velog-server/src/lib/elasticSearch/BuildQueryService.ts +++ b/apps/server/src/lib/elasticSearch/BuildQueryService.ts @@ -1,5 +1,5 @@ import { injectable, singleton } from 'tsyringe' -import { PostIncludeTags } from '@services/PostService/PostServiceInterface' +import { PostIncludeTags } from '@services/PostService/PostServiceInterface.js' @injectable() @singleton() diff --git a/packages/velog-server/src/lib/elasticSearch/ElasticSearch.test.ts b/apps/server/src/lib/elasticSearch/ElasticSearch.test.ts similarity index 76% rename from packages/velog-server/src/lib/elasticSearch/ElasticSearch.test.ts rename to apps/server/src/lib/elasticSearch/ElasticSearch.test.ts index 9a0191c5..b2825605 100644 --- a/packages/velog-server/src/lib/elasticSearch/ElasticSearch.test.ts +++ b/apps/server/src/lib/elasticSearch/ElasticSearch.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { ElasticSearchService } from './ElasticSearchService' +import { ElasticSearchService } from './ElasticSearchService.js' describe('ElasticSearchService', () => { const service = container.resolve(ElasticSearchService) diff --git a/packages/velog-server/src/lib/elasticSearch/ElasticSearchService.ts b/apps/server/src/lib/elasticSearch/ElasticSearchService.ts similarity index 75% rename from packages/velog-server/src/lib/elasticSearch/ElasticSearchService.ts rename to apps/server/src/lib/elasticSearch/ElasticSearchService.ts index 901b0cf4..395aa0eb 100644 --- a/packages/velog-server/src/lib/elasticSearch/ElasticSearchService.ts +++ b/apps/server/src/lib/elasticSearch/ElasticSearchService.ts @@ -3,18 +3,33 @@ import { ENV } from '@env' import { injectable, singleton } from 'tsyringe' import { BuildQueryService } from './BuildQueryService.js' import { PostIncludeTags } from '@services/PostService/PostServiceInterface.js' -import { Post } from '@prisma/client' +import { Post } from '@packages/database/velog-rds' +import { UserService } from '@services/UserService/index.js' interface Service { - get client(): Client + getClient(): Client keywordSearch(input: KeywordSearchArgs): Promise<{ count: number; posts: Post[] }> } @injectable() @singleton() export class ElasticSearchService implements Service { - constructor(private readonly buildQueryService: BuildQueryService) {} - public get client(): Client { + public client!: Client + constructor( + private readonly userService: UserService, + private readonly buildQueryService: BuildQueryService, + ) {} + public connection(): Promise { + return new Promise((resolve) => { + const client = new Client({ node: ENV.esHost }) + this.client = client + resolve(client) + }) + } + public getClient(): Client { + if (this.client) { + return this.client + } return new Client({ node: ENV.esHost }) } public get buildQuery() { @@ -147,14 +162,26 @@ export class ElasticSearchService implements Service { }, }) - const posts = result.body.hits.hits.map((hit: any) => hit._source) - posts.forEach((p: any) => { - p.released_at = new Date(p.released_at) + const sources = result.body.hits.hits + .map((hit: any) => hit._source) + .map((p: any) => ({ ...p, released_at: new Date(p.released_at) })) + + const promises = sources.map(async (post: any) => { + try { + const result = await this.userService.checkExistsUser(post?.user?.id) + return { id: post.id, result } + } catch (error) { + console.error('Error checking user:', error) + return { id: post.id, result: false } + } }) + const promiseResult = await Promise.all(promises) + const existsUserPosts = promiseResult.filter(({ result }) => result).map(({ id }) => id) + const data = { count: result.body.hits.total.value, - posts: result.body.hits.hits.map((hit: any) => hit._source), + posts: sources.filter((post: any) => existsUserPosts.includes(post.id)), } return data diff --git a/packages/velog-server/src/lib/file/File.test.ts b/apps/server/src/lib/file/File.test.ts similarity index 80% rename from packages/velog-server/src/lib/file/File.test.ts rename to apps/server/src/lib/file/File.test.ts index 93458386..e31cc2fa 100644 --- a/packages/velog-server/src/lib/file/File.test.ts +++ b/apps/server/src/lib/file/File.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { FileService } from './FileService' +import { FileService } from './FileService.js' describe('FileService', () => { const service = container.resolve(FileService) diff --git a/packages/velog-server/src/lib/file/FileService.ts b/apps/server/src/lib/file/FileService.ts similarity index 88% rename from packages/velog-server/src/lib/file/FileService.ts rename to apps/server/src/lib/file/FileService.ts index faa2c2f2..6a600a0e 100644 --- a/packages/velog-server/src/lib/file/FileService.ts +++ b/apps/server/src/lib/file/FileService.ts @@ -1,16 +1,16 @@ -import axios from 'axios' import { injectable, singleton } from 'tsyringe' import mimeTypes from 'mime-types' import tmp from 'tmp' import fs from 'fs' import path from 'path' import multer from 'fastify-multer' -import { StorageEngine } from 'fastify-multer/lib/interfaces' +import { StorageEngine } from 'fastify-multer/lib/interfaces.js' +import { axios } from '@packages/commonjs' interface Service { get multerStorage(): StorageEngine downloadFile(url: string): Promise - generateUploadPath(parmeter: GenerateUploadPathParameter): string + generateUploadPath(parmeter: GenerateUploadPathArgs): string } @injectable() @@ -59,7 +59,7 @@ export class FileService implements Service { cleanup, } } - public generateUploadPath = ({ id, type, username }: GenerateUploadPathParameter) => { + public generateUploadPath = ({ id, type, username }: GenerateUploadPathArgs) => { return `images/${username}/${type}/${id}` } } @@ -72,7 +72,7 @@ type DownloadFileResult = { cleanup: () => void } -type GenerateUploadPathParameter = { +type GenerateUploadPathArgs = { id: string type: string username: string diff --git a/packages/velog-server/src/lib/graphcdn/Graphcdn.test.ts b/apps/server/src/lib/graphcdn/Graphcdn.test.ts similarity index 78% rename from packages/velog-server/src/lib/graphcdn/Graphcdn.test.ts rename to apps/server/src/lib/graphcdn/Graphcdn.test.ts index 73046688..c0197779 100644 --- a/packages/velog-server/src/lib/graphcdn/Graphcdn.test.ts +++ b/apps/server/src/lib/graphcdn/Graphcdn.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { GraphcdnService } from './GraphcdnService' +import { GraphcdnService } from './GraphcdnService.js' describe('GraphcdnService', () => { const service = container.resolve(GraphcdnService) diff --git a/packages/velog-server/src/lib/graphcdn/GraphcdnService.ts b/apps/server/src/lib/graphcdn/GraphcdnService.ts similarity index 91% rename from packages/velog-server/src/lib/graphcdn/GraphcdnService.ts rename to apps/server/src/lib/graphcdn/GraphcdnService.ts index 49f65332..bd2b9625 100644 --- a/packages/velog-server/src/lib/graphcdn/GraphcdnService.ts +++ b/apps/server/src/lib/graphcdn/GraphcdnService.ts @@ -1,6 +1,8 @@ import { injectable, singleton } from 'tsyringe' -import axios, { AxiosResponse } from 'axios' + import { ENV } from '@env' +import { axios } from '@packages/commonjs' +import { AxiosResponse } from 'axios' interface Service { purgePost(id: string): Promise diff --git a/packages/velog-server/src/lib/jwt/Jwt.test.ts b/apps/server/src/lib/jwt/Jwt.test.ts similarity index 100% rename from packages/velog-server/src/lib/jwt/Jwt.test.ts rename to apps/server/src/lib/jwt/Jwt.test.ts diff --git a/packages/velog-server/src/lib/jwt/JwtService.ts b/apps/server/src/lib/jwt/JwtService.ts similarity index 66% rename from packages/velog-server/src/lib/jwt/JwtService.ts rename to apps/server/src/lib/jwt/JwtService.ts index 29426ac5..210fcf40 100644 --- a/packages/velog-server/src/lib/jwt/JwtService.ts +++ b/apps/server/src/lib/jwt/JwtService.ts @@ -1,43 +1,28 @@ -import jwt, { SignOptions } from 'jsonwebtoken' +import type { SignOptions } from 'jsonwebtoken' import { injectable, singleton } from 'tsyringe' import { Time } from '@constants/TimeConstants.js' import { ENV } from '@env' import { DbService } from '@lib/db/DbService.js' import { UnauthorizedError } from '@errors/UnauthorizedError.js' +import { JwtService as JWTService } from '@packages/library/jwt' + +interface Service { + generateUserToken(userId: string): Promise<{ refreshToken: string; accessToken: string }> + refreshUserToken( + userId: string, + tokenId: string, + refreshTokenExp: number, + originalRefreshToken: string, + ): Promise<{ accessToken: string; refreshToken: string }> + generateToken(payload: string | Buffer | Record, options?: SignOptions): Promise + decodeToken(token: string): Promise +} @injectable() @singleton() -export class JwtService { - constructor(private readonly db: DbService) {} - public generateToken( - payload: string | Buffer | Record, - options?: SignOptions, - ): Promise { - const jwtOptions: SignOptions = { - issuer: 'velog.io', - expiresIn: '7d', - ...options, - } - - if (!jwtOptions.expiresIn) { - // removes expiresIn when expiresIn is given as undefined - delete jwtOptions.expiresIn - } - return new Promise((resolve, reject) => { - if (!ENV.jwtSecretKey) return - jwt.sign(payload, ENV.jwtSecretKey, jwtOptions, (err, token) => { - if (err) reject(err) - resolve(token as string) - }) - }) - } - public decodeToken(token: string): Promise { - return new Promise((resolve, reject) => { - jwt.verify(token, ENV.jwtSecretKey, (err, decoded) => { - if (err) reject(err) - resolve(decoded as T) - }) - }) +export class JwtService extends JWTService implements Service { + constructor(private readonly db: DbService) { + super({ secretKey: ENV.jwtSecretKey }) } public async generateUserToken(userId: string) { const authToken = await this.db.authToken.create({ diff --git a/packages/velog-server/src/lib/mail/Mail.test.ts b/apps/server/src/lib/mail/Mail.test.ts similarity index 80% rename from packages/velog-server/src/lib/mail/Mail.test.ts rename to apps/server/src/lib/mail/Mail.test.ts index 7cdcaf13..cc57c154 100644 --- a/packages/velog-server/src/lib/mail/Mail.test.ts +++ b/apps/server/src/lib/mail/Mail.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { MailService } from './MailService' +import { MailService } from './MailService.js' describe('MailService', () => { const service = container.resolve(MailService) diff --git a/packages/velog-server/src/lib/mail/MailService.ts b/apps/server/src/lib/mail/MailService.ts similarity index 100% rename from packages/velog-server/src/lib/mail/MailService.ts rename to apps/server/src/lib/mail/MailService.ts diff --git a/packages/velog-cron/src/lib/redis/Redis.test.ts b/apps/server/src/lib/redis/Redis.test.ts similarity index 80% rename from packages/velog-cron/src/lib/redis/Redis.test.ts rename to apps/server/src/lib/redis/Redis.test.ts index 9aa21807..9fdeee9b 100644 --- a/packages/velog-cron/src/lib/redis/Redis.test.ts +++ b/apps/server/src/lib/redis/Redis.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { RedisService } from './RedisService' +import { RedisService } from './RedisService.js' describe('RedisService', () => { const service = container.resolve(RedisService) diff --git a/apps/server/src/lib/redis/RedisService.ts b/apps/server/src/lib/redis/RedisService.ts new file mode 100644 index 00000000..6a5453be --- /dev/null +++ b/apps/server/src/lib/redis/RedisService.ts @@ -0,0 +1,16 @@ +import { ENV } from '@env' +import { injectable, singleton } from 'tsyringe' +import { RedisService as Redis } from '@packages/database/velog-redis' +export type { + ChangeEmailArgs, + CheckPostSpamQueueData, + CreateFeedQueueData, +} from '@packages/database/velog-redis' + +@injectable() +@singleton() +export class RedisService extends Redis { + constructor() { + super({ port: ENV.redisPort, host: ENV.redisHost }) + } +} diff --git a/packages/velog-server/src/lib/slack/Slack.test.ts b/apps/server/src/lib/slack/Slack.test.ts similarity index 80% rename from packages/velog-server/src/lib/slack/Slack.test.ts rename to apps/server/src/lib/slack/Slack.test.ts index dffcc4f5..000158c7 100644 --- a/packages/velog-server/src/lib/slack/Slack.test.ts +++ b/apps/server/src/lib/slack/Slack.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { SlackService } from './SlackService' +import { SlackService } from './SlackService.js' describe('SlackService', () => { const service = container.resolve(SlackService) diff --git a/packages/velog-server/src/lib/slack/SlackService.ts b/apps/server/src/lib/slack/SlackService.ts similarity index 84% rename from packages/velog-server/src/lib/slack/SlackService.ts rename to apps/server/src/lib/slack/SlackService.ts index e20547a6..60d4abe5 100644 --- a/packages/velog-server/src/lib/slack/SlackService.ts +++ b/apps/server/src/lib/slack/SlackService.ts @@ -1,4 +1,4 @@ -import Axios from 'axios' +import { axios } from '@packages/commonjs' import { injectable, singleton } from 'tsyringe' interface Service { @@ -10,7 +10,7 @@ interface Service { export class SlackService implements Service { sendSlackMessage(message: string, customChannel?: string) { const slackUrl = `https://hooks.slack.com/services/${customChannel ?? process.env.SLACK_TOKEN}` - return Axios.post(slackUrl, { + return axios.post(slackUrl, { text: message, }) } diff --git a/packages/velog-server/src/lib/utils/Utils.test.ts b/apps/server/src/lib/utils/Utils.test.ts similarity index 94% rename from packages/velog-server/src/lib/utils/Utils.test.ts rename to apps/server/src/lib/utils/Utils.test.ts index 8f267c7f..3dd922f8 100644 --- a/packages/velog-server/src/lib/utils/Utils.test.ts +++ b/apps/server/src/lib/utils/Utils.test.ts @@ -1,4 +1,4 @@ -import { UtilsService } from '@lib/utils/UtilsService' +import { UtilsService } from '@lib/utils/UtilsService.js' import { container } from 'tsyringe' describe('Utils', () => { diff --git a/packages/velog-server/src/lib/utils/UtilsService.ts b/apps/server/src/lib/utils/UtilsService.ts similarity index 74% rename from packages/velog-server/src/lib/utils/UtilsService.ts rename to apps/server/src/lib/utils/UtilsService.ts index 14b67fe2..e3b8a50a 100644 --- a/packages/velog-server/src/lib/utils/UtilsService.ts +++ b/apps/server/src/lib/utils/UtilsService.ts @@ -5,6 +5,7 @@ import { fileURLToPath } from 'url' import { z } from 'zod' import { customAlphabet } from 'nanoid' import nanoidDictionary from 'nanoid-dictionary' +import { UtilsService as Utils } from '@packages/library/utils' interface Service { resolveDir(dir: string): string @@ -24,12 +25,11 @@ interface Service { validateEmail(email: string): boolean alphanumeric(): string randomNumber(max: number): number - spamFilter(text: string, isForeign: boolean, isTitle?: boolean): boolean } @injectable() @singleton() -export class UtilsService implements Service { +export class UtilsService extends Utils implements Service { public resolveDir(dir: string): string { const __filename = fileURLToPath(import.meta.url) const splited = dirname(__filename).split('/src') @@ -156,54 +156,4 @@ export class UtilsService implements Service { public randomNumber(max: number) { return Math.floor(Math.random() * (max + 1)) } - public spamFilter(text: string, isForeign: boolean, isTitle = false): boolean { - const includesCN = /[\u4e00-\u9fa5]/.test(text) - const includesKR = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/.test(text) - - if (includesCN && !includesKR) { - return true - } - - let replaced = text.replace(/```([\s\S]*?)```/g, '') // remove code blocks - // replace image markdown - replaced = replaced.replace(/!\[([\s\S]*?)\]\(([\s\S]*?)\)/g, '') - - const alphanumericKorean = replaced - .replace(/[^a-zA-Zㄱ-힣0-9 \n]/g, '') // remove non-korean - .toLowerCase() - - const hasLink = /http/.test(replaced) - - if (!isTitle && isForeign && hasLink) { - const lines = replaced.split('\n').filter((line) => line.trim().length > 1) - const koreanLinesCount = lines.filter((line) => this.hasKorean(line)).length - const confidence = koreanLinesCount / lines.length - return confidence < 0.3 - } - - const spaceReplaced = alphanumericKorean.replace(/\s/g, '') - - if ( - ENV.bannedKeywords.some((keyword) => - [text, alphanumericKorean, spaceReplaced].some((t) => t.includes(keyword)), - ) - ) { - return true - } - - const score = ENV.bannedAltKeywords.reduce((acc, current) => { - if (alphanumericKorean.includes(current)) { - return acc + 1 - } - return acc - }, 0) - - if (score >= 2 && isForeign) { - return true - } - return false - } - private hasKorean(text: string) { - return /[ㄱ-힣]/g.test(text) - } } diff --git a/packages/velog-server/src/main.ts b/apps/server/src/main.mts similarity index 61% rename from packages/velog-server/src/main.ts rename to apps/server/src/main.mts index 25ae33d8..f35d69b9 100644 --- a/packages/velog-server/src/main.ts +++ b/apps/server/src/main.mts @@ -1,26 +1,27 @@ import 'reflect-metadata' -import { ENV } from '@env' -import app from './app.js' +import { ENV } from './env.mjs' +import app from './app.mjs' import { container } from 'tsyringe' -import { startClosing } from '@plugins/global/keepAlivePlugin.js' +import { startClosing } from '@plugins/global/keepAlivePlugin.mjs' import { DbService } from '@lib/db/DbService.js' import { RedisService } from '@lib/redis/RedisService.js' import { DiscordService } from '@lib/discord/DiscordService.js' +import { ElasticSearchService } from '@lib/elasticSearch/ElasticSearchService.js' async function main() { app.listen({ port: ENV.port, host: '::' }) const dbService = container.resolve(DbService) - await dbService.$connect() + await dbService.connection() const redis = container.resolve(RedisService) - await redis.connection().then((message) => console.log(message)) + await redis.connection() const discord = container.resolve(DiscordService) await discord.connection() - console.info(`INFO: Database connected to "${ENV.databaseUrl.split('@')[1]}"`) - console.info(`INFO: Redis connected to "${ENV.redisHost}:${ENV.redisPort}"`) + const elasticSearch = container.resolve(ElasticSearchService) + elasticSearch.connection() process.send?.('ready') process.on('SIGINT', function () { diff --git a/packages/velog-server/src/routes/auth/index.ts b/apps/server/src/routes/auth/index.mts similarity index 86% rename from packages/velog-server/src/routes/auth/index.ts rename to apps/server/src/routes/auth/index.mts index 458614f0..cb0d0df2 100644 --- a/packages/velog-server/src/routes/auth/index.ts +++ b/apps/server/src/routes/auth/index.mts @@ -1,4 +1,4 @@ -import v3 from './v3/index.js' +import v3 from './v3/index.mjs' import { FastifyPluginCallback } from 'fastify' diff --git a/packages/velog-server/src/routes/auth/v3/index.ts b/apps/server/src/routes/auth/v3/index.mts similarity index 76% rename from packages/velog-server/src/routes/auth/v3/index.ts rename to apps/server/src/routes/auth/v3/index.mts index ff42f96a..63e1c7c0 100644 --- a/packages/velog-server/src/routes/auth/v3/index.ts +++ b/apps/server/src/routes/auth/v3/index.mts @@ -1,4 +1,4 @@ -import socialRoute from '@routes/auth/v3/social/index.js' +import socialRoute from '@routes/auth/v3/social/index.mjs' import { FastifyPluginCallback } from 'fastify' const v3: FastifyPluginCallback = (fastify, opts, done) => { diff --git a/packages/velog-server/src/routes/auth/v3/social/SocialController.ts b/apps/server/src/routes/auth/v3/social/SocialController.mts similarity index 94% rename from packages/velog-server/src/routes/auth/v3/social/SocialController.ts rename to apps/server/src/routes/auth/v3/social/SocialController.mts index 9b257e82..8179c4be 100644 --- a/packages/velog-server/src/routes/auth/v3/social/SocialController.ts +++ b/apps/server/src/routes/auth/v3/social/SocialController.mts @@ -5,7 +5,7 @@ import { CookieService } from '@lib/cookie/CookieService.js' import { DbService } from '@lib/db/DbService.js' import { FileService } from '@lib/file/FileService.js' import { JwtService } from '@lib/jwt/JwtService.js' -import { User, UserProfile } from '@prisma/client' +import { User, UserProfile } from '@packages/database/velog-rds' import { GetProfileFromSocial, SocialProfile, @@ -13,13 +13,14 @@ import { } from '@services/SocialService/SocialServiceInterface.js' import { SocialService } from '@services/SocialService/index.js' import { ExternalIntegrationService } from '@services/ExternalIntegrationService/index.js' -import { FastifyReply, FastifyRequest } from 'fastify' +import type { FastifyReply, FastifyRequest } from 'fastify' import { injectable, singleton } from 'tsyringe' import { google } from 'googleapis' import { UnauthorizedError } from '@errors/UnauthorizedError.js' import { ConfilctError } from '@errors/ConfilctError.js' import { BadRequestError } from '@errors/BadRequestErrors.js' import { NotFoundError } from '@errors/NotfoundError.js' +import { UtilsService } from '@lib/utils/UtilsService.js' interface Controller { googleCallback(request: FastifyRequest<{ Querystring: { code: string } }>): Promise @@ -32,7 +33,7 @@ interface Controller { }>, reply: FastifyReply, ): Promise - getSocialProfile(reply: FastifyReply): Promise + getSocialProfile(request: FastifyRequest): Promise socialRedirect(params: SocialRedirectParams): Promise } @@ -46,6 +47,7 @@ export class SocialController implements Controller { private readonly cookie: CookieService, private readonly file: FileService, private readonly b2Manager: B2ManagerService, + private readonly utils: UtilsService, private readonly externalInterationService: ExternalIntegrationService, ) {} private get redirectUri(): string { @@ -138,7 +140,7 @@ export class SocialController implements Controller { maxAge: Time.ONE_DAY_IN_S * 30, }) - const redirectUrl = ENV.clientV3Host + const redirectHost = ENV.clientV3Host const state = queryState ? (JSON.parse(queryState) as { next: string; integrateState?: string }) : null @@ -154,7 +156,10 @@ export class SocialController implements Controller { } } - reply.redirect(decodeURI(redirectUrl.concat(next))) + const redirectUri = this.utils + .removeKoreanChars(decodeURI(redirectHost.concat(next))) + .trim() + reply.redirect(redirectUri) return } } @@ -181,7 +186,7 @@ export class SocialController implements Controller { reply: FastifyReply, ): Promise { // check token existancy - const registerToken = this.cookie.getCookie(reply, 'register_token') + const registerToken = this.cookie.getCookie(request, 'register_token') if (!registerToken) { throw new UnauthorizedError() } @@ -312,8 +317,8 @@ export class SocialController implements Controller { const uploadResult = await this.b2Manager.upload(buffer, uploadPath) return uploadResult.url } - public async getSocialProfile(reply: FastifyReply): Promise { - const registerToken = this.cookie.getCookie(reply, 'register_token') + public async getSocialProfile(request: FastifyRequest): Promise { + const registerToken = this.cookie.getCookie(request, 'register_token') if (!registerToken) { throw new UnauthorizedError() } diff --git a/packages/velog-server/src/routes/auth/v3/social/index.ts b/apps/server/src/routes/auth/v3/social/index.mts similarity index 93% rename from packages/velog-server/src/routes/auth/v3/social/index.ts rename to apps/server/src/routes/auth/v3/social/index.mts index 2ec48202..8e5f3bdc 100644 --- a/packages/velog-server/src/routes/auth/v3/social/index.ts +++ b/apps/server/src/routes/auth/v3/social/index.mts @@ -1,5 +1,5 @@ -import { SocialController } from '@routes/auth/v3/social/SocialController.js' -import { SocialProvider } from '@services/SocialService/SocialServiceInterface' +import { SocialController } from '@routes/auth/v3/social/SocialController.mjs' +import { SocialProvider } from '@services/SocialService/SocialServiceInterface.js' import { FastifyPluginCallback, FastifyReply, FastifyRequest } from 'fastify' import { container } from 'tsyringe' @@ -13,7 +13,7 @@ const socialRoute: FastifyPluginCallback = (fastify, opts, done) => { request: FastifyRequest<{ Body: { display_name: string; username: string; short_bio: string } }>, - reply: FastifyReply, + reply, ) => await controller.socialRegister(request, reply), ) @@ -61,10 +61,7 @@ const socialRoute: FastifyPluginCallback = (fastify, opts, done) => { ) /* Login Token */ - fastify.get( - '/profile', - async (_, reply: FastifyReply) => await controller.getSocialProfile(reply), - ) + fastify.get('/profile', async (request) => await controller.getSocialProfile(request)) fastify.get( '/redirect/:provider', async ( diff --git a/apps/server/src/routes/files/index.mts b/apps/server/src/routes/files/index.mts new file mode 100644 index 00000000..3a3e7abc --- /dev/null +++ b/apps/server/src/routes/files/index.mts @@ -0,0 +1,9 @@ +import { FastifyPluginCallback } from 'fastify' +import v3 from './v3/index.mjs' + +const filesRoute: FastifyPluginCallback = (fastify, opts, done) => { + fastify.register(v3, { prefix: '/v3' }) + done() +} + +export default filesRoute diff --git a/packages/velog-server/src/routes/files/v3/filesController.ts b/apps/server/src/routes/files/v3/filesController.mts similarity index 95% rename from packages/velog-server/src/routes/files/v3/filesController.ts rename to apps/server/src/routes/files/v3/filesController.mts index d15180ce..fd2868fc 100644 --- a/packages/velog-server/src/routes/files/v3/filesController.ts +++ b/apps/server/src/routes/files/v3/filesController.mts @@ -1,13 +1,12 @@ -import Axios from 'axios' import { injectable, singleton } from 'tsyringe' import { AwsService } from '@lib/aws/AwsService.js' -import { CreateUrlBody, UploadBody } from './schema' +import { CreateUrlBody, UploadBody } from './schema.mjs' import { UnauthorizedError } from '@errors/UnauthorizedError.js' import { UserService } from '@services/UserService/index.js' import { DbService } from '@lib/db/DbService.js' import { FileService } from '@lib/file/FileService.js' import { ENV } from '@env' -import { File } from 'fastify-multer/lib/interfaces' +import { File } from 'fastify-multer/lib/interfaces.js' import { BadRequestError } from '@errors/BadRequestErrors.js' import { ImageService } from '@services/ImageService/index.js' import { HttpError } from '@errors/HttpError.js' @@ -15,6 +14,7 @@ import { PostService } from '@services/PostService/index.js' import { ForbiddenError } from '@errors/ForbiddenError.js' import { B2ManagerService } from '@lib/b2Manager/B2ManagerService.js' import { InternalServerError } from '@errors/InternalServerError.js' +import { axios } from '@packages/commonjs' interface Controller { createUrl({ body, ipaddr, signedUserId, ip }: CreateUrlArgs): Promise @@ -59,7 +59,7 @@ export class FilesController implements Controller { const path = this.file.generateUploadPath({ type, id: userImage.id, username: user.username }) if (ENV.blacklistUsername.includes(user.username) || ENV.blacklistIp.includes(ipaddr || ip)) { - await Axios.post(ENV.slackUrl, { + await axios.post(ENV.slackUrl, { text: `blacklist uploaded image | ${ip} ${user.username}`, }) } @@ -89,7 +89,7 @@ export class FilesController implements Controller { const { type, ref_id } = body - if (!['post', 'profile'].includes(type)) { + if (!['post', 'profile', 'book'].includes(type)) { throw new BadRequestError('Invalid type') } diff --git a/packages/velog-server/src/routes/files/v3/index.ts b/apps/server/src/routes/files/v3/index.mts similarity index 95% rename from packages/velog-server/src/routes/files/v3/index.ts rename to apps/server/src/routes/files/v3/index.mts index 8bc04474..86eb7850 100644 --- a/packages/velog-server/src/routes/files/v3/index.ts +++ b/apps/server/src/routes/files/v3/index.mts @@ -1,8 +1,8 @@ import { FastifyPluginCallback } from 'fastify' import { container } from 'tsyringe' import multer from 'fastify-multer' -import { CreateUrlBody, UploadBody, createUrlBodySchema } from './schema.js' -import { FilesController } from './filesController.js' +import { CreateUrlBody, UploadBody, createUrlBodySchema } from './schema.mjs' +import { FilesController } from './filesController.mjs' import authGuardPlugin from '@plugins/encapsulated/authGuardPlugin.js' const v3: FastifyPluginCallback = (fastify, opts, done) => { diff --git a/packages/velog-server/src/routes/files/v3/schema.ts b/apps/server/src/routes/files/v3/schema.mts similarity index 100% rename from packages/velog-server/src/routes/files/v3/schema.ts rename to apps/server/src/routes/files/v3/schema.mts diff --git a/packages/velog-server/src/routes/index.ts b/apps/server/src/routes/index.mts similarity index 80% rename from packages/velog-server/src/routes/index.ts rename to apps/server/src/routes/index.mts index a821727b..0ef46722 100644 --- a/packages/velog-server/src/routes/index.ts +++ b/apps/server/src/routes/index.mts @@ -1,8 +1,8 @@ -import authRoute from '@routes/auth/index.js' -import postsRoute from '@routes/posts/index.js' -import filesRoute from '@routes/files/index.js' +import authRoute from '@routes/auth/index.mjs' +import postsRoute from '@routes/posts/index.mjs' +import filesRoute from '@routes/files/index.mjs' import { format } from 'date-fns' -import { FastifyPluginCallback } from 'fastify' +import type { FastifyPluginCallback } from 'fastify' const api: FastifyPluginCallback = (fastify, opts, done) => { fastify.register(authRoute, { prefix: '/auth' }) diff --git a/packages/velog-server/src/routes/posts/index.ts b/apps/server/src/routes/posts/index.mts similarity index 86% rename from packages/velog-server/src/routes/posts/index.ts rename to apps/server/src/routes/posts/index.mts index 458614f0..cb0d0df2 100644 --- a/packages/velog-server/src/routes/posts/index.ts +++ b/apps/server/src/routes/posts/index.mts @@ -1,4 +1,4 @@ -import v3 from './v3/index.js' +import v3 from './v3/index.mjs' import { FastifyPluginCallback } from 'fastify' diff --git a/packages/velog-server/src/routes/posts/v3/index.ts b/apps/server/src/routes/posts/v3/index.mts similarity index 92% rename from packages/velog-server/src/routes/posts/v3/index.ts rename to apps/server/src/routes/posts/v3/index.mts index 448fa1a6..aa852cfc 100644 --- a/packages/velog-server/src/routes/posts/v3/index.ts +++ b/apps/server/src/routes/posts/v3/index.mts @@ -1,6 +1,6 @@ import { HttpStatus } from '@constants/HttpStatusConstants.js' import { HttpStatusMessage } from '@constants/HttpStatusMesageConstants.js' -import { postScoreParamsSchema } from '@routes/posts/v3/schame.js' +import { postScoreParamsSchema } from '@routes/posts/v3/schame.mjs' import { PostService } from '@services/PostService/index.js' import { FastifyPluginCallback } from 'fastify' import { FromSchema } from 'json-schema-to-ts' diff --git a/packages/velog-server/src/routes/posts/v3/schame.ts b/apps/server/src/routes/posts/v3/schame.mts similarity index 100% rename from packages/velog-server/src/routes/posts/v3/schame.ts rename to apps/server/src/routes/posts/v3/schame.mts diff --git a/packages/velog-server/src/services/AdService/AdService.test.ts b/apps/server/src/services/AdService/AdService.test.ts similarity index 100% rename from packages/velog-server/src/services/AdService/AdService.test.ts rename to apps/server/src/services/AdService/AdService.test.ts diff --git a/packages/velog-server/src/services/AdService/index.ts b/apps/server/src/services/AdService/index.ts similarity index 90% rename from packages/velog-server/src/services/AdService/index.ts rename to apps/server/src/services/AdService/index.ts index e1abe80b..5a89ba48 100644 --- a/packages/velog-server/src/services/AdService/index.ts +++ b/apps/server/src/services/AdService/index.ts @@ -1,9 +1,9 @@ import { ENV } from '@env' import { BadRequestError } from '@errors/BadRequestErrors.js' -import { AdsInput } from '@graphql/helpers/generated' +import { AdsInput } from '@graphql/helpers/generated.js' import { DbService } from '@lib/db/DbService.js' import { UtilsService } from '@lib/utils/UtilsService.js' -import { Ad, Prisma } from '@prisma/client' +import { Ad, Prisma } from '@packages/database/velog-rds' import { injectable, singleton } from 'tsyringe' interface Service { diff --git a/packages/velog-server/src/services/AuthService/AuthService.test.ts b/apps/server/src/services/AuthService/AuthService.test.ts similarity index 100% rename from packages/velog-server/src/services/AuthService/AuthService.test.ts rename to apps/server/src/services/AuthService/AuthService.test.ts diff --git a/packages/velog-server/src/services/AuthService/index.ts b/apps/server/src/services/AuthService/index.ts similarity index 97% rename from packages/velog-server/src/services/AuthService/index.ts rename to apps/server/src/services/AuthService/index.ts index 815c9a6c..d79a19a4 100644 --- a/packages/velog-server/src/services/AuthService/index.ts +++ b/apps/server/src/services/AuthService/index.ts @@ -6,7 +6,7 @@ import { createAuthTemplate } from '@template/createAuthTemplate.js' import { ENV } from '@env' import { FastifyReply } from 'fastify' import { CookieService } from '@lib/cookie/CookieService.js' -import { GraphQLContext } from '@interfaces/graphql' +import { GraphQLContext } from '@interfaces/graphql.js' import { UnauthorizedError } from '@errors/UnauthorizedError.js' interface Service { diff --git a/packages/velog-server/src/services/CommentService/CommentService.test.ts b/apps/server/src/services/CommentService/CommentService.test.ts similarity index 100% rename from packages/velog-server/src/services/CommentService/CommentService.test.ts rename to apps/server/src/services/CommentService/CommentService.test.ts diff --git a/packages/velog-server/src/services/CommentService/index.ts b/apps/server/src/services/CommentService/index.ts similarity index 96% rename from packages/velog-server/src/services/CommentService/index.ts rename to apps/server/src/services/CommentService/index.ts index 8e9769f6..81bae4a4 100644 --- a/packages/velog-server/src/services/CommentService/index.ts +++ b/apps/server/src/services/CommentService/index.ts @@ -2,7 +2,7 @@ import { DbService } from '@lib/db/DbService.js' import { injectable, singleton } from 'tsyringe' import DataLoader from 'dataloader' import { UtilsService } from '@lib/utils/UtilsService.js' -import { Comment, Prisma } from '@prisma/client' +import { Comment, Prisma } from '@packages/database/velog-rds' interface Service { count(postId: string): Promise diff --git a/packages/velog-server/src/services/DynamicConfigService/DynamicConfigService.test.ts b/apps/server/src/services/DynamicConfigService/DynamicConfigService.test.ts similarity index 100% rename from packages/velog-server/src/services/DynamicConfigService/DynamicConfigService.test.ts rename to apps/server/src/services/DynamicConfigService/DynamicConfigService.test.ts diff --git a/packages/velog-server/src/services/DynamicConfigService/index.ts b/apps/server/src/services/DynamicConfigService/index.ts similarity index 65% rename from packages/velog-server/src/services/DynamicConfigService/index.ts rename to apps/server/src/services/DynamicConfigService/index.ts index 7442c6ba..9870043f 100644 --- a/packages/velog-server/src/services/DynamicConfigService/index.ts +++ b/apps/server/src/services/DynamicConfigService/index.ts @@ -11,13 +11,23 @@ export class DynamicConfigService implements Service { constructor(private readonly db: DbService) {} public async isBlockedUser(username: string = ''): Promise { const list = await this.readBlockUserList() - const isBlocked = list.includes(username) - return isBlocked + return list.includes(username) + } + public async isBlockedUserById(userId: string): Promise { + const user = await this.db.user.findUnique({ + where: { + id: userId, + }, + }) + if (!user) { + return false + } + return this.isBlockedUser(user.username) } private async readBlockUserList(): Promise { const list = await this.db.dynamicConfigItem.findMany({ where: { - type: 'username', + type: 'blockUsername', }, }) return list.map((item) => item.value) diff --git a/packages/velog-server/src/services/ExternalIntegrationService/ExternalIntegrationService.test.ts b/apps/server/src/services/ExternalIntegrationService/ExternalIntegrationService.test.ts similarity index 100% rename from packages/velog-server/src/services/ExternalIntegrationService/ExternalIntegrationService.test.ts rename to apps/server/src/services/ExternalIntegrationService/ExternalIntegrationService.test.ts diff --git a/packages/velog-server/src/services/ExternalIntegrationService/index.ts b/apps/server/src/services/ExternalIntegrationService/index.ts similarity index 96% rename from packages/velog-server/src/services/ExternalIntegrationService/index.ts rename to apps/server/src/services/ExternalIntegrationService/index.ts index 269ce84a..e51fdddd 100644 --- a/packages/velog-server/src/services/ExternalIntegrationService/index.ts +++ b/apps/server/src/services/ExternalIntegrationService/index.ts @@ -4,9 +4,10 @@ import { nanoid } from 'nanoid' import { JwtService } from '@lib/jwt/JwtService.js' import { Time } from '@constants/TimeConstants.js' import { ENV } from '@env' -import axios from 'axios' + import { UnauthorizedError } from '@errors/UnauthorizedError.js' import { SerializePost } from '@services/PostService/index.js' +import { axios } from '@packages/commonjs' interface Service { createIntegrationCode(userId: string): Promise @@ -19,10 +20,7 @@ interface Service { @injectable() @singleton() export class ExternalIntegrationService implements Service { - constructor( - private readonly db: DbService, - private readonly jwt: JwtService, - ) {} + constructor(private readonly db: DbService, private readonly jwt: JwtService) {} public async createIntegrationCode(signedUserId?: string): Promise { if (!signedUserId) { throw new UnauthorizedError('Not logged in') diff --git a/packages/velog-cron/src/services/FeedService/FeedService.test.ts b/apps/server/src/services/FeedService/FeedService.test.ts similarity index 100% rename from packages/velog-cron/src/services/FeedService/FeedService.test.ts rename to apps/server/src/services/FeedService/FeedService.test.ts diff --git a/packages/velog-server/src/services/FeedService/index.ts b/apps/server/src/services/FeedService/index.ts similarity index 93% rename from packages/velog-server/src/services/FeedService/index.ts rename to apps/server/src/services/FeedService/index.ts index 7ffdf661..b68133fb 100644 --- a/packages/velog-server/src/services/FeedService/index.ts +++ b/apps/server/src/services/FeedService/index.ts @@ -1,8 +1,8 @@ import { BadRequestError } from '@errors/BadRequestErrors.js' -import { FeedPostsInput } from '@graphql/helpers/generated' +import { FeedPostsInput } from '@graphql/helpers/generated.js' import { DbService } from '@lib/db/DbService.js' import { UtilsService } from '@lib/utils/UtilsService.js' -import { Post } from '@prisma/client' +import { Post } from '@packages/database/velog-rds' import { subMonths, subYears } from 'date-fns' import { injectable, singleton } from 'tsyringe' @@ -15,10 +15,7 @@ interface Service { @injectable() @singleton() export class FeedService implements Service { - constructor( - private readonly db: DbService, - private readonly utils: UtilsService, - ) {} + constructor(private readonly db: DbService, private readonly utils: UtilsService) {} async getFeedPosts(input: FeedPostsInput, singedUserId?: string): Promise { if (!singedUserId) { return [] diff --git a/packages/velog-server/src/services/FollowUser/UserFollowService.test.ts b/apps/server/src/services/FollowUser/UserFollowService.test.ts similarity index 100% rename from packages/velog-server/src/services/FollowUser/UserFollowService.test.ts rename to apps/server/src/services/FollowUser/UserFollowService.test.ts diff --git a/packages/velog-server/src/services/FollowUser/index.ts b/apps/server/src/services/FollowUser/index.ts similarity index 98% rename from packages/velog-server/src/services/FollowUser/index.ts rename to apps/server/src/services/FollowUser/index.ts index ca09f61d..0b0b753e 100644 --- a/packages/velog-server/src/services/FollowUser/index.ts +++ b/apps/server/src/services/FollowUser/index.ts @@ -1,10 +1,10 @@ +import { FollowUser, Prisma, UserProfile } from '@packages/database/velog-rds' import { BadRequestError } from '@errors/BadRequestErrors.js' import { ConfilctError } from '@errors/ConfilctError.js' import { NotFoundError } from '@errors/NotfoundError.js' import { UnauthorizedError } from '@errors/UnauthorizedError.js' -import { GetFollowInput } from '@graphql/helpers/generated' +import { GetFollowInput } from '@graphql/helpers/generated.js' import { DbService } from '@lib/db/DbService.js' -import { FollowUser, Prisma, UserProfile } from '@prisma/client' import { injectable, singleton } from 'tsyringe' import { UserService } from '@services/UserService/index.js' import { FeedService } from '@services/FeedService/index.js' diff --git a/packages/velog-server/src/services/ImageService/ImageService.test.ts b/apps/server/src/services/ImageService/ImageService.test.ts similarity index 100% rename from packages/velog-server/src/services/ImageService/ImageService.test.ts rename to apps/server/src/services/ImageService/ImageService.test.ts diff --git a/packages/velog-server/src/services/ImageService/index.ts b/apps/server/src/services/ImageService/index.ts similarity index 87% rename from packages/velog-server/src/services/ImageService/index.ts rename to apps/server/src/services/ImageService/index.ts index 978434cf..de68257d 100644 --- a/packages/velog-server/src/services/ImageService/index.ts +++ b/apps/server/src/services/ImageService/index.ts @@ -1,7 +1,8 @@ +import { Time } from '@constants/TimeConstants.js' import { ENV } from '@env' import { DbService } from '@lib/db/DbService.js' import { SlackService } from '@lib/slack/SlackService.js' -import { UserImageNext } from '@prisma/client' +import { UserImageNext } from '@packages/database/velog-rds' import { UserService } from '@services/UserService/index.js' import { injectable, singleton } from 'tsyringe' @@ -94,8 +95,8 @@ export class ImageService implements Service { ) } public async detectAbuse(userId: string) { - const oneHourAgo = new Date(Date.now() - 1000 * 60 * 60) - const oneMinuteAgo = new Date(Date.now() - 1000 * 60) + const oneHourAgo = new Date(Date.now() - Time.ONE_HOUR_IN_MS) + const oneMinuteAgo = new Date(Date.now() - Time.ONE_MINUTE_IN_MS) const imageCountLastHour = await this.db.userImageNext.count({ where: { @@ -108,19 +109,21 @@ export class ImageService implements Service { const user = await this.userService.findById(userId) const username = user?.username + + if (imageCountLastHour > 150) { + this.slack.sendSlackMessage( + `User ${username} (${userId}) is blocked due to upload abuse.`, + ENV.slackImage, + ) + return true + } + if (imageCountLastHour > 100) { this.slack.sendSlackMessage( `User ${username} (${userId}) has uploaded ${imageCountLastHour} images in the last hour.`, ENV.slackImage, ) - - if (imageCountLastHour > 500) { - this.slack.sendSlackMessage( - `User ${username} (${userId}) is blocked due to upload abuse.`, - ENV.slackImage, - ) - return true - } + return true } const imageCountLastMinute = await this.db.userImageNext.count({ diff --git a/packages/velog-server/src/services/NotificationService/NotificationService.test.ts b/apps/server/src/services/NotificationService/NotificationService.test.ts similarity index 100% rename from packages/velog-server/src/services/NotificationService/NotificationService.test.ts rename to apps/server/src/services/NotificationService/NotificationService.test.ts diff --git a/packages/velog-server/src/services/NotificationService/index.ts b/apps/server/src/services/NotificationService/index.ts similarity index 99% rename from packages/velog-server/src/services/NotificationService/index.ts rename to apps/server/src/services/NotificationService/index.ts index 34d592df..a5bdc0ad 100644 --- a/packages/velog-server/src/services/NotificationService/index.ts +++ b/apps/server/src/services/NotificationService/index.ts @@ -8,10 +8,10 @@ import { NotificationType, NotificationsInput, NotificationActionInput, -} from '@graphql/helpers/generated' +} from '@graphql/helpers/generated.js' import { DbService } from '@lib/db/DbService.js' import { UtilsService } from '@lib/utils/UtilsService.js' -import { Prisma } from '@prisma/client' +import { Prisma } from '@packages/database/velog-rds' import { UserService } from '@services/UserService/index.js' import { injectable, singleton } from 'tsyringe' import { z } from 'zod' diff --git a/packages/velog-server/src/services/PostApiService/PostApiService.test.ts b/apps/server/src/services/PostApiService/PostApiService.test.ts similarity index 81% rename from packages/velog-server/src/services/PostApiService/PostApiService.test.ts rename to apps/server/src/services/PostApiService/PostApiService.test.ts index 2b87205e..c4f1b806 100644 --- a/packages/velog-server/src/services/PostApiService/PostApiService.test.ts +++ b/apps/server/src/services/PostApiService/PostApiService.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { PostApiService } from './index.js' +import { PostApiService } from './index.mjs' describe('PostApiService', () => { const service = container.resolve(PostApiService) diff --git a/packages/velog-server/src/services/PostApiService/index.ts b/apps/server/src/services/PostApiService/index.mts similarity index 73% rename from packages/velog-server/src/services/PostApiService/index.ts rename to apps/server/src/services/PostApiService/index.mts index a1a37287..2a0a3020 100644 --- a/packages/velog-server/src/services/PostApiService/index.ts +++ b/apps/server/src/services/PostApiService/index.mts @@ -1,24 +1,27 @@ -import { EditPostInput, WritePostInput } from '@graphql/helpers/generated' +import geoip from 'geoip-country' +import { customAlphabet } from 'nanoid' +import { EditPostInput, WritePostInput } from '@graphql/helpers/generated.js' import { DbService } from '@lib/db/DbService.js' import { UtilsService } from '@lib/utils/UtilsService.js' import { injectable, singleton } from 'tsyringe' -import { customAlphabet } from 'nanoid' import { TagService } from '@services/TagService/index.js' import { PostTagService } from '@services/PostTagService/index.js' import { Time } from '@constants/TimeConstants.js' import { DiscordService } from '@lib/discord/DiscordService.js' -import { CurrentUser } from '@interfaces/user' import { UnauthorizedError } from '@errors/UnauthorizedError.js' import { NotFoundError } from '@errors/NotfoundError.js' import { BadRequestError } from '@errors/BadRequestErrors.js' -import geoip from 'geoip-country' -import { Post, Series, Tag } from '@prisma/client' +import { Post, Series, Tag } from '@packages/database/velog-rds' import { ForbiddenError } from '@errors/ForbiddenError.js' -import { SeriesService } from '@services/SeriesService/index.js' +import { SeriesService } from '@services/SeriesService/index.mjs' import { SearchService } from '@services/SearchService/index.js' import { ExternalIntegrationService } from '@services/ExternalIntegrationService/index.js' import { PostService } from '@services/PostService/index.js' -import { RedisService } from '@lib/redis/RedisService.js' +import { + RedisService, + type CreateFeedQueueData, + type CheckPostSpamQueueData, +} from '@lib/redis/RedisService.js' import { GraphcdnService } from '@lib/graphcdn/GraphcdnService.js' import { ImageService } from '@services/ImageService/index.js' import { UserService } from '@services/UserService/index.js' @@ -58,13 +61,18 @@ export class PostApiService implements Service { ip, }) + if (!post) { + throw new NotFoundError('Not found post when create post') + } + if (series_id && !data.is_temp) { await this.seriesService.appendToSeries(series_id, post.id) } - if (!data.is_temp) { - await this.searchService.searchSync.update(post.id) - } + setTimeout(() => { + if (data.is_temp) return + this.searchService.searchSync.update(post.id).catch(console.error) + }) return post } @@ -76,12 +84,17 @@ export class PostApiService implements Service { ip, }) + if (!post) { + throw new NotFoundError('Not found post when edit post') + } + const prevSeriesPost = await this.db.seriesPost.findFirst({ where: { fk_post_id: post.id, }, }) + // 처음 시리즈를 등록하는 경우 if (!prevSeriesPost && series_id) { await this.seriesService.appendToSeries(series_id, post.id) } @@ -101,27 +114,38 @@ export class PostApiService implements Service { id: prevSeriesPost.id, }, }), + this.db.series.updateMany({ + where: { + id: prevSeriesPost.fk_series_id!, + }, + data: { + updated_at: new Date(), + }, + }), ]) } - await this.db.post.update({ - where: { - id: post.id, - }, - data, - }) - try { - await Promise.all([ - data.is_temp ? null : this.searchService.searchSync.update(post.id), - this.graphcdn.purgePost(post.id), - ]) + await this.db.post.update({ + where: { + id: post.id, + }, + data, + }) } catch (error) { - console.error(error) + console.error('Prisma update error:', error) + throw error } + setTimeout(() => { + Promise.all([ + data.is_temp ? null : this.searchService.searchSync.update(post.id), + this.graphcdn.purgePost(post.id), + ]).catch(console.error) + }) + if (!post.is_private && data.is_private) { - setImmediate(async () => { + setTimeout(async () => { if (!signedUserId) return const isIntegrated = await this.externalInterationService.checkIntegrated(signedUserId) if (!isIntegrated) return @@ -129,7 +153,7 @@ export class PostApiService implements Service { type: 'deleted', post_id: post.id, }) - }) + }, 0) } return { ...post, url_slug: data.url_slug } @@ -164,16 +188,15 @@ export class PostApiService implements Service { } data.title = data.title.slice(0, 255) ?? '' + data.body = this.utils.removeNullBytes(data.body) const isPublish = !data.is_temp && !data.is_private const country = geoip.lookup(ip)?.country ?? '' - const isSpam = this.isIncludeSpamKeyword({ input, user, country }) const isLimit = await this.isPostLimitReached(signedUserId) const isBlock = await this.dynamicConfigService.isBlockedUser(user.username) const checks = [ - { type: 'spam', value: isSpam }, { type: 'limit', value: isLimit }, { type: 'block', value: isBlock }, ] @@ -181,13 +204,24 @@ export class PostApiService implements Service { const isTusted = await this.userService.checkTrust(signedUserId) if (isPublish && !isTusted) { const isVerified = await this.verifyTurnstile(token) - checks.push({ - type: 'turnstile', - value: !isVerified, - }) + if (!isVerified) { + await this.alertIsSpam({ + action: type, + userId: user.id, + title: input.title!, + country, + ip, + type: 'turnstile', + }) + } + // checks.push({ + // type: 'turnstile', + // value: !isVerified, + // }) } - if (checks.map(({ value }) => value).some((check) => check)) { + const isSpam = checks.map(({ value }) => value).some((check) => check) + if (isSpam) { data.is_private = true await this.alertIsSpam({ action: type, @@ -216,15 +250,20 @@ export class PostApiService implements Service { let post: Post | null = null if (type === 'write') { - post = await this.db.post.create({ - data: { - ...(data as Omit), - fk_user_id: signedUserId, - }, - include: { - user: true, - }, - }) + try { + post = await this.db.post.create({ + data: { + ...(data as Omit), + fk_user_id: signedUserId, + }, + include: { + user: true, + }, + }) + } catch (error) { + console.log('Prisma create post error:', error) + throw error + } } if (type === 'edit') { @@ -237,9 +276,16 @@ export class PostApiService implements Service { }, }) - if (post?.is_temp && !data.is_temp) { + const createdAt = post?.created_at ? new Date(post.created_at).getTime() : NaN + const releasedAt = post?.released_at ? new Date(post.released_at).getTime() : NaN + const isNotReleased = createdAt === releasedAt + const isInitRelease = isNotReleased && post?.is_temp && !data.is_temp && !data.is_private + + if (isInitRelease) { Object.assign(data, { released_at: new Date() }) } + + Object.assign(data, { updated_at: new Date() }) } if (!post) { @@ -249,7 +295,7 @@ export class PostApiService implements Service { await this.handleTags(tags, post.id) if (isPublish) { - setImmediate(async () => { + setTimeout(async () => { if (!post) return if (!signedUserId) return @@ -269,6 +315,7 @@ export class PostApiService implements Service { user: true, }, }) + if (!targetPost) return const serializedPost = this.postService.serialize(targetPost) @@ -276,43 +323,42 @@ export class PostApiService implements Service { type: type === 'write' ? 'created' : 'updated', post: serializedPost, }) - }) + }, 0) - const queueData = { - fk_following_id: signedUserId, - fk_post_id: post.id, - } - this.redis.createFeedQueue(queueData) + // create feed + setTimeout(() => { + if (!post) return + const queueData: CreateFeedQueueData = { + fk_following_id: signedUserId, + fk_post_id: post.id, + } + this.redis.addToCreateFeedQueue(queueData) + }, 0) + + // check spam + setTimeout(() => { + if (!post) return + if (isSpam) return + if (isTusted) return + const queueData: CheckPostSpamQueueData = { + post_id: post.id, + user_id: signedUserId, + ip, + } + this.redis.addToCheckPostSpamQueue(queueData) + }, 0) } - setTimeout(async () => { - if (!post) return - const images = await this.imageService.getImagesOf(post.id) - await this.imageService.trackImages(images, data.body) - }, 0) + // setTimeout(async () => { + // if (!post) return + // const images = await this.imageService.getImagesOf(post.id) + // await this.imageService.trackImages(images, data.body) + // }, 0) - return { data, isPublish, post, userId: signedUserId, series_id } - } - private isIncludeSpamKeyword({ input, user, country }: IsIncludeSpamKeywordArgs): boolean { - const extraText = input.tags - .join('') - .concat(user?.profile?.short_bio ?? '', user?.profile?.display_name ?? '') - - const allowList = ['KR', 'GB', ''] - const blockList = ['IN', 'PK', 'CN', 'VN', 'TH', 'PH'] - const isForeign = !allowList.includes(country) - - if ( - blockList.includes(country) || - this.utils.spamFilter(input.body!.concat(extraText), isForeign) || - this.utils.spamFilter(input.title!, isForeign, true) - ) { - return true - } - return false + return { data: { ...data }, isPublish, post, userId: signedUserId, series_id } } private async generateUrlSlug({ input, urlSlug, userId }: GenerateUrlSlugArgs) { - let processedUrlSlug = this.utils.escapeForUrl(urlSlug) + let processedUrlSlug = this.utils.escapeForUrl(urlSlug).slice(0, 240) const urlSlugDuplicate = await this.db.post.findFirst({ where: { fk_user_id: userId, @@ -322,16 +368,17 @@ export class PostApiService implements Service { const generate = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 8) const isEditArgs = this.isEditArgs(input) + const isWriteArgs = !isEditArgs if (isEditArgs && urlSlugDuplicate && urlSlugDuplicate.id !== input.id) { const randomString = generate(8) - processedUrlSlug = processedUrlSlug.slice(0, 245) + processedUrlSlug = processedUrlSlug.slice(0, 240) processedUrlSlug += `-${randomString}` } - if (!isEditArgs && urlSlugDuplicate) { + if (isWriteArgs && urlSlugDuplicate) { const randomString = generate(8) - processedUrlSlug = processedUrlSlug.slice(0, 245) + processedUrlSlug = processedUrlSlug.slice(0, 240) processedUrlSlug += `-${randomString}` } @@ -346,9 +393,12 @@ export class PostApiService implements Service { return false } private async handleTags(tags: string[], postId: string): Promise { - const tagsData = await Promise.all(tags.map((tag) => this.tagService.findOrCreate(tag))) - await this.postTagService.syncPostTags(postId, tagsData) - return tagsData + const tagsData = await Promise.all( + tags.map((tag) => this.tagService.findOrCreate(tag.trim().slice(0, 255))), + ) + const validTags = tagsData.filter((tag): tag is Tag => !!tag) + await this.postTagService.syncPostTags(postId, validTags) + return validTags } private async isPostLimitReached(signedUserId: string): Promise { const recentPostCount = await this.db.post.count({ @@ -373,7 +423,6 @@ export class PostApiService implements Service { is_private: true, }, }) - return true } private async alertIsSpam({ @@ -422,12 +471,6 @@ type AlertSpamArgs = { type: string } -type IsIncludeSpamKeywordArgs = { - input: PostInput - user: CurrentUser - country: string -} - type GenerateUrlSlugArgs = { input: PostInput urlSlug: string diff --git a/packages/velog-server/src/services/PostLikeService/PostLikeService.test.ts b/apps/server/src/services/PostLikeService/PostLikeService.test.ts similarity index 100% rename from packages/velog-server/src/services/PostLikeService/PostLikeService.test.ts rename to apps/server/src/services/PostLikeService/PostLikeService.test.ts diff --git a/packages/velog-server/src/services/PostLikeService/index.ts b/apps/server/src/services/PostLikeService/index.ts similarity index 95% rename from packages/velog-server/src/services/PostLikeService/index.ts rename to apps/server/src/services/PostLikeService/index.ts index 007eea34..aae1534c 100644 --- a/packages/velog-server/src/services/PostLikeService/index.ts +++ b/apps/server/src/services/PostLikeService/index.ts @@ -3,7 +3,7 @@ import { NotFoundError } from '@errors/NotfoundError.js' import { UnauthorizedError } from '@errors/UnauthorizedError.js' import { DbService } from '@lib/db/DbService.js' import { UtilsService } from '@lib/utils/UtilsService.js' -import { Post } from '@prisma/client' +import { Post } from '@packages/database/velog-rds' import { injectable, singleton } from 'tsyringe' import { PostService } from '@services/PostService/index.js' import { SearchService } from '@services/SearchService/index.js' @@ -120,9 +120,9 @@ export class PostLikeService implements Service { await this.postService.updatePostScore(postId) } - setTimeout(() => { + setTimeout(async () => { try { - this.searchService.searchSync.update(post.id) + await this.searchService.searchSync.update(post.id) } catch (error) { console.log('likePost searchSync update error', error) } @@ -190,9 +190,9 @@ export class PostLikeService implements Service { }) await this.postService.updatePostScore(postId) - setTimeout(() => { + setTimeout(async () => { try { - this.searchService.searchSync.update(post.id) + await this.searchService.searchSync.update(post.id) } catch (error) { console.log('unlikePost searchSync update error', error) } diff --git a/packages/velog-server/src/services/PostReadLogService/PostReadLogInterface.ts b/apps/server/src/services/PostReadLogService/PostReadLogInterface.ts similarity index 100% rename from packages/velog-server/src/services/PostReadLogService/PostReadLogInterface.ts rename to apps/server/src/services/PostReadLogService/PostReadLogInterface.ts diff --git a/packages/velog-server/src/services/PostReadLogService/PostReadLogService.test.ts b/apps/server/src/services/PostReadLogService/PostReadLogService.test.ts similarity index 100% rename from packages/velog-server/src/services/PostReadLogService/PostReadLogService.test.ts rename to apps/server/src/services/PostReadLogService/PostReadLogService.test.ts diff --git a/packages/velog-server/src/services/PostReadLogService/index.ts b/apps/server/src/services/PostReadLogService/index.ts similarity index 89% rename from packages/velog-server/src/services/PostReadLogService/index.ts rename to apps/server/src/services/PostReadLogService/index.ts index 19eab7e1..b8ec1ff2 100644 --- a/packages/velog-server/src/services/PostReadLogService/index.ts +++ b/apps/server/src/services/PostReadLogService/index.ts @@ -1,7 +1,7 @@ import { DbService } from '@lib/db/DbService.js' import { injectable, singleton } from 'tsyringe' -import { PostReadLog } from '@prisma/client' -import { LogParams } from './PostReadLogInterface' +import { PostReadLog } from '@packages/database/velog-rds' +import { LogParams } from './PostReadLogInterface.js' interface Service { log(params: LogParams): Promise diff --git a/packages/velog-server/src/services/PostService/PostService.test.ts b/apps/server/src/services/PostService/PostService.test.ts similarity index 92% rename from packages/velog-server/src/services/PostService/PostService.test.ts rename to apps/server/src/services/PostService/PostService.test.ts index ffafc886..61499734 100644 --- a/packages/velog-server/src/services/PostService/PostService.test.ts +++ b/apps/server/src/services/PostService/PostService.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { PostService } from './index' +import { PostService } from './index.js' // import { StubDbService, stubDbService } from 'test/stub/prisma/DbService' describe('PostService', () => { diff --git a/packages/velog-server/src/services/PostService/PostServiceInterface.ts b/apps/server/src/services/PostService/PostServiceInterface.ts similarity index 82% rename from packages/velog-server/src/services/PostService/PostServiceInterface.ts rename to apps/server/src/services/PostService/PostServiceInterface.ts index 92a18f3e..62a02836 100644 --- a/packages/velog-server/src/services/PostService/PostServiceInterface.ts +++ b/apps/server/src/services/PostService/PostServiceInterface.ts @@ -1,4 +1,4 @@ -import { Comment, Post, Tag, User } from '@prisma/client' +import { Comment, Post, Tag, User } from '@packages/database/velog-rds' export type PostIncludeUser = Post & { user?: User } export type PostIncludeComment = Post & { comments?: Comment[] } diff --git a/packages/velog-server/src/services/PostService/index.ts b/apps/server/src/services/PostService/index.ts similarity index 97% rename from packages/velog-server/src/services/PostService/index.ts rename to apps/server/src/services/PostService/index.ts index a2aa6b7b..d1f8c33e 100644 --- a/packages/velog-server/src/services/PostService/index.ts +++ b/apps/server/src/services/PostService/index.ts @@ -1,5 +1,5 @@ import removeMd from 'remove-markdown' -import { Post, PostTag, Prisma, Tag, User } from '@prisma/client' +import { Post, PostTag, Prisma, Tag, User } from '@packages/database/velog-rds' import { container, injectable, singleton } from 'tsyringe' import { GetPostsInput, @@ -11,17 +11,17 @@ import { } from '@graphql/helpers/generated.js' import { DbService } from '@lib/db/DbService.js' import { BadRequestError, ConfilctError, NotFoundError, UnauthorizedError } from '@errors/index.js' -import { GetPostsByTypeParams, Timeframe } from './PostServiceInterface' +import { GetPostsByTypeParams, Timeframe } from './PostServiceInterface.js' import { CacheService } from '@lib/cache/CacheService.js' import { UtilsService } from '@lib/utils/UtilsService.js' import { PostReadLogService } from '@services/PostReadLogService/index.js' import { subDays, subYears } from 'date-fns' -import axios from 'axios' import { ENV } from '@env' import { RedisService } from '@lib/redis/RedisService.js' import { ElasticSearchService } from '@lib/elasticSearch/ElasticSearchService.js' import { Time } from '@constants/TimeConstants.js' import { TagService } from '@services/TagService/index.js' +import { axios } from '@packages/commonjs' interface Service { findById(id: string, include?: Prisma.PostInclude): Promise findPostsByIds(ids: string[], include?: Prisma.PostInclude): Promise @@ -258,7 +258,7 @@ export class PostService implements Service { throw new BadRequestError('Invalid timeframe') } - if (timeframe === 'year' && offset > 1000) { + if (timeframe === 'year' && (offset > 1000 || limit > 20)) { console.log('Detected GraphQL Abuse', ip) return [] } @@ -291,7 +291,7 @@ export class PostService implements Service { id: true, }, orderBy: { - [timeframe === 'year' ? 'likes' : 'score']: 'desc', + score: 'desc', }, take: limit, skip: offset, @@ -400,18 +400,12 @@ export class PostService implements Service { fk_user_id: post.fk_user_id, url_slug: post.url_slug, likes: post.likes, + is_private: post.is_private, + is_temp: post.is_temp || false, } } public async updatePostScore(postId: string) { - await axios.patch( - `${ENV.cronHost}/api/posts/v1/score/${postId}`, - {}, - { - headers: { - 'Cron-Api-Key': ENV.cronApiKey, - }, - }, - ) + await this.redis.addToScorePostQueue({ post_id: postId }) } public shortDescription(post: Post): string { if (post.short_description) return post.short_description @@ -521,8 +515,9 @@ export class PostService implements Service { if (temp_only) { if (!username) throw new BadRequestError('username is missing') if (!user) throw new NotFoundError('Invalid username') - if (user.id !== signedUserId) + if (user.id !== signedUserId) { throw new UnauthorizedError('You have no permission to load temp posts') + } Object.assign(whereQuery, { is_temp: true }) } else { @@ -712,6 +707,8 @@ export type SerializePost = { short_description: string tags: string[] fk_user_id: string + is_temp: boolean + is_private: boolean } type SerializeArgs = Post & { diff --git a/packages/velog-server/src/services/PostTagService/PostTagService.test.ts b/apps/server/src/services/PostTagService/PostTagService.test.ts similarity index 100% rename from packages/velog-server/src/services/PostTagService/PostTagService.test.ts rename to apps/server/src/services/PostTagService/PostTagService.test.ts diff --git a/packages/velog-server/src/services/PostTagService/index.ts b/apps/server/src/services/PostTagService/index.ts similarity index 96% rename from packages/velog-server/src/services/PostTagService/index.ts rename to apps/server/src/services/PostTagService/index.ts index 035e1df5..6cfb0551 100644 --- a/packages/velog-server/src/services/PostTagService/index.ts +++ b/apps/server/src/services/PostTagService/index.ts @@ -1,6 +1,6 @@ import { DbService } from '@lib/db/DbService.js' import { UtilsService } from '@lib/utils/UtilsService.js' -import { Tag } from '@prisma/client' +import { Tag } from '@packages/database/velog-rds' import { injectable, singleton } from 'tsyringe' interface Service { diff --git a/packages/velog-server/src/services/SearchService/SearchService.test.ts b/apps/server/src/services/SearchService/SearchService.test.ts similarity index 82% rename from packages/velog-server/src/services/SearchService/SearchService.test.ts rename to apps/server/src/services/SearchService/SearchService.test.ts index ec668a2f..170f9b94 100644 --- a/packages/velog-server/src/services/SearchService/SearchService.test.ts +++ b/apps/server/src/services/SearchService/SearchService.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { SearchService } from '.' +import { SearchService } from './index.js' describe('SearchService', () => { const service = container.resolve(SearchService) diff --git a/packages/velog-server/src/services/SearchService/index.ts b/apps/server/src/services/SearchService/index.ts similarity index 98% rename from packages/velog-server/src/services/SearchService/index.ts rename to apps/server/src/services/SearchService/index.ts index 57b46fc5..72f85821 100644 --- a/packages/velog-server/src/services/SearchService/index.ts +++ b/apps/server/src/services/SearchService/index.ts @@ -1,7 +1,7 @@ import { pick } from 'rambda' import { DbService } from '@lib/db/DbService.js' import { injectable, singleton } from 'tsyringe' -import { Prisma, Tag } from '@prisma/client' +import { Prisma, Tag } from '@packages/database/velog-rds' import { ElasticSearchService } from '@lib/elasticSearch/ElasticSearchService.js' import { ApiResponse } from '@elastic/elasticsearch' import { TagService } from '@services/TagService/index.js' diff --git a/packages/velog-server/src/services/SeriesService/SeriesService.test.ts b/apps/server/src/services/SeriesService/SeriesService.test.ts similarity index 81% rename from packages/velog-server/src/services/SeriesService/SeriesService.test.ts rename to apps/server/src/services/SeriesService/SeriesService.test.ts index 420442bd..f67b0dbf 100644 --- a/packages/velog-server/src/services/SeriesService/SeriesService.test.ts +++ b/apps/server/src/services/SeriesService/SeriesService.test.ts @@ -1,5 +1,5 @@ import { container } from 'tsyringe' -import { SeriesService } from './index.js' +import { SeriesService } from './index.mjs' describe('SeriesService', () => { const service = container.resolve(SeriesService) diff --git a/packages/velog-server/src/services/SeriesService/index.ts b/apps/server/src/services/SeriesService/index.mts similarity index 92% rename from packages/velog-server/src/services/SeriesService/index.ts rename to apps/server/src/services/SeriesService/index.mts index e9e536f2..69ce4598 100644 --- a/packages/velog-server/src/services/SeriesService/index.ts +++ b/apps/server/src/services/SeriesService/index.mts @@ -1,7 +1,8 @@ +import { NotFoundError } from '@errors/NotfoundError.js' import { UnauthorizedError } from '@errors/UnauthorizedError.js' -import { GetSeriesInput } from '@graphql/helpers/generated' +import { GetSeriesInput } from '@graphql/helpers/generated.js' import { DbService } from '@lib/db/DbService.js' -import { SeriesPost, Series } from '@prisma/client' +import { SeriesPost, Series } from '@packages/database/velog-rds' import DataLoader from 'dataloader' import { injectable, singleton } from 'tsyringe' @@ -169,7 +170,9 @@ export class SeriesService implements Service { }, }) - if (!series) return + if (!series) { + throw new NotFoundError('Series not found') + } await this.db.seriesPost.create({ data: { @@ -178,6 +181,15 @@ export class SeriesService implements Service { index: nextIndex, }, }) + + await this.db.series.update({ + where: { + id: seriesId, + }, + data: { + updated_at: new Date(), + }, + }) } public async subtractIndexAfter(seriesId: string, afterIndex: number) { diff --git a/packages/velog-server/src/services/SocialService/SocialService.test.ts b/apps/server/src/services/SocialService/SocialService.test.ts similarity index 100% rename from packages/velog-server/src/services/SocialService/SocialService.test.ts rename to apps/server/src/services/SocialService/SocialService.test.ts diff --git a/packages/velog-server/src/services/SocialService/SocialServiceInterface.ts b/apps/server/src/services/SocialService/SocialServiceInterface.ts similarity index 94% rename from packages/velog-server/src/services/SocialService/SocialServiceInterface.ts rename to apps/server/src/services/SocialService/SocialServiceInterface.ts index f402261c..e97e111d 100644 --- a/packages/velog-server/src/services/SocialService/SocialServiceInterface.ts +++ b/apps/server/src/services/SocialService/SocialServiceInterface.ts @@ -1,4 +1,4 @@ -import { SocialAccount } from '@prisma/client' +import { SocialAccount } from '@packages/database/velog-rds' export type SocialProvider = 'google' | 'facebook' | 'github' export type GetSocialAccountParams = { diff --git a/packages/velog-server/src/services/SocialService/index.ts b/apps/server/src/services/SocialService/index.ts similarity index 98% rename from packages/velog-server/src/services/SocialService/index.ts rename to apps/server/src/services/SocialService/index.ts index 8353f1f8..53aecb86 100644 --- a/packages/velog-server/src/services/SocialService/index.ts +++ b/apps/server/src/services/SocialService/index.ts @@ -3,9 +3,9 @@ import { DbService } from '@lib/db/DbService.js' import { injectable, singleton } from 'tsyringe' import { google } from 'googleapis' import qs from 'qs' -import axios from 'axios' + import { Octokit } from '@octokit/rest' -import { SocialAccount } from '@prisma/client' +import { SocialAccount } from '@packages/database/velog-rds' import { FacebookProfile, FacebookTokenResult, @@ -15,6 +15,7 @@ import { GithubOAuthResult, SocialProfile, } from '@services/SocialService/SocialServiceInterface.js' +import { axios } from '@packages/commonjs' interface Service { getSocialAccount({ uid, provider }: GetSocialAccountParams): Promise diff --git a/packages/velog-server/src/services/TagService/TagService.test.ts b/apps/server/src/services/TagService/TagService.test.ts similarity index 100% rename from packages/velog-server/src/services/TagService/TagService.test.ts rename to apps/server/src/services/TagService/TagService.test.ts diff --git a/packages/velog-server/src/services/TagService/index.ts b/apps/server/src/services/TagService/index.ts similarity index 87% rename from packages/velog-server/src/services/TagService/index.ts rename to apps/server/src/services/TagService/index.ts index 5a66dad3..cb3b6300 100644 --- a/packages/velog-server/src/services/TagService/index.ts +++ b/apps/server/src/services/TagService/index.ts @@ -1,11 +1,11 @@ import DataLoader from 'dataloader' import { DbService } from '@lib/db/DbService.js' -import { Prisma, Tag } from '@prisma/client' +import { Prisma, Tag } from '@packages/database/velog-rds' import { injectable, singleton } from 'tsyringe' import { UtilsService } from '@lib/utils/UtilsService.js' import { UserService } from '@services/UserService/index.js' import { NotFoundError } from '@errors/NotfoundError.js' -import { UserTags } from '@graphql/helpers/generated' +import { UserTags } from '@graphql/helpers/generated.js' interface Service { findByNameFiltered(name: string): Promise @@ -14,7 +14,7 @@ interface Service { tagLoader(): DataLoader getOriginTag(tagname: string): Promise getUserTags(username: string, signedUserId?: string): Promise - findOrCreate(name: string): Promise + findOrCreate(name: string): Promise } @injectable() @@ -159,19 +159,30 @@ export class TagService implements Service { return rawData.map((data) => ({ ...data, posts_count: Number(data.posts_count) })) } - public async findOrCreate(name: string): Promise { + public async findOrCreate(name: string): Promise { const tag = await this.findByName(name) if (tag) return tag const filtered = this.utils.escapeForUrl(name).toLowerCase() - const freshTag = await this.db.tag.create({ - data: { - name, - name_filtered: filtered, - }, - }) - - return freshTag + try { + const freshTag = await this.db.tag.create({ + data: { + name, + name_filtered: filtered, + }, + }) + return freshTag + } catch (error) { + console.log('create tag error', error) + console.log('name', name) + console.log('name_filtered', filtered) + const tag = await this.db.tag.findFirst({ + where: { + name: name, + }, + }) + return tag + } } } diff --git a/packages/velog-server/src/services/UserMetaService/UserMetaService.test.ts b/apps/server/src/services/UserMetaService/UserMetaService.test.ts similarity index 100% rename from packages/velog-server/src/services/UserMetaService/UserMetaService.test.ts rename to apps/server/src/services/UserMetaService/UserMetaService.test.ts diff --git a/packages/velog-server/src/services/UserMetaService/index.ts b/apps/server/src/services/UserMetaService/index.ts similarity index 96% rename from packages/velog-server/src/services/UserMetaService/index.ts rename to apps/server/src/services/UserMetaService/index.ts index 18068e83..9320416a 100644 --- a/packages/velog-server/src/services/UserMetaService/index.ts +++ b/apps/server/src/services/UserMetaService/index.ts @@ -1,6 +1,6 @@ import { UnauthorizedError } from '@errors/UnauthorizedError.js' import { DbService } from '@lib/db/DbService.js' -import { Prisma, User, UserMeta } from '@prisma/client' +import { Prisma, User, UserMeta } from '@packages/database/velog-rds' import { injectable, singleton } from 'tsyringe' interface Service { diff --git a/packages/velog-server/src/services/UserProfileService/UserProfileService.test.ts b/apps/server/src/services/UserProfileService/UserProfileService.test.ts similarity index 100% rename from packages/velog-server/src/services/UserProfileService/UserProfileService.test.ts rename to apps/server/src/services/UserProfileService/UserProfileService.test.ts diff --git a/packages/velog-server/src/services/UserProfileService/index.ts b/apps/server/src/services/UserProfileService/index.ts similarity index 97% rename from packages/velog-server/src/services/UserProfileService/index.ts rename to apps/server/src/services/UserProfileService/index.ts index 301cdff8..beecb365 100644 --- a/packages/velog-server/src/services/UserProfileService/index.ts +++ b/apps/server/src/services/UserProfileService/index.ts @@ -3,7 +3,7 @@ import { NotFoundError } from '@errors/NotfoundError.js' import { UnauthorizedError } from '@errors/UnauthorizedError.js' import { DbService } from '@lib/db/DbService.js' import { UtilsService } from '@lib/utils/UtilsService.js' -import { Prisma, UserProfile } from '@prisma/client' +import { Prisma, UserProfile } from '@packages/database/velog-rds' import DataLoader from 'dataloader' import { injectable, singleton } from 'tsyringe' diff --git a/packages/velog-server/src/services/UserService/UserService.test.ts b/apps/server/src/services/UserService/UserService.test.ts similarity index 100% rename from packages/velog-server/src/services/UserService/UserService.test.ts rename to apps/server/src/services/UserService/UserService.test.ts diff --git a/packages/velog-server/src/services/UserService/index.ts b/apps/server/src/services/UserService/index.ts similarity index 87% rename from packages/velog-server/src/services/UserService/index.ts rename to apps/server/src/services/UserService/index.ts index 35a4e388..7af23436 100644 --- a/packages/velog-server/src/services/UserService/index.ts +++ b/apps/server/src/services/UserService/index.ts @@ -1,11 +1,11 @@ -import { CurrentUser } from '@interfaces/user' +import { CurrentUser } from '@interfaces/user.js' import { CookieService } from '@lib/cookie/CookieService.js' import { DbService } from '@lib/db/DbService.js' -import { Prisma, User } from '@prisma/client' +import { Prisma, User } from '@packages/database/velog-rds' import { injectable, singleton } from 'tsyringe' -import { GraphQLContext } from '@interfaces/graphql' +import { GraphQLContext } from '@interfaces/graphql.js' import { JwtService } from '@lib/jwt/JwtService.js' -import { RefreshTokenData } from '@lib/jwt/JwtInterface.js' +import type { RefreshTokenData } from '@packages/library/jwt' import { Time } from '@constants/TimeConstants.js' import { UnauthorizedError, @@ -14,16 +14,15 @@ import { ForbiddenError, ConfilctError, } from '@errors/index.js' -import { UserToken } from '@graphql/helpers/generated' +import { UserToken } from '@graphql/helpers/generated.js' import { UtilsService } from '@lib/utils/UtilsService.js' import DataLoader from 'dataloader' import { AuthService } from '@services/AuthService/index.js' import { FastifyReply } from 'fastify' -import { RedisService } from '@lib/redis/RedisService.js' +import { RedisService, ChangeEmailArgs } from '@lib/redis/RedisService.js' import { changeEmailTemplate } from '@template/changeEmailTemplate.js' import { ENV } from '@env' import { MailService } from '@lib/mail/MailService.js' -import { ChangeEmailDataType } from '@lib/redis/RedisInterface.js' import { differenceInDays } from 'date-fns' interface Service { @@ -41,6 +40,7 @@ interface Service { initiateChangeEmail(email: string, signedUserId?: string): Promise confirmChangeEmail(code: string, signedUserId?: string): Promise checkTrust(userId: string): Promise + checkExistsUser(userId: string): Promise } @injectable() @@ -122,14 +122,18 @@ export class UserService implements Service { public async updateLastAccessedAt(userId?: string): Promise { if (!userId) return - await this.db.userProfile.update({ - where: { - fk_user_id: userId, - }, - data: { - last_accessed_at: this.utils.now, - }, - }) + try { + await this.db.userProfile.updateMany({ + where: { + fk_user_id: userId, + }, + data: { + last_accessed_at: this.utils.now, + }, + }) + } catch (error) { + console.error('updateLastAccessedAt error', error) + } } public async restoreToken(ctx: Pick): Promise { const refreshToken: string | undefined = ctx.request.cookies['refresh_token'] @@ -246,7 +250,7 @@ export class UserService implements Service { const data = JSON.stringify({ userId: user.id, email: email.toLowerCase(), - } as ChangeEmailDataType) + } as ChangeEmailArgs) const template = changeEmailTemplate(user.username, email, code) @@ -279,7 +283,7 @@ export class UserService implements Service { throw new NotFoundError('Not found data') } - const { userId, email }: ChangeEmailDataType = JSON.parse(metadata) + const { userId, email }: ChangeEmailArgs = JSON.parse(metadata) if (userId !== signedUserId) { throw new UnauthorizedError('No permission to change the email') @@ -312,6 +316,19 @@ export class UserService implements Service { const diffDays = differenceInDays(today, joinDay) return diffDays > 20 } + public async checkExistsUser(userId?: string): Promise { + if (!userId) return false + + const key = this.redis.generateKey.existsUser(userId) + const value = await this.redis.get(key) + if (value === 'true') return true + if (value === 'false') return false + + const user = await this.findById(userId) + const save = user ? 'true' : 'false' + await this.redis.set(key, save, 'EX', Time.ONE_MINUTE_IN_S * 10) + return !!user + } } type FindByIdOrUsernameArgs = { diff --git a/packages/velog-server/src/services/VelogConfigService/VelogConfigService.test.ts b/apps/server/src/services/VelogConfigService/VelogConfigService.test.ts similarity index 100% rename from packages/velog-server/src/services/VelogConfigService/VelogConfigService.test.ts rename to apps/server/src/services/VelogConfigService/VelogConfigService.test.ts diff --git a/packages/velog-server/src/services/VelogConfigService/index.ts b/apps/server/src/services/VelogConfigService/index.ts similarity index 97% rename from packages/velog-server/src/services/VelogConfigService/index.ts rename to apps/server/src/services/VelogConfigService/index.ts index f5ea84c6..85396735 100644 --- a/packages/velog-server/src/services/VelogConfigService/index.ts +++ b/apps/server/src/services/VelogConfigService/index.ts @@ -3,7 +3,7 @@ import { NotFoundError } from '@errors/NotfoundError.js' import { UnauthorizedError } from '@errors/UnauthorizedError.js' import { DbService } from '@lib/db/DbService.js' import { UtilsService } from '@lib/utils/UtilsService.js' -import { VelogConfig } from '@prisma/client' +import { VelogConfig } from '@packages/database/velog-rds' import DataLoader from 'dataloader' import { injectable, singleton } from 'tsyringe' diff --git a/packages/velog-cron/src/services/WriterService/WriterService.test.ts b/apps/server/src/services/WriterService/WriterService.test.ts similarity index 100% rename from packages/velog-cron/src/services/WriterService/WriterService.test.ts rename to apps/server/src/services/WriterService/WriterService.test.ts diff --git a/packages/velog-server/src/services/WriterService/index.ts b/apps/server/src/services/WriterService/index.ts similarity index 94% rename from packages/velog-server/src/services/WriterService/index.ts rename to apps/server/src/services/WriterService/index.ts index 84730235..0a8d7573 100644 --- a/packages/velog-server/src/services/WriterService/index.ts +++ b/apps/server/src/services/WriterService/index.ts @@ -1,5 +1,5 @@ import { BadRequestError } from '@errors/BadRequestErrors.js' -import { TrendingWriter } from '@graphql/helpers/generated' +import { TrendingWriter } from '@graphql/helpers/generated.js' import { RedisService } from '@lib/redis/RedisService.js' import { injectable, singleton } from 'tsyringe' diff --git a/packages/velog-server/src/template/changeEmailTemplate.ts b/apps/server/src/template/changeEmailTemplate.ts similarity index 100% rename from packages/velog-server/src/template/changeEmailTemplate.ts rename to apps/server/src/template/changeEmailTemplate.ts diff --git a/packages/velog-server/src/template/commentTemplate.ts b/apps/server/src/template/commentTemplate.ts similarity index 100% rename from packages/velog-server/src/template/commentTemplate.ts rename to apps/server/src/template/commentTemplate.ts diff --git a/packages/velog-server/src/template/createAuthTemplate.ts b/apps/server/src/template/createAuthTemplate.ts similarity index 100% rename from packages/velog-server/src/template/createAuthTemplate.ts rename to apps/server/src/template/createAuthTemplate.ts diff --git a/packages/velog-server/src/types/fastify.d.ts b/apps/server/src/types/fastify.d.mts similarity index 100% rename from packages/velog-server/src/types/fastify.d.ts rename to apps/server/src/types/fastify.d.mts diff --git a/apps/server/src/types/graphql.d.mts b/apps/server/src/types/graphql.d.mts new file mode 100644 index 00000000..f2cfaef9 --- /dev/null +++ b/apps/server/src/types/graphql.d.mts @@ -0,0 +1,7 @@ +declare module '*.gql' { + import { DocumentNode } from 'graphql' + const value: DocumentNode + export = value +} + +declare module '@swc-node/register' {} diff --git a/packages/velog-server/test/mock/mockComment.ts b/apps/server/test/mock/mockComment.ts similarity index 100% rename from packages/velog-server/test/mock/mockComment.ts rename to apps/server/test/mock/mockComment.ts diff --git a/packages/velog-server/test/mock/mockPost.ts b/apps/server/test/mock/mockPost.ts similarity index 100% rename from packages/velog-server/test/mock/mockPost.ts rename to apps/server/test/mock/mockPost.ts diff --git a/packages/velog-server/test/mock/mockUser.ts b/apps/server/test/mock/mockUser.ts similarity index 100% rename from packages/velog-server/test/mock/mockUser.ts rename to apps/server/test/mock/mockUser.ts diff --git a/packages/velog-server/test/stub/prisma/DbService.ts b/apps/server/test/stub/prisma/DbService.ts similarity index 79% rename from packages/velog-server/test/stub/prisma/DbService.ts rename to apps/server/test/stub/prisma/DbService.ts index 4e36b173..c7f8ac11 100644 --- a/packages/velog-server/test/stub/prisma/DbService.ts +++ b/apps/server/test/stub/prisma/DbService.ts @@ -1,7 +1,7 @@ -import { PrismaClient } from '@prisma/client' +import { PrismaClient } from '@packages/database/velog-rds' import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended' -import prisma from './client' +import prisma from './client.js' jest.mock('./client', () => ({ __esModule: true, diff --git a/apps/server/test/stub/prisma/client.ts b/apps/server/test/stub/prisma/client.ts new file mode 100644 index 00000000..2536052f --- /dev/null +++ b/apps/server/test/stub/prisma/client.ts @@ -0,0 +1,4 @@ +import { PrismaClient } from '@packages/database/velog-rds' + +const prisma = new PrismaClient() +export default prisma diff --git a/apps/server/tsconfig.build.json b/apps/server/tsconfig.build.json new file mode 100644 index 00000000..6d4c51eb --- /dev/null +++ b/apps/server/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["./src"], + "exclude": ["node_modules", "**/*.test.ts", "./test/**/*.ts", "./scripts/**/*.ts"] +} diff --git a/packages/velog-server/tsconfig.json b/apps/server/tsconfig.json similarity index 51% rename from packages/velog-server/tsconfig.json rename to apps/server/tsconfig.json index a2ef7cf3..db979d7b 100644 --- a/packages/velog-server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -1,12 +1,9 @@ { + "extends": "@packages/tsconfig/base.json", "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "lib": ["ES2022"], - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "moduleResolution": "Node", + "rootDir": "./", "baseUrl": "./", + "outDir": "./dist", "paths": { "@plugins/*": ["src/common/plugins/*"], "@constants/*": ["src/common/constants/*"], @@ -15,27 +12,22 @@ "@interfaces/*": ["src/common/interfaces/*"], "@graphql/*": ["src/graphql/*"], "@test/*": ["src/test/*"], - "@lib/*": ["src/lib/*"], + "@lib/*": ["./src/lib/*"], "@routes/*": ["src/routes/*"], "@services/*": ["src/services/*"], - "@env": ["src/env.ts"], + "@env": ["src/env.mts"], "@template/*": ["src/template/*"] - }, - "typeRoots": ["./node_modules/@types", "./src/types"], - "outDir": "./dist", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true + } }, "include": [ "./src", - ".eslintrc.cjs", "jest.*", "codegen.ts", - "./scripts/**/*.ts", - "test/**/*.ts", - "../velog-prisma/scripts/createMock.ts" + "./test/**/*.ts", + "scripts/**/*.ts", + "scripts/**.mts", + "scripts/**/*.mts", + "eslint.config.js" ], - "exclude": [] + "exclude": ["node_modules", "dist"] } diff --git a/packages/velog-web/.dockerignore b/apps/web/.dockerignore similarity index 93% rename from packages/velog-web/.dockerignore rename to apps/web/.dockerignore index 39e18369..bce63c56 100644 --- a/packages/velog-web/.dockerignore +++ b/apps/web/.dockerignore @@ -7,3 +7,4 @@ README.md .out .prettierrc .env +.npmrc \ No newline at end of file diff --git a/packages/velog-web/.gitignore b/apps/web/.gitignore similarity index 100% rename from packages/velog-web/.gitignore rename to apps/web/.gitignore diff --git a/packages/velog-web/.prettierignore b/apps/web/.prettierignore similarity index 100% rename from packages/velog-web/.prettierignore rename to apps/web/.prettierignore diff --git a/apps/web/.prettierrc b/apps/web/.prettierrc new file mode 100644 index 00000000..819595c5 --- /dev/null +++ b/apps/web/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "printWidth": 100, + "trailingComma": "all" +} diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 00000000..5a70e7f0 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,66 @@ +FROM node:20.16.0-alpine AS base + +RUN corepack enable + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +RUN pnpm add -g turbo +RUN turbo telemetry disable + +ARG DOCKER_ENV +ENV DOCKER_ENV=${DOCKER_ENV} +RUN echo "DOCKER_ENV value is: ${DOCKER_ENV}" + +ARG AWS_ACCESS_KEY_ID +ENV AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} + +ARG AWS_SECRET_ACCESS_KEY +ENV AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} + +ARG TURBO_TEAM +ENV TURBO_TEAM=${TURBO_TEAM} + +ARG TURBO_TOKEN +ENV TURBO_TOKEN=${TURBO_TOKEN} + +ARG TURBO_REMOTE_CACHE_SIGNATURE_KEY +ENV TURBO_REMOTE_CACHE_SIGNATURE_KEY=${TURBO_REMOTE_CACHE_SIGNATURE_KEY} + +ENV APP_NAME="web" +ENV APP_DIR="apps/web" +WORKDIR /app + +FROM base AS pruner +COPY . . +RUN turbo prune web --docker + +FROM base As builder +COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml +COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml +COPY --from=pruner /app/out/full/ ./ + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prefer-frozen-lockfile + +RUN pnpm --filter ${APP_NAME} ssm pull -e ${DOCKER_ENV} +RUN pnpm --filter ${APP_NAME} env:copy -e ${DOCKER_ENV} +RUN turbo build --filter=${APP_NAME} --remote-only + +FROM base AS runner +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/${APP_DIR}/public /app/standalone/public +COPY --from=builder --chown=nextjs:nodejs /app/${APP_DIR}/.next/standalone /app/standalone +COPY --from=builder --chown=nextjs:nodejs /app/${APP_DIR}/.next/static /app/standalone/${APP_DIR}/.next/static + +USER nextjs + +WORKDIR /app/standalone/apps/${APP_NAME} + +EXPOSE 3001 + +ENV PORT=3001 +ENV HOSTNAME=0.0.0.0 + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/packages/velog-web/README.md b/apps/web/README.md similarity index 100% rename from packages/velog-web/README.md rename to apps/web/README.md diff --git a/apps/web/codegen.ts b/apps/web/codegen.ts new file mode 100644 index 00000000..31834dbe --- /dev/null +++ b/apps/web/codegen.ts @@ -0,0 +1,68 @@ +import { ENV } from './src/env' +import type { CodegenConfig } from '@graphql-codegen/cli' +import type { Types } from '@graphql-codegen/plugin-helpers' + +const commonGenerateOptions: Types.ConfiguredOutput = { + config: { + enumsAsTypes: true, + reactQueryVersion: 5, + addSuspenseQuery: true, + exposeQueryKeys: true, + exposeFetcher: true, + exposeMutationKeys: true, + skipTypename: true, + inputMaybeValue: 'T | null | undefined', + maybeValue: 'T | null', + avoidOptionals: { + field: true, + inputValue: false, + object: true, + defaultValue: true, + }, + scalars: { + Date: 'Date', + JSON: 'Record', + ID: 'string', + Void: 'void', + PositiveInt: 'number', + }, + }, + plugins: [ + 'typescript', + '@graphql-codegen/typescript-operations', + '@graphql-codegen/typescript-react-query', + ], +} + +const config: CodegenConfig = { + overwrite: true, + hooks: { + afterOneFileWrite: ['pnpm lint'], + }, + generates: { + 'src/graphql/server/generated/server.ts': { + schema: `${ENV.graphqlServerHost}/graphql`, + documents: './src/graphql/server/*.gql', + config: { + ...commonGenerateOptions.config, + fetcher: { + func: '../helpers/serverFetcher#fetcher', + }, + }, + plugins: commonGenerateOptions.plugins, + }, + 'src/graphql/bookServer/generated/bookServer.ts': { + schema: `${ENV.graphqlBookServerHost}/graphql`, + documents: './src/graphql/bookServer/*.gql', + config: { + ...commonGenerateOptions.config, + fetcher: { + func: '../helpers/bookServerFetcher#fetcher', + }, + }, + plugins: commonGenerateOptions.plugins, + }, + }, +} + +export default config diff --git a/apps/web/docker-compose.sh b/apps/web/docker-compose.sh new file mode 100755 index 00000000..c8c5724b --- /dev/null +++ b/apps/web/docker-compose.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +if [ -z "$AWS_PROFILE" ]; then + echo "AWS_PROFILE이 설정되어 있지 않습니다." + exit 1 +fi + +# AWS 프로필 설정 읽기 +export AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id --profile $AWS_PROFILE) +export AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key --profile $AWS_PROFILE) +export TURBO_TEAM=$(aws configure get turbo_team --profile $AWS_PROFILE) +export TURBO_TOKEN=$(aws configure get turbo_token --profile $AWS_PROFILE) +export TURBO_REMOTE_CACHE_SIGNATURE_KEY=$(aws configure get turbo_remote_cache_signature_key --profile $AWS_PROFILE) + +# echo "AWS 프로필 설정이 완료되었습니다. $AWS_PROFILE 프로필을 사용합니다." +# echo "AWS_ACCESSKEY_ID: $AWS_ACCESS_KEY_ID" +# echo "AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY" +# echo "TURBO_TEAM: $TURBO_TEAM" +# echo "TURBO_TOKEN: $TURBO_TOKEN" +# echo "TURBO_REMOTE_CACHE_SIGNATURE_KEY: $TURBO_REMOTE_CACHE_SIGNATURE_KEY" + +docker-compose up --build \ No newline at end of file diff --git a/apps/web/docker-compose.yml b/apps/web/docker-compose.yml new file mode 100644 index 00000000..cd396313 --- /dev/null +++ b/apps/web/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.9' +services: + web: + build: + context: ../../ + dockerfile: ./apps/web/Dockerfile + no_cache: true + args: + DOCKER_ENV: stage + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + TURBO_TEAM: ${TURBO_TEAM} + TURBO_TOKEN: ${TURBO_TOKEN} + TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${TURBO_REMOTE_CACHE_SIGNATURE_KEY} + tty: true + stdin_open: true + ports: + - 3001:3001 diff --git a/packages/velog-web/env/.env.example b/apps/web/env/.env.example similarity index 59% rename from packages/velog-web/env/.env.example rename to apps/web/env/.env.example index 1c4b1d8d..ccae4dfd 100644 --- a/packages/velog-web/env/.env.example +++ b/apps/web/env/.env.example @@ -5,7 +5,9 @@ NEXT_PUBLIC_CLIENT_V2_HOST= NEXT_PUBLIC_API_V2_HOST= NEXT_PUBLIC_API_V3_HOST= -NEXT_PUBLIC_GRAPHQL_HOST= -NEXT_PUBLIC_GRAPHQL_HOST_NOCDN= +NEXT_PUBLIC_GRAPHQL_SEVER_HOST= +NEXT_PUBLIC_GRAPHQL_SERVER_HOST_NOCDN= + +NEXT_PUBLIC_GRAPHQL_BOOK_SERVER_HOST= NEXT_PUBLIC_GA_MEASUREMENT_ID= \ No newline at end of file diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js new file mode 100644 index 00000000..a82c5b0e --- /dev/null +++ b/apps/web/eslint.config.js @@ -0,0 +1,7 @@ +import nextConfig from '@packages/eslint-config/next.mjs' +import { resolve } from 'node:path' + +const projectPath = resolve(process.cwd()) + +/** @type {import("eslint").Linter.Config} */ +export default [...nextConfig(projectPath)] diff --git a/packages/velog-web/next.config.mjs b/apps/web/next.config.mjs similarity index 96% rename from packages/velog-web/next.config.mjs rename to apps/web/next.config.mjs index 41540d93..b8d4cb2e 100644 --- a/packages/velog-web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -18,6 +18,7 @@ const nextConfig = { esmExternals: true, // support esm taint: true, // for security }, + output: 'standalone', } export default nextConfig diff --git a/packages/velog-web/package.json b/apps/web/package.json similarity index 65% rename from packages/velog-web/package.json rename to apps/web/package.json index a6e4af17..ad491a96 100644 --- a/packages/velog-web/package.json +++ b/apps/web/package.json @@ -1,10 +1,10 @@ { - "name": "velog-web", + "name": "web", "version": "3.0.0", "private": true, "license": "MIT", "engines": { - "node": ">=18.16" + "node": ">=20.11.1" }, "author": { "name": "velopert", @@ -16,36 +16,33 @@ "type": "module", "scripts": { "dev": "pnpm env:copy -e development && next dev -p 3001 --turbo", - "stage": "pnpm env:copy -e stage && pnpm start", - "prod": "pnpm env:copy -e production && pnpm start", "build": "next build", "start": "next start -H 0.0.0.0 -p 3001", - "lint": "next lint", "env:copy": "tsx ./scripts/copyEnv.ts", + "lint": "eslint --fix", "create-component": "tsx ./scripts/createComponent.ts", "create-svg-component": "tsx ./scripts/createSvgComponent.ts", - "codegen": "pnpm env:copy -e development && graphql-codegen --config codegen.ts -r dotenv/config", - "ssm": "tsx ./scripts/ssm/index.ts" + "codegen": "pnpm env:copy -e development && graphql-codegen-esm --config codegen.ts -r dotenv/config", + "ssm": "tsx ./scripts/ssm.mts" }, "dependencies": { - "@aws-sdk/client-ssm": "^3.431.0", + "@packages/markdown-editor": "workspace:*", "@sentry/browser": "^7.75.0", - "@tanstack/react-query": "^5.18.0", + "@tanstack/react-query": "^5.29.0", "axios": "^1.4.0", "codemirror": "5.51", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", - "dotenv": "^16.3.1", + "dotenv": "^16.4.5", "framer-motion": "^10.12.17", "graphql": "^16.7.1", "graphql-request": "^6.1.0", "graphql-tag": "^2.12.6", "html-react-parser": "^5.0.6", - "inquirer": "^9.2.7", - "nanoid": "^4.0.2", - "next": "^14.1.0", - "postcss-flexbugs-fixes": "^5.0.2", - "postcss-preset-env": "^8.5.0", + "inquirer": "^9.2.23", + "nanoid": "^5.0.7", + "next": "^14.2.5", + "next-seo": "^6.0.0", "prismjs": "^1.29.0", "react": "^18.2.0", "react-dom": "18.2.0", @@ -68,35 +65,31 @@ "sharp": "^0.32.4", "strip-markdown": "^6.0.0", "throttle-debounce": "^5.0.0", - "tsx": "^4.6.2", + "tsx": "^4.7.1", "unist-util-visit": "^2.0.1", "zod": "^3.21.4" }, "devDependencies": { - "@graphql-codegen/cli": "^5.0.0", - "@graphql-codegen/typescript-operations": "^4.0.1", - "@graphql-codegen/typescript-react-query": "^6.0.0", + "@graphql-codegen/cli": "^5.0.2", + "@graphql-codegen/plugin-helpers": "^5.0.3", + "@graphql-codegen/typescript-operations": "^4.2.0", + "@graphql-codegen/typescript-react-query": "^6.1.0", + "@packages/commonjs": "workspace:*", + "@packages/eslint-config": "workspace:*", + "@packages/scripts": "workspace:*", + "@packages/tsconfig": "workspace:*", "@svgr/cli": "^8.0.1", "@testing-library/react": "^14.0.0", "@types/codemirror": "^0.0.84", "@types/gtag.js": "^0.0.12", - "@types/inquirer": "^9.0.3", + "@types/inquirer": "^9.0.7", "@types/jest": "^29.5.2", - "@types/node": "^20.3.1", "@types/prismjs": "^1.26.2", "@types/react": "^18.2.48", "@types/react-dom": "18.2.6", "@types/sanitize-html": "^2.9.0", "@types/throttle-debounce": "^5.0.1", - "@typescript-eslint/eslint-plugin": "^6.6.0", - "@typescript-eslint/parser": "^5.59.11", "encoding": "^0.1.13", - "eslint": "8.43.0", - "eslint-config-next": "^13.4.19", - "husky": "^8.0.0", - "husky-init": "^8.0.0", - "prettier": "^2.8.8", - "typescript": "^5.3.3", "typescript-plugin-css-modules": "^5.0.1" } } diff --git a/packages/velog-web/public/.gitkeep b/apps/web/public/.gitkeep similarity index 100% rename from packages/velog-web/public/.gitkeep rename to apps/web/public/.gitkeep diff --git a/packages/velog-web/public/images/user-thumbnail.png b/apps/web/public/images/user-thumbnail.png similarity index 100% rename from packages/velog-web/public/images/user-thumbnail.png rename to apps/web/public/images/user-thumbnail.png diff --git a/apps/web/scripts/.gitignore b/apps/web/scripts/.gitignore new file mode 100644 index 00000000..1998c294 --- /dev/null +++ b/apps/web/scripts/.gitignore @@ -0,0 +1 @@ +.cache \ No newline at end of file diff --git a/apps/web/scripts/copyEnv.ts b/apps/web/scripts/copyEnv.ts new file mode 100644 index 00000000..0d46eaff --- /dev/null +++ b/apps/web/scripts/copyEnv.ts @@ -0,0 +1,4 @@ +import { CopyEnvScript } from '@packages/scripts' + +const copyEnvScript = new CopyEnvScript() +copyEnvScript.execute() diff --git a/packages/velog-web/scripts/createComponent.ts b/apps/web/scripts/createComponent.ts similarity index 96% rename from packages/velog-web/scripts/createComponent.ts rename to apps/web/scripts/createComponent.ts index a8b63152..c20e6489 100644 --- a/packages/velog-web/scripts/createComponent.ts +++ b/apps/web/scripts/createComponent.ts @@ -1,9 +1,9 @@ -import url from 'url' +import { URL } from 'url' import path from 'path' import fs from 'fs' import inquirer from 'inquirer' -const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) +const __dirname = new URL('.', import.meta.url).pathname const templateDir = path.resolve(__dirname, './templates/component') const files = fs.readdirSync(templateDir) const filesData = new Map() diff --git a/packages/velog-web/scripts/createSvgComponent.ts b/apps/web/scripts/createSvgComponent.ts similarity index 100% rename from packages/velog-web/scripts/createSvgComponent.ts rename to apps/web/scripts/createSvgComponent.ts diff --git a/packages/velog-web/scripts/createSvgTemplate.ts b/apps/web/scripts/createSvgTemplate.ts similarity index 100% rename from packages/velog-web/scripts/createSvgTemplate.ts rename to apps/web/scripts/createSvgTemplate.ts diff --git a/apps/web/scripts/ssm.mts b/apps/web/scripts/ssm.mts new file mode 100644 index 00000000..344054bb --- /dev/null +++ b/apps/web/scripts/ssm.mts @@ -0,0 +1,4 @@ +import { SSMScript } from '@packages/scripts' + +const ssm = new SSMScript({ packageName: 'web' }) +ssm.execute() diff --git a/packages/velog-web/src/features/home/components/RecentPosts/RecentPosts.module.css b/apps/web/scripts/templates/component/MyComponent.module.css similarity index 100% rename from packages/velog-web/src/features/home/components/RecentPosts/RecentPosts.module.css rename to apps/web/scripts/templates/component/MyComponent.module.css diff --git a/apps/web/scripts/templates/component/MyComponent.test.tsx b/apps/web/scripts/templates/component/MyComponent.test.tsx new file mode 100644 index 00000000..865cabdb --- /dev/null +++ b/apps/web/scripts/templates/component/MyComponent.test.tsx @@ -0,0 +1,8 @@ +import MyComponent from './MyComponent' +import { render } from '@testing-library/react' + +describe('MyComponent', () => { + it('renders successfully', () => { + render() + }) +}) diff --git a/packages/velog-web/scripts/templates/component/MyComponent.tsx b/apps/web/scripts/templates/component/MyComponent.tsx similarity index 100% rename from packages/velog-web/scripts/templates/component/MyComponent.tsx rename to apps/web/scripts/templates/component/MyComponent.tsx diff --git a/packages/velog-web/scripts/templates/component/index.tsx b/apps/web/scripts/templates/component/index.tsx similarity index 100% rename from packages/velog-web/scripts/templates/component/index.tsx rename to apps/web/scripts/templates/component/index.tsx diff --git a/packages/velog-web/src/app/(email)/email-change/layout.tsx b/apps/web/src/app/(email)/email-change/layout.tsx similarity index 100% rename from packages/velog-web/src/app/(email)/email-change/layout.tsx rename to apps/web/src/app/(email)/email-change/layout.tsx diff --git a/packages/velog-web/src/app/(email)/email-change/page.tsx b/apps/web/src/app/(email)/email-change/page.tsx similarity index 91% rename from packages/velog-web/src/app/(email)/email-change/page.tsx rename to apps/web/src/app/(email)/email-change/page.tsx index d88dda49..7eb9824c 100644 --- a/packages/velog-web/src/app/(email)/email-change/page.tsx +++ b/apps/web/src/app/(email)/email-change/page.tsx @@ -1,7 +1,7 @@ 'use client' import SpinnerBlock from '@/components/SpinnerBlock' -import { useConfirmChangeEmailMutation } from '@/graphql/helpers/generated' +import { useConfirmChangeEmailMutation } from '@/graphql/server/generated/server' import { usePopup } from '@/state/popup' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useRef } from 'react' @@ -13,7 +13,7 @@ type Props = { export default function Page({ searchParams }: Props) { const code = searchParams.code - const { mutateAsync } = useConfirmChangeEmailMutation() + const { mutateAsync } = useConfirmChangeEmailMutation({}) const router = useRouter() const { actions } = usePopup() const hasChecked = useRef(false) diff --git a/packages/velog-web/src/app/(list)/feed/page.tsx b/apps/web/src/app/(list)/feed/page.tsx similarity index 100% rename from packages/velog-web/src/app/(list)/feed/page.tsx rename to apps/web/src/app/(list)/feed/page.tsx diff --git a/packages/velog-web/src/app/(list)/layout.tsx b/apps/web/src/app/(list)/layout.tsx similarity index 100% rename from packages/velog-web/src/app/(list)/layout.tsx rename to apps/web/src/app/(list)/layout.tsx diff --git a/packages/velog-web/src/app/(list)/page.tsx b/apps/web/src/app/(list)/page.tsx similarity index 100% rename from packages/velog-web/src/app/(list)/page.tsx rename to apps/web/src/app/(list)/page.tsx diff --git a/packages/velog-web/src/app/(list)/recent/page.tsx b/apps/web/src/app/(list)/recent/page.tsx similarity index 100% rename from packages/velog-web/src/app/(list)/recent/page.tsx rename to apps/web/src/app/(list)/recent/page.tsx diff --git a/packages/velog-web/src/app/(list)/trending/[timeframe]/page.tsx b/apps/web/src/app/(list)/trending/[timeframe]/page.tsx similarity index 100% rename from packages/velog-web/src/app/(list)/trending/[timeframe]/page.tsx rename to apps/web/src/app/(list)/trending/[timeframe]/page.tsx diff --git a/packages/velog-web/src/app/(list)/trending/page.tsx b/apps/web/src/app/(list)/trending/page.tsx similarity index 100% rename from packages/velog-web/src/app/(list)/trending/page.tsx rename to apps/web/src/app/(list)/trending/page.tsx diff --git a/packages/velog-web/src/app/[username]/(follow)/followers/page.tsx b/apps/web/src/app/[username]/(follow)/followers/page.tsx similarity index 100% rename from packages/velog-web/src/app/[username]/(follow)/followers/page.tsx rename to apps/web/src/app/[username]/(follow)/followers/page.tsx diff --git a/packages/velog-web/src/app/[username]/(follow)/followings/page.tsx b/apps/web/src/app/[username]/(follow)/followings/page.tsx similarity index 100% rename from packages/velog-web/src/app/[username]/(follow)/followings/page.tsx rename to apps/web/src/app/[username]/(follow)/followings/page.tsx diff --git a/packages/velog-web/src/app/[username]/(follow)/layout.tsx b/apps/web/src/app/[username]/(follow)/layout.tsx similarity index 100% rename from packages/velog-web/src/app/[username]/(follow)/layout.tsx rename to apps/web/src/app/[username]/(follow)/layout.tsx diff --git a/packages/velog-web/src/app/[username]/(tab)/about/page.tsx b/apps/web/src/app/[username]/(tab)/about/page.tsx similarity index 100% rename from packages/velog-web/src/app/[username]/(tab)/about/page.tsx rename to apps/web/src/app/[username]/(tab)/about/page.tsx diff --git a/packages/velog-web/src/app/[username]/(tab)/layout.tsx b/apps/web/src/app/[username]/(tab)/layout.tsx similarity index 100% rename from packages/velog-web/src/app/[username]/(tab)/layout.tsx rename to apps/web/src/app/[username]/(tab)/layout.tsx diff --git a/packages/velog-web/src/app/[username]/(tab)/page.tsx b/apps/web/src/app/[username]/(tab)/page.tsx similarity index 100% rename from packages/velog-web/src/app/[username]/(tab)/page.tsx rename to apps/web/src/app/[username]/(tab)/page.tsx diff --git a/packages/velog-web/src/app/[username]/(tab)/posts/page.tsx b/apps/web/src/app/[username]/(tab)/posts/page.tsx similarity index 100% rename from packages/velog-web/src/app/[username]/(tab)/posts/page.tsx rename to apps/web/src/app/[username]/(tab)/posts/page.tsx diff --git a/packages/velog-web/src/app/[username]/(tab)/series/page.tsx b/apps/web/src/app/[username]/(tab)/series/page.tsx similarity index 100% rename from packages/velog-web/src/app/[username]/(tab)/series/page.tsx rename to apps/web/src/app/[username]/(tab)/series/page.tsx diff --git a/packages/velog-web/src/app/[username]/[urlSlug]/layout.tsx b/apps/web/src/app/[username]/[urlSlug]/layout.tsx similarity index 100% rename from packages/velog-web/src/app/[username]/[urlSlug]/layout.tsx rename to apps/web/src/app/[username]/[urlSlug]/layout.tsx diff --git a/packages/velog-web/src/app/[username]/[urlSlug]/page.tsx b/apps/web/src/app/[username]/[urlSlug]/page.tsx similarity index 100% rename from packages/velog-web/src/app/[username]/[urlSlug]/page.tsx rename to apps/web/src/app/[username]/[urlSlug]/page.tsx diff --git a/packages/velog-web/src/app/error.tsx b/apps/web/src/app/error.tsx similarity index 100% rename from packages/velog-web/src/app/error.tsx rename to apps/web/src/app/error.tsx diff --git a/packages/velog-web/src/app/favicon.ico b/apps/web/src/app/favicon.ico similarity index 100% rename from packages/velog-web/src/app/favicon.ico rename to apps/web/src/app/favicon.ico diff --git a/packages/velog-web/src/app/favicon/apple/apple-icon-152x152.png b/apps/web/src/app/favicon/apple/apple-icon-152x152.png similarity index 100% rename from packages/velog-web/src/app/favicon/apple/apple-icon-152x152.png rename to apps/web/src/app/favicon/apple/apple-icon-152x152.png diff --git a/packages/velog-web/src/app/favicon/apple/apple-icon-180x180.png b/apps/web/src/app/favicon/apple/apple-icon-180x180.png similarity index 100% rename from packages/velog-web/src/app/favicon/apple/apple-icon-180x180.png rename to apps/web/src/app/favicon/apple/apple-icon-180x180.png diff --git a/packages/velog-web/src/app/favicon/apple/apple-icon-240x240.png b/apps/web/src/app/favicon/apple/apple-icon-240x240.png similarity index 100% rename from packages/velog-web/src/app/favicon/apple/apple-icon-240x240.png rename to apps/web/src/app/favicon/apple/apple-icon-240x240.png diff --git a/packages/velog-web/src/app/favicon/favicon-16x16.png b/apps/web/src/app/favicon/favicon-16x16.png similarity index 100% rename from packages/velog-web/src/app/favicon/favicon-16x16.png rename to apps/web/src/app/favicon/favicon-16x16.png diff --git a/packages/velog-web/src/app/favicon/favicon-32x32.png b/apps/web/src/app/favicon/favicon-32x32.png similarity index 100% rename from packages/velog-web/src/app/favicon/favicon-32x32.png rename to apps/web/src/app/favicon/favicon-32x32.png diff --git a/packages/velog-web/src/app/favicon/favicon-96x96.png b/apps/web/src/app/favicon/favicon-96x96.png similarity index 100% rename from packages/velog-web/src/app/favicon/favicon-96x96.png rename to apps/web/src/app/favicon/favicon-96x96.png diff --git a/packages/velog-web/src/app/layout.tsx b/apps/web/src/app/layout.tsx similarity index 86% rename from packages/velog-web/src/app/layout.tsx rename to apps/web/src/app/layout.tsx index 87856127..1bf5e557 100644 --- a/packages/velog-web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -66,6 +66,14 @@ type Props = { export default function RootLayout({ children }: Props) { return ( + + + {children} diff --git a/packages/velog-web/src/app/not-found.tsx b/apps/web/src/app/not-found.tsx similarity index 100% rename from packages/velog-web/src/app/not-found.tsx rename to apps/web/src/app/not-found.tsx diff --git a/packages/velog-web/src/app/notifications/layout.tsx b/apps/web/src/app/notifications/layout.tsx similarity index 100% rename from packages/velog-web/src/app/notifications/layout.tsx rename to apps/web/src/app/notifications/layout.tsx diff --git a/packages/velog-web/src/app/notifications/not-read/page.tsx b/apps/web/src/app/notifications/not-read/page.tsx similarity index 100% rename from packages/velog-web/src/app/notifications/not-read/page.tsx rename to apps/web/src/app/notifications/not-read/page.tsx diff --git a/packages/velog-web/src/app/notifications/page.tsx b/apps/web/src/app/notifications/page.tsx similarity index 100% rename from packages/velog-web/src/app/notifications/page.tsx rename to apps/web/src/app/notifications/page.tsx diff --git a/packages/velog-web/src/app/search/layout.tsx b/apps/web/src/app/search/layout.tsx similarity index 100% rename from packages/velog-web/src/app/search/layout.tsx rename to apps/web/src/app/search/layout.tsx diff --git a/packages/velog-web/src/app/search/page.tsx b/apps/web/src/app/search/page.tsx similarity index 100% rename from packages/velog-web/src/app/search/page.tsx rename to apps/web/src/app/search/page.tsx diff --git a/packages/velog-web/src/app/setting/layout.tsx b/apps/web/src/app/setting/layout.tsx similarity index 100% rename from packages/velog-web/src/app/setting/layout.tsx rename to apps/web/src/app/setting/layout.tsx diff --git a/packages/velog-web/src/app/setting/page.tsx b/apps/web/src/app/setting/page.tsx similarity index 100% rename from packages/velog-web/src/app/setting/page.tsx rename to apps/web/src/app/setting/page.tsx diff --git a/packages/velog-web/src/app/trending-writers/layout.tsx b/apps/web/src/app/trending-writers/layout.tsx similarity index 100% rename from packages/velog-web/src/app/trending-writers/layout.tsx rename to apps/web/src/app/trending-writers/layout.tsx diff --git a/packages/velog-web/src/app/trending-writers/page.tsx b/apps/web/src/app/trending-writers/page.tsx similarity index 100% rename from packages/velog-web/src/app/trending-writers/page.tsx rename to apps/web/src/app/trending-writers/page.tsx diff --git a/packages/velog-web/src/assets/icons/components/AddListIcon.tsx b/apps/web/src/assets/icons/components/AddListIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/AddListIcon.tsx rename to apps/web/src/assets/icons/components/AddListIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/CheckIcon.tsx b/apps/web/src/assets/icons/components/CheckIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/CheckIcon.tsx rename to apps/web/src/assets/icons/components/CheckIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/ClipIcon.tsx b/apps/web/src/assets/icons/components/ClipIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/ClipIcon.tsx rename to apps/web/src/assets/icons/components/ClipIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/EmailIcon.tsx b/apps/web/src/assets/icons/components/EmailIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/EmailIcon.tsx rename to apps/web/src/assets/icons/components/EmailIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/FacebookIcon.tsx b/apps/web/src/assets/icons/components/FacebookIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/FacebookIcon.tsx rename to apps/web/src/assets/icons/components/FacebookIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/FacebookSquareIcon.tsx b/apps/web/src/assets/icons/components/FacebookSquareIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/FacebookSquareIcon.tsx rename to apps/web/src/assets/icons/components/FacebookSquareIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/GithubIcon.tsx b/apps/web/src/assets/icons/components/GithubIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/GithubIcon.tsx rename to apps/web/src/assets/icons/components/GithubIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/GlobeIcon.tsx b/apps/web/src/assets/icons/components/GlobeIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/GlobeIcon.tsx rename to apps/web/src/assets/icons/components/GlobeIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/GoogleIcon.tsx b/apps/web/src/assets/icons/components/GoogleIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/GoogleIcon.tsx rename to apps/web/src/assets/icons/components/GoogleIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/LikeIcon.tsx b/apps/web/src/assets/icons/components/LikeIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/LikeIcon.tsx rename to apps/web/src/assets/icons/components/LikeIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/LockIcon.tsx b/apps/web/src/assets/icons/components/LockIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/LockIcon.tsx rename to apps/web/src/assets/icons/components/LockIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/Logo.tsx b/apps/web/src/assets/icons/components/Logo.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/Logo.tsx rename to apps/web/src/assets/icons/components/Logo.tsx diff --git a/packages/velog-web/src/assets/icons/components/MinusBoxIcon.tsx b/apps/web/src/assets/icons/components/MinusBoxIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/MinusBoxIcon.tsx rename to apps/web/src/assets/icons/components/MinusBoxIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/MoonIcon.tsx b/apps/web/src/assets/icons/components/MoonIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/MoonIcon.tsx rename to apps/web/src/assets/icons/components/MoonIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/NotificationIcon.tsx b/apps/web/src/assets/icons/components/NotificationIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/NotificationIcon.tsx rename to apps/web/src/assets/icons/components/NotificationIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/PlusBoxIcon.tsx b/apps/web/src/assets/icons/components/PlusBoxIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/PlusBoxIcon.tsx rename to apps/web/src/assets/icons/components/PlusBoxIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/Search2Icon.tsx b/apps/web/src/assets/icons/components/Search2Icon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/Search2Icon.tsx rename to apps/web/src/assets/icons/components/Search2Icon.tsx diff --git a/packages/velog-web/src/assets/icons/components/SearchIcon.tsx b/apps/web/src/assets/icons/components/SearchIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/SearchIcon.tsx rename to apps/web/src/assets/icons/components/SearchIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/SearchIconLegacy.tsx b/apps/web/src/assets/icons/components/SearchIconLegacy.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/SearchIconLegacy.tsx rename to apps/web/src/assets/icons/components/SearchIconLegacy.tsx diff --git a/packages/velog-web/src/assets/icons/components/SeriesIcon.tsx b/apps/web/src/assets/icons/components/SeriesIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/SeriesIcon.tsx rename to apps/web/src/assets/icons/components/SeriesIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/Share2Icon.tsx b/apps/web/src/assets/icons/components/Share2Icon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/Share2Icon.tsx rename to apps/web/src/assets/icons/components/Share2Icon.tsx diff --git a/packages/velog-web/src/assets/icons/components/ShareIcon.tsx b/apps/web/src/assets/icons/components/ShareIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/ShareIcon.tsx rename to apps/web/src/assets/icons/components/ShareIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/SunIcon.tsx b/apps/web/src/assets/icons/components/SunIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/SunIcon.tsx rename to apps/web/src/assets/icons/components/SunIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/ThemeDark.tsx b/apps/web/src/assets/icons/components/ThemeDark.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/ThemeDark.tsx rename to apps/web/src/assets/icons/components/ThemeDark.tsx diff --git a/packages/velog-web/src/assets/icons/components/ThemeLight.tsx b/apps/web/src/assets/icons/components/ThemeLight.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/ThemeLight.tsx rename to apps/web/src/assets/icons/components/ThemeLight.tsx diff --git a/packages/velog-web/src/assets/icons/components/TwitterIcon.tsx b/apps/web/src/assets/icons/components/TwitterIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/TwitterIcon.tsx rename to apps/web/src/assets/icons/components/TwitterIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/VectorImage.tsx b/apps/web/src/assets/icons/components/VectorImage.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/VectorImage.tsx rename to apps/web/src/assets/icons/components/VectorImage.tsx diff --git a/packages/velog-web/src/assets/icons/components/VelogIcon.tsx b/apps/web/src/assets/icons/components/VelogIcon.tsx similarity index 100% rename from packages/velog-web/src/assets/icons/components/VelogIcon.tsx rename to apps/web/src/assets/icons/components/VelogIcon.tsx diff --git a/packages/velog-web/src/assets/icons/components/index.ts b/apps/web/src/assets/icons/components/index.ts similarity index 100% rename from packages/velog-web/src/assets/icons/components/index.ts rename to apps/web/src/assets/icons/components/index.ts diff --git a/packages/velog-web/src/assets/icons/svg/add-list-icon.svg b/apps/web/src/assets/icons/svg/add-list-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/add-list-icon.svg rename to apps/web/src/assets/icons/svg/add-list-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/check-icon.svg b/apps/web/src/assets/icons/svg/check-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/check-icon.svg rename to apps/web/src/assets/icons/svg/check-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/clip-icon.svg b/apps/web/src/assets/icons/svg/clip-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/clip-icon.svg rename to apps/web/src/assets/icons/svg/clip-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/email-icon.svg b/apps/web/src/assets/icons/svg/email-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/email-icon.svg rename to apps/web/src/assets/icons/svg/email-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/facebook-icon.svg b/apps/web/src/assets/icons/svg/facebook-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/facebook-icon.svg rename to apps/web/src/assets/icons/svg/facebook-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/facebook-square-icon.svg b/apps/web/src/assets/icons/svg/facebook-square-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/facebook-square-icon.svg rename to apps/web/src/assets/icons/svg/facebook-square-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/github-icon.svg b/apps/web/src/assets/icons/svg/github-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/github-icon.svg rename to apps/web/src/assets/icons/svg/github-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/globe-icon.svg b/apps/web/src/assets/icons/svg/globe-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/globe-icon.svg rename to apps/web/src/assets/icons/svg/globe-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/google-icon.svg b/apps/web/src/assets/icons/svg/google-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/google-icon.svg rename to apps/web/src/assets/icons/svg/google-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/like-icon.svg b/apps/web/src/assets/icons/svg/like-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/like-icon.svg rename to apps/web/src/assets/icons/svg/like-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/lock-icon.svg b/apps/web/src/assets/icons/svg/lock-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/lock-icon.svg rename to apps/web/src/assets/icons/svg/lock-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/logo.svg b/apps/web/src/assets/icons/svg/logo.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/logo.svg rename to apps/web/src/assets/icons/svg/logo.svg diff --git a/packages/velog-web/src/assets/icons/svg/minus-box-icon.svg b/apps/web/src/assets/icons/svg/minus-box-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/minus-box-icon.svg rename to apps/web/src/assets/icons/svg/minus-box-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/notification-icon.svg b/apps/web/src/assets/icons/svg/notification-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/notification-icon.svg rename to apps/web/src/assets/icons/svg/notification-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/plus-box-icon.svg b/apps/web/src/assets/icons/svg/plus-box-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/plus-box-icon.svg rename to apps/web/src/assets/icons/svg/plus-box-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/search-2-icon.svg b/apps/web/src/assets/icons/svg/search-2-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/search-2-icon.svg rename to apps/web/src/assets/icons/svg/search-2-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/search-icon-legacy.svg b/apps/web/src/assets/icons/svg/search-icon-legacy.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/search-icon-legacy.svg rename to apps/web/src/assets/icons/svg/search-icon-legacy.svg diff --git a/packages/velog-web/src/assets/icons/svg/search-icon.svg b/apps/web/src/assets/icons/svg/search-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/search-icon.svg rename to apps/web/src/assets/icons/svg/search-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/series-icon.svg b/apps/web/src/assets/icons/svg/series-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/series-icon.svg rename to apps/web/src/assets/icons/svg/series-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/share-2-icon.svg b/apps/web/src/assets/icons/svg/share-2-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/share-2-icon.svg rename to apps/web/src/assets/icons/svg/share-2-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/share-icon.svg b/apps/web/src/assets/icons/svg/share-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/share-icon.svg rename to apps/web/src/assets/icons/svg/share-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/theme-dark.svg b/apps/web/src/assets/icons/svg/theme-dark.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/theme-dark.svg rename to apps/web/src/assets/icons/svg/theme-dark.svg diff --git a/packages/velog-web/src/assets/icons/svg/theme-light.svg b/apps/web/src/assets/icons/svg/theme-light.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/theme-light.svg rename to apps/web/src/assets/icons/svg/theme-light.svg diff --git a/packages/velog-web/src/assets/icons/svg/twitter-icon.svg b/apps/web/src/assets/icons/svg/twitter-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/twitter-icon.svg rename to apps/web/src/assets/icons/svg/twitter-icon.svg diff --git a/packages/velog-web/src/assets/icons/svg/vector-image.svg b/apps/web/src/assets/icons/svg/vector-image.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/vector-image.svg rename to apps/web/src/assets/icons/svg/vector-image.svg diff --git a/packages/velog-web/src/assets/icons/svg/velog-icon.svg b/apps/web/src/assets/icons/svg/velog-icon.svg similarity index 100% rename from packages/velog-web/src/assets/icons/svg/velog-icon.svg rename to apps/web/src/assets/icons/svg/velog-icon.svg diff --git a/packages/velog-web/src/assets/vectors/components/EmptyThumbnail.tsx b/apps/web/src/assets/vectors/components/EmptyThumbnail.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/EmptyThumbnail.tsx rename to apps/web/src/assets/vectors/components/EmptyThumbnail.tsx diff --git a/packages/velog-web/src/assets/vectors/components/SeriesThumbnail.tsx b/apps/web/src/assets/vectors/components/SeriesThumbnail.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/SeriesThumbnail.tsx rename to apps/web/src/assets/vectors/components/SeriesThumbnail.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawBlankCanvas.tsx b/apps/web/src/assets/vectors/components/UndrawBlankCanvas.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawBlankCanvas.tsx rename to apps/web/src/assets/vectors/components/UndrawBlankCanvas.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawBugFixing.tsx b/apps/web/src/assets/vectors/components/UndrawBugFixing.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawBugFixing.tsx rename to apps/web/src/assets/vectors/components/UndrawBugFixing.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawEmpty.tsx b/apps/web/src/assets/vectors/components/UndrawEmpty.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawEmpty.tsx rename to apps/web/src/assets/vectors/components/UndrawEmpty.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawEmptyNotification.tsx b/apps/web/src/assets/vectors/components/UndrawEmptyNotification.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawEmptyNotification.tsx rename to apps/web/src/assets/vectors/components/UndrawEmptyNotification.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawFollowEmpty.tsx b/apps/web/src/assets/vectors/components/UndrawFollowEmpty.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawFollowEmpty.tsx rename to apps/web/src/assets/vectors/components/UndrawFollowEmpty.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawJoyrideHnno.tsx b/apps/web/src/assets/vectors/components/UndrawJoyrideHnno.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawJoyrideHnno.tsx rename to apps/web/src/assets/vectors/components/UndrawJoyrideHnno.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawLogin.tsx b/apps/web/src/assets/vectors/components/UndrawLogin.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawLogin.tsx rename to apps/web/src/assets/vectors/components/UndrawLogin.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawLoginV483.tsx b/apps/web/src/assets/vectors/components/UndrawLoginV483.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawLoginV483.tsx rename to apps/web/src/assets/vectors/components/UndrawLoginV483.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawPageNotFound.tsx b/apps/web/src/assets/vectors/components/UndrawPageNotFound.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawPageNotFound.tsx rename to apps/web/src/assets/vectors/components/UndrawPageNotFound.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawSearching.tsx b/apps/web/src/assets/vectors/components/UndrawSearching.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawSearching.tsx rename to apps/web/src/assets/vectors/components/UndrawSearching.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawServerDown.tsx b/apps/web/src/assets/vectors/components/UndrawServerDown.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawServerDown.tsx rename to apps/web/src/assets/vectors/components/UndrawServerDown.tsx diff --git a/packages/velog-web/src/assets/vectors/components/UndrawUpdate.tsx b/apps/web/src/assets/vectors/components/UndrawUpdate.tsx similarity index 100% rename from packages/velog-web/src/assets/vectors/components/UndrawUpdate.tsx rename to apps/web/src/assets/vectors/components/UndrawUpdate.tsx diff --git a/packages/velog-web/src/assets/vectors/components/index.ts b/apps/web/src/assets/vectors/components/index.ts similarity index 100% rename from packages/velog-web/src/assets/vectors/components/index.ts rename to apps/web/src/assets/vectors/components/index.ts diff --git a/packages/velog-web/src/assets/vectors/svg/empty-thumbnail.svg b/apps/web/src/assets/vectors/svg/empty-thumbnail.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/empty-thumbnail.svg rename to apps/web/src/assets/vectors/svg/empty-thumbnail.svg diff --git a/packages/velog-web/src/assets/vectors/svg/pluto-welcome.png b/apps/web/src/assets/vectors/svg/pluto-welcome.png similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/pluto-welcome.png rename to apps/web/src/assets/vectors/svg/pluto-welcome.png diff --git a/packages/velog-web/src/assets/vectors/svg/series-thumbnail.svg b/apps/web/src/assets/vectors/svg/series-thumbnail.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/series-thumbnail.svg rename to apps/web/src/assets/vectors/svg/series-thumbnail.svg diff --git a/packages/velog-web/src/assets/vectors/svg/undraw_blank_canvas.svg b/apps/web/src/assets/vectors/svg/undraw_blank_canvas.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/undraw_blank_canvas.svg rename to apps/web/src/assets/vectors/svg/undraw_blank_canvas.svg diff --git a/packages/velog-web/src/assets/vectors/svg/undraw_bug_fixing.svg b/apps/web/src/assets/vectors/svg/undraw_bug_fixing.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/undraw_bug_fixing.svg rename to apps/web/src/assets/vectors/svg/undraw_bug_fixing.svg diff --git a/packages/velog-web/src/assets/vectors/svg/undraw_empty.svg b/apps/web/src/assets/vectors/svg/undraw_empty.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/undraw_empty.svg rename to apps/web/src/assets/vectors/svg/undraw_empty.svg diff --git a/packages/velog-web/src/assets/vectors/svg/undraw_empty_notification.svg b/apps/web/src/assets/vectors/svg/undraw_empty_notification.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/undraw_empty_notification.svg rename to apps/web/src/assets/vectors/svg/undraw_empty_notification.svg diff --git a/packages/velog-web/src/assets/vectors/svg/undraw_follow_empty.svg b/apps/web/src/assets/vectors/svg/undraw_follow_empty.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/undraw_follow_empty.svg rename to apps/web/src/assets/vectors/svg/undraw_follow_empty.svg diff --git a/packages/velog-web/src/assets/vectors/svg/undraw_joyride_hnno.svg b/apps/web/src/assets/vectors/svg/undraw_joyride_hnno.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/undraw_joyride_hnno.svg rename to apps/web/src/assets/vectors/svg/undraw_joyride_hnno.svg diff --git a/packages/velog-web/src/assets/vectors/svg/undraw_login.svg b/apps/web/src/assets/vectors/svg/undraw_login.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/undraw_login.svg rename to apps/web/src/assets/vectors/svg/undraw_login.svg diff --git a/packages/velog-web/src/assets/vectors/svg/undraw_page_not_found.svg b/apps/web/src/assets/vectors/svg/undraw_page_not_found.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/undraw_page_not_found.svg rename to apps/web/src/assets/vectors/svg/undraw_page_not_found.svg diff --git a/packages/velog-web/src/assets/vectors/svg/undraw_searching.svg b/apps/web/src/assets/vectors/svg/undraw_searching.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/undraw_searching.svg rename to apps/web/src/assets/vectors/svg/undraw_searching.svg diff --git a/packages/velog-web/src/assets/vectors/svg/undraw_server_down.svg b/apps/web/src/assets/vectors/svg/undraw_server_down.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/undraw_server_down.svg rename to apps/web/src/assets/vectors/svg/undraw_server_down.svg diff --git a/packages/velog-web/src/assets/vectors/svg/undraw_update.svg b/apps/web/src/assets/vectors/svg/undraw_update.svg similarity index 100% rename from packages/velog-web/src/assets/vectors/svg/undraw_update.svg rename to apps/web/src/assets/vectors/svg/undraw_update.svg diff --git a/packages/velog-web/src/components/ActiveLink/ActiveLink.test.tsx b/apps/web/src/components/ActiveLink/ActiveLink.test.tsx similarity index 100% rename from packages/velog-web/src/components/ActiveLink/ActiveLink.test.tsx rename to apps/web/src/components/ActiveLink/ActiveLink.test.tsx diff --git a/packages/velog-web/src/components/ActiveLink/ActiveLink.tsx b/apps/web/src/components/ActiveLink/ActiveLink.tsx similarity index 100% rename from packages/velog-web/src/components/ActiveLink/ActiveLink.tsx rename to apps/web/src/components/ActiveLink/ActiveLink.tsx diff --git a/packages/velog-web/src/components/ActiveLink/index.tsx b/apps/web/src/components/ActiveLink/index.tsx similarity index 100% rename from packages/velog-web/src/components/ActiveLink/index.tsx rename to apps/web/src/components/ActiveLink/index.tsx diff --git a/packages/velog-web/src/components/Button/Button.module.css b/apps/web/src/components/Button/Button.module.css similarity index 100% rename from packages/velog-web/src/components/Button/Button.module.css rename to apps/web/src/components/Button/Button.module.css diff --git a/packages/velog-web/src/components/Button/Button.test.tsx b/apps/web/src/components/Button/Button.test.tsx similarity index 100% rename from packages/velog-web/src/components/Button/Button.test.tsx rename to apps/web/src/components/Button/Button.test.tsx diff --git a/packages/velog-web/src/components/Button/Button.tsx b/apps/web/src/components/Button/Button.tsx similarity index 100% rename from packages/velog-web/src/components/Button/Button.tsx rename to apps/web/src/components/Button/Button.tsx diff --git a/packages/velog-web/src/components/Button/index.tsx b/apps/web/src/components/Button/index.tsx similarity index 100% rename from packages/velog-web/src/components/Button/index.tsx rename to apps/web/src/components/Button/index.tsx diff --git a/packages/velog-web/src/components/CommonPopup/CommonPopup.test.tsx b/apps/web/src/components/CommonPopup/CommonPopup.test.tsx similarity index 100% rename from packages/velog-web/src/components/CommonPopup/CommonPopup.test.tsx rename to apps/web/src/components/CommonPopup/CommonPopup.test.tsx diff --git a/packages/velog-web/src/components/CommonPopup/CommonPopup.tsx b/apps/web/src/components/CommonPopup/CommonPopup.tsx similarity index 100% rename from packages/velog-web/src/components/CommonPopup/CommonPopup.tsx rename to apps/web/src/components/CommonPopup/CommonPopup.tsx diff --git a/packages/velog-web/src/components/CommonPopup/index.tsx b/apps/web/src/components/CommonPopup/index.tsx similarity index 100% rename from packages/velog-web/src/components/CommonPopup/index.tsx rename to apps/web/src/components/CommonPopup/index.tsx diff --git a/packages/velog-web/src/components/ConditionalBackground/ConditionalBackground.module.css b/apps/web/src/components/ConditionalBackground/ConditionalBackground.module.css similarity index 100% rename from packages/velog-web/src/components/ConditionalBackground/ConditionalBackground.module.css rename to apps/web/src/components/ConditionalBackground/ConditionalBackground.module.css diff --git a/packages/velog-web/src/components/ConditionalBackground/ConditionalBackground.test.tsx b/apps/web/src/components/ConditionalBackground/ConditionalBackground.test.tsx similarity index 100% rename from packages/velog-web/src/components/ConditionalBackground/ConditionalBackground.test.tsx rename to apps/web/src/components/ConditionalBackground/ConditionalBackground.test.tsx diff --git a/packages/velog-web/src/components/ConditionalBackground/ConditionalBackground.tsx b/apps/web/src/components/ConditionalBackground/ConditionalBackground.tsx similarity index 100% rename from packages/velog-web/src/components/ConditionalBackground/ConditionalBackground.tsx rename to apps/web/src/components/ConditionalBackground/ConditionalBackground.tsx diff --git a/packages/velog-web/src/components/ConditionalBackground/index.tsx b/apps/web/src/components/ConditionalBackground/index.tsx similarity index 100% rename from packages/velog-web/src/components/ConditionalBackground/index.tsx rename to apps/web/src/components/ConditionalBackground/index.tsx diff --git a/packages/velog-web/src/features/post/components/MobileLikeButton/MobileLikeButton.module.css b/apps/web/src/components/Error/CrashErrorScreen/CrashErrorScreen.module.css similarity index 100% rename from packages/velog-web/src/features/post/components/MobileLikeButton/MobileLikeButton.module.css rename to apps/web/src/components/Error/CrashErrorScreen/CrashErrorScreen.module.css diff --git a/packages/velog-web/src/components/Error/CrashErrorScreen/CrashErrorScreen.test.tsx b/apps/web/src/components/Error/CrashErrorScreen/CrashErrorScreen.test.tsx similarity index 100% rename from packages/velog-web/src/components/Error/CrashErrorScreen/CrashErrorScreen.test.tsx rename to apps/web/src/components/Error/CrashErrorScreen/CrashErrorScreen.test.tsx diff --git a/packages/velog-web/src/components/Error/CrashErrorScreen/CrashErrorScreen.tsx b/apps/web/src/components/Error/CrashErrorScreen/CrashErrorScreen.tsx similarity index 100% rename from packages/velog-web/src/components/Error/CrashErrorScreen/CrashErrorScreen.tsx rename to apps/web/src/components/Error/CrashErrorScreen/CrashErrorScreen.tsx diff --git a/packages/velog-web/src/components/Error/CrashErrorScreen/index.tsx b/apps/web/src/components/Error/CrashErrorScreen/index.tsx similarity index 100% rename from packages/velog-web/src/components/Error/CrashErrorScreen/index.tsx rename to apps/web/src/components/Error/CrashErrorScreen/index.tsx diff --git a/packages/velog-web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.module.css b/apps/web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.module.css similarity index 98% rename from packages/velog-web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.module.css rename to apps/web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.module.css index 95178782..b3c5a7cb 100644 --- a/packages/velog-web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.module.css +++ b/apps/web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.module.css @@ -5,7 +5,7 @@ align-items: center; justify-content: center; flex-direction: column; - svg { + & svg { width: 320px; height: auto; diff --git a/packages/velog-web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.test.tsx b/apps/web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.test.tsx similarity index 100% rename from packages/velog-web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.test.tsx rename to apps/web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.test.tsx diff --git a/packages/velog-web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.tsx b/apps/web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.tsx similarity index 100% rename from packages/velog-web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.tsx rename to apps/web/src/components/Error/ErrorScreenTemplate/ErrorScreenTemplate.tsx diff --git a/packages/velog-web/src/components/Error/ErrorScreenTemplate/index.tsx b/apps/web/src/components/Error/ErrorScreenTemplate/index.tsx similarity index 100% rename from packages/velog-web/src/components/Error/ErrorScreenTemplate/index.tsx rename to apps/web/src/components/Error/ErrorScreenTemplate/index.tsx diff --git a/packages/velog-web/src/components/Error/NetworkErrorScreen/NetworkErrorScreen.test.tsx b/apps/web/src/components/Error/NetworkErrorScreen/NetworkErrorScreen.test.tsx similarity index 100% rename from packages/velog-web/src/components/Error/NetworkErrorScreen/NetworkErrorScreen.test.tsx rename to apps/web/src/components/Error/NetworkErrorScreen/NetworkErrorScreen.test.tsx diff --git a/packages/velog-web/src/components/Error/NetworkErrorScreen/NetworkErrorScreen.tsx b/apps/web/src/components/Error/NetworkErrorScreen/NetworkErrorScreen.tsx similarity index 100% rename from packages/velog-web/src/components/Error/NetworkErrorScreen/NetworkErrorScreen.tsx rename to apps/web/src/components/Error/NetworkErrorScreen/NetworkErrorScreen.tsx diff --git a/packages/velog-web/src/components/Error/NetworkErrorScreen/index.tsx b/apps/web/src/components/Error/NetworkErrorScreen/index.tsx similarity index 100% rename from packages/velog-web/src/components/Error/NetworkErrorScreen/index.tsx rename to apps/web/src/components/Error/NetworkErrorScreen/index.tsx diff --git a/packages/velog-web/src/components/Error/NotFoundError/NotFoundError.test.tsx b/apps/web/src/components/Error/NotFoundError/NotFoundError.test.tsx similarity index 100% rename from packages/velog-web/src/components/Error/NotFoundError/NotFoundError.test.tsx rename to apps/web/src/components/Error/NotFoundError/NotFoundError.test.tsx diff --git a/packages/velog-web/src/components/Error/NotFoundError/NotFoundError.tsx b/apps/web/src/components/Error/NotFoundError/NotFoundError.tsx similarity index 100% rename from packages/velog-web/src/components/Error/NotFoundError/NotFoundError.tsx rename to apps/web/src/components/Error/NotFoundError/NotFoundError.tsx diff --git a/packages/velog-web/src/components/Error/NotFoundError/index.tsx b/apps/web/src/components/Error/NotFoundError/index.tsx similarity index 100% rename from packages/velog-web/src/components/Error/NotFoundError/index.tsx rename to apps/web/src/components/Error/NotFoundError/index.tsx diff --git a/packages/velog-web/src/components/FlatPost/FlatPostCard/FlatPostCard.module.css b/apps/web/src/components/FlatPost/FlatPostCard/FlatPostCard.module.css similarity index 98% rename from packages/velog-web/src/components/FlatPost/FlatPostCard/FlatPostCard.module.css rename to apps/web/src/components/FlatPost/FlatPostCard/FlatPostCard.module.css index 3bdcb65c..30c16b81 100644 --- a/packages/velog-web/src/components/FlatPost/FlatPostCard/FlatPostCard.module.css +++ b/apps/web/src/components/FlatPost/FlatPostCard/FlatPostCard.module.css @@ -24,7 +24,7 @@ border-top: 1px solid var(--border4); } - h2 { + & h2 { font-size: 1.5rem; margin: revert; color: var(--text1); @@ -34,7 +34,7 @@ } } - p { + & p { margin-bottom: 2rem; margin-top: 0.5rem; font-size: 1rem; @@ -51,7 +51,7 @@ .userInfo { display: flex; align-items: center; - img { + & img { width: 48px; height: 48px; display: block; @@ -70,7 +70,7 @@ font-size: 0.875rem; color: var(--text1); font-weight: bold; - a { + & a { color: inherit; text-decoration: none; &:hover { @@ -120,7 +120,7 @@ display: flex; align-items: center; - svg { + & svg { width: 0.875rem; height: 0.875rem; margin-right: 0.25rem; @@ -136,7 +136,7 @@ } .skeletonBlock { - h2 { + & h2 { display: flex; margin-top: 1.375rem; margin-bottom: 0.375rem; diff --git a/packages/velog-web/src/components/FlatPost/FlatPostCard/FlatPostCard.test.tsx b/apps/web/src/components/FlatPost/FlatPostCard/FlatPostCard.test.tsx similarity index 100% rename from packages/velog-web/src/components/FlatPost/FlatPostCard/FlatPostCard.test.tsx rename to apps/web/src/components/FlatPost/FlatPostCard/FlatPostCard.test.tsx diff --git a/packages/velog-web/src/components/FlatPost/FlatPostCard/FlatPostCard.tsx b/apps/web/src/components/FlatPost/FlatPostCard/FlatPostCard.tsx similarity index 90% rename from packages/velog-web/src/components/FlatPost/FlatPostCard/FlatPostCard.tsx rename to apps/web/src/components/FlatPost/FlatPostCard/FlatPostCard.tsx index 22ba5284..869382ae 100644 --- a/packages/velog-web/src/components/FlatPost/FlatPostCard/FlatPostCard.tsx +++ b/apps/web/src/components/FlatPost/FlatPostCard/FlatPostCard.tsx @@ -1,4 +1,4 @@ -import { Post } from '@/graphql/helpers/generated' +import { Post } from '@/graphql/server/generated/server' import styles from './FlatPostCard.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' import Link from 'next/link' @@ -32,7 +32,7 @@ function FlatPostCard({ post, hideUser }: Props) {
post-card-thumbnail
- {post.tags?.map((tag) => )} + {post.tags?.map((tag) => ( + + ))}
{releasedAt} diff --git a/packages/velog-web/src/components/FlatPost/FlatPostCard/FlatPostCardSkeleton.tsx b/apps/web/src/components/FlatPost/FlatPostCard/FlatPostCardSkeleton.tsx similarity index 100% rename from packages/velog-web/src/components/FlatPost/FlatPostCard/FlatPostCardSkeleton.tsx rename to apps/web/src/components/FlatPost/FlatPostCard/FlatPostCardSkeleton.tsx diff --git a/packages/velog-web/src/components/FlatPost/FlatPostCard/index.tsx b/apps/web/src/components/FlatPost/FlatPostCard/index.tsx similarity index 100% rename from packages/velog-web/src/components/FlatPost/FlatPostCard/index.tsx rename to apps/web/src/components/FlatPost/FlatPostCard/index.tsx diff --git a/packages/velog-web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.module.css b/apps/web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.module.css similarity index 100% rename from packages/velog-web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.module.css rename to apps/web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.module.css diff --git a/packages/velog-web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.test.tsx b/apps/web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.test.tsx similarity index 100% rename from packages/velog-web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.test.tsx rename to apps/web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.test.tsx diff --git a/packages/velog-web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.tsx b/apps/web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.tsx similarity index 90% rename from packages/velog-web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.tsx rename to apps/web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.tsx index db01e1d0..9a58a282 100644 --- a/packages/velog-web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.tsx +++ b/apps/web/src/components/FlatPost/FlatPostCardList/FlatPostCardList.tsx @@ -1,4 +1,4 @@ -import { Post } from '@/graphql/helpers/generated' +import { Post } from '@/graphql/server/generated/server' import styles from './FlatPostCardList.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' import FlatPostCard from '../FlatPostCard/FlatPostCard' diff --git a/packages/velog-web/src/components/FlatPost/FlatPostCardList/FlatPostCardListSkeleton.tsx b/apps/web/src/components/FlatPost/FlatPostCardList/FlatPostCardListSkeleton.tsx similarity index 100% rename from packages/velog-web/src/components/FlatPost/FlatPostCardList/FlatPostCardListSkeleton.tsx rename to apps/web/src/components/FlatPost/FlatPostCardList/FlatPostCardListSkeleton.tsx diff --git a/packages/velog-web/src/components/FlatPost/FlatPostCardList/index.tsx b/apps/web/src/components/FlatPost/FlatPostCardList/index.tsx similarity index 100% rename from packages/velog-web/src/components/FlatPost/FlatPostCardList/index.tsx rename to apps/web/src/components/FlatPost/FlatPostCardList/index.tsx diff --git a/packages/velog-web/src/components/FollowButton/FollowButton.module.css b/apps/web/src/components/FollowButton/FollowButton.module.css similarity index 100% rename from packages/velog-web/src/components/FollowButton/FollowButton.module.css rename to apps/web/src/components/FollowButton/FollowButton.module.css diff --git a/packages/velog-web/src/components/FollowButton/FollowButton.test.tsx b/apps/web/src/components/FollowButton/FollowButton.test.tsx similarity index 100% rename from packages/velog-web/src/components/FollowButton/FollowButton.test.tsx rename to apps/web/src/components/FollowButton/FollowButton.test.tsx diff --git a/packages/velog-web/src/components/FollowButton/FollowButton.tsx b/apps/web/src/components/FollowButton/FollowButton.tsx similarity index 98% rename from packages/velog-web/src/components/FollowButton/FollowButton.tsx rename to apps/web/src/components/FollowButton/FollowButton.tsx index 87234e62..62869e4f 100644 --- a/packages/velog-web/src/components/FollowButton/FollowButton.tsx +++ b/apps/web/src/components/FollowButton/FollowButton.tsx @@ -6,7 +6,7 @@ import { useFollowMutation, useGetUserFollowInfoQuery, useUnfollowMutation, -} from '@/graphql/helpers/generated' +} from '@/graphql/server/generated/server' import { useAuth } from '@/state/auth' import { debounce } from 'throttle-debounce' import { useModal } from '@/state/modal' diff --git a/packages/velog-web/src/components/FollowButton/index.tsx b/apps/web/src/components/FollowButton/index.tsx similarity index 100% rename from packages/velog-web/src/components/FollowButton/index.tsx rename to apps/web/src/components/FollowButton/index.tsx diff --git a/packages/velog-web/src/components/Header/Header.module.css b/apps/web/src/components/Header/Header.module.css similarity index 100% rename from packages/velog-web/src/components/Header/Header.module.css rename to apps/web/src/components/Header/Header.module.css diff --git a/packages/velog-web/src/components/Header/Header.test.tsx b/apps/web/src/components/Header/Header.test.tsx similarity index 100% rename from packages/velog-web/src/components/Header/Header.test.tsx rename to apps/web/src/components/Header/Header.test.tsx diff --git a/packages/velog-web/src/components/Header/Header.tsx b/apps/web/src/components/Header/Header.tsx similarity index 98% rename from packages/velog-web/src/components/Header/Header.tsx rename to apps/web/src/components/Header/Header.tsx index be8bdf97..0e651c9d 100644 --- a/packages/velog-web/src/components/Header/Header.tsx +++ b/apps/web/src/components/Header/Header.tsx @@ -13,7 +13,7 @@ import HeaderSkeleton from '@/components/Header/HeaderSkeleton' import { useCurrentUserQuery, useNotNoticeNotificationCountQuery, -} from '@/graphql/helpers/generated' +} from '@/graphql/server/generated/server' import HeaderLogo from './HeaderLogo' import { useParams, usePathname, useRouter } from 'next/navigation' import { getUsernameFromParams } from '@/lib/utils' diff --git a/packages/velog-web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.module.css b/apps/web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.module.css similarity index 97% rename from packages/velog-web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.module.css rename to apps/web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.module.css index 2ed4b62c..b7707dc7 100644 --- a/packages/velog-web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.module.css +++ b/apps/web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.module.css @@ -10,7 +10,7 @@ .logo { display: flex; - svg { + & svg { width: 28px; height: 28px; @@ -21,7 +21,7 @@ } } - svg { + & svg { color: var(--text1); } diff --git a/packages/velog-web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.test.tsx b/apps/web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.test.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.test.tsx rename to apps/web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.test.tsx diff --git a/packages/velog-web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.tsx b/apps/web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.tsx rename to apps/web/src/components/Header/HeaderCustomLogo/HeaderCustomLogo.tsx diff --git a/packages/velog-web/src/components/Header/HeaderCustomLogo/index.tsx b/apps/web/src/components/Header/HeaderCustomLogo/index.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderCustomLogo/index.tsx rename to apps/web/src/components/Header/HeaderCustomLogo/index.tsx diff --git a/packages/velog-web/src/components/Header/HeaderIcon/HeaderIcon.module.css b/apps/web/src/components/Header/HeaderIcon/HeaderIcon.module.css similarity index 93% rename from packages/velog-web/src/components/Header/HeaderIcon/HeaderIcon.module.css rename to apps/web/src/components/Header/HeaderIcon/HeaderIcon.module.css index d221d11b..8060b05f 100644 --- a/packages/velog-web/src/components/Header/HeaderIcon/HeaderIcon.module.css +++ b/apps/web/src/components/Header/HeaderIcon/HeaderIcon.module.css @@ -12,12 +12,12 @@ background: var(--slight-layer); } - svg { + & svg { flex-shrink: 0; width: 24px; height: 24px; - path { + & path { fill: var(--text1); } } diff --git a/packages/velog-web/src/components/Header/HeaderIcon/HeaderIcon.test.tsx b/apps/web/src/components/Header/HeaderIcon/HeaderIcon.test.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderIcon/HeaderIcon.test.tsx rename to apps/web/src/components/Header/HeaderIcon/HeaderIcon.test.tsx diff --git a/packages/velog-web/src/components/Header/HeaderIcon/HeaderIcon.tsx b/apps/web/src/components/Header/HeaderIcon/HeaderIcon.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderIcon/HeaderIcon.tsx rename to apps/web/src/components/Header/HeaderIcon/HeaderIcon.tsx diff --git a/packages/velog-web/src/components/Header/HeaderIcon/index.tsx b/apps/web/src/components/Header/HeaderIcon/index.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderIcon/index.tsx rename to apps/web/src/components/Header/HeaderIcon/index.tsx diff --git a/packages/velog-web/src/components/Header/HeaderLogo/HeaderLogo.module.css b/apps/web/src/components/Header/HeaderLogo/HeaderLogo.module.css similarity index 96% rename from packages/velog-web/src/components/Header/HeaderLogo/HeaderLogo.module.css rename to apps/web/src/components/Header/HeaderLogo/HeaderLogo.module.css index 4d584cb9..5a66defa 100644 --- a/packages/velog-web/src/components/Header/HeaderLogo/HeaderLogo.module.css +++ b/apps/web/src/components/Header/HeaderLogo/HeaderLogo.module.css @@ -8,7 +8,7 @@ height: 100%; line-height: 10px; - svg { + & svg { color: var(--text1); height: 24px; @media screen and (max-width: 1024px) { diff --git a/packages/velog-web/src/components/Header/HeaderLogo/HeaderLogo.test.tsx b/apps/web/src/components/Header/HeaderLogo/HeaderLogo.test.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderLogo/HeaderLogo.test.tsx rename to apps/web/src/components/Header/HeaderLogo/HeaderLogo.test.tsx diff --git a/packages/velog-web/src/components/Header/HeaderLogo/HeaderLogo.tsx b/apps/web/src/components/Header/HeaderLogo/HeaderLogo.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderLogo/HeaderLogo.tsx rename to apps/web/src/components/Header/HeaderLogo/HeaderLogo.tsx diff --git a/packages/velog-web/src/components/Header/HeaderLogo/index.tsx b/apps/web/src/components/Header/HeaderLogo/index.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderLogo/index.tsx rename to apps/web/src/components/Header/HeaderLogo/index.tsx diff --git a/packages/velog-web/src/components/Header/HeaderSkeleton/HeaderSkeleton.module.css b/apps/web/src/components/Header/HeaderSkeleton/HeaderSkeleton.module.css similarity index 100% rename from packages/velog-web/src/components/Header/HeaderSkeleton/HeaderSkeleton.module.css rename to apps/web/src/components/Header/HeaderSkeleton/HeaderSkeleton.module.css diff --git a/packages/velog-web/src/components/Header/HeaderSkeleton/HeaderSkeleton.test.tsx b/apps/web/src/components/Header/HeaderSkeleton/HeaderSkeleton.test.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderSkeleton/HeaderSkeleton.test.tsx rename to apps/web/src/components/Header/HeaderSkeleton/HeaderSkeleton.test.tsx diff --git a/packages/velog-web/src/components/Header/HeaderSkeleton/HeaderSkeleton.tsx b/apps/web/src/components/Header/HeaderSkeleton/HeaderSkeleton.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderSkeleton/HeaderSkeleton.tsx rename to apps/web/src/components/Header/HeaderSkeleton/HeaderSkeleton.tsx diff --git a/packages/velog-web/src/components/Header/HeaderSkeleton/index.tsx b/apps/web/src/components/Header/HeaderSkeleton/index.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderSkeleton/index.tsx rename to apps/web/src/components/Header/HeaderSkeleton/index.tsx diff --git a/packages/velog-web/src/components/Header/HeaderUserIcon/HeaderUserIcon.module.css b/apps/web/src/components/Header/HeaderUserIcon/HeaderUserIcon.module.css similarity index 92% rename from packages/velog-web/src/components/Header/HeaderUserIcon/HeaderUserIcon.module.css rename to apps/web/src/components/Header/HeaderUserIcon/HeaderUserIcon.module.css index 1e3bbd01..b17c932a 100644 --- a/packages/velog-web/src/components/Header/HeaderUserIcon/HeaderUserIcon.module.css +++ b/apps/web/src/components/Header/HeaderUserIcon/HeaderUserIcon.module.css @@ -1,7 +1,7 @@ .block { cursor: pointer; user-select: none; - img { + & img { display: block; height: 2.5rem; width: 2.5rem; @@ -10,7 +10,7 @@ object-fit: cover; transition: 0.125s all ease-in; } - svg { + & svg { font-size: 1.5rem; margin-left: 0.25rem; color: var(--text3); @@ -20,10 +20,10 @@ display: flex; align-items: center; &:hover { - img { + & img { box-shadow: 0px 0 12px rgba(0, 0, 0, 0.1); } - svg { + & svg { color: var(--text1); } } diff --git a/packages/velog-web/src/components/Header/HeaderUserIcon/HeaderUserIcon.test.tsx b/apps/web/src/components/Header/HeaderUserIcon/HeaderUserIcon.test.tsx similarity index 100% rename from packages/velog-web/src/components/Header/HeaderUserIcon/HeaderUserIcon.test.tsx rename to apps/web/src/components/Header/HeaderUserIcon/HeaderUserIcon.test.tsx diff --git a/packages/velog-web/src/components/Header/HeaderUserIcon/HeaderUserIcon.tsx b/apps/web/src/components/Header/HeaderUserIcon/HeaderUserIcon.tsx similarity index 87% rename from packages/velog-web/src/components/Header/HeaderUserIcon/HeaderUserIcon.tsx rename to apps/web/src/components/Header/HeaderUserIcon/HeaderUserIcon.tsx index 8f94c585..602521c5 100644 --- a/packages/velog-web/src/components/Header/HeaderUserIcon/HeaderUserIcon.tsx +++ b/apps/web/src/components/Header/HeaderUserIcon/HeaderUserIcon.tsx @@ -15,7 +15,7 @@ function HeaderUserIcon({ user, onClick }: Props) { return (
user thumbnail code[class*='language-'], - pre[class*='language-'] { + & :not(pre) > code[class*='language-'], + & pre[class*='language-'] { background: var(--prism-code-block-bg); } /* Inline code */ - :not(pre) > code[class*='language-'] { + & :not(pre) > code[class*='language-'] { padding: 0.1em; border-radius: 0.3em; white-space: normal; @@ -183,12 +183,12 @@ * Github-like theme for Prism.js * @author Luke Askew http://github.com/lukeaskew */ - code, - code[class*='language-'], - pre[class*='language-'] { + & code, + & code[class*='language-'], + & pre[class*='language-'] { color: #24292e; } - pre { + & pre { color: #24292e; background: #f6f8fa; } @@ -248,18 +248,18 @@ * @license MIT 2015 */ - code[class*='language-'], - pre[class*='language-'] { + & code[class*='language-'], + & pre[class*='language-'] { color: #f8f8f2; text-shadow: 0 1px rgba(0, 0, 0, 0.3); } - :not(pre) > code[class*='language-'], - pre[class*='language-'] { + & :not(pre) > code[class*='language-'], + & pre[class*='language-'] { background: #272822; } - pre { + & pre { color: #f8f8f2; text-shadow: 0 1px rgba(0, 0, 0, 0.3); background: #272822; @@ -361,36 +361,36 @@ http://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascr * @license MIT 2016-2018 */ - code[class*='language-'], - pre[class*='language-'] { + & code[class*='language-'], + & pre[class*='language-'] { color: #ccc; background: rgb(40, 41, 54); } - pre[class*='language-']::-moz-selection, - pre[class*='language-'] ::-moz-selection, - code[class*='language-']::-moz-selection, - code[class*='language-'] ::-moz-selection { + & pre[class*='language-']::-moz-selection, + & pre[class*='language-'] ::-moz-selection, + & code[class*='language-']::-moz-selection, + & code[class*='language-'] ::-moz-selection { text-shadow: none; background-color: #5a5f80; } - pre[class*='language-']::selection, - pre[class*='language-'] ::selection, - code[class*='language-']::selection, - code[class*='language-'] ::selection { + & pre[class*='language-']::selection, + & pre[class*='language-'] ::selection, + & code[class*='language-']::selection, + & code[class*='language-'] ::selection { text-shadow: none; background-color: #5a5f80; } /* Inline code */ - :not(pre) > code[class*='language-'] { + & :not(pre) > code[class*='language-'] { border-radius: 0.3em; white-space: normal; } - pre { + & pre { color: #ccc; background: rgb(40, 41, 54); } @@ -695,7 +695,7 @@ http://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascr } } - pre { + & pre { font-family: 'Fira Mono', source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; font-size: 0.875rem; padding: 1rem; @@ -710,7 +710,7 @@ http://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascr } } - img { + & img { max-width: 100%; height: auto; display: block; @@ -718,7 +718,7 @@ http://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascr margin-bottom: 1.5rem; } - iframe { + & iframe { width: 768px; height: 430px; max-width: 100%; @@ -739,31 +739,31 @@ http://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascr padding: none; } - table { + & table { min-width: 40%; max-width: 100%; border: 1px solid var(--border2); border-collapse: collapse; font-size: 0.875rem; - thead > tr > th { + & thead > tr > th { /* text-align: left; */ border-bottom: 4px solid var(--border2); } - th, - td { + & th, + & td { word-break: break-word; padding: 0.5rem; } - td + td, - th + th { + & td + td, + & th + th { border-left: 1px solid var(--border2); } - tr:nth-child(even) { + & tr:nth-child(even) { background: var(--bg-element2); } - tr:nth-child(odd) { + & tr:nth-child(odd) { background: var(--bg-page1); } } diff --git a/apps/web/src/components/MarkdownRender/MarkdownRender.test.tsx b/apps/web/src/components/MarkdownRender/MarkdownRender.test.tsx new file mode 100644 index 00000000..245191d6 --- /dev/null +++ b/apps/web/src/components/MarkdownRender/MarkdownRender.test.tsx @@ -0,0 +1,8 @@ +import MarkdownRender from './MarkdownRender' +import { render } from '@testing-library/react' + +describe('MarkdownRender', () => { + it('renders successfully', () => { + render() + }) +}) diff --git a/packages/velog-web/src/components/MarkdownRender/MarkdownRender.tsx b/apps/web/src/components/MarkdownRender/MarkdownRender.tsx similarity index 89% rename from packages/velog-web/src/components/MarkdownRender/MarkdownRender.tsx rename to apps/web/src/components/MarkdownRender/MarkdownRender.tsx index 19a78339..6ecb09eb 100644 --- a/packages/velog-web/src/components/MarkdownRender/MarkdownRender.tsx +++ b/apps/web/src/components/MarkdownRender/MarkdownRender.tsx @@ -47,15 +47,7 @@ function MarkdownRender({ markdown, codeTheme = 'atom-one', onConvertFinish, isE .use(math) .use(katex) .use(stringify) - .use(rehypeDocument, { - // Get the latest one from: { if (err) { console.log('markdown-render error', err) diff --git a/packages/velog-web/src/components/MarkdownRender/index.tsx b/apps/web/src/components/MarkdownRender/index.tsx similarity index 100% rename from packages/velog-web/src/components/MarkdownRender/index.tsx rename to apps/web/src/components/MarkdownRender/index.tsx diff --git a/packages/velog-web/src/components/MarkdownRender/utils.ts b/apps/web/src/components/MarkdownRender/utils.ts similarity index 100% rename from packages/velog-web/src/components/MarkdownRender/utils.ts rename to apps/web/src/components/MarkdownRender/utils.ts diff --git a/packages/velog-web/src/features/post/components/PostHead/PostHead.module.css b/apps/web/src/components/MarkdownRender2/MarkdownRender2.module.css similarity index 100% rename from packages/velog-web/src/features/post/components/PostHead/PostHead.module.css rename to apps/web/src/components/MarkdownRender2/MarkdownRender2.module.css diff --git a/apps/web/src/components/MarkdownRender2/MarkdownRender2.test.tsx b/apps/web/src/components/MarkdownRender2/MarkdownRender2.test.tsx new file mode 100644 index 00000000..14ace53d --- /dev/null +++ b/apps/web/src/components/MarkdownRender2/MarkdownRender2.test.tsx @@ -0,0 +1,8 @@ +import MarkdownRender2 from './MarkdownRender2' +import { render } from '@testing-library/react' + +describe('MarkdownRender2', () => { + it('renders successfully', () => { + render() + }) +}) diff --git a/apps/web/src/components/MarkdownRender2/MarkdownRender2.tsx b/apps/web/src/components/MarkdownRender2/MarkdownRender2.tsx new file mode 100644 index 00000000..d81e9eb8 --- /dev/null +++ b/apps/web/src/components/MarkdownRender2/MarkdownRender2.tsx @@ -0,0 +1,14 @@ +'use client' + +import styles from './MarkdownRender2.module.css' +import { bindClassNames } from '@/lib/styles/bindClassNames' + +const cx = bindClassNames(styles) + +type Props = {} + +function MarkdownRender2({}: Props) { + return
+} + +export default MarkdownRender2 diff --git a/apps/web/src/components/MarkdownRender2/index.tsx b/apps/web/src/components/MarkdownRender2/index.tsx new file mode 100644 index 00000000..5ac34adf --- /dev/null +++ b/apps/web/src/components/MarkdownRender2/index.tsx @@ -0,0 +1 @@ +export { default } from './MarkdownRender2' diff --git a/packages/velog-web/src/components/Modal/Modal.module.css b/apps/web/src/components/Modal/Modal.module.css similarity index 100% rename from packages/velog-web/src/components/Modal/Modal.module.css rename to apps/web/src/components/Modal/Modal.module.css diff --git a/packages/velog-web/src/components/Modal/Modal.test.tsx b/apps/web/src/components/Modal/Modal.test.tsx similarity index 100% rename from packages/velog-web/src/components/Modal/Modal.test.tsx rename to apps/web/src/components/Modal/Modal.test.tsx diff --git a/packages/velog-web/src/components/Modal/Modal.tsx b/apps/web/src/components/Modal/Modal.tsx similarity index 100% rename from packages/velog-web/src/components/Modal/Modal.tsx rename to apps/web/src/components/Modal/Modal.tsx diff --git a/packages/velog-web/src/components/Modal/index.tsx b/apps/web/src/components/Modal/index.tsx similarity index 100% rename from packages/velog-web/src/components/Modal/index.tsx rename to apps/web/src/components/Modal/index.tsx diff --git a/packages/velog-web/src/components/OpaqueLayer/OpaqueLayer.module.css b/apps/web/src/components/OpaqueLayer/OpaqueLayer.module.css similarity index 100% rename from packages/velog-web/src/components/OpaqueLayer/OpaqueLayer.module.css rename to apps/web/src/components/OpaqueLayer/OpaqueLayer.module.css diff --git a/packages/velog-web/src/components/OpaqueLayer/OpaqueLayer.test.tsx b/apps/web/src/components/OpaqueLayer/OpaqueLayer.test.tsx similarity index 100% rename from packages/velog-web/src/components/OpaqueLayer/OpaqueLayer.test.tsx rename to apps/web/src/components/OpaqueLayer/OpaqueLayer.test.tsx diff --git a/packages/velog-web/src/components/OpaqueLayer/OpaqueLayer.tsx b/apps/web/src/components/OpaqueLayer/OpaqueLayer.tsx similarity index 100% rename from packages/velog-web/src/components/OpaqueLayer/OpaqueLayer.tsx rename to apps/web/src/components/OpaqueLayer/OpaqueLayer.tsx diff --git a/packages/velog-web/src/components/OpaqueLayer/index.tsx b/apps/web/src/components/OpaqueLayer/index.tsx similarity index 100% rename from packages/velog-web/src/components/OpaqueLayer/index.tsx rename to apps/web/src/components/OpaqueLayer/index.tsx diff --git a/packages/velog-web/src/components/PlainLink/PlainLink.test.tsx b/apps/web/src/components/PlainLink/PlainLink.test.tsx similarity index 100% rename from packages/velog-web/src/components/PlainLink/PlainLink.test.tsx rename to apps/web/src/components/PlainLink/PlainLink.test.tsx diff --git a/packages/velog-web/src/components/PlainLink/PlainLink.tsx b/apps/web/src/components/PlainLink/PlainLink.tsx similarity index 100% rename from packages/velog-web/src/components/PlainLink/PlainLink.tsx rename to apps/web/src/components/PlainLink/PlainLink.tsx diff --git a/packages/velog-web/src/components/PlainLink/index.tsx b/apps/web/src/components/PlainLink/index.tsx similarity index 100% rename from packages/velog-web/src/components/PlainLink/index.tsx rename to apps/web/src/components/PlainLink/index.tsx diff --git a/packages/velog-web/src/components/PopupBase/PopupBase.module.css b/apps/web/src/components/PopupBase/PopupBase.module.css similarity index 91% rename from packages/velog-web/src/components/PopupBase/PopupBase.module.css rename to apps/web/src/components/PopupBase/PopupBase.module.css index 75e77391..c3e45f63 100644 --- a/packages/velog-web/src/components/PopupBase/PopupBase.module.css +++ b/apps/web/src/components/PopupBase/PopupBase.module.css @@ -17,7 +17,7 @@ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.09); @media screen and (max-width: 768px) { - width: calc(100% - 2rem); + width: calc(100% - 2rem); } } } diff --git a/packages/velog-web/src/components/PopupBase/PopupBase.test.tsx b/apps/web/src/components/PopupBase/PopupBase.test.tsx similarity index 100% rename from packages/velog-web/src/components/PopupBase/PopupBase.test.tsx rename to apps/web/src/components/PopupBase/PopupBase.test.tsx diff --git a/packages/velog-web/src/components/PopupBase/PopupBase.tsx b/apps/web/src/components/PopupBase/PopupBase.tsx similarity index 100% rename from packages/velog-web/src/components/PopupBase/PopupBase.tsx rename to apps/web/src/components/PopupBase/PopupBase.tsx diff --git a/packages/velog-web/src/components/PopupBase/index.tsx b/apps/web/src/components/PopupBase/index.tsx similarity index 100% rename from packages/velog-web/src/components/PopupBase/index.tsx rename to apps/web/src/components/PopupBase/index.tsx diff --git a/packages/velog-web/src/components/PopupOKCancel/PopupOKCancel.module.css b/apps/web/src/components/PopupOKCancel/PopupOKCancel.module.css similarity index 93% rename from packages/velog-web/src/components/PopupOKCancel/PopupOKCancel.module.css rename to apps/web/src/components/PopupOKCancel/PopupOKCancel.module.css index ccd2902e..a28fef36 100644 --- a/packages/velog-web/src/components/PopupOKCancel/PopupOKCancel.module.css +++ b/apps/web/src/components/PopupOKCancel/PopupOKCancel.module.css @@ -1,5 +1,5 @@ .block { - h3 { + & h3 { margin: 0; font-size: 24px; color: var(--text1); @@ -18,7 +18,7 @@ margin-top: 2rem; display: flex; justify-content: flex-end; - button + button { + & button + button { margin-left: 0.75rem; } } diff --git a/packages/velog-web/src/components/PopupOKCancel/PopupOKCancel.test.tsx b/apps/web/src/components/PopupOKCancel/PopupOKCancel.test.tsx similarity index 100% rename from packages/velog-web/src/components/PopupOKCancel/PopupOKCancel.test.tsx rename to apps/web/src/components/PopupOKCancel/PopupOKCancel.test.tsx diff --git a/packages/velog-web/src/components/PopupOKCancel/PopupOKCancel.tsx b/apps/web/src/components/PopupOKCancel/PopupOKCancel.tsx similarity index 100% rename from packages/velog-web/src/components/PopupOKCancel/PopupOKCancel.tsx rename to apps/web/src/components/PopupOKCancel/PopupOKCancel.tsx diff --git a/packages/velog-web/src/components/PopupOKCancel/index.tsx b/apps/web/src/components/PopupOKCancel/index.tsx similarity index 100% rename from packages/velog-web/src/components/PopupOKCancel/index.tsx rename to apps/web/src/components/PopupOKCancel/index.tsx diff --git a/packages/velog-web/src/components/PrivatePostLabel/PrivatePostLabel.module.css b/apps/web/src/components/PrivatePostLabel/PrivatePostLabel.module.css similarity index 98% rename from packages/velog-web/src/components/PrivatePostLabel/PrivatePostLabel.module.css rename to apps/web/src/components/PrivatePostLabel/PrivatePostLabel.module.css index 39c4ad15..77c9e065 100644 --- a/packages/velog-web/src/components/PrivatePostLabel/PrivatePostLabel.module.css +++ b/apps/web/src/components/PrivatePostLabel/PrivatePostLabel.module.css @@ -12,7 +12,7 @@ 나눔고딕, 'Nanum Gothic', 돋움, Dotum, Tahoma, Geneva, sans-serif; display: inline-flex; align-items: center; - svg { + & svg { margin-right: 8px; width: 14px; height: 14px; diff --git a/packages/velog-web/src/components/PrivatePostLabel/PrivatePostLabel.test.tsx b/apps/web/src/components/PrivatePostLabel/PrivatePostLabel.test.tsx similarity index 100% rename from packages/velog-web/src/components/PrivatePostLabel/PrivatePostLabel.test.tsx rename to apps/web/src/components/PrivatePostLabel/PrivatePostLabel.test.tsx diff --git a/packages/velog-web/src/components/PrivatePostLabel/PrivatePostLabel.tsx b/apps/web/src/components/PrivatePostLabel/PrivatePostLabel.tsx similarity index 85% rename from packages/velog-web/src/components/PrivatePostLabel/PrivatePostLabel.tsx rename to apps/web/src/components/PrivatePostLabel/PrivatePostLabel.tsx index dc6b183e..66d60de4 100644 --- a/packages/velog-web/src/components/PrivatePostLabel/PrivatePostLabel.tsx +++ b/apps/web/src/components/PrivatePostLabel/PrivatePostLabel.tsx @@ -4,9 +4,7 @@ import { bindClassNames } from '@/lib/styles/bindClassNames' const cx = bindClassNames(styles) -type Props = {} - -function PrivatePostLabel({}: Props) { +function PrivatePostLabel() { return (
비공개 diff --git a/packages/velog-web/src/components/PrivatePostLabel/index.tsx b/apps/web/src/components/PrivatePostLabel/index.tsx similarity index 100% rename from packages/velog-web/src/components/PrivatePostLabel/index.tsx rename to apps/web/src/components/PrivatePostLabel/index.tsx diff --git a/packages/velog-web/src/components/RatioImage/RatioImage.module.css b/apps/web/src/components/RatioImage/RatioImage.module.css similarity index 94% rename from packages/velog-web/src/components/RatioImage/RatioImage.module.css rename to apps/web/src/components/RatioImage/RatioImage.module.css index 9ba41832..b35337b8 100644 --- a/packages/velog-web/src/components/RatioImage/RatioImage.module.css +++ b/apps/web/src/components/RatioImage/RatioImage.module.css @@ -1,7 +1,7 @@ .block { width: 100%; position: relative; - img { + & img { position: absolute; top: 0; left: 0; diff --git a/packages/velog-web/src/components/RatioImage/RatioImage.test.tsx b/apps/web/src/components/RatioImage/RatioImage.test.tsx similarity index 100% rename from packages/velog-web/src/components/RatioImage/RatioImage.test.tsx rename to apps/web/src/components/RatioImage/RatioImage.test.tsx diff --git a/packages/velog-web/src/components/RatioImage/RatioImage.tsx b/apps/web/src/components/RatioImage/RatioImage.tsx similarity index 100% rename from packages/velog-web/src/components/RatioImage/RatioImage.tsx rename to apps/web/src/components/RatioImage/RatioImage.tsx diff --git a/packages/velog-web/src/components/RatioImage/index.tsx b/apps/web/src/components/RatioImage/index.tsx similarity index 100% rename from packages/velog-web/src/components/RatioImage/index.tsx rename to apps/web/src/components/RatioImage/index.tsx diff --git a/packages/velog-web/src/components/RequireLogin/RequireLogin.module.css b/apps/web/src/components/RequireLogin/RequireLogin.module.css similarity index 100% rename from packages/velog-web/src/components/RequireLogin/RequireLogin.module.css rename to apps/web/src/components/RequireLogin/RequireLogin.module.css diff --git a/packages/velog-web/src/components/RequireLogin/RequireLogin.test.tsx b/apps/web/src/components/RequireLogin/RequireLogin.test.tsx similarity index 100% rename from packages/velog-web/src/components/RequireLogin/RequireLogin.test.tsx rename to apps/web/src/components/RequireLogin/RequireLogin.test.tsx diff --git a/packages/velog-web/src/components/RequireLogin/RequireLogin.tsx b/apps/web/src/components/RequireLogin/RequireLogin.tsx similarity index 100% rename from packages/velog-web/src/components/RequireLogin/RequireLogin.tsx rename to apps/web/src/components/RequireLogin/RequireLogin.tsx diff --git a/packages/velog-web/src/components/RequireLogin/index.tsx b/apps/web/src/components/RequireLogin/index.tsx similarity index 100% rename from packages/velog-web/src/components/RequireLogin/index.tsx rename to apps/web/src/components/RequireLogin/index.tsx diff --git a/packages/velog-web/src/components/RoundButton/RoundButton.module.css b/apps/web/src/components/RoundButton/RoundButton.module.css similarity index 100% rename from packages/velog-web/src/components/RoundButton/RoundButton.module.css rename to apps/web/src/components/RoundButton/RoundButton.module.css diff --git a/packages/velog-web/src/components/RoundButton/RoundButton.test.tsx b/apps/web/src/components/RoundButton/RoundButton.test.tsx similarity index 100% rename from packages/velog-web/src/components/RoundButton/RoundButton.test.tsx rename to apps/web/src/components/RoundButton/RoundButton.test.tsx diff --git a/packages/velog-web/src/components/RoundButton/RoundButton.tsx b/apps/web/src/components/RoundButton/RoundButton.tsx similarity index 100% rename from packages/velog-web/src/components/RoundButton/RoundButton.tsx rename to apps/web/src/components/RoundButton/RoundButton.tsx diff --git a/packages/velog-web/src/components/RoundButton/index.tsx b/apps/web/src/components/RoundButton/index.tsx similarity index 100% rename from packages/velog-web/src/components/RoundButton/index.tsx rename to apps/web/src/components/RoundButton/index.tsx diff --git a/packages/velog-web/src/components/Skeleton/Skeleton.module.css b/apps/web/src/components/Skeleton/Skeleton.module.css similarity index 54% rename from packages/velog-web/src/components/Skeleton/Skeleton.module.css rename to apps/web/src/components/Skeleton/Skeleton.module.css index 68164483..2ac6d1b4 100644 --- a/packages/velog-web/src/components/Skeleton/Skeleton.module.css +++ b/apps/web/src/components/Skeleton/Skeleton.module.css @@ -1,7 +1,6 @@ .block { - background: var(--bg-element4); - animation: shining 1s ease-in-out infinite; display: inline-block; + background: var(--bg-element4); border-radius: 4px; height: 1em; @@ -14,16 +13,4 @@ &.circle { border-radius: 50%; } - - @keyframes shining { - 0% { - opacity: 0.5; - } - 50% { - opacity: 1; - } - 100% { - opacity: 0.5; - } - } } diff --git a/packages/velog-web/src/components/Skeleton/Skeleton.test.tsx b/apps/web/src/components/Skeleton/Skeleton.test.tsx similarity index 100% rename from packages/velog-web/src/components/Skeleton/Skeleton.test.tsx rename to apps/web/src/components/Skeleton/Skeleton.test.tsx diff --git a/packages/velog-web/src/components/Skeleton/Skeleton.tsx b/apps/web/src/components/Skeleton/Skeleton.tsx similarity index 92% rename from packages/velog-web/src/components/Skeleton/Skeleton.tsx rename to apps/web/src/components/Skeleton/Skeleton.tsx index ea54c61c..bf0eea3f 100644 --- a/packages/velog-web/src/components/Skeleton/Skeleton.tsx +++ b/apps/web/src/components/Skeleton/Skeleton.tsx @@ -26,7 +26,7 @@ function Skeleton({ }: Props) { return (
{alt} *:first-child { margin-top: 0; } - *:last-child { + & > *:last-child { margin-bottom: 0; } } diff --git a/packages/velog-web/src/components/Typography/Typography.test.tsx b/apps/web/src/components/Typography/Typography.test.tsx similarity index 100% rename from packages/velog-web/src/components/Typography/Typography.test.tsx rename to apps/web/src/components/Typography/Typography.test.tsx diff --git a/packages/velog-web/src/components/Typography/Typography.tsx b/apps/web/src/components/Typography/Typography.tsx similarity index 100% rename from packages/velog-web/src/components/Typography/Typography.tsx rename to apps/web/src/components/Typography/Typography.tsx diff --git a/packages/velog-web/src/components/Typography/index.tsx b/apps/web/src/components/Typography/index.tsx similarity index 100% rename from packages/velog-web/src/components/Typography/index.tsx rename to apps/web/src/components/Typography/index.tsx diff --git a/packages/velog-web/src/components/UserProfile/UserProfile.module.css b/apps/web/src/components/UserProfile/UserProfile.module.css similarity index 96% rename from packages/velog-web/src/components/UserProfile/UserProfile.module.css rename to apps/web/src/components/UserProfile/UserProfile.module.css index a818cc07..f2bf57ae 100644 --- a/packages/velog-web/src/components/UserProfile/UserProfile.module.css +++ b/apps/web/src/components/UserProfile/UserProfile.module.css @@ -17,7 +17,7 @@ } } - img { + & img { display: block; border-radius: 50%; object-fit: cover; @@ -41,7 +41,7 @@ font-weight: bold; color: var(--text1); - a { + & a { color: inherit; text-decoration: none; &:hover { @@ -127,7 +127,7 @@ display: flex; flex-direction: row; align-items: center; - svg { + & svg { cursor: pointer; width: 32px; height: 32px; @@ -143,12 +143,12 @@ } } - a { + & a { color: inherit; display: block; } - a + a, - a + svg { + & a + a, + & a + svg { margin-left: 16px; } @@ -156,7 +156,7 @@ padding-left: 0.5rem; padding-right: 0.5rem; - div { + & div { background: var(--gray7); box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.05); padding: 0.5rem; diff --git a/packages/velog-web/src/components/UserProfile/UserProfile.test.tsx b/apps/web/src/components/UserProfile/UserProfile.test.tsx similarity index 100% rename from packages/velog-web/src/components/UserProfile/UserProfile.test.tsx rename to apps/web/src/components/UserProfile/UserProfile.test.tsx diff --git a/packages/velog-web/src/components/UserProfile/UserProfile.tsx b/apps/web/src/components/UserProfile/UserProfile.tsx similarity index 95% rename from packages/velog-web/src/components/UserProfile/UserProfile.tsx rename to apps/web/src/components/UserProfile/UserProfile.tsx index 7e8b3491..5725fe3f 100644 --- a/packages/velog-web/src/components/UserProfile/UserProfile.tsx +++ b/apps/web/src/components/UserProfile/UserProfile.tsx @@ -11,11 +11,14 @@ import { includeProtocol } from '@/lib/includeProtocol' import { MdHome } from 'react-icons/md' import FollowButton from '../FollowButton' import { useAuth } from '@/state/auth' -import { UserProfile as Profile, useGetUserFollowInfoQuery } from '@/graphql/helpers/generated' +import { + UserProfile as Profile, + useGetUserFollowInfoQuery, +} from '@/graphql/server/generated/server' import { useParams } from 'next/navigation' import { getUsernameFromParams } from '@/lib/utils' import { useQueryClient } from '@tanstack/react-query' -import { infiniteGetFollowersQueryKey } from '@/graphql/helpers/queryKey' +import { infiniteGetFollowersQueryKey } from '@/graphql/server/helpers/queryKey' const cx = bindClassNames(styles) @@ -87,7 +90,7 @@ function UserProfile({ style, userId, profile, followersCount, followingsCount }
profile a { display: flex; - position: relative; - - a { - display: flex; - align-items: center; - justify-content: center; - font-size: 18px; - text-decoration: none; - white-space: nowrap; - color: var(--text3); - - &.active { - color: var(--text1); - font-weight: bold; - } + align-items: center; + justify-content: center; + font-size: 18px; + text-decoration: none; + white-space: nowrap; + color: var(--text3); - svg { - font-size: 24px; - margin-right: 8px; - } + &.active { + color: var(--text1); + font-weight: bold; + } - @media screen and (max-width: 768px) { - svg { - font-size: 18px; - } - } + & svg { + font-size: 24px; + margin-right: 8px; + } - @media screen and (max-width: 425px) { - svg { - font-size: 16px; - } + @media screen and (max-width: 768px) { + & svg { + font-size: 18px; } } - a + a { - margin-left: 20px; - @media screen and (max-width: 768px) { - margin-left: 8px; + @media screen and (max-width: 425px) { + & svg { + font-size: 16px; } + } + } - @media screen and (max-width: 425px) { - margin-left: 5px; - } + & a + a { + margin-left: 20px; + @media screen and (max-width: 768px) { + margin-left: 8px; } - .indicator { - position: absolute; - width: 32%; - height: 2px; - background: var(--border1); - bottom: -10px; + @media screen and (max-width: 425px) { + margin-left: 5px; + } + } - @media screen and (max-width: 430px) { - width: 34%; - margin-left: 3px; - } + .indicator { + position: absolute; + width: 32%; + height: 2px; + background: var(--border1); + bottom: -10px; + + @media screen and (max-width: 430px) { + width: 34%; + margin-left: 3px; } } } @@ -115,7 +115,7 @@ width: 70px; } - svg { + & svg { width: 24px; height: 24px; } diff --git a/packages/velog-web/src/features/home/components/HomeTab/HomeTab.test.tsx b/apps/web/src/features/home/components/HomeTab/HomeTab.test.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/HomeTab/HomeTab.test.tsx rename to apps/web/src/features/home/components/HomeTab/HomeTab.test.tsx diff --git a/packages/velog-web/src/features/home/components/HomeTab/HomeTab.tsx b/apps/web/src/features/home/components/HomeTab/HomeTab.tsx similarity index 97% rename from packages/velog-web/src/features/home/components/HomeTab/HomeTab.tsx rename to apps/web/src/features/home/components/HomeTab/HomeTab.tsx index b3ab6bcc..afccefcb 100644 --- a/packages/velog-web/src/features/home/components/HomeTab/HomeTab.tsx +++ b/apps/web/src/features/home/components/HomeTab/HomeTab.tsx @@ -60,7 +60,7 @@ function HomeTab({ isFloatingHeader = false }: Props) { diff --git a/packages/velog-web/src/features/home/components/HomeTab/index.tsx b/apps/web/src/features/home/components/HomeTab/index.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/HomeTab/index.tsx rename to apps/web/src/features/home/components/HomeTab/index.tsx diff --git a/packages/velog-web/src/features/home/components/PostCard/AdPostCard.tsx b/apps/web/src/features/home/components/PostCard/AdPostCard.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/PostCard/AdPostCard.tsx rename to apps/web/src/features/home/components/PostCard/AdPostCard.tsx diff --git a/packages/velog-web/src/features/home/components/PostCard/PostCard.module.css b/apps/web/src/features/home/components/PostCard/PostCard.module.css similarity index 96% rename from packages/velog-web/src/features/home/components/PostCard/PostCard.module.css rename to apps/web/src/features/home/components/PostCard/PostCard.module.css index 5aa0aa2a..d547e4a4 100644 --- a/packages/velog-web/src/features/home/components/PostCard/PostCard.module.css +++ b/apps/web/src/features/home/components/PostCard/PostCard.module.css @@ -16,10 +16,6 @@ } } - &.isRef { - background-color: red; - } - .styleLink { display: block; color: inherit; @@ -48,7 +44,7 @@ flex: 1; } - p { + & p { margin: 0; word-break: break-word; overflow-wrap: break-word; @@ -92,7 +88,7 @@ color: inherit; display: flex; align-items: center; - img { + & img { object-fit: cover; border-radius: 50%; width: 1.5rem; @@ -100,9 +96,9 @@ display: block; margin-right: 0.5rem; } - span { + & span { color: var(--text3); - b { + & b { color: var(--text1); } } @@ -111,7 +107,7 @@ display: flex; align-items: center; color: var(--text1); - svg { + & svg { width: 0.75rem; height: 0.75rem; margin-right: 0.5rem; diff --git a/packages/velog-web/src/features/home/components/PostCard/PostCard.test.tsx b/apps/web/src/features/home/components/PostCard/PostCard.test.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/PostCard/PostCard.test.tsx rename to apps/web/src/features/home/components/PostCard/PostCard.test.tsx diff --git a/packages/velog-web/src/features/home/components/PostCard/PostCard.tsx b/apps/web/src/features/home/components/PostCard/PostCard.tsx similarity index 94% rename from packages/velog-web/src/features/home/components/PostCard/PostCard.tsx rename to apps/web/src/features/home/components/PostCard/PostCard.tsx index 455c00fa..71e395d0 100644 --- a/packages/velog-web/src/features/home/components/PostCard/PostCard.tsx +++ b/apps/web/src/features/home/components/PostCard/PostCard.tsx @@ -7,7 +7,7 @@ import Image from 'next/image' import { LikeIcon } from '@/assets/icons/components' import { useTimeFormat } from '@/hooks/useTimeFormat' import { PostCardSkeleton } from '@/features/home/components/PostCard/PostCardSkeleton' -import { Post } from '@/graphql/helpers/generated' +import { Post } from '@/graphql/server/generated/server' import VLink from '@/components/VLink' import Link from 'next/link' @@ -58,7 +58,7 @@ function PostCard({ post, forHome = false, forPost = false, onClick }: Props) {
{`user - return (
    {posts.map((post, i) => { diff --git a/packages/velog-web/src/features/home/components/PostCardGrid/PostCardSkeletonGrid.tsx b/apps/web/src/features/home/components/PostCardGrid/PostCardSkeletonGrid.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/PostCardGrid/PostCardSkeletonGrid.tsx rename to apps/web/src/features/home/components/PostCardGrid/PostCardSkeletonGrid.tsx diff --git a/packages/velog-web/src/features/home/components/PostCardGrid/index.tsx b/apps/web/src/features/home/components/PostCardGrid/index.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/PostCardGrid/index.tsx rename to apps/web/src/features/home/components/PostCardGrid/index.tsx diff --git a/packages/velog-web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.module.css b/apps/web/src/features/home/components/RecentPosts/RecentPosts.module.css similarity index 100% rename from packages/velog-web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.module.css rename to apps/web/src/features/home/components/RecentPosts/RecentPosts.module.css diff --git a/packages/velog-web/src/features/home/components/RecentPosts/RecentPosts.test.tsx b/apps/web/src/features/home/components/RecentPosts/RecentPosts.test.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/RecentPosts/RecentPosts.test.tsx rename to apps/web/src/features/home/components/RecentPosts/RecentPosts.test.tsx diff --git a/packages/velog-web/src/features/home/components/RecentPosts/RecentPosts.tsx b/apps/web/src/features/home/components/RecentPosts/RecentPosts.tsx similarity index 96% rename from packages/velog-web/src/features/home/components/RecentPosts/RecentPosts.tsx rename to apps/web/src/features/home/components/RecentPosts/RecentPosts.tsx index 8fe1154d..8fb7f2e7 100644 --- a/packages/velog-web/src/features/home/components/RecentPosts/RecentPosts.tsx +++ b/apps/web/src/features/home/components/RecentPosts/RecentPosts.tsx @@ -3,7 +3,7 @@ import PostCardGrid from '@/features/home/components/PostCardGrid/PostCardGrid' import useRecentPosts from '@/features/home/hooks/useRecentPosts' import { useEffect, useRef, useState } from 'react' -import { Post } from '@/graphql/helpers/generated' +import { Post } from '@/graphql/server/generated/server' type Props = { data: Post[] diff --git a/packages/velog-web/src/features/home/components/RecentPosts/index.tsx b/apps/web/src/features/home/components/RecentPosts/index.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/RecentPosts/index.tsx rename to apps/web/src/features/home/components/RecentPosts/index.tsx diff --git a/packages/velog-web/src/features/home/components/TimeframePicker/TimeframePicker.module.css b/apps/web/src/features/home/components/TimeframePicker/TimeframePicker.module.css similarity index 95% rename from packages/velog-web/src/features/home/components/TimeframePicker/TimeframePicker.module.css rename to apps/web/src/features/home/components/TimeframePicker/TimeframePicker.module.css index 49e1775f..251ad400 100644 --- a/packages/velog-web/src/features/home/components/TimeframePicker/TimeframePicker.module.css +++ b/apps/web/src/features/home/components/TimeframePicker/TimeframePicker.module.css @@ -19,16 +19,16 @@ width: 160px; } - ul { + & ul { padding-left: 0; margin: 0; user-select: none; list-style: none; } - li { + & li { cursor: pointer; - a { + & a { display: block; padding: 0.75rem 1rem; text-decoration: none; @@ -48,7 +48,7 @@ } } - li + li { + & li + li { border-top: 1px solid var(--border4); } } diff --git a/packages/velog-web/src/features/home/components/TimeframePicker/TimeframePicker.test.tsx b/apps/web/src/features/home/components/TimeframePicker/TimeframePicker.test.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/TimeframePicker/TimeframePicker.test.tsx rename to apps/web/src/features/home/components/TimeframePicker/TimeframePicker.test.tsx diff --git a/packages/velog-web/src/features/home/components/TimeframePicker/TimeframePicker.tsx b/apps/web/src/features/home/components/TimeframePicker/TimeframePicker.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/TimeframePicker/TimeframePicker.tsx rename to apps/web/src/features/home/components/TimeframePicker/TimeframePicker.tsx diff --git a/packages/velog-web/src/features/home/components/TimeframePicker/index.tsx b/apps/web/src/features/home/components/TimeframePicker/index.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/TimeframePicker/index.tsx rename to apps/web/src/features/home/components/TimeframePicker/index.tsx diff --git a/packages/velog-web/src/features/home/components/TrendingPosts/TrendingPosts.test.tsx b/apps/web/src/features/home/components/TrendingPosts/TrendingPosts.test.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/TrendingPosts/TrendingPosts.test.tsx rename to apps/web/src/features/home/components/TrendingPosts/TrendingPosts.test.tsx diff --git a/packages/velog-web/src/features/home/components/TrendingPosts/TrendingPosts.tsx b/apps/web/src/features/home/components/TrendingPosts/TrendingPosts.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/TrendingPosts/TrendingPosts.tsx rename to apps/web/src/features/home/components/TrendingPosts/TrendingPosts.tsx diff --git a/packages/velog-web/src/features/home/components/TrendingPosts/index.tsx b/apps/web/src/features/home/components/TrendingPosts/index.tsx similarity index 100% rename from packages/velog-web/src/features/home/components/TrendingPosts/index.tsx rename to apps/web/src/features/home/components/TrendingPosts/index.tsx diff --git a/packages/velog-web/src/features/home/hooks/useFeedPosts.ts b/apps/web/src/features/home/hooks/useFeedPosts.ts similarity index 92% rename from packages/velog-web/src/features/home/hooks/useFeedPosts.ts rename to apps/web/src/features/home/hooks/useFeedPosts.ts index 14851665..0a3d051e 100644 --- a/packages/velog-web/src/features/home/hooks/useFeedPosts.ts +++ b/apps/web/src/features/home/hooks/useFeedPosts.ts @@ -4,8 +4,8 @@ import { FeedPostsQuery, FeedPostsQueryVariables, Post, -} from '@/graphql/helpers/generated' -import { infiniteFeedPostsQueryKey } from '@/graphql/helpers/queryKey' +} from '@/graphql/server/generated/server' +import { infiniteFeedPostsQueryKey } from '@/graphql/server/helpers/queryKey' import useCustomInfiniteQuery from '@/hooks/useCustomInfiniteQuery' import { useMemo } from 'react' diff --git a/packages/velog-web/src/features/home/hooks/useRecentPosts.ts b/apps/web/src/features/home/hooks/useRecentPosts.ts similarity index 91% rename from packages/velog-web/src/features/home/hooks/useRecentPosts.ts rename to apps/web/src/features/home/hooks/useRecentPosts.ts index f00155d8..d93d5e52 100644 --- a/packages/velog-web/src/features/home/hooks/useRecentPosts.ts +++ b/apps/web/src/features/home/hooks/useRecentPosts.ts @@ -4,8 +4,8 @@ import { RecentPostsDocument, RecentPostsQuery, RecentPostsQueryVariables, -} from '@/graphql/helpers/generated' -import { infiniteRecentPostsQueryKey } from '@/graphql/helpers/queryKey' +} from '@/graphql/server/generated/server' +import { infiniteRecentPostsQueryKey } from '@/graphql/server/helpers/queryKey' import useCustomInfiniteQuery from '@/hooks/useCustomInfiniteQuery' import { useMemo } from 'react' diff --git a/packages/velog-web/src/features/home/hooks/useTrendingPosts.ts b/apps/web/src/features/home/hooks/useTrendingPosts.ts similarity index 94% rename from packages/velog-web/src/features/home/hooks/useTrendingPosts.ts rename to apps/web/src/features/home/hooks/useTrendingPosts.ts index fe42c9d3..451131f5 100644 --- a/packages/velog-web/src/features/home/hooks/useTrendingPosts.ts +++ b/apps/web/src/features/home/hooks/useTrendingPosts.ts @@ -6,8 +6,8 @@ import { TrendingPostsDocument, TrendingPostsQuery, TrendingPostsQueryVariables, -} from '@/graphql/helpers/generated' -import { infiniteTrendingPostsQueryKey } from '@/graphql/helpers/queryKey' +} from '@/graphql/server/generated/server' +import { infiniteTrendingPostsQueryKey } from '@/graphql/server/helpers/queryKey' import useCustomInfiniteQuery from '@/hooks/useCustomInfiniteQuery' import { useParams } from 'next/navigation' import { useEffect, useMemo, useRef } from 'react' diff --git a/packages/velog-web/src/features/home/interface/post.ts b/apps/web/src/features/home/interface/post.ts similarity index 63% rename from packages/velog-web/src/features/home/interface/post.ts rename to apps/web/src/features/home/interface/post.ts index df9cc501..6e4b01e6 100644 --- a/packages/velog-web/src/features/home/interface/post.ts +++ b/apps/web/src/features/home/interface/post.ts @@ -1,4 +1,4 @@ -import { Post } from '@/graphql/helpers/generated' +import { Post } from '@/graphql/server/generated/server' import { AdsQueryResult } from '@/prefetch/getAds' export type TrendingPost = Post | AdsQueryResult diff --git a/packages/velog-web/src/features/home/state/timeframe.ts b/apps/web/src/features/home/state/timeframe.ts similarity index 100% rename from packages/velog-web/src/features/home/state/timeframe.ts rename to apps/web/src/features/home/state/timeframe.ts diff --git a/packages/velog-web/src/features/home/utils/timeframeMap.ts b/apps/web/src/features/home/utils/timeframeMap.ts similarity index 100% rename from packages/velog-web/src/features/home/utils/timeframeMap.ts rename to apps/web/src/features/home/utils/timeframeMap.ts diff --git a/packages/velog-web/src/features/notification/components/NotificationEmpty/NotificationEmpty.module.css b/apps/web/src/features/notification/components/NotificationEmpty/NotificationEmpty.module.css similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationEmpty/NotificationEmpty.module.css rename to apps/web/src/features/notification/components/NotificationEmpty/NotificationEmpty.module.css diff --git a/packages/velog-web/src/features/notification/components/NotificationEmpty/NotificationEmpty.test.tsx b/apps/web/src/features/notification/components/NotificationEmpty/NotificationEmpty.test.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationEmpty/NotificationEmpty.test.tsx rename to apps/web/src/features/notification/components/NotificationEmpty/NotificationEmpty.test.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationEmpty/NotificationEmpty.tsx b/apps/web/src/features/notification/components/NotificationEmpty/NotificationEmpty.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationEmpty/NotificationEmpty.tsx rename to apps/web/src/features/notification/components/NotificationEmpty/NotificationEmpty.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationEmpty/index.tsx b/apps/web/src/features/notification/components/NotificationEmpty/index.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationEmpty/index.tsx rename to apps/web/src/features/notification/components/NotificationEmpty/index.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.module.css b/apps/web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.module.css similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.module.css rename to apps/web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.module.css diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.test.tsx b/apps/web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.test.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.test.tsx rename to apps/web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.test.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.tsx b/apps/web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.tsx similarity index 96% rename from packages/velog-web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.tsx rename to apps/web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.tsx index 6e6546d7..cc1ac547 100644 --- a/packages/velog-web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.tsx +++ b/apps/web/src/features/notification/components/NotificationItem/CommentActionItem/CommentActionItem.tsx @@ -3,7 +3,7 @@ import styles from './CommentActionItem.module.css' import itemStyles from '../NotificationItem.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' -import { CommentNotificationActionInput } from '@/graphql/helpers/generated' +import { CommentNotificationActionInput } from '@/graphql/server/generated/server' import { NotificationNotMerged } from '@/features/notification/hooks/useNotificationMerge' import Link from 'next/link' import Thumbnail from '@/components/Thumbnail' diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/CommentActionItem/index.tsx b/apps/web/src/features/notification/components/NotificationItem/CommentActionItem/index.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/CommentActionItem/index.tsx rename to apps/web/src/features/notification/components/NotificationItem/CommentActionItem/index.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.module.css b/apps/web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.module.css similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.module.css rename to apps/web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.module.css diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.test.tsx b/apps/web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.test.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.test.tsx rename to apps/web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.test.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.tsx b/apps/web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.tsx similarity index 96% rename from packages/velog-web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.tsx rename to apps/web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.tsx index 52433e79..71f4c753 100644 --- a/packages/velog-web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.tsx +++ b/apps/web/src/features/notification/components/NotificationItem/CommentReplyActionItem/CommentReplyActionItem.tsx @@ -1,6 +1,6 @@ 'use client' -import { CommentReplyNotifictionActionInput } from '@/graphql/helpers/generated' +import { CommentReplyNotifictionActionInput } from '@/graphql/server/generated/server' import itemStyles from '../NotificationItem.module.css' import styles from './CommentReplyActionItem.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/CommentReplyActionItem/index.tsx b/apps/web/src/features/notification/components/NotificationItem/CommentReplyActionItem/index.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/CommentReplyActionItem/index.tsx rename to apps/web/src/features/notification/components/NotificationItem/CommentReplyActionItem/index.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.module.css b/apps/web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.module.css similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.module.css rename to apps/web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.module.css diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.test.tsx b/apps/web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.test.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.test.tsx rename to apps/web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.test.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.tsx b/apps/web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.tsx similarity index 96% rename from packages/velog-web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.tsx rename to apps/web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.tsx index c3ab123f..e28b2632 100644 --- a/packages/velog-web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.tsx +++ b/apps/web/src/features/notification/components/NotificationItem/FollowActionItem/FollowActionItem.tsx @@ -4,7 +4,7 @@ import { NotificationNotMerged } from '@/features/notification/hooks/useNotifica import itemStyles from '../NotificationItem.module.css' import styles from './FollowActionItem.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' -import { FollowNotificationActionInput } from '@/graphql/helpers/generated' +import { FollowNotificationActionInput } from '@/graphql/server/generated/server' import { useTimeFormat } from '@/hooks/useTimeFormat' import Link from 'next/link' import Thumbnail from '@/components/Thumbnail' diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/FollowActionItem/index.tsx b/apps/web/src/features/notification/components/NotificationItem/FollowActionItem/index.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/FollowActionItem/index.tsx rename to apps/web/src/features/notification/components/NotificationItem/FollowActionItem/index.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/NotificationItem.module.css b/apps/web/src/features/notification/components/NotificationItem/NotificationItem.module.css similarity index 97% rename from packages/velog-web/src/features/notification/components/NotificationItem/NotificationItem.module.css rename to apps/web/src/features/notification/components/NotificationItem/NotificationItem.module.css index bc0d43cc..5c70916f 100644 --- a/packages/velog-web/src/features/notification/components/NotificationItem/NotificationItem.module.css +++ b/apps/web/src/features/notification/components/NotificationItem/NotificationItem.module.css @@ -10,7 +10,7 @@ &.isRead { background-color: var(--bg-element7); - p { + & p { color: var(--text3) !important; font-weight: 400 !important; } @@ -30,7 +30,7 @@ height: 32px; } - img { + & img { border-radius: 50%; box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.06); } @@ -48,14 +48,14 @@ font-weight: 400; max-width: 100%; - p { + & p { white-space: pre; word-wrap: break-word; word-break: break-all; color: var(--text2); } - span { + & span { display: inline; } diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/NotificationItem.test.tsx b/apps/web/src/features/notification/components/NotificationItem/NotificationItem.test.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/NotificationItem.test.tsx rename to apps/web/src/features/notification/components/NotificationItem/NotificationItem.test.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/NotificationSkeletonItem.tsx b/apps/web/src/features/notification/components/NotificationItem/NotificationSkeletonItem.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/NotificationSkeletonItem.tsx rename to apps/web/src/features/notification/components/NotificationItem/NotificationSkeletonItem.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.module.css b/apps/web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.module.css similarity index 96% rename from packages/velog-web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.module.css rename to apps/web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.module.css index 4def1fb7..7155c1af 100644 --- a/packages/velog-web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.module.css +++ b/apps/web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.module.css @@ -3,7 +3,7 @@ width: 40px; height: 40px; - a { + & a { display: block; width: 25px; height: 25px; @@ -30,7 +30,7 @@ width: 25px; height: 25px; - img { + & img { border-radius: 50%; box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.06); } diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.test.tsx b/apps/web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.test.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.test.tsx rename to apps/web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.test.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.tsx b/apps/web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.tsx similarity index 98% rename from packages/velog-web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.tsx rename to apps/web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.tsx index 5ec3261c..6ca10f0f 100644 --- a/packages/velog-web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.tsx +++ b/apps/web/src/features/notification/components/NotificationItem/PostLikeActionItem/PostLikeActionItem.tsx @@ -1,6 +1,6 @@ 'use client' -import { PostLikeNotificationActionInput } from '@/graphql/helpers/generated' +import { PostLikeNotificationActionInput } from '@/graphql/server/generated/server' import itemStyles from '../NotificationItem.module.css' import styles from './PostLikeActionItem.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' diff --git a/packages/velog-web/src/features/notification/components/NotificationItem/PostLikeActionItem/index.tsx b/apps/web/src/features/notification/components/NotificationItem/PostLikeActionItem/index.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationItem/PostLikeActionItem/index.tsx rename to apps/web/src/features/notification/components/NotificationItem/PostLikeActionItem/index.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationList/NotificationList.module.css b/apps/web/src/features/notification/components/NotificationList/NotificationList.module.css similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationList/NotificationList.module.css rename to apps/web/src/features/notification/components/NotificationList/NotificationList.module.css diff --git a/packages/velog-web/src/features/notification/components/NotificationList/NotificationList.test.tsx b/apps/web/src/features/notification/components/NotificationList/NotificationList.test.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationList/NotificationList.test.tsx rename to apps/web/src/features/notification/components/NotificationList/NotificationList.test.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationList/NotificationList.tsx b/apps/web/src/features/notification/components/NotificationList/NotificationList.tsx similarity index 98% rename from packages/velog-web/src/features/notification/components/NotificationList/NotificationList.tsx rename to apps/web/src/features/notification/components/NotificationList/NotificationList.tsx index c5b7b3d8..be7c7989 100644 --- a/packages/velog-web/src/features/notification/components/NotificationList/NotificationList.tsx +++ b/apps/web/src/features/notification/components/NotificationList/NotificationList.tsx @@ -1,6 +1,6 @@ 'use client' -import { useCurrentUserQuery, useNotificationQuery } from '@/graphql/helpers/generated' +import { useCurrentUserQuery, useNotificationQuery } from '@/graphql/server/generated/server' import styles from './NotificationList.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' import useNotificationMerge from '../../hooks/useNotificationMerge' diff --git a/packages/velog-web/src/features/notification/components/NotificationList/NotificationSkeletonList.tsx b/apps/web/src/features/notification/components/NotificationList/NotificationSkeletonList.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationList/NotificationSkeletonList.tsx rename to apps/web/src/features/notification/components/NotificationList/NotificationSkeletonList.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationList/index.tsx b/apps/web/src/features/notification/components/NotificationList/index.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationList/index.tsx rename to apps/web/src/features/notification/components/NotificationList/index.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationSelector/NotificationSelector.module.css b/apps/web/src/features/notification/components/NotificationSelector/NotificationSelector.module.css similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationSelector/NotificationSelector.module.css rename to apps/web/src/features/notification/components/NotificationSelector/NotificationSelector.module.css diff --git a/packages/velog-web/src/features/notification/components/NotificationSelector/NotificationSelector.test.tsx b/apps/web/src/features/notification/components/NotificationSelector/NotificationSelector.test.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationSelector/NotificationSelector.test.tsx rename to apps/web/src/features/notification/components/NotificationSelector/NotificationSelector.test.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationSelector/NotificationSelector.tsx b/apps/web/src/features/notification/components/NotificationSelector/NotificationSelector.tsx similarity index 98% rename from packages/velog-web/src/features/notification/components/NotificationSelector/NotificationSelector.tsx rename to apps/web/src/features/notification/components/NotificationSelector/NotificationSelector.tsx index ea08d5ce..ec855cb7 100644 --- a/packages/velog-web/src/features/notification/components/NotificationSelector/NotificationSelector.tsx +++ b/apps/web/src/features/notification/components/NotificationSelector/NotificationSelector.tsx @@ -10,7 +10,7 @@ import { useNotificationQuery, useReadAllNotificationsMutation, useRemoveAllNotificationsMutation, -} from '@/graphql/helpers/generated' +} from '@/graphql/server/generated/server' import PopupOKCancel from '@/components/PopupOKCancel' import { useEffect, useState } from 'react' import { useQueries } from '@tanstack/react-query' diff --git a/packages/velog-web/src/features/notification/components/NotificationSelector/index.tsx b/apps/web/src/features/notification/components/NotificationSelector/index.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationSelector/index.tsx rename to apps/web/src/features/notification/components/NotificationSelector/index.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationTitle/NotificationTitle.module.css b/apps/web/src/features/notification/components/NotificationTitle/NotificationTitle.module.css similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationTitle/NotificationTitle.module.css rename to apps/web/src/features/notification/components/NotificationTitle/NotificationTitle.module.css diff --git a/packages/velog-web/src/features/notification/components/NotificationTitle/NotificationTitle.test.tsx b/apps/web/src/features/notification/components/NotificationTitle/NotificationTitle.test.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationTitle/NotificationTitle.test.tsx rename to apps/web/src/features/notification/components/NotificationTitle/NotificationTitle.test.tsx diff --git a/packages/velog-web/src/features/notification/components/NotificationTitle/NotificationTitle.tsx b/apps/web/src/features/notification/components/NotificationTitle/NotificationTitle.tsx similarity index 94% rename from packages/velog-web/src/features/notification/components/NotificationTitle/NotificationTitle.tsx rename to apps/web/src/features/notification/components/NotificationTitle/NotificationTitle.tsx index 6c182700..7371a4c2 100644 --- a/packages/velog-web/src/features/notification/components/NotificationTitle/NotificationTitle.tsx +++ b/apps/web/src/features/notification/components/NotificationTitle/NotificationTitle.tsx @@ -3,7 +3,7 @@ import { useNotNoticeNotificationCountQuery, useUpdateNotNoticeNotificationMutation, -} from '@/graphql/helpers/generated' +} from '@/graphql/server/generated/server' import styles from './NotificationTitle.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' import { useEffect } from 'react' diff --git a/packages/velog-web/src/features/notification/components/NotificationTitle/index.tsx b/apps/web/src/features/notification/components/NotificationTitle/index.tsx similarity index 100% rename from packages/velog-web/src/features/notification/components/NotificationTitle/index.tsx rename to apps/web/src/features/notification/components/NotificationTitle/index.tsx diff --git a/packages/velog-web/src/features/notification/hooks/useNotificationMerge.tsx b/apps/web/src/features/notification/hooks/useNotificationMerge.tsx similarity index 97% rename from packages/velog-web/src/features/notification/hooks/useNotificationMerge.tsx rename to apps/web/src/features/notification/hooks/useNotificationMerge.tsx index d3cc21f8..b56f59c0 100644 --- a/packages/velog-web/src/features/notification/hooks/useNotificationMerge.tsx +++ b/apps/web/src/features/notification/hooks/useNotificationMerge.tsx @@ -1,4 +1,4 @@ -import { Notification } from '@/graphql/helpers/generated' +import { Notification } from '@/graphql/server/generated/server' import { useMemo } from 'react' export default function useNotificationMerge(notifications: NotificationQueryData[] = []) { diff --git a/packages/velog-web/src/features/notification/hooks/useNotificationToJSX.tsx b/apps/web/src/features/notification/hooks/useNotificationToJSX.tsx similarity index 98% rename from packages/velog-web/src/features/notification/hooks/useNotificationToJSX.tsx rename to apps/web/src/features/notification/hooks/useNotificationToJSX.tsx index 577e6fc6..b60832b5 100644 --- a/packages/velog-web/src/features/notification/hooks/useNotificationToJSX.tsx +++ b/apps/web/src/features/notification/hooks/useNotificationToJSX.tsx @@ -1,4 +1,4 @@ -import { useNotificationQuery, useReadNoticationMutation } from '@/graphql/helpers/generated' +import { useNotificationQuery, useReadNoticationMutation } from '@/graphql/server/generated/server' import { MergedNotifications } from './useNotificationMerge' import { isCommentAction, diff --git a/packages/velog-web/src/features/notification/utils/notificationAction.ts b/apps/web/src/features/notification/utils/notificationAction.ts similarity index 96% rename from packages/velog-web/src/features/notification/utils/notificationAction.ts rename to apps/web/src/features/notification/utils/notificationAction.ts index 564f898f..ab3dbe6b 100644 --- a/packages/velog-web/src/features/notification/utils/notificationAction.ts +++ b/apps/web/src/features/notification/utils/notificationAction.ts @@ -4,7 +4,7 @@ import { FollowNotificationActionInput, NotificationType, PostLikeNotificationActionInput, -} from '@/graphql/helpers/generated' +} from '@/graphql/server/generated/server' import { NotificationMerged, NotificationNotMerged } from '../hooks/useNotificationMerge' export const isFollowAction = ( diff --git a/packages/velog-web/src/features/post/components/PostToc/PostToc.module.css b/apps/web/src/features/post/components/MobileLikeButton/MobileLikeButton.module.css similarity index 100% rename from packages/velog-web/src/features/post/components/PostToc/PostToc.module.css rename to apps/web/src/features/post/components/MobileLikeButton/MobileLikeButton.module.css diff --git a/packages/velog-web/src/features/post/components/MobileLikeButton/MobileLikeButton.test.tsx b/apps/web/src/features/post/components/MobileLikeButton/MobileLikeButton.test.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/MobileLikeButton/MobileLikeButton.test.tsx rename to apps/web/src/features/post/components/MobileLikeButton/MobileLikeButton.test.tsx diff --git a/packages/velog-web/src/features/post/components/MobileLikeButton/MobileLikeButton.tsx b/apps/web/src/features/post/components/MobileLikeButton/MobileLikeButton.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/MobileLikeButton/MobileLikeButton.tsx rename to apps/web/src/features/post/components/MobileLikeButton/MobileLikeButton.tsx diff --git a/packages/velog-web/src/features/post/components/MobileLikeButton/index.tsx b/apps/web/src/features/post/components/MobileLikeButton/index.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/MobileLikeButton/index.tsx rename to apps/web/src/features/post/components/MobileLikeButton/index.tsx diff --git a/packages/velog-web/src/features/post/components/PostViewer/PostViewer.module.css b/apps/web/src/features/post/components/PostContent/PostContent.module.css similarity index 100% rename from packages/velog-web/src/features/post/components/PostViewer/PostViewer.module.css rename to apps/web/src/features/post/components/PostContent/PostContent.module.css diff --git a/packages/velog-web/src/features/post/components/PostContent/PostContent.test.tsx b/apps/web/src/features/post/components/PostContent/PostContent.test.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostContent/PostContent.test.tsx rename to apps/web/src/features/post/components/PostContent/PostContent.test.tsx diff --git a/packages/velog-web/src/features/post/components/PostContent/PostContent.tsx b/apps/web/src/features/post/components/PostContent/PostContent.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostContent/PostContent.tsx rename to apps/web/src/features/post/components/PostContent/PostContent.tsx diff --git a/packages/velog-web/src/features/post/components/PostContent/index.tsx b/apps/web/src/features/post/components/PostContent/index.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostContent/index.tsx rename to apps/web/src/features/post/components/PostContent/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.module.css b/apps/web/src/features/post/components/PostFollowButton/PostFollowButton.module.css similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.module.css rename to apps/web/src/features/post/components/PostFollowButton/PostFollowButton.module.css diff --git a/packages/velog-web/src/features/post/components/PostFollowButton/PostFollowButton.test.tsx b/apps/web/src/features/post/components/PostFollowButton/PostFollowButton.test.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostFollowButton/PostFollowButton.test.tsx rename to apps/web/src/features/post/components/PostFollowButton/PostFollowButton.test.tsx diff --git a/packages/velog-web/src/features/post/components/PostFollowButton/PostFollowButton.tsx b/apps/web/src/features/post/components/PostFollowButton/PostFollowButton.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostFollowButton/PostFollowButton.tsx rename to apps/web/src/features/post/components/PostFollowButton/PostFollowButton.tsx diff --git a/packages/velog-web/src/features/post/components/PostFollowButton/index.tsx b/apps/web/src/features/post/components/PostFollowButton/index.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostFollowButton/index.tsx rename to apps/web/src/features/post/components/PostFollowButton/index.tsx diff --git a/apps/web/src/features/post/components/PostHead/PostHead.module.css b/apps/web/src/features/post/components/PostHead/PostHead.module.css new file mode 100644 index 00000000..b81dae98 --- /dev/null +++ b/apps/web/src/features/post/components/PostHead/PostHead.module.css @@ -0,0 +1,2 @@ +.block { +} diff --git a/packages/velog-web/src/features/post/components/PostHead/PostHead.test.tsx b/apps/web/src/features/post/components/PostHead/PostHead.test.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostHead/PostHead.test.tsx rename to apps/web/src/features/post/components/PostHead/PostHead.test.tsx diff --git a/packages/velog-web/src/features/post/components/PostHead/PostHead.tsx b/apps/web/src/features/post/components/PostHead/PostHead.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostHead/PostHead.tsx rename to apps/web/src/features/post/components/PostHead/PostHead.tsx diff --git a/packages/velog-web/src/features/post/components/PostHead/index.tsx b/apps/web/src/features/post/components/PostHead/index.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostHead/index.tsx rename to apps/web/src/features/post/components/PostHead/index.tsx diff --git a/apps/web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.module.css b/apps/web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.module.css new file mode 100644 index 00000000..b81dae98 --- /dev/null +++ b/apps/web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.module.css @@ -0,0 +1,2 @@ +.block { +} diff --git a/packages/velog-web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.test.tsx b/apps/web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.test.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.test.tsx rename to apps/web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.test.tsx diff --git a/packages/velog-web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.tsx b/apps/web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.tsx rename to apps/web/src/features/post/components/PostLikeShareButtons/PostLikeShareButtons.tsx diff --git a/packages/velog-web/src/features/post/components/PostLikeShareButtons/index.tsx b/apps/web/src/features/post/components/PostLikeShareButtons/index.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostLikeShareButtons/index.tsx rename to apps/web/src/features/post/components/PostLikeShareButtons/index.tsx diff --git a/apps/web/src/features/post/components/PostToc/PostToc.module.css b/apps/web/src/features/post/components/PostToc/PostToc.module.css new file mode 100644 index 00000000..b81dae98 --- /dev/null +++ b/apps/web/src/features/post/components/PostToc/PostToc.module.css @@ -0,0 +1,2 @@ +.block { +} diff --git a/packages/velog-web/src/features/post/components/PostToc/PostToc.test.tsx b/apps/web/src/features/post/components/PostToc/PostToc.test.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostToc/PostToc.test.tsx rename to apps/web/src/features/post/components/PostToc/PostToc.test.tsx diff --git a/packages/velog-web/src/features/post/components/PostToc/PostToc.tsx b/apps/web/src/features/post/components/PostToc/PostToc.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostToc/PostToc.tsx rename to apps/web/src/features/post/components/PostToc/PostToc.tsx diff --git a/packages/velog-web/src/features/post/components/PostToc/index.tsx b/apps/web/src/features/post/components/PostToc/index.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostToc/index.tsx rename to apps/web/src/features/post/components/PostToc/index.tsx diff --git a/apps/web/src/features/post/components/PostViewer/PostViewer.module.css b/apps/web/src/features/post/components/PostViewer/PostViewer.module.css new file mode 100644 index 00000000..b81dae98 --- /dev/null +++ b/apps/web/src/features/post/components/PostViewer/PostViewer.module.css @@ -0,0 +1,2 @@ +.block { +} diff --git a/packages/velog-web/src/features/post/components/PostViewer/PostViewer.test.tsx b/apps/web/src/features/post/components/PostViewer/PostViewer.test.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostViewer/PostViewer.test.tsx rename to apps/web/src/features/post/components/PostViewer/PostViewer.test.tsx diff --git a/packages/velog-web/src/features/post/components/PostViewer/PostViewer.tsx b/apps/web/src/features/post/components/PostViewer/PostViewer.tsx similarity index 83% rename from packages/velog-web/src/features/post/components/PostViewer/PostViewer.tsx rename to apps/web/src/features/post/components/PostViewer/PostViewer.tsx index d996eabc..5cdc0889 100644 --- a/packages/velog-web/src/features/post/components/PostViewer/PostViewer.tsx +++ b/apps/web/src/features/post/components/PostViewer/PostViewer.tsx @@ -1,4 +1,4 @@ -import { Post } from '@/graphql/helpers/generated' +import { Post } from '@/graphql/server/generated/server' import styles from './PostViewer.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' diff --git a/packages/velog-web/src/features/post/components/PostViewer/index.tsx b/apps/web/src/features/post/components/PostViewer/index.tsx similarity index 100% rename from packages/velog-web/src/features/post/components/PostViewer/index.tsx rename to apps/web/src/features/post/components/PostViewer/index.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingEditButton/SettingEditButton.module.css b/apps/web/src/features/setting/components/SettingEditButton/SettingEditButton.module.css similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEditButton/SettingEditButton.module.css rename to apps/web/src/features/setting/components/SettingEditButton/SettingEditButton.module.css diff --git a/packages/velog-web/src/features/setting/components/SettingEditButton/SettingEditButton.test.tsx b/apps/web/src/features/setting/components/SettingEditButton/SettingEditButton.test.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEditButton/SettingEditButton.test.tsx rename to apps/web/src/features/setting/components/SettingEditButton/SettingEditButton.test.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingEditButton/SettingEditButton.tsx b/apps/web/src/features/setting/components/SettingEditButton/SettingEditButton.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEditButton/SettingEditButton.tsx rename to apps/web/src/features/setting/components/SettingEditButton/SettingEditButton.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingEditButton/index.tsx b/apps/web/src/features/setting/components/SettingEditButton/index.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEditButton/index.tsx rename to apps/web/src/features/setting/components/SettingEditButton/index.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingEmailRow/SettingEmailRow.module.css b/apps/web/src/features/setting/components/SettingEmailRow/SettingEmailRow.module.css similarity index 92% rename from packages/velog-web/src/features/setting/components/SettingEmailRow/SettingEmailRow.module.css rename to apps/web/src/features/setting/components/SettingEmailRow/SettingEmailRow.module.css index 69a61f52..158fee49 100644 --- a/packages/velog-web/src/features/setting/components/SettingEmailRow/SettingEmailRow.module.css +++ b/apps/web/src/features/setting/components/SettingEmailRow/SettingEmailRow.module.css @@ -5,7 +5,7 @@ .form { display: flex; align-items: center; - input { + & input { flex: 1; margin-right: 1rem; } diff --git a/packages/velog-web/src/features/setting/components/SettingEmailRow/SettingEmailRow.test.tsx b/apps/web/src/features/setting/components/SettingEmailRow/SettingEmailRow.test.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEmailRow/SettingEmailRow.test.tsx rename to apps/web/src/features/setting/components/SettingEmailRow/SettingEmailRow.test.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingEmailRow/SettingEmailRow.tsx b/apps/web/src/features/setting/components/SettingEmailRow/SettingEmailRow.tsx similarity index 98% rename from packages/velog-web/src/features/setting/components/SettingEmailRow/SettingEmailRow.tsx rename to apps/web/src/features/setting/components/SettingEmailRow/SettingEmailRow.tsx index a13abf84..4731c00b 100644 --- a/packages/velog-web/src/features/setting/components/SettingEmailRow/SettingEmailRow.tsx +++ b/apps/web/src/features/setting/components/SettingEmailRow/SettingEmailRow.tsx @@ -13,7 +13,7 @@ import { toast } from 'react-toastify' import { useCheckEmailExistsQuery, useInitiateChangeEmailMutation, -} from '@/graphql/helpers/generated' +} from '@/graphql/server/generated/server' const cx = bindClassNames(styles) diff --git a/packages/velog-web/src/features/setting/components/SettingEmailRow/index.tsx b/apps/web/src/features/setting/components/SettingEmailRow/index.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEmailRow/index.tsx rename to apps/web/src/features/setting/components/SettingEmailRow/index.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.module.css b/apps/web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.module.css similarity index 94% rename from packages/velog-web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.module.css rename to apps/web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.module.css index abf6c206..bc76312b 100644 --- a/packages/velog-web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.module.css +++ b/apps/web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.module.css @@ -5,7 +5,7 @@ margin: 0; padding: 0; - li { + & li { display: flex; justify-content: space-between; user-select: none; @@ -24,7 +24,7 @@ } } - li + li { + & li + li { margin-top: 8px; } } diff --git a/packages/velog-web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.test.tsx b/apps/web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.test.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.test.tsx rename to apps/web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.test.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.tsx b/apps/web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.tsx similarity index 98% rename from packages/velog-web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.tsx rename to apps/web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.tsx index 18f99f3a..8644edcc 100644 --- a/packages/velog-web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.tsx +++ b/apps/web/src/features/setting/components/SettingEmailRulesRow/SettingEmailRulesRow.tsx @@ -5,7 +5,7 @@ import SettingRow from '../SettingRow' import styles from './SettingEmailRulesRow.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' import { useEffect, useState } from 'react' -import { useCurrentUserQuery, useUpdateEmailRulesMutation } from '@/graphql/helpers/generated' +import { useCurrentUserQuery, useUpdateEmailRulesMutation } from '@/graphql/server/generated/server' const cx = bindClassNames(styles) diff --git a/packages/velog-web/src/features/setting/components/SettingEmailRulesRow/index.tsx b/apps/web/src/features/setting/components/SettingEmailRulesRow/index.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEmailRulesRow/index.tsx rename to apps/web/src/features/setting/components/SettingEmailRulesRow/index.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.module.css b/apps/web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.module.css similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.module.css rename to apps/web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.module.css diff --git a/packages/velog-web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.test.tsx b/apps/web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.test.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.test.tsx rename to apps/web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.test.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.tsx b/apps/web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.tsx rename to apps/web/src/features/setting/components/SettingEmailSuccess/SettingEmailSuccess.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingEmailSuccess/index.tsx b/apps/web/src/features/setting/components/SettingEmailSuccess/index.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingEmailSuccess/index.tsx rename to apps/web/src/features/setting/components/SettingEmailSuccess/index.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingInput/SettingInput.module.css b/apps/web/src/features/setting/components/SettingInput/SettingInput.module.css similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingInput/SettingInput.module.css rename to apps/web/src/features/setting/components/SettingInput/SettingInput.module.css diff --git a/packages/velog-web/src/features/setting/components/SettingInput/SettingInput.test.tsx b/apps/web/src/features/setting/components/SettingInput/SettingInput.test.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingInput/SettingInput.test.tsx rename to apps/web/src/features/setting/components/SettingInput/SettingInput.test.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingInput/SettingInput.tsx b/apps/web/src/features/setting/components/SettingInput/SettingInput.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingInput/SettingInput.tsx rename to apps/web/src/features/setting/components/SettingInput/SettingInput.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingInput/index.tsx b/apps/web/src/features/setting/components/SettingInput/index.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingInput/index.tsx rename to apps/web/src/features/setting/components/SettingInput/index.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingRow/SettingRow.module.css b/apps/web/src/features/setting/components/SettingRow/SettingRow.module.css similarity index 98% rename from packages/velog-web/src/features/setting/components/SettingRow/SettingRow.module.css rename to apps/web/src/features/setting/components/SettingRow/SettingRow.module.css index 07951674..2aa80fd8 100644 --- a/packages/velog-web/src/features/setting/components/SettingRow/SettingRow.module.css +++ b/apps/web/src/features/setting/components/SettingRow/SettingRow.module.css @@ -12,7 +12,7 @@ .titleWrapper { width: 9.5rem; flex-shrink: 0; - h3 { + & h3 { line-height: 1.5; color: var(--text1); margin: 0; diff --git a/packages/velog-web/src/features/setting/components/SettingRow/SettingRow.test.tsx b/apps/web/src/features/setting/components/SettingRow/SettingRow.test.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingRow/SettingRow.test.tsx rename to apps/web/src/features/setting/components/SettingRow/SettingRow.test.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingRow/SettingRow.tsx b/apps/web/src/features/setting/components/SettingRow/SettingRow.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingRow/SettingRow.tsx rename to apps/web/src/features/setting/components/SettingRow/SettingRow.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingRow/index.tsx b/apps/web/src/features/setting/components/SettingRow/index.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingRow/index.tsx rename to apps/web/src/features/setting/components/SettingRow/index.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.module.css b/apps/web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.module.css similarity index 91% rename from packages/velog-web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.module.css rename to apps/web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.module.css index 65d725c9..111e2208 100644 --- a/packages/velog-web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.module.css +++ b/apps/web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.module.css @@ -16,23 +16,23 @@ .infoList { list-style: none; - padding: 0; + padding: 0; margin: 0; - li { + & li { display: flex; align-items: center; - svg { + & svg { width: 1rem; height: 1rem; font-size: 1rem; margin-right: 0.5rem; flex-shrink: 0; } - span { + & span { font-size: 1rem; } } - li + li { + & li + li { margin-top: 1rem; } } @@ -50,11 +50,11 @@ border-radius: 4px; height: 2.25rem; align-items: center; - span { + & span { color: var(--text3); margin-right: 0.25rem; } - input { + & input { padding: 0; border: 0; outline: none; diff --git a/packages/velog-web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.test.tsx b/apps/web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.test.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.test.tsx rename to apps/web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.test.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.tsx b/apps/web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.tsx similarity index 99% rename from packages/velog-web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.tsx rename to apps/web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.tsx index 3296053e..fa7ab6f3 100644 --- a/packages/velog-web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.tsx +++ b/apps/web/src/features/setting/components/SettingSocialInfoRow/SettingSocialInfoRow.tsx @@ -10,7 +10,7 @@ import Button from '@/components/Button' import SettingRow from '../SettingRow' import SettingEditButton from '../SettingEditButton' import useInputs from '@/hooks/useInputs' -import { useCurrentUserQuery, useUpdateSocialInfoMutation } from '@/graphql/helpers/generated' +import { useCurrentUserQuery, useUpdateSocialInfoMutation } from '@/graphql/server/generated/server' const cx = bindClassNames(styles) diff --git a/packages/velog-web/src/features/setting/components/SettingSocialInfoRow/index.tsx b/apps/web/src/features/setting/components/SettingSocialInfoRow/index.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingSocialInfoRow/index.tsx rename to apps/web/src/features/setting/components/SettingSocialInfoRow/index.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingThemeRow/SettingThemeRow.module.css b/apps/web/src/features/setting/components/SettingThemeRow/SettingThemeRow.module.css similarity index 98% rename from packages/velog-web/src/features/setting/components/SettingThemeRow/SettingThemeRow.module.css rename to apps/web/src/features/setting/components/SettingThemeRow/SettingThemeRow.module.css index 5d807042..c210a735 100644 --- a/packages/velog-web/src/features/setting/components/SettingThemeRow/SettingThemeRow.module.css +++ b/apps/web/src/features/setting/components/SettingThemeRow/SettingThemeRow.module.css @@ -42,7 +42,7 @@ .system { display: flex; - div { + & div { width: 50%; height: 100%; } diff --git a/packages/velog-web/src/features/setting/components/SettingThemeRow/SettingThemeRow.test.tsx b/apps/web/src/features/setting/components/SettingThemeRow/SettingThemeRow.test.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingThemeRow/SettingThemeRow.test.tsx rename to apps/web/src/features/setting/components/SettingThemeRow/SettingThemeRow.test.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingThemeRow/SettingThemeRow.tsx b/apps/web/src/features/setting/components/SettingThemeRow/SettingThemeRow.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingThemeRow/SettingThemeRow.tsx rename to apps/web/src/features/setting/components/SettingThemeRow/SettingThemeRow.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingThemeRow/index.tsx b/apps/web/src/features/setting/components/SettingThemeRow/index.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingThemeRow/index.tsx rename to apps/web/src/features/setting/components/SettingThemeRow/index.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingTitleRow/SettingTitleRow.module.css b/apps/web/src/features/setting/components/SettingTitleRow/SettingTitleRow.module.css similarity index 93% rename from packages/velog-web/src/features/setting/components/SettingTitleRow/SettingTitleRow.module.css rename to apps/web/src/features/setting/components/SettingTitleRow/SettingTitleRow.module.css index c70ef7e0..003ec223 100644 --- a/packages/velog-web/src/features/setting/components/SettingTitleRow/SettingTitleRow.module.css +++ b/apps/web/src/features/setting/components/SettingTitleRow/SettingTitleRow.module.css @@ -9,7 +9,7 @@ display: flex; align-items: center; - input { + & input { flex: 1; margin-right: 1rem; } diff --git a/packages/velog-web/src/features/setting/components/SettingTitleRow/SettingTitleRow.test.tsx b/apps/web/src/features/setting/components/SettingTitleRow/SettingTitleRow.test.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingTitleRow/SettingTitleRow.test.tsx rename to apps/web/src/features/setting/components/SettingTitleRow/SettingTitleRow.test.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingTitleRow/SettingTitleRow.tsx b/apps/web/src/features/setting/components/SettingTitleRow/SettingTitleRow.tsx similarity index 97% rename from packages/velog-web/src/features/setting/components/SettingTitleRow/SettingTitleRow.tsx rename to apps/web/src/features/setting/components/SettingTitleRow/SettingTitleRow.tsx index 77a03c38..883b27f7 100644 --- a/packages/velog-web/src/features/setting/components/SettingTitleRow/SettingTitleRow.tsx +++ b/apps/web/src/features/setting/components/SettingTitleRow/SettingTitleRow.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react' import SettingRow from '../SettingRow' import SettingInput from '../SettingInput' import Button from '@/components/Button' -import { useUpdateVelogTitleMutation, useVelogConfigQuery } from '@/graphql/helpers/generated' +import { useUpdateVelogTitleMutation, useVelogConfigQuery } from '@/graphql/server/generated/server' const cx = bindClassNames(styles) diff --git a/packages/velog-web/src/features/setting/components/SettingTitleRow/index.tsx b/apps/web/src/features/setting/components/SettingTitleRow/index.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingTitleRow/index.tsx rename to apps/web/src/features/setting/components/SettingTitleRow/index.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingUnregisterRow/SettingUnregisterRow.test.tsx b/apps/web/src/features/setting/components/SettingUnregisterRow/SettingUnregisterRow.test.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingUnregisterRow/SettingUnregisterRow.test.tsx rename to apps/web/src/features/setting/components/SettingUnregisterRow/SettingUnregisterRow.test.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingUnregisterRow/SettingUnregisterRow.tsx b/apps/web/src/features/setting/components/SettingUnregisterRow/SettingUnregisterRow.tsx similarity index 98% rename from packages/velog-web/src/features/setting/components/SettingUnregisterRow/SettingUnregisterRow.tsx rename to apps/web/src/features/setting/components/SettingUnregisterRow/SettingUnregisterRow.tsx index 5256f497..9d60e5e4 100644 --- a/packages/velog-web/src/features/setting/components/SettingUnregisterRow/SettingUnregisterRow.tsx +++ b/apps/web/src/features/setting/components/SettingUnregisterRow/SettingUnregisterRow.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react' import SettingRow from '../SettingRow' import Button from '@/components/Button' import PopupOKCancel from '@/components/PopupOKCancel' -import { useUnregisterMutation, useUnregisterTokenQuery } from '@/graphql/helpers/generated' +import { useUnregisterMutation, useUnregisterTokenQuery } from '@/graphql/server/generated/server' import { toast } from 'react-toastify' type Props = {} diff --git a/packages/velog-web/src/features/setting/components/SettingUnregisterRow/index.tsx b/apps/web/src/features/setting/components/SettingUnregisterRow/index.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingUnregisterRow/index.tsx rename to apps/web/src/features/setting/components/SettingUnregisterRow/index.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingUserProfile/SettingUserProfile.module.css b/apps/web/src/features/setting/components/SettingUserProfile/SettingUserProfile.module.css similarity index 91% rename from packages/velog-web/src/features/setting/components/SettingUserProfile/SettingUserProfile.module.css rename to apps/web/src/features/setting/components/SettingUserProfile/SettingUserProfile.module.css index d9fef910..6d4ed52a 100644 --- a/packages/velog-web/src/features/setting/components/SettingUserProfile/SettingUserProfile.module.css +++ b/apps/web/src/features/setting/components/SettingUserProfile/SettingUserProfile.module.css @@ -19,18 +19,18 @@ overflow: hidden; } - button + button { + & button + button { margin-top: 0.5rem; margin-left: 0; } @media screen and (max-width: 768px) { - img { + & img { width: 6rem; height: 6rem; margin-bottom: 1rem; } - button { + & button { width: 10rem; } align-items: center; @@ -44,13 +44,13 @@ padding-left: 1.5rem; border-left: 1px solid var(--border4); - h2 { + & h2 { font-size: 2.25rem; margin: 0; line-height: 1.5; color: var(--text1); } - p { + & p { font-size: 1rem; margin-top: 0.25rem; margin-bottom: 0.5rem; @@ -66,10 +66,10 @@ border-bottom: 1px solid var(--border4); border-left: none; padding-left: 0; - h2 { + & h2 { font-size: 1.25rem; } - p { + & p { font-size: 0.875rem; } } @@ -77,7 +77,7 @@ } .form { - input + input { + & input + input { margin-top: 1rem; } .displayName { @@ -93,10 +93,10 @@ .display-name { font-size: 1.25rem; } - input { + & input { font-size: 0.875rem; } - input + input { + & input + input { margin-top: 0.5rem; } .button-wrapper { diff --git a/packages/velog-web/src/features/setting/components/SettingUserProfile/SettingUserProfile.test.tsx b/apps/web/src/features/setting/components/SettingUserProfile/SettingUserProfile.test.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingUserProfile/SettingUserProfile.test.tsx rename to apps/web/src/features/setting/components/SettingUserProfile/SettingUserProfile.test.tsx diff --git a/packages/velog-web/src/features/setting/components/SettingUserProfile/SettingUserProfile.tsx b/apps/web/src/features/setting/components/SettingUserProfile/SettingUserProfile.tsx similarity index 99% rename from packages/velog-web/src/features/setting/components/SettingUserProfile/SettingUserProfile.tsx rename to apps/web/src/features/setting/components/SettingUserProfile/SettingUserProfile.tsx index 013f0916..e868ee38 100644 --- a/packages/velog-web/src/features/setting/components/SettingUserProfile/SettingUserProfile.tsx +++ b/apps/web/src/features/setting/components/SettingUserProfile/SettingUserProfile.tsx @@ -15,7 +15,7 @@ import { useCurrentUserQuery, useUpdateProfileMutation, useUpdateThumbnailMutation, -} from '@/graphql/helpers/generated' +} from '@/graphql/server/generated/server' import JazzbarContext from '@/providers/JazzbarProvider' const cx = bindClassNames(styles) diff --git a/packages/velog-web/src/features/setting/components/SettingUserProfile/index.tsx b/apps/web/src/features/setting/components/SettingUserProfile/index.tsx similarity index 100% rename from packages/velog-web/src/features/setting/components/SettingUserProfile/index.tsx rename to apps/web/src/features/setting/components/SettingUserProfile/index.tsx diff --git a/packages/velog-web/src/features/setting/hooks/useClickTheme.ts b/apps/web/src/features/setting/hooks/useClickTheme.ts similarity index 100% rename from packages/velog-web/src/features/setting/hooks/useClickTheme.ts rename to apps/web/src/features/setting/hooks/useClickTheme.ts diff --git a/packages/velog-web/src/features/setting/hooks/useUpdateThumbnail.ts b/apps/web/src/features/setting/hooks/useUpdateThumbnail.ts similarity index 100% rename from packages/velog-web/src/features/setting/hooks/useUpdateThumbnail.ts rename to apps/web/src/features/setting/hooks/useUpdateThumbnail.ts diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.module.css b/apps/web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.module.css similarity index 99% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.module.css rename to apps/web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.module.css index 58bc84b6..6a4ac8e7 100644 --- a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.module.css +++ b/apps/web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.module.css @@ -17,7 +17,7 @@ .thumbnail { width: 32px; height: 32px; - img { + & img { border-radius: 50%; box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.06); } diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.test.tsx b/apps/web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.test.tsx similarity index 100% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.test.tsx rename to apps/web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.test.tsx diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.tsx b/apps/web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.tsx similarity index 95% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.tsx rename to apps/web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.tsx index a30abc9b..7042ed51 100644 --- a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.tsx +++ b/apps/web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCard.tsx @@ -1,4 +1,4 @@ -import { TrendingWriterPosts } from '@/graphql/helpers/generated' +import { TrendingWriterPosts } from '@/graphql/server/generated/server' import styles from './TrendingWriterCard.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' import Link from 'next/link' diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCardSkeleton.tsx b/apps/web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCardSkeleton.tsx similarity index 100% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCardSkeleton.tsx rename to apps/web/src/features/trendingWriter/components/TrendingWriterCard/TrendingWriterCardSkeleton.tsx diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/index.tsx b/apps/web/src/features/trendingWriter/components/TrendingWriterCard/index.tsx similarity index 100% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterCard/index.tsx rename to apps/web/src/features/trendingWriter/components/TrendingWriterCard/index.tsx diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.module.css b/apps/web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.module.css similarity index 100% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.module.css rename to apps/web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.module.css diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.test.tsx b/apps/web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.test.tsx similarity index 100% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.test.tsx rename to apps/web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.test.tsx diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.tsx b/apps/web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.tsx similarity index 95% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.tsx rename to apps/web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.tsx index 26765e87..c182b182 100644 --- a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.tsx +++ b/apps/web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGrid.tsx @@ -8,7 +8,7 @@ import { useInfiniteScroll } from '@/hooks/useInfiniteScroll' import TrendingWriterGridSkeleton from './TrendingWriterGridSkeleton' import TrendingWriterCardSkeleton from '../TrendingWriterCard/TrendingWriterCardSkeleton' import TrendingWriterCard from '../TrendingWriterCard' -import { TrendingWriter } from '@/graphql/helpers/generated' +import { TrendingWriter } from '@/graphql/server/generated/server' const cx = bindClassNames(styles) diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGridSkeleton.tsx b/apps/web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGridSkeleton.tsx similarity index 100% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGridSkeleton.tsx rename to apps/web/src/features/trendingWriter/components/TrendingWriterGrid/TrendingWriterGridSkeleton.tsx diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterGrid/index.tsx b/apps/web/src/features/trendingWriter/components/TrendingWriterGrid/index.tsx similarity index 100% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterGrid/index.tsx rename to apps/web/src/features/trendingWriter/components/TrendingWriterGrid/index.tsx diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.module.css b/apps/web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.module.css similarity index 100% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.module.css rename to apps/web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.module.css diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.test.tsx b/apps/web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.test.tsx similarity index 100% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.test.tsx rename to apps/web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.test.tsx diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.tsx b/apps/web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.tsx similarity index 100% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.tsx rename to apps/web/src/features/trendingWriter/components/TrendingWriterHeader/TrendingWriterHeader.tsx diff --git a/packages/velog-web/src/features/trendingWriter/components/TrendingWriterHeader/index.tsx b/apps/web/src/features/trendingWriter/components/TrendingWriterHeader/index.tsx similarity index 100% rename from packages/velog-web/src/features/trendingWriter/components/TrendingWriterHeader/index.tsx rename to apps/web/src/features/trendingWriter/components/TrendingWriterHeader/index.tsx diff --git a/packages/velog-web/src/features/trendingWriter/hooks/useTrendingWriter.ts b/apps/web/src/features/trendingWriter/hooks/useTrendingWriter.ts similarity index 92% rename from packages/velog-web/src/features/trendingWriter/hooks/useTrendingWriter.ts rename to apps/web/src/features/trendingWriter/hooks/useTrendingWriter.ts index 5add37bb..adef5082 100644 --- a/packages/velog-web/src/features/trendingWriter/hooks/useTrendingWriter.ts +++ b/apps/web/src/features/trendingWriter/hooks/useTrendingWriter.ts @@ -3,8 +3,8 @@ import { TrendingWritersQuery, TrendingWritersQueryVariables, TrendingWriter, -} from '@/graphql/helpers/generated' -import { infiniteTrendingWritersQueryKey } from '@/graphql/helpers/queryKey' +} from '@/graphql/server/generated/server' +import { infiniteTrendingWritersQueryKey } from '@/graphql/server/helpers/queryKey' import useCustomInfiniteQuery from '@/hooks/useCustomInfiniteQuery' import { useMemo } from 'react' diff --git a/packages/velog-web/src/features/velog/components/MobileSeparator/MobileSeparator.module.css b/apps/web/src/features/velog/components/MobileSeparator/MobileSeparator.module.css similarity index 61% rename from packages/velog-web/src/features/velog/components/MobileSeparator/MobileSeparator.module.css rename to apps/web/src/features/velog/components/MobileSeparator/MobileSeparator.module.css index 60f57b05..734461e3 100644 --- a/packages/velog-web/src/features/velog/components/MobileSeparator/MobileSeparator.module.css +++ b/apps/web/src/features/velog/components/MobileSeparator/MobileSeparator.module.css @@ -3,7 +3,9 @@ height: 16px; margin-top: 32px; - box-shadow: inset 0 8px 8px -8px rgba(0, 0, 0, 0.04), inset 0 -8px 8px -8px rgba(0, 0, 0, 0.04); + box-shadow: + inset 0 8px 8px -8px rgba(0, 0, 0, 0.04), + inset 0 -8px 8px -8px rgba(0, 0, 0, 0.04); display: none; @media screen and (max-width: 768px) { display: block; diff --git a/packages/velog-web/src/features/velog/components/MobileSeparator/MobileSeparator.test.tsx b/apps/web/src/features/velog/components/MobileSeparator/MobileSeparator.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/MobileSeparator/MobileSeparator.test.tsx rename to apps/web/src/features/velog/components/MobileSeparator/MobileSeparator.test.tsx diff --git a/packages/velog-web/src/features/velog/components/MobileSeparator/MobileSeparator.tsx b/apps/web/src/features/velog/components/MobileSeparator/MobileSeparator.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/MobileSeparator/MobileSeparator.tsx rename to apps/web/src/features/velog/components/MobileSeparator/MobileSeparator.tsx diff --git a/packages/velog-web/src/features/velog/components/MobileSeparator/index.tsx b/apps/web/src/features/velog/components/MobileSeparator/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/MobileSeparator/index.tsx rename to apps/web/src/features/velog/components/MobileSeparator/index.tsx diff --git a/packages/velog-web/src/features/velog/components/SeriesItem/SeriesItem.module.css b/apps/web/src/features/velog/components/SeriesItem/SeriesItem.module.css similarity index 98% rename from packages/velog-web/src/features/velog/components/SeriesItem/SeriesItem.module.css rename to apps/web/src/features/velog/components/SeriesItem/SeriesItem.module.css index f46ae050..274f0e5a 100644 --- a/packages/velog-web/src/features/velog/components/SeriesItem/SeriesItem.module.css +++ b/apps/web/src/features/velog/components/SeriesItem/SeriesItem.module.css @@ -1,6 +1,6 @@ .block { width: 100%; - a { + & a { display: block; color: inherit; } @@ -16,7 +16,7 @@ } } - h4 { + & h4 { font-size: 16px; margin-top: 16px; margin-bottom: 8px; diff --git a/packages/velog-web/src/features/velog/components/SeriesItem/SeriesItem.test.tsx b/apps/web/src/features/velog/components/SeriesItem/SeriesItem.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/SeriesItem/SeriesItem.test.tsx rename to apps/web/src/features/velog/components/SeriesItem/SeriesItem.test.tsx diff --git a/packages/velog-web/src/features/velog/components/SeriesItem/SeriesItem.tsx b/apps/web/src/features/velog/components/SeriesItem/SeriesItem.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/SeriesItem/SeriesItem.tsx rename to apps/web/src/features/velog/components/SeriesItem/SeriesItem.tsx diff --git a/packages/velog-web/src/features/velog/components/SeriesItem/SeriesItemSkeleton.tsx b/apps/web/src/features/velog/components/SeriesItem/SeriesItemSkeleton.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/SeriesItem/SeriesItemSkeleton.tsx rename to apps/web/src/features/velog/components/SeriesItem/SeriesItemSkeleton.tsx diff --git a/packages/velog-web/src/features/velog/components/SeriesItem/index.tsx b/apps/web/src/features/velog/components/SeriesItem/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/SeriesItem/index.tsx rename to apps/web/src/features/velog/components/SeriesItem/index.tsx diff --git a/packages/velog-web/src/features/velog/components/SeriesList/SeriesList.module.css b/apps/web/src/features/velog/components/SeriesList/SeriesList.module.css similarity index 98% rename from packages/velog-web/src/features/velog/components/SeriesList/SeriesList.module.css rename to apps/web/src/features/velog/components/SeriesList/SeriesList.module.css index 65a55722..c36f9b78 100644 --- a/packages/velog-web/src/features/velog/components/SeriesList/SeriesList.module.css +++ b/apps/web/src/features/velog/components/SeriesList/SeriesList.module.css @@ -22,7 +22,7 @@ flex-direction: column; grid-template-columns: repeat(1, 100%); - svg { + & svg { width: 320px; height: 320px; margin-bottom: 32px; diff --git a/packages/velog-web/src/features/velog/components/SeriesList/SeriesList.test.tsx b/apps/web/src/features/velog/components/SeriesList/SeriesList.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/SeriesList/SeriesList.test.tsx rename to apps/web/src/features/velog/components/SeriesList/SeriesList.test.tsx diff --git a/packages/velog-web/src/features/velog/components/SeriesList/SeriesList.tsx b/apps/web/src/features/velog/components/SeriesList/SeriesList.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/SeriesList/SeriesList.tsx rename to apps/web/src/features/velog/components/SeriesList/SeriesList.tsx diff --git a/packages/velog-web/src/features/velog/components/SeriesList/SeriesListSkeleton.tsx b/apps/web/src/features/velog/components/SeriesList/SeriesListSkeleton.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/SeriesList/SeriesListSkeleton.tsx rename to apps/web/src/features/velog/components/SeriesList/SeriesListSkeleton.tsx diff --git a/packages/velog-web/src/features/velog/components/SeriesList/index.tsx b/apps/web/src/features/velog/components/SeriesList/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/SeriesList/index.tsx rename to apps/web/src/features/velog/components/SeriesList/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAbout/VelogAbout.module.css b/apps/web/src/features/velog/components/VelogAbout/VelogAbout.module.css similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAbout/VelogAbout.module.css rename to apps/web/src/features/velog/components/VelogAbout/VelogAbout.module.css diff --git a/packages/velog-web/src/features/velog/components/VelogAbout/VelogAbout.test.tsx b/apps/web/src/features/velog/components/VelogAbout/VelogAbout.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAbout/VelogAbout.test.tsx rename to apps/web/src/features/velog/components/VelogAbout/VelogAbout.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAbout/VelogAbout.tsx b/apps/web/src/features/velog/components/VelogAbout/VelogAbout.tsx similarity index 97% rename from packages/velog-web/src/features/velog/components/VelogAbout/VelogAbout.tsx rename to apps/web/src/features/velog/components/VelogAbout/VelogAbout.tsx index 6cccc330..4a092ced 100644 --- a/packages/velog-web/src/features/velog/components/VelogAbout/VelogAbout.tsx +++ b/apps/web/src/features/velog/components/VelogAbout/VelogAbout.tsx @@ -4,7 +4,7 @@ import { useCurrentUserQuery, useGetUserAboutQuery, useUpdateAboutMutation, -} from '@/graphql/helpers/generated' +} from '@/graphql/server/generated/server' import styles from './VelogAbout.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' import { VelogAboutContent, VelogAboutContentSkeleton } from '../VelogAboutContent' diff --git a/packages/velog-web/src/features/velog/components/VelogAbout/index.tsx b/apps/web/src/features/velog/components/VelogAbout/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAbout/index.tsx rename to apps/web/src/features/velog/components/VelogAbout/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAboutButton/VelogAboutButton.module.css b/apps/web/src/features/velog/components/VelogAboutButton/VelogAboutButton.module.css similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutButton/VelogAboutButton.module.css rename to apps/web/src/features/velog/components/VelogAboutButton/VelogAboutButton.module.css diff --git a/packages/velog-web/src/features/velog/components/VelogAboutButton/VelogAboutButton.test.tsx b/apps/web/src/features/velog/components/VelogAboutButton/VelogAboutButton.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutButton/VelogAboutButton.test.tsx rename to apps/web/src/features/velog/components/VelogAboutButton/VelogAboutButton.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAboutButton/VelogAboutButton.tsx b/apps/web/src/features/velog/components/VelogAboutButton/VelogAboutButton.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutButton/VelogAboutButton.tsx rename to apps/web/src/features/velog/components/VelogAboutButton/VelogAboutButton.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAboutButton/index.tsx b/apps/web/src/features/velog/components/VelogAboutButton/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutButton/index.tsx rename to apps/web/src/features/velog/components/VelogAboutButton/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAboutContent/VelogAboutContent.module.css b/apps/web/src/features/velog/components/VelogAboutContent/VelogAboutContent.module.css similarity index 97% rename from packages/velog-web/src/features/velog/components/VelogAboutContent/VelogAboutContent.module.css rename to apps/web/src/features/velog/components/VelogAboutContent/VelogAboutContent.module.css index 3fdbe78d..8571d61f 100644 --- a/packages/velog-web/src/features/velog/components/VelogAboutContent/VelogAboutContent.module.css +++ b/apps/web/src/features/velog/components/VelogAboutContent/VelogAboutContent.module.css @@ -6,7 +6,7 @@ justify-content: center; align-items: center; flex-direction: column; - svg { + & svg { width: 320px; height: 320px; margin-bottom: 32px; @@ -31,7 +31,7 @@ padding-left: 16px; padding-right: 16px; } - h1 { + & h1 { font-size: 48px; margin-bottom: 32px; diff --git a/packages/velog-web/src/features/velog/components/VelogAboutContent/VelogAboutContent.test.tsx b/apps/web/src/features/velog/components/VelogAboutContent/VelogAboutContent.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutContent/VelogAboutContent.test.tsx rename to apps/web/src/features/velog/components/VelogAboutContent/VelogAboutContent.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAboutContent/VelogAboutContent.tsx b/apps/web/src/features/velog/components/VelogAboutContent/VelogAboutContent.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutContent/VelogAboutContent.tsx rename to apps/web/src/features/velog/components/VelogAboutContent/VelogAboutContent.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAboutContent/VelogAboutContentSkeleton.tsx b/apps/web/src/features/velog/components/VelogAboutContent/VelogAboutContentSkeleton.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutContent/VelogAboutContentSkeleton.tsx rename to apps/web/src/features/velog/components/VelogAboutContent/VelogAboutContentSkeleton.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAboutContent/index.tsx b/apps/web/src/features/velog/components/VelogAboutContent/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutContent/index.tsx rename to apps/web/src/features/velog/components/VelogAboutContent/index.tsx diff --git a/apps/web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.module.css b/apps/web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.module.css new file mode 100644 index 00000000..b81dae98 --- /dev/null +++ b/apps/web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.module.css @@ -0,0 +1,2 @@ +.block { +} diff --git a/packages/velog-web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.test.tsx b/apps/web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.test.tsx rename to apps/web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.tsx b/apps/web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.tsx rename to apps/web/src/features/velog/components/VelogAboutEdit/VelogAboutEdit.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogAboutEdit/index.tsx b/apps/web/src/features/velog/components/VelogAboutEdit/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogAboutEdit/index.tsx rename to apps/web/src/features/velog/components/VelogAboutEdit/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowItem/VelogFollowItem.module.css b/apps/web/src/features/velog/components/VelogFollowItem/VelogFollowItem.module.css similarity index 99% rename from packages/velog-web/src/features/velog/components/VelogFollowItem/VelogFollowItem.module.css rename to apps/web/src/features/velog/components/VelogFollowItem/VelogFollowItem.module.css index 1313a00c..b77276c3 100644 --- a/packages/velog-web/src/features/velog/components/VelogFollowItem/VelogFollowItem.module.css +++ b/apps/web/src/features/velog/components/VelogFollowItem/VelogFollowItem.module.css @@ -15,7 +15,7 @@ height: 32px; } - img { + & img { border-radius: 50%; box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.06); } diff --git a/packages/velog-web/src/features/velog/components/VelogFollowItem/VelogFollowItem.test.tsx b/apps/web/src/features/velog/components/VelogFollowItem/VelogFollowItem.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowItem/VelogFollowItem.test.tsx rename to apps/web/src/features/velog/components/VelogFollowItem/VelogFollowItem.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowItem/VelogFollowItem.tsx b/apps/web/src/features/velog/components/VelogFollowItem/VelogFollowItem.tsx similarity index 95% rename from packages/velog-web/src/features/velog/components/VelogFollowItem/VelogFollowItem.tsx rename to apps/web/src/features/velog/components/VelogFollowItem/VelogFollowItem.tsx index c5713361..0131fbf5 100644 --- a/packages/velog-web/src/features/velog/components/VelogFollowItem/VelogFollowItem.tsx +++ b/apps/web/src/features/velog/components/VelogFollowItem/VelogFollowItem.tsx @@ -7,7 +7,7 @@ import FollowButton from '@/components/FollowButton' import Link from 'next/link' import { Noto_Sans_KR } from 'next/font/google' import { useQueryClient } from '@tanstack/react-query' -import { useGetUserFollowInfoQuery } from '@/graphql/helpers/generated' +import { useGetUserFollowInfoQuery } from '@/graphql/server/generated/server' import { useParams } from 'next/navigation' import { getUsernameFromParams } from '@/lib/utils' diff --git a/packages/velog-web/src/features/velog/components/VelogFollowItem/VelogFollowItemSkeleton.tsx b/apps/web/src/features/velog/components/VelogFollowItem/VelogFollowItemSkeleton.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowItem/VelogFollowItemSkeleton.tsx rename to apps/web/src/features/velog/components/VelogFollowItem/VelogFollowItemSkeleton.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowItem/index.tsx b/apps/web/src/features/velog/components/VelogFollowItem/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowItem/index.tsx rename to apps/web/src/features/velog/components/VelogFollowItem/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowList/VelogFollowList.module.css b/apps/web/src/features/velog/components/VelogFollowList/VelogFollowList.module.css similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowList/VelogFollowList.module.css rename to apps/web/src/features/velog/components/VelogFollowList/VelogFollowList.module.css diff --git a/packages/velog-web/src/features/velog/components/VelogFollowList/VelogFollowList.test.tsx b/apps/web/src/features/velog/components/VelogFollowList/VelogFollowList.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowList/VelogFollowList.test.tsx rename to apps/web/src/features/velog/components/VelogFollowList/VelogFollowList.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowList/VelogFollowList.tsx b/apps/web/src/features/velog/components/VelogFollowList/VelogFollowList.tsx similarity index 92% rename from packages/velog-web/src/features/velog/components/VelogFollowList/VelogFollowList.tsx rename to apps/web/src/features/velog/components/VelogFollowList/VelogFollowList.tsx index 014ce45d..3f57bacd 100644 --- a/packages/velog-web/src/features/velog/components/VelogFollowList/VelogFollowList.tsx +++ b/apps/web/src/features/velog/components/VelogFollowList/VelogFollowList.tsx @@ -1,4 +1,4 @@ -import { FollowResult } from '@/graphql/helpers/generated' +import { FollowResult } from '@/graphql/server/generated/server' import styles from './VelogFollowList.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' import { VelogFollowItem } from '../VelogFollowItem' diff --git a/packages/velog-web/src/features/velog/components/VelogFollowList/VelogFollowListSkeleton.tsx b/apps/web/src/features/velog/components/VelogFollowList/VelogFollowListSkeleton.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowList/VelogFollowListSkeleton.tsx rename to apps/web/src/features/velog/components/VelogFollowList/VelogFollowListSkeleton.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowList/index.tsx b/apps/web/src/features/velog/components/VelogFollowList/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowList/index.tsx rename to apps/web/src/features/velog/components/VelogFollowList/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowStats/VelogFollowStats.module.css b/apps/web/src/features/velog/components/VelogFollowStats/VelogFollowStats.module.css similarity index 94% rename from packages/velog-web/src/features/velog/components/VelogFollowStats/VelogFollowStats.module.css rename to apps/web/src/features/velog/components/VelogFollowStats/VelogFollowStats.module.css index 9237496f..1baf03e4 100644 --- a/packages/velog-web/src/features/velog/components/VelogFollowStats/VelogFollowStats.module.css +++ b/apps/web/src/features/velog/components/VelogFollowStats/VelogFollowStats.module.css @@ -14,7 +14,7 @@ .thumbnail { width: 32px; height: 32px; - img { + & img { border-radius: 50%; box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.06); } @@ -36,6 +36,7 @@ .count { margin-top: 16px; font-size: 32px; + color: var(--text2); @media screen and (max-width: 768px) { font-size: 24px; diff --git a/packages/velog-web/src/features/velog/components/VelogFollowStats/VelogFollowStats.test.tsx b/apps/web/src/features/velog/components/VelogFollowStats/VelogFollowStats.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowStats/VelogFollowStats.test.tsx rename to apps/web/src/features/velog/components/VelogFollowStats/VelogFollowStats.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowStats/VelogFollowStats.tsx b/apps/web/src/features/velog/components/VelogFollowStats/VelogFollowStats.tsx similarity index 96% rename from packages/velog-web/src/features/velog/components/VelogFollowStats/VelogFollowStats.tsx rename to apps/web/src/features/velog/components/VelogFollowStats/VelogFollowStats.tsx index d5505280..0529014f 100644 --- a/packages/velog-web/src/features/velog/components/VelogFollowStats/VelogFollowStats.tsx +++ b/apps/web/src/features/velog/components/VelogFollowStats/VelogFollowStats.tsx @@ -3,7 +3,7 @@ import Thumbnail from '@/components/Thumbnail' import styles from './VelogFollowStats.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' -import { useGetUserFollowInfoQuery } from '@/graphql/helpers/generated' +import { useGetUserFollowInfoQuery } from '@/graphql/server/generated/server' import Link from 'next/link' import { useEffect, useState } from 'react' diff --git a/packages/velog-web/src/features/velog/components/VelogFollowStats/index.tsx b/apps/web/src/features/velog/components/VelogFollowStats/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowStats/index.tsx rename to apps/web/src/features/velog/components/VelogFollowStats/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowers/VelogFollowers.test.tsx b/apps/web/src/features/velog/components/VelogFollowers/VelogFollowers.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowers/VelogFollowers.test.tsx rename to apps/web/src/features/velog/components/VelogFollowers/VelogFollowers.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowers/VelogFollowers.tsx b/apps/web/src/features/velog/components/VelogFollowers/VelogFollowers.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowers/VelogFollowers.tsx rename to apps/web/src/features/velog/components/VelogFollowers/VelogFollowers.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowers/VelogFollowersEmpty.module.css b/apps/web/src/features/velog/components/VelogFollowers/VelogFollowersEmpty.module.css similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowers/VelogFollowersEmpty.module.css rename to apps/web/src/features/velog/components/VelogFollowers/VelogFollowersEmpty.module.css diff --git a/packages/velog-web/src/features/velog/components/VelogFollowers/VelogFollowersEmpty.tsx b/apps/web/src/features/velog/components/VelogFollowers/VelogFollowersEmpty.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowers/VelogFollowersEmpty.tsx rename to apps/web/src/features/velog/components/VelogFollowers/VelogFollowersEmpty.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowers/index.tsx b/apps/web/src/features/velog/components/VelogFollowers/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowers/index.tsx rename to apps/web/src/features/velog/components/VelogFollowers/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowings/VelogFollowings.test.tsx b/apps/web/src/features/velog/components/VelogFollowings/VelogFollowings.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowings/VelogFollowings.test.tsx rename to apps/web/src/features/velog/components/VelogFollowings/VelogFollowings.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowings/VelogFollowings.tsx b/apps/web/src/features/velog/components/VelogFollowings/VelogFollowings.tsx similarity index 92% rename from packages/velog-web/src/features/velog/components/VelogFollowings/VelogFollowings.tsx rename to apps/web/src/features/velog/components/VelogFollowings/VelogFollowings.tsx index 7040796c..9c937ac9 100644 --- a/packages/velog-web/src/features/velog/components/VelogFollowings/VelogFollowings.tsx +++ b/apps/web/src/features/velog/components/VelogFollowings/VelogFollowings.tsx @@ -16,7 +16,7 @@ function VelogFollowings({ username }: Props) { useInfiniteScroll(ref, fetchMore) - if (isLoading || isFetching) return + if (isLoading) return if (followings.length === 0) return return ( <> diff --git a/packages/velog-web/src/features/velog/components/VelogFollowings/VelogFollowingsEmpty.module.css b/apps/web/src/features/velog/components/VelogFollowings/VelogFollowingsEmpty.module.css similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowings/VelogFollowingsEmpty.module.css rename to apps/web/src/features/velog/components/VelogFollowings/VelogFollowingsEmpty.module.css diff --git a/packages/velog-web/src/features/velog/components/VelogFollowings/VelogFollowingsEmpty.tsx b/apps/web/src/features/velog/components/VelogFollowings/VelogFollowingsEmpty.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowings/VelogFollowingsEmpty.tsx rename to apps/web/src/features/velog/components/VelogFollowings/VelogFollowingsEmpty.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogFollowings/index.tsx b/apps/web/src/features/velog/components/VelogFollowings/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogFollowings/index.tsx rename to apps/web/src/features/velog/components/VelogFollowings/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogPosts/VelogPosts.module.css b/apps/web/src/features/velog/components/VelogPosts/VelogPosts.module.css similarity index 97% rename from packages/velog-web/src/features/velog/components/VelogPosts/VelogPosts.module.css rename to apps/web/src/features/velog/components/VelogPosts/VelogPosts.module.css index e18cc32d..91102313 100644 --- a/packages/velog-web/src/features/velog/components/VelogPosts/VelogPosts.module.css +++ b/apps/web/src/features/velog/components/VelogPosts/VelogPosts.module.css @@ -14,7 +14,7 @@ justify-content: center; align-items: center; flex-direction: column; - svg { + & svg { display: block; width: 320px; height: 320px; diff --git a/packages/velog-web/src/features/velog/components/VelogPosts/VelogPosts.test.tsx b/apps/web/src/features/velog/components/VelogPosts/VelogPosts.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogPosts/VelogPosts.test.tsx rename to apps/web/src/features/velog/components/VelogPosts/VelogPosts.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogPosts/VelogPosts.tsx b/apps/web/src/features/velog/components/VelogPosts/VelogPosts.tsx similarity index 95% rename from packages/velog-web/src/features/velog/components/VelogPosts/VelogPosts.tsx rename to apps/web/src/features/velog/components/VelogPosts/VelogPosts.tsx index 6e31dbcc..ecacb68a 100644 --- a/packages/velog-web/src/features/velog/components/VelogPosts/VelogPosts.tsx +++ b/apps/web/src/features/velog/components/VelogPosts/VelogPosts.tsx @@ -8,7 +8,7 @@ import { useInfiniteScroll } from '@/hooks/useInfiniteScroll' import { FlatPostCardList, FlatPostCardListSkeleton } from '@/components/FlatPost/FlatPostCardList' import { UndrawBlankCanvas } from '@/assets/vectors/components' import VelogTag from '../VelogTag' -import { Post, UserTags } from '@/graphql/helpers/generated' +import { Post, UserTags } from '@/graphql/server/generated/server' const cx = bindClassNames(styles) diff --git a/packages/velog-web/src/features/velog/components/VelogPosts/index.tsx b/apps/web/src/features/velog/components/VelogPosts/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogPosts/index.tsx rename to apps/web/src/features/velog/components/VelogPosts/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogSearchInput/VelogSearchInput.module.css b/apps/web/src/features/velog/components/VelogSearchInput/VelogSearchInput.module.css similarity index 96% rename from packages/velog-web/src/features/velog/components/VelogSearchInput/VelogSearchInput.module.css rename to apps/web/src/features/velog/components/VelogSearchInput/VelogSearchInput.module.css index 043c8733..ea4d683a 100644 --- a/packages/velog-web/src/features/velog/components/VelogSearchInput/VelogSearchInput.module.css +++ b/apps/web/src/features/velog/components/VelogSearchInput/VelogSearchInput.module.css @@ -24,7 +24,7 @@ padding-right: 8px; } - svg { + & svg { width: 16px; height: 16px; transition: all 0.125s ease-in; @@ -37,7 +37,7 @@ } } - input { + & input { transition: all 0.125s ease-in; font-size: 14px; flex: 1; @@ -62,10 +62,10 @@ .focus { border: 1px solid var(--border1); - svg { + & svg { fill: var(--gray9); } - input { + & input { color: var(--text1); } } diff --git a/packages/velog-web/src/features/velog/components/VelogSearchInput/VelogSearchInput.test.tsx b/apps/web/src/features/velog/components/VelogSearchInput/VelogSearchInput.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogSearchInput/VelogSearchInput.test.tsx rename to apps/web/src/features/velog/components/VelogSearchInput/VelogSearchInput.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogSearchInput/VelogSearchInput.tsx b/apps/web/src/features/velog/components/VelogSearchInput/VelogSearchInput.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogSearchInput/VelogSearchInput.tsx rename to apps/web/src/features/velog/components/VelogSearchInput/VelogSearchInput.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogSearchInput/index.tsx b/apps/web/src/features/velog/components/VelogSearchInput/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogSearchInput/index.tsx rename to apps/web/src/features/velog/components/VelogSearchInput/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.module.css b/apps/web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.module.css similarity index 96% rename from packages/velog-web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.module.css rename to apps/web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.module.css index 9ba75090..5a57991a 100644 --- a/packages/velog-web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.module.css +++ b/apps/web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.module.css @@ -11,7 +11,7 @@ margin-bottom: 1rem; } - b { + & b { color: var(--text1); } } diff --git a/packages/velog-web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.test.tsx b/apps/web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.test.tsx rename to apps/web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.tsx b/apps/web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.tsx similarity index 95% rename from packages/velog-web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.tsx rename to apps/web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.tsx index 688234c5..27b19b5e 100644 --- a/packages/velog-web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.tsx +++ b/apps/web/src/features/velog/components/VelogSearchPosts/VelogSearchPosts.tsx @@ -1,6 +1,6 @@ 'use client' -import { UserTags } from '@/graphql/helpers/generated' +import { UserTags } from '@/graphql/server/generated/server' import VelogTag from '../VelogTag' import styles from './VelogSearchPosts.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' diff --git a/packages/velog-web/src/features/velog/components/VelogSearchPosts/index.tsx b/apps/web/src/features/velog/components/VelogSearchPosts/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogSearchPosts/index.tsx rename to apps/web/src/features/velog/components/VelogSearchPosts/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogSeries/VelogSeries.test.tsx b/apps/web/src/features/velog/components/VelogSeries/VelogSeries.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogSeries/VelogSeries.test.tsx rename to apps/web/src/features/velog/components/VelogSeries/VelogSeries.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogSeries/VelogSeries.tsx b/apps/web/src/features/velog/components/VelogSeries/VelogSeries.tsx similarity index 88% rename from packages/velog-web/src/features/velog/components/VelogSeries/VelogSeries.tsx rename to apps/web/src/features/velog/components/VelogSeries/VelogSeries.tsx index c603f7c4..0d27ced8 100644 --- a/packages/velog-web/src/features/velog/components/VelogSeries/VelogSeries.tsx +++ b/apps/web/src/features/velog/components/VelogSeries/VelogSeries.tsx @@ -1,6 +1,6 @@ 'use client' -import { useGetUserSeriesListQuery } from '@/graphql/helpers/generated' +import { useGetUserSeriesListQuery } from '@/graphql/server/generated/server' import SeriesListSkeleton from '../SeriesList/SeriesListSkeleton' import SeriesList from '../SeriesList' diff --git a/packages/velog-web/src/features/velog/components/VelogSeries/index.tsx b/apps/web/src/features/velog/components/VelogSeries/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogSeries/index.tsx rename to apps/web/src/features/velog/components/VelogSeries/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogTab/VelogTab.module.css b/apps/web/src/features/velog/components/VelogTab/VelogTab.module.css similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogTab/VelogTab.module.css rename to apps/web/src/features/velog/components/VelogTab/VelogTab.module.css diff --git a/packages/velog-web/src/features/velog/components/VelogTab/VelogTab.test.tsx b/apps/web/src/features/velog/components/VelogTab/VelogTab.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogTab/VelogTab.test.tsx rename to apps/web/src/features/velog/components/VelogTab/VelogTab.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogTab/VelogTab.tsx b/apps/web/src/features/velog/components/VelogTab/VelogTab.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogTab/VelogTab.tsx rename to apps/web/src/features/velog/components/VelogTab/VelogTab.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogTab/index.tsx b/apps/web/src/features/velog/components/VelogTab/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogTab/index.tsx rename to apps/web/src/features/velog/components/VelogTab/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogTag/VelogTag.test.tsx b/apps/web/src/features/velog/components/VelogTag/VelogTag.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogTag/VelogTag.test.tsx rename to apps/web/src/features/velog/components/VelogTag/VelogTag.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogTag/VelogTag.tsx b/apps/web/src/features/velog/components/VelogTag/VelogTag.tsx similarity index 90% rename from packages/velog-web/src/features/velog/components/VelogTag/VelogTag.tsx rename to apps/web/src/features/velog/components/VelogTag/VelogTag.tsx index 7420f3ca..3d531946 100644 --- a/packages/velog-web/src/features/velog/components/VelogTag/VelogTag.tsx +++ b/apps/web/src/features/velog/components/VelogTag/VelogTag.tsx @@ -1,4 +1,4 @@ -import { UserTags } from '@/graphql/helpers/generated' +import { UserTags } from '@/graphql/server/generated/server' import VelogTagVerticalList from '../VelogTagVerticalList' import VelogTagHorizontalList from '../VelogTagHorizontalList' diff --git a/packages/velog-web/src/features/velog/components/VelogTag/index.tsx b/apps/web/src/features/velog/components/VelogTag/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogTag/index.tsx rename to apps/web/src/features/velog/components/VelogTag/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.module.css b/apps/web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.module.css similarity index 97% rename from packages/velog-web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.module.css rename to apps/web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.module.css index 908bbcd7..76b016ed 100644 --- a/packages/velog-web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.module.css +++ b/apps/web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.module.css @@ -36,13 +36,13 @@ &.active { background: var(--primary1); color: var(--button-text); - span { + & span { color: var(--button-text); opacity: 0.8; } } - span { + & span { color: var(--text3); font-size: 12px; margin-left: 4px; diff --git a/packages/velog-web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.test.tsx b/apps/web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.test.tsx rename to apps/web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.tsx b/apps/web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.tsx similarity index 94% rename from packages/velog-web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.tsx rename to apps/web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.tsx index ff25faf8..dcaafd67 100644 --- a/packages/velog-web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.tsx +++ b/apps/web/src/features/velog/components/VelogTagHorizontalList/VelogTagHorizontalList.tsx @@ -1,4 +1,4 @@ -import { Tag } from '@/graphql/helpers/generated' +import { Tag } from '@/graphql/server/generated/server' import styles from './VelogTagHorizontalList.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' import Link from 'next/link' diff --git a/packages/velog-web/src/features/velog/components/VelogTagHorizontalList/index.tsx b/apps/web/src/features/velog/components/VelogTagHorizontalList/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogTagHorizontalList/index.tsx rename to apps/web/src/features/velog/components/VelogTagHorizontalList/index.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.module.css b/apps/web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.module.css similarity index 95% rename from packages/velog-web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.module.css rename to apps/web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.module.css index f2aaa9e0..9d4f031e 100644 --- a/packages/velog-web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.module.css +++ b/apps/web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.module.css @@ -18,7 +18,7 @@ color: var(--text2); font-weight: bold; } - ul { + & ul { list-style: none; padding-left: 0; } @@ -39,7 +39,7 @@ color: var(--text1); text-decoration: underline; - span { + & span { text-decoration: none; } } @@ -49,12 +49,12 @@ color: var(--primary2); font-weight: bold; - a { + & a { color: var(--primary2); } } - span { + & span { margin-left: 8px; color: var(--text3); font-weight: normal; diff --git a/packages/velog-web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.test.tsx b/apps/web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.test.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.test.tsx rename to apps/web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.test.tsx diff --git a/packages/velog-web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.tsx b/apps/web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.tsx similarity index 95% rename from packages/velog-web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.tsx rename to apps/web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.tsx index 2cce9bc1..59bb1b70 100644 --- a/packages/velog-web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.tsx +++ b/apps/web/src/features/velog/components/VelogTagVerticalList/VelogTagVerticalList.tsx @@ -1,4 +1,4 @@ -import { Tag } from '@/graphql/helpers/generated' +import { Tag } from '@/graphql/server/generated/server' import styles from './VelogTagVerticalList.module.css' import { bindClassNames } from '@/lib/styles/bindClassNames' import Link from 'next/link' diff --git a/packages/velog-web/src/features/velog/components/VelogTagVerticalList/index.tsx b/apps/web/src/features/velog/components/VelogTagVerticalList/index.tsx similarity index 100% rename from packages/velog-web/src/features/velog/components/VelogTagVerticalList/index.tsx rename to apps/web/src/features/velog/components/VelogTagVerticalList/index.tsx diff --git a/packages/velog-web/src/features/velog/hooks/useApplyVelogConfig.ts b/apps/web/src/features/velog/hooks/useApplyVelogConfig.ts similarity index 93% rename from packages/velog-web/src/features/velog/hooks/useApplyVelogConfig.ts rename to apps/web/src/features/velog/hooks/useApplyVelogConfig.ts index 90e8d470..60c626d4 100644 --- a/packages/velog-web/src/features/velog/hooks/useApplyVelogConfig.ts +++ b/apps/web/src/features/velog/hooks/useApplyVelogConfig.ts @@ -1,4 +1,4 @@ -import { useVelogConfigQuery } from '@/graphql/helpers/generated' +import { useVelogConfigQuery } from '@/graphql/server/generated/server' import { UserLogo, useHeader } from '@/state/header' import { notFound } from 'next/navigation' import { useEffect, useState } from 'react' diff --git a/packages/velog-web/src/features/velog/hooks/useFollowers.ts b/apps/web/src/features/velog/hooks/useFollowers.ts similarity index 91% rename from packages/velog-web/src/features/velog/hooks/useFollowers.ts rename to apps/web/src/features/velog/hooks/useFollowers.ts index 98661d21..4cf0f51e 100644 --- a/packages/velog-web/src/features/velog/hooks/useFollowers.ts +++ b/apps/web/src/features/velog/hooks/useFollowers.ts @@ -3,8 +3,8 @@ import { GetFollowersDocument, GetFollowersQuery, GetFollowersQueryVariables, -} from '@/graphql/helpers/generated' -import { infiniteGetFollowersQueryKey } from '@/graphql/helpers/queryKey' +} from '@/graphql/server/generated/server' +import { infiniteGetFollowersQueryKey } from '@/graphql/server/helpers/queryKey' import useCustomInfiniteQuery from '@/hooks/useCustomInfiniteQuery' import { useMemo } from 'react' diff --git a/packages/velog-web/src/features/velog/hooks/useFollowings.ts b/apps/web/src/features/velog/hooks/useFollowings.ts similarity index 91% rename from packages/velog-web/src/features/velog/hooks/useFollowings.ts rename to apps/web/src/features/velog/hooks/useFollowings.ts index 8b1b282f..ae793825 100644 --- a/packages/velog-web/src/features/velog/hooks/useFollowings.ts +++ b/apps/web/src/features/velog/hooks/useFollowings.ts @@ -5,8 +5,8 @@ import { GetFollowingsDocument, GetFollowingsQuery, GetFollowingsQueryVariables, -} from '@/graphql/helpers/generated' -import { infiniteGetFollowingsQueryKey } from '@/graphql/helpers/queryKey' +} from '@/graphql/server/generated/server' +import { infiniteGetFollowingsQueryKey } from '@/graphql/server/helpers/queryKey' import useCustomInfiniteQuery from '@/hooks/useCustomInfiniteQuery' import { useMemo } from 'react' diff --git a/packages/velog-web/src/features/velog/hooks/useVelogPosts.ts b/apps/web/src/features/velog/hooks/useVelogPosts.ts similarity index 92% rename from packages/velog-web/src/features/velog/hooks/useVelogPosts.ts rename to apps/web/src/features/velog/hooks/useVelogPosts.ts index 0e49c785..59f6650c 100644 --- a/packages/velog-web/src/features/velog/hooks/useVelogPosts.ts +++ b/apps/web/src/features/velog/hooks/useVelogPosts.ts @@ -3,8 +3,8 @@ import { VelogPostsDocument, VelogPostsQuery, VelogPostsQueryVariables, -} from '@/graphql/helpers/generated' -import { infiniteVelogPostsQueryKey } from '@/graphql/helpers/queryKey' +} from '@/graphql/server/generated/server' +import { infiniteVelogPostsQueryKey } from '@/graphql/server/helpers/queryKey' import useCustomInfiniteQuery from '@/hooks/useCustomInfiniteQuery' import { useMemo } from 'react' diff --git a/apps/web/src/graphql/bookServer/book.gql b/apps/web/src/graphql/bookServer/book.gql new file mode 100644 index 00000000..aab4469e --- /dev/null +++ b/apps/web/src/graphql/bookServer/book.gql @@ -0,0 +1,3 @@ +mutation deploy($input: BookIdInput!) { + deploy(input: $input) +} diff --git a/apps/web/src/graphql/bookServer/generated/bookServer.ts b/apps/web/src/graphql/bookServer/generated/bookServer.ts new file mode 100644 index 00000000..32f689e9 --- /dev/null +++ b/apps/web/src/graphql/bookServer/generated/bookServer.ts @@ -0,0 +1,159 @@ +import { useMutation, UseMutationOptions } from '@tanstack/react-query' +import { fetcher } from '../helpers/bookServerFetcher' +export type Maybe = T | null +export type InputMaybe = T | null | undefined +export type Exact = { [K in keyof T]: T[K] } +export type MakeOptional = Omit & { [SubKey in K]?: Maybe } +export type MakeMaybe = Omit & { [SubKey in K]: Maybe } +export type MakeEmpty = { + [_ in K]?: never +} +export type Incremental = + | T + | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never } +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string } + String: { input: string; output: string } + Boolean: { input: boolean; output: boolean } + Int: { input: number; output: number } + Float: { input: number; output: number } + Date: { input: Date; output: Date } + JSON: { input: Record; output: Record } + PositiveInt: { input: number; output: number } + Void: { input: void; output: void } +} + +export type Book = { + fk_writer_id: Scalars['String']['output'] + id: Scalars['ID']['output'] + is_published: Scalars['Boolean']['output'] + pages: Array + thumbnail: Scalars['String']['output'] +} + +export type BookIdInput = { + book_id: Scalars['ID']['input'] +} + +export type CreatePageInput = { + book_url_slug: Scalars['String']['input'] + index: Scalars['Int']['input'] + parent_url_slug: Scalars['String']['input'] + title: Scalars['String']['input'] + type: PageType +} + +export type GetPagesInput = { + book_url_slug: Scalars['String']['input'] +} + +export type Mutation = { + build: Maybe + create: Maybe + deploy: Maybe +} + +export type MutationBuildArgs = { + input: BookIdInput +} + +export type MutationCreateArgs = { + input: CreatePageInput +} + +export type MutationDeployArgs = { + input: BookIdInput +} + +export type Page = { + body: Scalars['String']['output'] + book_id: Scalars['ID']['output'] + childrens: Array + code: Scalars['String']['output'] + created_at: Scalars['Date']['output'] + fk_writer_id: Scalars['ID']['output'] + id: Scalars['ID']['output'] + index: Scalars['Int']['output'] + level: Scalars['Int']['output'] + parent_id: Maybe + title: Scalars['String']['output'] + type: Scalars['String']['output'] + updated_at: Scalars['Date']['output'] + url_slug: Scalars['String']['output'] +} + +export type PageType = 'folder' | 'page' | 'separator' + +export type Query = { + book: Maybe + pages: Array +} + +export type QueryBookArgs = { + input: BookIdInput +} + +export type QueryPagesArgs = { + input: GetPagesInput +} + +export type SubScriptionPayload = { + message: Scalars['String']['output'] +} + +export type Subscription = { + buildCompleted: Maybe + buildInstalled: Maybe + deployCompleted: Maybe +} + +export type SubscriptionBookBuildCompletedArgs = { + input: BookIdInput +} + +export type SubscriptionBookBuildInstalledArgs = { + input: BookIdInput +} + +export type SubscriptionBookDeployCompletedArgs = { + input: BookIdInput +} + +export type Writer = { + email: Scalars['String']['output'] + fk_user_id: Scalars['String']['output'] + id: Scalars['ID']['output'] + short_bio: Maybe + username: Scalars['String']['output'] +} + +export type DeployMutationVariables = Exact<{ + input: BookIdInput +}> + +export type DeployMutation = { deploy: void | null } + +export const DeployDocument = ` + mutation deploy($input: BookIDInput!) { + deploy(input: $input) +} + ` + +export const useDeployMutation = ( + options?: UseMutationOptions, +) => { + return useMutation({ + mutationKey: ['deploy'], + mutationFn: (variables?: DeployMutationVariables) => + fetcher(DeployDocument, variables)(), + ...options, + }) +} + +useDeployMutation.getKey = () => ['deploy'] + +useDeployMutation.fetcher = ( + variables: DeployMutationVariables, + options?: RequestInit['headers'], +) => fetcher(DeployDocument, variables, options) diff --git a/apps/web/src/graphql/bookServer/helpers/bookServerFetcher.ts b/apps/web/src/graphql/bookServer/helpers/bookServerFetcher.ts new file mode 100644 index 00000000..7f03a87e --- /dev/null +++ b/apps/web/src/graphql/bookServer/helpers/bookServerFetcher.ts @@ -0,0 +1,21 @@ +import { ENV } from '@/env' +import graphqlFetch from '@/lib/graphqlFetch' + +export function fetcher>( + query: string, + variables?: TVariables, + headers?: RequestInit['headers'], +) { + return async (): Promise => { + const data = await graphqlFetch({ + url: `${ENV.graphqlBookServerHost}/graphql`, + method: 'POST', + body: { query, variables: variables ?? {} }, + headers: { + ...headers, + }, + }) + + return data + } +} diff --git a/packages/velog-web/src/graphql/ad.gql b/apps/web/src/graphql/server/ad.gql similarity index 100% rename from packages/velog-web/src/graphql/ad.gql rename to apps/web/src/graphql/server/ad.gql diff --git a/packages/velog-web/src/graphql/auth.gql b/apps/web/src/graphql/server/auth.gql similarity index 100% rename from packages/velog-web/src/graphql/auth.gql rename to apps/web/src/graphql/server/auth.gql diff --git a/packages/velog-web/src/graphql/follow.gql b/apps/web/src/graphql/server/follow.gql similarity index 100% rename from packages/velog-web/src/graphql/follow.gql rename to apps/web/src/graphql/server/follow.gql diff --git a/apps/web/src/graphql/server/generated/server.ts b/apps/web/src/graphql/server/generated/server.ts new file mode 100644 index 00000000..680b3861 --- /dev/null +++ b/apps/web/src/graphql/server/generated/server.ts @@ -0,0 +1,2704 @@ +import { useQuery, useSuspenseQuery, useMutation, UseQueryOptions, UseSuspenseQueryOptions, UseMutationOptions } from '@tanstack/react-query'; +import { fetcher } from '../helpers/serverFetcher'; +export type Maybe = T | null; +export type InputMaybe = T | null | undefined; +export type Exact = { [K in keyof T]: T[K] }; +export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; +export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; +export type MakeEmpty = { [_ in K]?: never }; +export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string; } + String: { input: string; output: string; } + Boolean: { input: boolean; output: boolean; } + Int: { input: number; output: number; } + Float: { input: number; output: number; } + DateTimeISO: { input: any; output: any; } + JSON: { input: Record; output: Record; } + PositiveInt: { input: number; output: number; } + Void: { input: void; output: void; } +}; + +export type Ad = { + body: Scalars['String']['output']; + end_date: Scalars['DateTimeISO']['output']; + id: Scalars['ID']['output']; + image: Scalars['String']['output']; + is_disabled: Scalars['Boolean']['output']; + start_date: Scalars['DateTimeISO']['output']; + title: Scalars['String']['output']; + type: Scalars['String']['output']; + url: Scalars['String']['output']; +}; + +export type AdsInput = { + limit?: InputMaybe; + type: Scalars['String']['input']; + writer_username?: InputMaybe; +}; + +export type CheckEmailExistsInput = { + email: Scalars['String']['input']; +}; + +export type Comment = { + created_at: Maybe; + deleted: Maybe; + has_replies: Maybe; + id: Scalars['ID']['output']; + level: Maybe; + likes: Maybe; + replies: Array; + replies_count: Maybe; + text: Maybe; + user: Maybe; +}; + +export type CommentNotificationActionInput = { + actor_display_name: Scalars['String']['input']; + actor_thumbnail: Scalars['String']['input']; + actor_username: Scalars['String']['input']; + comment_id: Scalars['ID']['input']; + comment_text: Scalars['String']['input']; + post_id: Scalars['ID']['input']; + post_title: Scalars['String']['input']; + post_url_slug: Scalars['String']['input']; + post_writer_username: Scalars['String']['input']; + type: NotificationType; +}; + +export type CommentReplyNotifictionActionInput = { + actor_display_name: Scalars['String']['input']; + actor_thumbnail: Scalars['String']['input']; + actor_username: Scalars['String']['input']; + comment_id: Scalars['ID']['input']; + parent_comment_text: Scalars['String']['input']; + post_id: Scalars['ID']['input']; + post_url_slug: Scalars['String']['input']; + post_writer_username: Scalars['String']['input']; + reply_comment_text: Scalars['String']['input']; + type: NotificationType; +}; + +export type ConfirmChangeEmailInput = { + code: Scalars['String']['input']; +}; + +export type CreateNotificationInput = { + action: NotificationActionInput; + action_id?: InputMaybe; + actor_id?: InputMaybe; + fk_user_id: Scalars['String']['input']; + type: NotificationType; +}; + +export type EditPostInput = { + body: Scalars['String']['input']; + id: Scalars['ID']['input']; + is_markdown: Scalars['Boolean']['input']; + is_private: Scalars['Boolean']['input']; + is_temp: Scalars['Boolean']['input']; + meta: Scalars['JSON']['input']; + series_id?: InputMaybe; + tags: Array; + thumbnail?: InputMaybe; + title: Scalars['String']['input']; + token?: InputMaybe; + url_slug: Scalars['String']['input']; +}; + +export type FeedPostsInput = { + limit?: InputMaybe; + offset?: InputMaybe; +}; + +export type FollowInput = { + followingUserId: Scalars['ID']['input']; +}; + +export type FollowNotificationActionInput = { + actor_display_name: Scalars['String']['input']; + actor_thumbnail: Scalars['String']['input']; + actor_user_id: Scalars['ID']['input']; + actor_username: Scalars['String']['input']; + follow_id: Scalars['ID']['input']; + type: NotificationType; +}; + +export type FollowResult = { + id: Scalars['ID']['output']; + is_followed: Scalars['Boolean']['output']; + profile: UserProfile; + userId: Scalars['ID']['output']; + username: Scalars['String']['output']; +}; + +export type GetFollowInput = { + cursor?: InputMaybe; + limit?: InputMaybe; + username: Scalars['String']['input']; +}; + +export type GetPostsInput = { + cursor?: InputMaybe; + limit?: InputMaybe; + tag?: InputMaybe; + temp_only?: InputMaybe; + username?: InputMaybe; +}; + +export type GetSearchPostsInput = { + keyword: Scalars['String']['input']; + limit?: InputMaybe; + offset?: InputMaybe; + username?: InputMaybe; +}; + +export type GetSeriesInput = { + id?: InputMaybe; + url_slug?: InputMaybe; + username?: InputMaybe; +}; + +export type GetSeriesListInput = { + username: Scalars['String']['input']; +}; + +export type GetUserInput = { + id?: InputMaybe; + username?: InputMaybe; +}; + +export type GetVelogConfigInput = { + username: Scalars['String']['input']; +}; + +export type InitiateChangeEmailInput = { + email: Scalars['String']['input']; +}; + +export type LikePostInput = { + postId?: InputMaybe; +}; + +export type LinkedPosts = { + next: Maybe; + previous: Maybe; +}; + +export type Mutation = { + acceptIntegration: Scalars['String']['output']; + confirmChangeEmail: Maybe; + createNotification: Notification; + editPost: Post; + follow: Maybe; + initiateChangeEmail: Maybe; + likePost: Post; + logout: Maybe; + readAllNotifications: Maybe; + readNotification: Maybe; + removeAllNotifications: Maybe; + sendMail: Maybe; + unfollow: Maybe; + unlikePost: Post; + unregister: Maybe; + updateAbout: Maybe; + updateEmailRules: Maybe; + updateNotNoticeNotification: Maybe; + updateProfile: Maybe; + updateSocialInfo: Maybe; + updateThumbnail: Maybe; + updateVelogTitle: Maybe; + writePost: Post; +}; + + +export type MutationConfirmChangeEmailArgs = { + input: ConfirmChangeEmailInput; +}; + + +export type MutationCreateNotificationArgs = { + input: CreateNotificationInput; +}; + + +export type MutationEditPostArgs = { + input: EditPostInput; +}; + + +export type MutationFollowArgs = { + input: FollowInput; +}; + + +export type MutationInitiateChangeEmailArgs = { + input: InitiateChangeEmailInput; +}; + + +export type MutationLikePostArgs = { + input: LikePostInput; +}; + + +export type MutationReadNotificationArgs = { + input: ReadNotificationInput; +}; + + +export type MutationSendMailArgs = { + input: SendMailInput; +}; + + +export type MutationUnfollowArgs = { + input: UnfollowInput; +}; + + +export type MutationUnlikePostArgs = { + input: UnlikePostInput; +}; + + +export type MutationUnregisterArgs = { + input: UnregisterInput; +}; + + +export type MutationUpdateAboutArgs = { + input: UpdateAboutInput; +}; + + +export type MutationUpdateEmailRulesArgs = { + input: UpdateEmailRulesInput; +}; + + +export type MutationUpdateProfileArgs = { + input: UpdateProfileInput; +}; + + +export type MutationUpdateSocialInfoArgs = { + input: UpdateSocialInfoInput; +}; + + +export type MutationUpdateThumbnailArgs = { + input: UpdateThumbnailInput; +}; + + +export type MutationUpdateVelogTitleArgs = { + input: UpdateVelogTitleInput; +}; + + +export type MutationWritePostArgs = { + input: WritePostInput; +}; + +export type Notification = { + action: Scalars['JSON']['output']; + action_id: Maybe; + actor_id: Maybe; + created_at: Scalars['DateTimeISO']['output']; + fk_user_id: Scalars['String']['output']; + id: Scalars['ID']['output']; + is_deleted: Scalars['Boolean']['output']; + is_read: Scalars['Boolean']['output']; + type: NotificationType; +}; + +export type NotificationActionInput = { + comment?: InputMaybe; + commentReply?: InputMaybe; + follow?: InputMaybe; + postLike?: InputMaybe; +}; + +export type NotificationType = + | 'comment' + | 'commentReply' + | 'follow' + | 'postLike'; + +export type NotificationsInput = { + is_read?: InputMaybe; +}; + +export type Post = { + body: Maybe; + comments: Array; + comments_count: Maybe; + created_at: Scalars['DateTimeISO']['output']; + fk_user_id: Scalars['String']['output']; + id: Scalars['ID']['output']; + is_followed: Maybe; + is_liked: Maybe; + is_markdown: Maybe; + is_private: Scalars['Boolean']['output']; + is_temp: Maybe; + last_read_at: Maybe; + likes: Maybe; + linked_posts: Maybe; + meta: Maybe; + original_post_id: Maybe; + recommended_posts: Array; + released_at: Maybe; + series: Maybe; + short_description: Maybe; + tags: Array; + thumbnail: Maybe; + title: Maybe; + updated_at: Scalars['DateTimeISO']['output']; + url_slug: Maybe; + user: Maybe; + views: Maybe; +}; + +export type PostHistory = { + body: Maybe; + created_at: Maybe; + fk_post_id: Maybe; + id: Maybe; + is_markdown: Maybe; + title: Maybe; +}; + +export type PostLikeNotificationActionInput = { + actor_display_name: Scalars['String']['input']; + actor_thumbnail: Scalars['String']['input']; + actor_username: Scalars['String']['input']; + post_id: Scalars['ID']['input']; + post_like_id: Scalars['ID']['input']; + post_title: Scalars['String']['input']; + post_url_slug: Scalars['String']['input']; + post_writer_username: Scalars['String']['input']; + type: NotificationType; +}; + +export type Query = { + ads: Array; + checkEmailExists: Scalars['Boolean']['output']; + currentUser: Maybe; + feedPosts: Array; + followers: Array; + followings: Array; + isLogged: Maybe; + notNoticeNotificationCount: Scalars['Int']['output']; + notifications: Array; + post: Maybe; + posts: Array; + readingList: Array; + recentPosts: Array; + restoreToken: UserToken; + searchPosts: SearchResult; + series: Maybe; + seriesList: Array; + trendingPosts: Array; + trendingWriters: Array; + unregisterToken: Scalars['String']['output']; + user: Maybe; + userTags: Maybe; + velogConfig: VelogConfig; +}; + + +export type QueryAdsArgs = { + input: AdsInput; +}; + + +export type QueryCheckEmailExistsArgs = { + input: CheckEmailExistsInput; +}; + + +export type QueryFeedPostsArgs = { + input: FeedPostsInput; +}; + + +export type QueryFollowersArgs = { + input: GetFollowInput; +}; + + +export type QueryFollowingsArgs = { + input: GetFollowInput; +}; + + +export type QueryNotificationsArgs = { + input: NotificationsInput; +}; + + +export type QueryPostArgs = { + input: ReadPostInput; +}; + + +export type QueryPostsArgs = { + input: GetPostsInput; +}; + + +export type QueryReadingListArgs = { + input: ReadingListInput; +}; + + +export type QueryRecentPostsArgs = { + input: RecentPostsInput; +}; + + +export type QuerySearchPostsArgs = { + input: GetSearchPostsInput; +}; + + +export type QuerySeriesArgs = { + input: GetSeriesInput; +}; + + +export type QuerySeriesListArgs = { + input: GetSeriesListInput; +}; + + +export type QueryTrendingPostsArgs = { + input: TrendingPostsInput; +}; + + +export type QueryTrendingWritersArgs = { + input: TrendingWritersInput; +}; + + +export type QueryUserArgs = { + input: GetUserInput; +}; + + +export type QueryUserTagsArgs = { + input: UserTagsInput; +}; + + +export type QueryVelogConfigArgs = { + input: GetVelogConfigInput; +}; + +export type ReadCountByDay = { + count: Maybe; + day: Maybe; +}; + +export type ReadNotificationInput = { + notification_ids: Array; +}; + +export type ReadPostInput = { + id?: InputMaybe; + url_slug?: InputMaybe; + username?: InputMaybe; +}; + +export type ReadingListInput = { + cursor?: InputMaybe; + limit?: InputMaybe; + type: ReadingListOption; +}; + +export type ReadingListOption = + | 'LIKED' + | 'READ'; + +export type RecentPostsInput = { + cursor?: InputMaybe; + limit?: InputMaybe; +}; + +export type SearchResult = { + count: Scalars['Int']['output']; + posts: Array; +}; + +export type SendMailInput = { + email: Scalars['String']['input']; +}; + +export type SendMailResponse = { + registered: Maybe; +}; + +export type Series = { + created_at: Scalars['DateTimeISO']['output']; + description: Maybe; + fk_user_id: Maybe; + id: Scalars['ID']['output']; + name: Maybe; + posts_count: Maybe; + series_posts: Maybe>; + thumbnail: Maybe; + updated_at: Scalars['DateTimeISO']['output']; + url_slug: Maybe; + user: Maybe; +}; + +export type SeriesPost = { + id: Scalars['ID']['output']; + index: Maybe; + post: Maybe; +}; + +export type Stats = { + count_by_day: Maybe>>; + total: Maybe; +}; + +export type Tag = { + created_at: Maybe; + description: Maybe; + id: Scalars['ID']['output']; + name: Maybe; + posts_count: Maybe; + thumbnail: Maybe; +}; + +export type TrendingPostsInput = { + limit?: InputMaybe; + offset?: InputMaybe; + timeframe?: InputMaybe; +}; + +export type TrendingWriter = { + id: Scalars['ID']['output']; + index: Scalars['Int']['output']; + posts: Array; + user: TrendingWriterUser; +}; + +export type TrendingWriterPosts = { + id: Scalars['ID']['output']; + thumbnail: Scalars['String']['output']; + title: Scalars['String']['output']; + url_slug: Scalars['String']['output']; +}; + +export type TrendingWriterProfile = { + display_name: Scalars['String']['output']; + short_bio: Scalars['String']['output']; + thumbnail: Maybe; +}; + +export type TrendingWriterUser = { + id: Scalars['ID']['output']; + profile: TrendingWriterProfile; + username: Scalars['String']['output']; +}; + +export type TrendingWritersInput = { + cursor: Scalars['Int']['input']; + limit: Scalars['PositiveInt']['input']; +}; + +export type UnfollowInput = { + followingUserId: Scalars['ID']['input']; +}; + +export type UnlikePostInput = { + postId?: InputMaybe; +}; + +export type UnregisterInput = { + token: Scalars['String']['input']; +}; + +export type UpdateAboutInput = { + about: Scalars['String']['input']; +}; + +export type UpdateEmailRulesInput = { + notification: Scalars['Boolean']['input']; + promotion: Scalars['Boolean']['input']; +}; + +export type UpdateProfileInput = { + display_name: Scalars['String']['input']; + short_bio: Scalars['String']['input']; +}; + +export type UpdateSocialInfoInput = { + profile_links: Scalars['JSON']['input']; +}; + +export type UpdateThumbnailInput = { + url?: InputMaybe; +}; + +export type UpdateVelogTitleInput = { + title: Scalars['String']['input']; +}; + +export type User = { + created_at: Scalars['DateTimeISO']['output']; + email: Maybe; + followers_count: Scalars['Int']['output']; + followings_count: Scalars['Int']['output']; + id: Scalars['ID']['output']; + is_certified: Scalars['Boolean']['output']; + is_followed: Scalars['Boolean']['output']; + is_trusted: Scalars['Boolean']['output']; + profile: UserProfile; + series_list: Array; + updated_at: Scalars['DateTimeISO']['output']; + user_meta: Maybe; + username: Scalars['String']['output']; + velog_config: Maybe; +}; + +export type UserMeta = { + email_notification: Maybe; + email_promotion: Maybe; + id: Scalars['ID']['output']; +}; + +export type UserProfile = { + about: Scalars['String']['output']; + created_at: Scalars['DateTimeISO']['output']; + display_name: Scalars['String']['output']; + id: Scalars['ID']['output']; + profile_links: Scalars['JSON']['output']; + short_bio: Scalars['String']['output']; + thumbnail: Maybe; + updated_at: Scalars['DateTimeISO']['output']; +}; + +export type UserTags = { + posts_count: Scalars['Int']['output']; + tags: Array; +}; + +export type UserTagsInput = { + username: Scalars['String']['input']; +}; + +export type UserToken = { + accessToken: Scalars['String']['output']; + refreshToken: Scalars['String']['output']; +}; + +export type VelogConfig = { + id: Scalars['ID']['output']; + logo_image: Maybe; + title: Maybe; +}; + +export type WritePostInput = { + body: Scalars['String']['input']; + is_markdown: Scalars['Boolean']['input']; + is_private: Scalars['Boolean']['input']; + is_temp: Scalars['Boolean']['input']; + meta: Scalars['JSON']['input']; + series_id?: InputMaybe; + tags: Array; + thumbnail?: InputMaybe; + title: Scalars['String']['input']; + token?: InputMaybe; + url_slug: Scalars['String']['input']; +}; + +export type AdsQueryVariables = Exact<{ + input: AdsInput; +}>; + + +export type AdsQuery = { ads: Array<{ id: string, title: string, body: string, image: string, url: string, start_date: any }> }; + +export type IsLoggedQueryVariables = Exact<{ [key: string]: never; }>; + + +export type IsLoggedQuery = { isLogged: boolean | null }; + +export type SendMailMutationVariables = Exact<{ + input: SendMailInput; +}>; + + +export type SendMailMutation = { sendMail: { registered: boolean | null } | null }; + +export type FollowMutationVariables = Exact<{ + input: FollowInput; +}>; + + +export type FollowMutation = { follow: boolean | null }; + +export type UnfollowMutationVariables = Exact<{ + input: UnfollowInput; +}>; + + +export type UnfollowMutation = { unfollow: boolean | null }; + +export type GetFollowersQueryVariables = Exact<{ + input: GetFollowInput; +}>; + + +export type GetFollowersQuery = { followers: Array<{ id: string, userId: string, username: string, is_followed: boolean, profile: { short_bio: string, thumbnail: string | null, display_name: string } }> }; + +export type GetFollowingsQueryVariables = Exact<{ + input: GetFollowInput; +}>; + + +export type GetFollowingsQuery = { followings: Array<{ id: string, userId: string, username: string, is_followed: boolean, profile: { short_bio: string, thumbnail: string | null, display_name: string } }> }; + +export type NotificationQueryVariables = Exact<{ + input: NotificationsInput; +}>; + + +export type NotificationQuery = { notifications: Array<{ id: string, type: NotificationType, action: Record, actor_id: string | null, action_id: string | null, is_read: boolean, created_at: any }> }; + +export type NotNoticeNotificationCountQueryVariables = Exact<{ [key: string]: never; }>; + + +export type NotNoticeNotificationCountQuery = { notNoticeNotificationCount: number }; + +export type ReadAllNotificationsMutationVariables = Exact<{ [key: string]: never; }>; + + +export type ReadAllNotificationsMutation = { readAllNotifications: void | null }; + +export type RemoveAllNotificationsMutationVariables = Exact<{ [key: string]: never; }>; + + +export type RemoveAllNotificationsMutation = { removeAllNotifications: void | null }; + +export type ReadNoticationMutationVariables = Exact<{ + input: ReadNotificationInput; +}>; + + +export type ReadNoticationMutation = { readNotification: void | null }; + +export type UpdateNotNoticeNotificationMutationVariables = Exact<{ [key: string]: never; }>; + + +export type UpdateNotNoticeNotificationMutation = { updateNotNoticeNotification: void | null }; + +export type ReadPostQueryVariables = Exact<{ + input: ReadPostInput; +}>; + + +export type ReadPostQuery = { post: { id: string, title: string | null, released_at: any | null, updated_at: any, body: string | null, short_description: string | null, is_markdown: boolean | null, is_private: boolean, is_temp: boolean | null, thumbnail: string | null, comments_count: number | null, url_slug: string | null, likes: number | null, is_liked: boolean | null, is_followed: boolean | null, user: { id: string, username: string, profile: { id: string, display_name: string, thumbnail: string | null, short_bio: string, profile_links: Record }, velog_config: { title: string | null } | null } | null, comments: Array<{ id: string, text: string | null, replies_count: number | null, level: number | null, created_at: any | null, deleted: boolean | null, user: { id: string, username: string, profile: { id: string, thumbnail: string | null } } | null }>, series: { id: string, name: string | null, url_slug: string | null, series_posts: Array<{ id: string, post: { id: string, title: string | null, url_slug: string | null, user: { id: string, username: string } | null } | null }> | null } | null, linked_posts: { previous: { id: string, title: string | null, url_slug: string | null, user: { id: string, username: string } | null } | null, next: { id: string, title: string | null, url_slug: string | null, user: { id: string, username: string } | null } | null } | null } | null }; + +export type RecentPostsQueryVariables = Exact<{ + input: RecentPostsInput; +}>; + + +export type RecentPostsQuery = { recentPosts: Array<{ id: string, title: string | null, short_description: string | null, thumbnail: string | null, url_slug: string | null, released_at: any | null, updated_at: any, is_private: boolean, likes: number | null, comments_count: number | null, user: { id: string, username: string, profile: { id: string, thumbnail: string | null, display_name: string } } | null }> }; + +export type TrendingPostsQueryVariables = Exact<{ + input: TrendingPostsInput; +}>; + + +export type TrendingPostsQuery = { trendingPosts: Array<{ id: string, title: string | null, short_description: string | null, thumbnail: string | null, likes: number | null, url_slug: string | null, released_at: any | null, updated_at: any, is_private: boolean, comments_count: number | null, user: { id: string, username: string, profile: { id: string, thumbnail: string | null, display_name: string } } | null }> }; + +export type FeedPostsQueryVariables = Exact<{ + input: FeedPostsInput; +}>; + + +export type FeedPostsQuery = { feedPosts: Array<{ id: string, title: string | null, short_description: string | null, thumbnail: string | null, likes: number | null, url_slug: string | null, released_at: any | null, updated_at: any, is_private: boolean, comments_count: number | null, user: { id: string, username: string, profile: { id: string, thumbnail: string | null, display_name: string } } | null }> }; + +export type VelogPostsQueryVariables = Exact<{ + input: GetPostsInput; +}>; + + +export type VelogPostsQuery = { posts: Array<{ id: string, title: string | null, short_description: string | null, thumbnail: string | null, url_slug: string | null, released_at: any | null, updated_at: any, comments_count: number | null, tags: Array, is_private: boolean, likes: number | null, user: { id: string, username: string, profile: { id: string, thumbnail: string | null, display_name: string } } | null }> }; + +export type SearchPostsQueryVariables = Exact<{ + input: GetSearchPostsInput; +}>; + + +export type SearchPostsQuery = { searchPosts: { count: number, posts: Array<{ id: string, title: string | null, short_description: string | null, thumbnail: string | null, url_slug: string | null, released_at: any | null, tags: Array, is_private: boolean, comments_count: number | null, user: { id: string, username: string, profile: { id: string, thumbnail: string | null, display_name: string } } | null }> } }; + +export type UserTagsQueryVariables = Exact<{ + input: UserTagsInput; +}>; + + +export type UserTagsQuery = { userTags: { posts_count: number, tags: Array<{ id: string, name: string | null, description: string | null, posts_count: number | null, thumbnail: string | null }> } | null }; + +export type GetUserQueryVariables = Exact<{ + input: GetUserInput; +}>; + + +export type GetUserQuery = { user: { id: string, username: string, profile: { id: string, display_name: string, short_bio: string, thumbnail: string | null, profile_links: Record } } | null }; + +export type GetUserFollowInfoQueryVariables = Exact<{ + input: GetUserInput; +}>; + + +export type GetUserFollowInfoQuery = { user: { id: string, username: string, followers_count: number, followings_count: number, is_followed: boolean, profile: { id: string, display_name: string, short_bio: string, thumbnail: string | null, profile_links: Record } } | null }; + +export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>; + + +export type CurrentUserQuery = { currentUser: { id: string, username: string, email: string | null, profile: { id: string, thumbnail: string | null, display_name: string, short_bio: string, profile_links: Record }, user_meta: { id: string, email_notification: boolean | null, email_promotion: boolean | null } | null } | null }; + +export type VelogConfigQueryVariables = Exact<{ + input: GetVelogConfigInput; +}>; + + +export type VelogConfigQuery = { velogConfig: { title: string | null, logo_image: string | null } }; + +export type GetUserAboutQueryVariables = Exact<{ + input: GetUserInput; +}>; + + +export type GetUserAboutQuery = { user: { id: string, profile: { id: string, about: string, display_name: string } } | null }; + +export type GetUserSeriesListQueryVariables = Exact<{ + input: GetUserInput; +}>; + + +export type GetUserSeriesListQuery = { user: { id: string, series_list: Array<{ id: string, name: string | null, description: string | null, url_slug: string | null, thumbnail: string | null, updated_at: any, posts_count: number | null }> } | null }; + +export type UnregisterTokenQueryVariables = Exact<{ [key: string]: never; }>; + + +export type UnregisterTokenQuery = { unregisterToken: string }; + +export type CheckEmailExistsQueryVariables = Exact<{ + input: CheckEmailExistsInput; +}>; + + +export type CheckEmailExistsQuery = { checkEmailExists: boolean }; + +export type UpdateAboutMutationVariables = Exact<{ + input: UpdateAboutInput; +}>; + + +export type UpdateAboutMutation = { updateAbout: { id: string, about: string } | null }; + +export type UpdateThumbnailMutationVariables = Exact<{ + input: UpdateThumbnailInput; +}>; + + +export type UpdateThumbnailMutation = { updateThumbnail: { id: string, thumbnail: string | null } | null }; + +export type UpdateProfileMutationVariables = Exact<{ + input: UpdateProfileInput; +}>; + + +export type UpdateProfileMutation = { updateProfile: { id: string, display_name: string, short_bio: string } | null }; + +export type UpdateVelogTitleMutationVariables = Exact<{ + input: UpdateVelogTitleInput; +}>; + + +export type UpdateVelogTitleMutation = { updateVelogTitle: { id: string, title: string | null } | null }; + +export type UpdateSocialInfoMutationVariables = Exact<{ + input: UpdateSocialInfoInput; +}>; + + +export type UpdateSocialInfoMutation = { updateSocialInfo: { id: string, profile_links: Record } | null }; + +export type UpdateEmailRulesMutationVariables = Exact<{ + input: UpdateEmailRulesInput; +}>; + + +export type UpdateEmailRulesMutation = { updateEmailRules: { email_notification: boolean | null, email_promotion: boolean | null } | null }; + +export type UnregisterMutationVariables = Exact<{ + input: UnregisterInput; +}>; + + +export type UnregisterMutation = { unregister: void | null }; + +export type InitiateChangeEmailMutationVariables = Exact<{ + input: InitiateChangeEmailInput; +}>; + + +export type InitiateChangeEmailMutation = { initiateChangeEmail: void | null }; + +export type ConfirmChangeEmailMutationVariables = Exact<{ + input: ConfirmChangeEmailInput; +}>; + + +export type ConfirmChangeEmailMutation = { confirmChangeEmail: void | null }; + +export type LogoutMutationVariables = Exact<{ [key: string]: never; }>; + + +export type LogoutMutation = { logout: void | null }; + +export type TrendingWritersQueryVariables = Exact<{ + input: TrendingWritersInput; +}>; + + +export type TrendingWritersQuery = { trendingWriters: Array<{ index: number, id: string, user: { id: string, username: string, profile: { display_name: string, thumbnail: string | null } }, posts: Array<{ title: string, url_slug: string }> }> }; + + + +export const AdsDocument = ` + query ads($input: AdsInput!) { + ads(input: $input) { + id + title + body + image + url + start_date + } +} + `; + +export const useAdsQuery = < + TData = AdsQuery, + TError = unknown + >( + variables: AdsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['ads', variables], + queryFn: fetcher(AdsDocument, variables), + ...options + } + )}; + +useAdsQuery.getKey = (variables: AdsQueryVariables) => ['ads', variables]; + +export const useSuspenseAdsQuery = < + TData = AdsQuery, + TError = unknown + >( + variables: AdsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['adsSuspense', variables], + queryFn: fetcher(AdsDocument, variables), + ...options + } + )}; + +useSuspenseAdsQuery.getKey = (variables: AdsQueryVariables) => ['adsSuspense', variables]; + + +useAdsQuery.fetcher = (variables: AdsQueryVariables, options?: RequestInit['headers']) => fetcher(AdsDocument, variables, options); + +export const IsLoggedDocument = ` + query isLogged { + isLogged +} + `; + +export const useIsLoggedQuery = < + TData = IsLoggedQuery, + TError = unknown + >( + variables?: IsLoggedQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: variables === undefined ? ['isLogged'] : ['isLogged', variables], + queryFn: fetcher(IsLoggedDocument, variables), + ...options + } + )}; + +useIsLoggedQuery.getKey = (variables?: IsLoggedQueryVariables) => variables === undefined ? ['isLogged'] : ['isLogged', variables]; + +export const useSuspenseIsLoggedQuery = < + TData = IsLoggedQuery, + TError = unknown + >( + variables?: IsLoggedQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: variables === undefined ? ['isLoggedSuspense'] : ['isLoggedSuspense', variables], + queryFn: fetcher(IsLoggedDocument, variables), + ...options + } + )}; + +useSuspenseIsLoggedQuery.getKey = (variables?: IsLoggedQueryVariables) => variables === undefined ? ['isLoggedSuspense'] : ['isLoggedSuspense', variables]; + + +useIsLoggedQuery.fetcher = (variables?: IsLoggedQueryVariables, options?: RequestInit['headers']) => fetcher(IsLoggedDocument, variables, options); + +export const SendMailDocument = ` + mutation sendMail($input: SendMailInput!) { + sendMail(input: $input) { + registered + } +} + `; + +export const useSendMailMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['sendMail'], + mutationFn: (variables?: SendMailMutationVariables) => fetcher(SendMailDocument, variables)(), + ...options + } + )}; + +useSendMailMutation.getKey = () => ['sendMail']; + + +useSendMailMutation.fetcher = (variables: SendMailMutationVariables, options?: RequestInit['headers']) => fetcher(SendMailDocument, variables, options); + +export const FollowDocument = ` + mutation follow($input: FollowInput!) { + follow(input: $input) +} + `; + +export const useFollowMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['follow'], + mutationFn: (variables?: FollowMutationVariables) => fetcher(FollowDocument, variables)(), + ...options + } + )}; + +useFollowMutation.getKey = () => ['follow']; + + +useFollowMutation.fetcher = (variables: FollowMutationVariables, options?: RequestInit['headers']) => fetcher(FollowDocument, variables, options); + +export const UnfollowDocument = ` + mutation unfollow($input: UnfollowInput!) { + unfollow(input: $input) +} + `; + +export const useUnfollowMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['unfollow'], + mutationFn: (variables?: UnfollowMutationVariables) => fetcher(UnfollowDocument, variables)(), + ...options + } + )}; + +useUnfollowMutation.getKey = () => ['unfollow']; + + +useUnfollowMutation.fetcher = (variables: UnfollowMutationVariables, options?: RequestInit['headers']) => fetcher(UnfollowDocument, variables, options); + +export const GetFollowersDocument = ` + query getFollowers($input: GetFollowInput!) { + followers(input: $input) { + id + userId + username + profile { + short_bio + thumbnail + display_name + } + is_followed + } +} + `; + +export const useGetFollowersQuery = < + TData = GetFollowersQuery, + TError = unknown + >( + variables: GetFollowersQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['getFollowers', variables], + queryFn: fetcher(GetFollowersDocument, variables), + ...options + } + )}; + +useGetFollowersQuery.getKey = (variables: GetFollowersQueryVariables) => ['getFollowers', variables]; + +export const useSuspenseGetFollowersQuery = < + TData = GetFollowersQuery, + TError = unknown + >( + variables: GetFollowersQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['getFollowersSuspense', variables], + queryFn: fetcher(GetFollowersDocument, variables), + ...options + } + )}; + +useSuspenseGetFollowersQuery.getKey = (variables: GetFollowersQueryVariables) => ['getFollowersSuspense', variables]; + + +useGetFollowersQuery.fetcher = (variables: GetFollowersQueryVariables, options?: RequestInit['headers']) => fetcher(GetFollowersDocument, variables, options); + +export const GetFollowingsDocument = ` + query getFollowings($input: GetFollowInput!) { + followings(input: $input) { + id + userId + username + profile { + short_bio + thumbnail + display_name + } + is_followed + } +} + `; + +export const useGetFollowingsQuery = < + TData = GetFollowingsQuery, + TError = unknown + >( + variables: GetFollowingsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['getFollowings', variables], + queryFn: fetcher(GetFollowingsDocument, variables), + ...options + } + )}; + +useGetFollowingsQuery.getKey = (variables: GetFollowingsQueryVariables) => ['getFollowings', variables]; + +export const useSuspenseGetFollowingsQuery = < + TData = GetFollowingsQuery, + TError = unknown + >( + variables: GetFollowingsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['getFollowingsSuspense', variables], + queryFn: fetcher(GetFollowingsDocument, variables), + ...options + } + )}; + +useSuspenseGetFollowingsQuery.getKey = (variables: GetFollowingsQueryVariables) => ['getFollowingsSuspense', variables]; + + +useGetFollowingsQuery.fetcher = (variables: GetFollowingsQueryVariables, options?: RequestInit['headers']) => fetcher(GetFollowingsDocument, variables, options); + +export const NotificationDocument = ` + query notification($input: NotificationsInput!) { + notifications(input: $input) { + id + type + action + actor_id + action_id + is_read + created_at + } +} + `; + +export const useNotificationQuery = < + TData = NotificationQuery, + TError = unknown + >( + variables: NotificationQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['notification', variables], + queryFn: fetcher(NotificationDocument, variables), + ...options + } + )}; + +useNotificationQuery.getKey = (variables: NotificationQueryVariables) => ['notification', variables]; + +export const useSuspenseNotificationQuery = < + TData = NotificationQuery, + TError = unknown + >( + variables: NotificationQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['notificationSuspense', variables], + queryFn: fetcher(NotificationDocument, variables), + ...options + } + )}; + +useSuspenseNotificationQuery.getKey = (variables: NotificationQueryVariables) => ['notificationSuspense', variables]; + + +useNotificationQuery.fetcher = (variables: NotificationQueryVariables, options?: RequestInit['headers']) => fetcher(NotificationDocument, variables, options); + +export const NotNoticeNotificationCountDocument = ` + query notNoticeNotificationCount { + notNoticeNotificationCount +} + `; + +export const useNotNoticeNotificationCountQuery = < + TData = NotNoticeNotificationCountQuery, + TError = unknown + >( + variables?: NotNoticeNotificationCountQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: variables === undefined ? ['notNoticeNotificationCount'] : ['notNoticeNotificationCount', variables], + queryFn: fetcher(NotNoticeNotificationCountDocument, variables), + ...options + } + )}; + +useNotNoticeNotificationCountQuery.getKey = (variables?: NotNoticeNotificationCountQueryVariables) => variables === undefined ? ['notNoticeNotificationCount'] : ['notNoticeNotificationCount', variables]; + +export const useSuspenseNotNoticeNotificationCountQuery = < + TData = NotNoticeNotificationCountQuery, + TError = unknown + >( + variables?: NotNoticeNotificationCountQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: variables === undefined ? ['notNoticeNotificationCountSuspense'] : ['notNoticeNotificationCountSuspense', variables], + queryFn: fetcher(NotNoticeNotificationCountDocument, variables), + ...options + } + )}; + +useSuspenseNotNoticeNotificationCountQuery.getKey = (variables?: NotNoticeNotificationCountQueryVariables) => variables === undefined ? ['notNoticeNotificationCountSuspense'] : ['notNoticeNotificationCountSuspense', variables]; + + +useNotNoticeNotificationCountQuery.fetcher = (variables?: NotNoticeNotificationCountQueryVariables, options?: RequestInit['headers']) => fetcher(NotNoticeNotificationCountDocument, variables, options); + +export const ReadAllNotificationsDocument = ` + mutation readAllNotifications { + readAllNotifications +} + `; + +export const useReadAllNotificationsMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['readAllNotifications'], + mutationFn: (variables?: ReadAllNotificationsMutationVariables) => fetcher(ReadAllNotificationsDocument, variables)(), + ...options + } + )}; + +useReadAllNotificationsMutation.getKey = () => ['readAllNotifications']; + + +useReadAllNotificationsMutation.fetcher = (variables?: ReadAllNotificationsMutationVariables, options?: RequestInit['headers']) => fetcher(ReadAllNotificationsDocument, variables, options); + +export const RemoveAllNotificationsDocument = ` + mutation removeAllNotifications { + removeAllNotifications +} + `; + +export const useRemoveAllNotificationsMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['removeAllNotifications'], + mutationFn: (variables?: RemoveAllNotificationsMutationVariables) => fetcher(RemoveAllNotificationsDocument, variables)(), + ...options + } + )}; + +useRemoveAllNotificationsMutation.getKey = () => ['removeAllNotifications']; + + +useRemoveAllNotificationsMutation.fetcher = (variables?: RemoveAllNotificationsMutationVariables, options?: RequestInit['headers']) => fetcher(RemoveAllNotificationsDocument, variables, options); + +export const ReadNoticationDocument = ` + mutation readNotication($input: ReadNotificationInput!) { + readNotification(input: $input) +} + `; + +export const useReadNoticationMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['readNotication'], + mutationFn: (variables?: ReadNoticationMutationVariables) => fetcher(ReadNoticationDocument, variables)(), + ...options + } + )}; + +useReadNoticationMutation.getKey = () => ['readNotication']; + + +useReadNoticationMutation.fetcher = (variables: ReadNoticationMutationVariables, options?: RequestInit['headers']) => fetcher(ReadNoticationDocument, variables, options); + +export const UpdateNotNoticeNotificationDocument = ` + mutation updateNotNoticeNotification { + updateNotNoticeNotification +} + `; + +export const useUpdateNotNoticeNotificationMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['updateNotNoticeNotification'], + mutationFn: (variables?: UpdateNotNoticeNotificationMutationVariables) => fetcher(UpdateNotNoticeNotificationDocument, variables)(), + ...options + } + )}; + +useUpdateNotNoticeNotificationMutation.getKey = () => ['updateNotNoticeNotification']; + + +useUpdateNotNoticeNotificationMutation.fetcher = (variables?: UpdateNotNoticeNotificationMutationVariables, options?: RequestInit['headers']) => fetcher(UpdateNotNoticeNotificationDocument, variables, options); + +export const ReadPostDocument = ` + query readPost($input: ReadPostInput!) { + post(input: $input) { + id + title + released_at + updated_at + body + short_description + is_markdown + is_private + is_temp + thumbnail + comments_count + url_slug + likes + is_liked + is_followed + user { + id + username + profile { + id + display_name + thumbnail + short_bio + profile_links + } + velog_config { + title + } + } + comments { + id + user { + id + username + profile { + id + thumbnail + } + } + text + replies_count + level + created_at + level + deleted + } + series { + id + name + url_slug + series_posts { + id + post { + id + title + url_slug + user { + id + username + } + } + } + } + linked_posts { + previous { + id + title + url_slug + user { + id + username + } + } + next { + id + title + url_slug + user { + id + username + } + } + } + } +} + `; + +export const useReadPostQuery = < + TData = ReadPostQuery, + TError = unknown + >( + variables: ReadPostQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['readPost', variables], + queryFn: fetcher(ReadPostDocument, variables), + ...options + } + )}; + +useReadPostQuery.getKey = (variables: ReadPostQueryVariables) => ['readPost', variables]; + +export const useSuspenseReadPostQuery = < + TData = ReadPostQuery, + TError = unknown + >( + variables: ReadPostQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['readPostSuspense', variables], + queryFn: fetcher(ReadPostDocument, variables), + ...options + } + )}; + +useSuspenseReadPostQuery.getKey = (variables: ReadPostQueryVariables) => ['readPostSuspense', variables]; + + +useReadPostQuery.fetcher = (variables: ReadPostQueryVariables, options?: RequestInit['headers']) => fetcher(ReadPostDocument, variables, options); + +export const RecentPostsDocument = ` + query recentPosts($input: RecentPostsInput!) { + recentPosts(input: $input) { + id + title + short_description + thumbnail + user { + id + username + profile { + id + thumbnail + display_name + } + } + url_slug + released_at + updated_at + is_private + likes + comments_count + } +} + `; + +export const useRecentPostsQuery = < + TData = RecentPostsQuery, + TError = unknown + >( + variables: RecentPostsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['recentPosts', variables], + queryFn: fetcher(RecentPostsDocument, variables), + ...options + } + )}; + +useRecentPostsQuery.getKey = (variables: RecentPostsQueryVariables) => ['recentPosts', variables]; + +export const useSuspenseRecentPostsQuery = < + TData = RecentPostsQuery, + TError = unknown + >( + variables: RecentPostsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['recentPostsSuspense', variables], + queryFn: fetcher(RecentPostsDocument, variables), + ...options + } + )}; + +useSuspenseRecentPostsQuery.getKey = (variables: RecentPostsQueryVariables) => ['recentPostsSuspense', variables]; + + +useRecentPostsQuery.fetcher = (variables: RecentPostsQueryVariables, options?: RequestInit['headers']) => fetcher(RecentPostsDocument, variables, options); + +export const TrendingPostsDocument = ` + query trendingPosts($input: TrendingPostsInput!) { + trendingPosts(input: $input) { + id + title + short_description + thumbnail + likes + user { + id + username + profile { + id + thumbnail + display_name + } + } + url_slug + released_at + updated_at + is_private + comments_count + } +} + `; + +export const useTrendingPostsQuery = < + TData = TrendingPostsQuery, + TError = unknown + >( + variables: TrendingPostsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['trendingPosts', variables], + queryFn: fetcher(TrendingPostsDocument, variables), + ...options + } + )}; + +useTrendingPostsQuery.getKey = (variables: TrendingPostsQueryVariables) => ['trendingPosts', variables]; + +export const useSuspenseTrendingPostsQuery = < + TData = TrendingPostsQuery, + TError = unknown + >( + variables: TrendingPostsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['trendingPostsSuspense', variables], + queryFn: fetcher(TrendingPostsDocument, variables), + ...options + } + )}; + +useSuspenseTrendingPostsQuery.getKey = (variables: TrendingPostsQueryVariables) => ['trendingPostsSuspense', variables]; + + +useTrendingPostsQuery.fetcher = (variables: TrendingPostsQueryVariables, options?: RequestInit['headers']) => fetcher(TrendingPostsDocument, variables, options); + +export const FeedPostsDocument = ` + query feedPosts($input: FeedPostsInput!) { + feedPosts(input: $input) { + id + title + short_description + thumbnail + likes + user { + id + username + profile { + id + thumbnail + display_name + } + } + url_slug + released_at + updated_at + is_private + comments_count + } +} + `; + +export const useFeedPostsQuery = < + TData = FeedPostsQuery, + TError = unknown + >( + variables: FeedPostsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['feedPosts', variables], + queryFn: fetcher(FeedPostsDocument, variables), + ...options + } + )}; + +useFeedPostsQuery.getKey = (variables: FeedPostsQueryVariables) => ['feedPosts', variables]; + +export const useSuspenseFeedPostsQuery = < + TData = FeedPostsQuery, + TError = unknown + >( + variables: FeedPostsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['feedPostsSuspense', variables], + queryFn: fetcher(FeedPostsDocument, variables), + ...options + } + )}; + +useSuspenseFeedPostsQuery.getKey = (variables: FeedPostsQueryVariables) => ['feedPostsSuspense', variables]; + + +useFeedPostsQuery.fetcher = (variables: FeedPostsQueryVariables, options?: RequestInit['headers']) => fetcher(FeedPostsDocument, variables, options); + +export const VelogPostsDocument = ` + query velogPosts($input: GetPostsInput!) { + posts(input: $input) { + id + title + short_description + thumbnail + user { + id + username + profile { + id + thumbnail + display_name + } + } + url_slug + released_at + updated_at + comments_count + tags + is_private + likes + } +} + `; + +export const useVelogPostsQuery = < + TData = VelogPostsQuery, + TError = unknown + >( + variables: VelogPostsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['velogPosts', variables], + queryFn: fetcher(VelogPostsDocument, variables), + ...options + } + )}; + +useVelogPostsQuery.getKey = (variables: VelogPostsQueryVariables) => ['velogPosts', variables]; + +export const useSuspenseVelogPostsQuery = < + TData = VelogPostsQuery, + TError = unknown + >( + variables: VelogPostsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['velogPostsSuspense', variables], + queryFn: fetcher(VelogPostsDocument, variables), + ...options + } + )}; + +useSuspenseVelogPostsQuery.getKey = (variables: VelogPostsQueryVariables) => ['velogPostsSuspense', variables]; + + +useVelogPostsQuery.fetcher = (variables: VelogPostsQueryVariables, options?: RequestInit['headers']) => fetcher(VelogPostsDocument, variables, options); + +export const SearchPostsDocument = ` + query searchPosts($input: GetSearchPostsInput!) { + searchPosts(input: $input) { + count + posts { + id + title + short_description + thumbnail + user { + id + username + profile { + id + thumbnail + display_name + } + } + url_slug + released_at + tags + is_private + comments_count + } + } +} + `; + +export const useSearchPostsQuery = < + TData = SearchPostsQuery, + TError = unknown + >( + variables: SearchPostsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['searchPosts', variables], + queryFn: fetcher(SearchPostsDocument, variables), + ...options + } + )}; + +useSearchPostsQuery.getKey = (variables: SearchPostsQueryVariables) => ['searchPosts', variables]; + +export const useSuspenseSearchPostsQuery = < + TData = SearchPostsQuery, + TError = unknown + >( + variables: SearchPostsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['searchPostsSuspense', variables], + queryFn: fetcher(SearchPostsDocument, variables), + ...options + } + )}; + +useSuspenseSearchPostsQuery.getKey = (variables: SearchPostsQueryVariables) => ['searchPostsSuspense', variables]; + + +useSearchPostsQuery.fetcher = (variables: SearchPostsQueryVariables, options?: RequestInit['headers']) => fetcher(SearchPostsDocument, variables, options); + +export const UserTagsDocument = ` + query userTags($input: UserTagsInput!) { + userTags(input: $input) { + tags { + id + name + description + posts_count + thumbnail + } + posts_count + } +} + `; + +export const useUserTagsQuery = < + TData = UserTagsQuery, + TError = unknown + >( + variables: UserTagsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['userTags', variables], + queryFn: fetcher(UserTagsDocument, variables), + ...options + } + )}; + +useUserTagsQuery.getKey = (variables: UserTagsQueryVariables) => ['userTags', variables]; + +export const useSuspenseUserTagsQuery = < + TData = UserTagsQuery, + TError = unknown + >( + variables: UserTagsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['userTagsSuspense', variables], + queryFn: fetcher(UserTagsDocument, variables), + ...options + } + )}; + +useSuspenseUserTagsQuery.getKey = (variables: UserTagsQueryVariables) => ['userTagsSuspense', variables]; + + +useUserTagsQuery.fetcher = (variables: UserTagsQueryVariables, options?: RequestInit['headers']) => fetcher(UserTagsDocument, variables, options); + +export const GetUserDocument = ` + query getUser($input: GetUserInput!) { + user(input: $input) { + id + username + profile { + id + display_name + short_bio + thumbnail + profile_links + } + } +} + `; + +export const useGetUserQuery = < + TData = GetUserQuery, + TError = unknown + >( + variables: GetUserQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['getUser', variables], + queryFn: fetcher(GetUserDocument, variables), + ...options + } + )}; + +useGetUserQuery.getKey = (variables: GetUserQueryVariables) => ['getUser', variables]; + +export const useSuspenseGetUserQuery = < + TData = GetUserQuery, + TError = unknown + >( + variables: GetUserQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['getUserSuspense', variables], + queryFn: fetcher(GetUserDocument, variables), + ...options + } + )}; + +useSuspenseGetUserQuery.getKey = (variables: GetUserQueryVariables) => ['getUserSuspense', variables]; + + +useGetUserQuery.fetcher = (variables: GetUserQueryVariables, options?: RequestInit['headers']) => fetcher(GetUserDocument, variables, options); + +export const GetUserFollowInfoDocument = ` + query getUserFollowInfo($input: GetUserInput!) { + user(input: $input) { + id + username + profile { + id + display_name + short_bio + thumbnail + profile_links + } + followers_count + followings_count + is_followed + } +} + `; + +export const useGetUserFollowInfoQuery = < + TData = GetUserFollowInfoQuery, + TError = unknown + >( + variables: GetUserFollowInfoQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['getUserFollowInfo', variables], + queryFn: fetcher(GetUserFollowInfoDocument, variables), + ...options + } + )}; + +useGetUserFollowInfoQuery.getKey = (variables: GetUserFollowInfoQueryVariables) => ['getUserFollowInfo', variables]; + +export const useSuspenseGetUserFollowInfoQuery = < + TData = GetUserFollowInfoQuery, + TError = unknown + >( + variables: GetUserFollowInfoQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['getUserFollowInfoSuspense', variables], + queryFn: fetcher(GetUserFollowInfoDocument, variables), + ...options + } + )}; + +useSuspenseGetUserFollowInfoQuery.getKey = (variables: GetUserFollowInfoQueryVariables) => ['getUserFollowInfoSuspense', variables]; + + +useGetUserFollowInfoQuery.fetcher = (variables: GetUserFollowInfoQueryVariables, options?: RequestInit['headers']) => fetcher(GetUserFollowInfoDocument, variables, options); + +export const CurrentUserDocument = ` + query currentUser { + currentUser { + id + username + email + profile { + id + thumbnail + display_name + short_bio + profile_links + } + user_meta { + id + email_notification + email_promotion + } + } +} + `; + +export const useCurrentUserQuery = < + TData = CurrentUserQuery, + TError = unknown + >( + variables?: CurrentUserQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: variables === undefined ? ['currentUser'] : ['currentUser', variables], + queryFn: fetcher(CurrentUserDocument, variables), + ...options + } + )}; + +useCurrentUserQuery.getKey = (variables?: CurrentUserQueryVariables) => variables === undefined ? ['currentUser'] : ['currentUser', variables]; + +export const useSuspenseCurrentUserQuery = < + TData = CurrentUserQuery, + TError = unknown + >( + variables?: CurrentUserQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: variables === undefined ? ['currentUserSuspense'] : ['currentUserSuspense', variables], + queryFn: fetcher(CurrentUserDocument, variables), + ...options + } + )}; + +useSuspenseCurrentUserQuery.getKey = (variables?: CurrentUserQueryVariables) => variables === undefined ? ['currentUserSuspense'] : ['currentUserSuspense', variables]; + + +useCurrentUserQuery.fetcher = (variables?: CurrentUserQueryVariables, options?: RequestInit['headers']) => fetcher(CurrentUserDocument, variables, options); + +export const VelogConfigDocument = ` + query velogConfig($input: GetVelogConfigInput!) { + velogConfig(input: $input) { + title + logo_image + } +} + `; + +export const useVelogConfigQuery = < + TData = VelogConfigQuery, + TError = unknown + >( + variables: VelogConfigQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['velogConfig', variables], + queryFn: fetcher(VelogConfigDocument, variables), + ...options + } + )}; + +useVelogConfigQuery.getKey = (variables: VelogConfigQueryVariables) => ['velogConfig', variables]; + +export const useSuspenseVelogConfigQuery = < + TData = VelogConfigQuery, + TError = unknown + >( + variables: VelogConfigQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['velogConfigSuspense', variables], + queryFn: fetcher(VelogConfigDocument, variables), + ...options + } + )}; + +useSuspenseVelogConfigQuery.getKey = (variables: VelogConfigQueryVariables) => ['velogConfigSuspense', variables]; + + +useVelogConfigQuery.fetcher = (variables: VelogConfigQueryVariables, options?: RequestInit['headers']) => fetcher(VelogConfigDocument, variables, options); + +export const GetUserAboutDocument = ` + query getUserAbout($input: GetUserInput!) { + user(input: $input) { + id + profile { + id + about + display_name + } + } +} + `; + +export const useGetUserAboutQuery = < + TData = GetUserAboutQuery, + TError = unknown + >( + variables: GetUserAboutQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['getUserAbout', variables], + queryFn: fetcher(GetUserAboutDocument, variables), + ...options + } + )}; + +useGetUserAboutQuery.getKey = (variables: GetUserAboutQueryVariables) => ['getUserAbout', variables]; + +export const useSuspenseGetUserAboutQuery = < + TData = GetUserAboutQuery, + TError = unknown + >( + variables: GetUserAboutQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['getUserAboutSuspense', variables], + queryFn: fetcher(GetUserAboutDocument, variables), + ...options + } + )}; + +useSuspenseGetUserAboutQuery.getKey = (variables: GetUserAboutQueryVariables) => ['getUserAboutSuspense', variables]; + + +useGetUserAboutQuery.fetcher = (variables: GetUserAboutQueryVariables, options?: RequestInit['headers']) => fetcher(GetUserAboutDocument, variables, options); + +export const GetUserSeriesListDocument = ` + query getUserSeriesList($input: GetUserInput!) { + user(input: $input) { + id + series_list { + id + name + description + url_slug + thumbnail + updated_at + posts_count + } + } +} + `; + +export const useGetUserSeriesListQuery = < + TData = GetUserSeriesListQuery, + TError = unknown + >( + variables: GetUserSeriesListQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['getUserSeriesList', variables], + queryFn: fetcher(GetUserSeriesListDocument, variables), + ...options + } + )}; + +useGetUserSeriesListQuery.getKey = (variables: GetUserSeriesListQueryVariables) => ['getUserSeriesList', variables]; + +export const useSuspenseGetUserSeriesListQuery = < + TData = GetUserSeriesListQuery, + TError = unknown + >( + variables: GetUserSeriesListQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['getUserSeriesListSuspense', variables], + queryFn: fetcher(GetUserSeriesListDocument, variables), + ...options + } + )}; + +useSuspenseGetUserSeriesListQuery.getKey = (variables: GetUserSeriesListQueryVariables) => ['getUserSeriesListSuspense', variables]; + + +useGetUserSeriesListQuery.fetcher = (variables: GetUserSeriesListQueryVariables, options?: RequestInit['headers']) => fetcher(GetUserSeriesListDocument, variables, options); + +export const UnregisterTokenDocument = ` + query unregisterToken { + unregisterToken +} + `; + +export const useUnregisterTokenQuery = < + TData = UnregisterTokenQuery, + TError = unknown + >( + variables?: UnregisterTokenQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: variables === undefined ? ['unregisterToken'] : ['unregisterToken', variables], + queryFn: fetcher(UnregisterTokenDocument, variables), + ...options + } + )}; + +useUnregisterTokenQuery.getKey = (variables?: UnregisterTokenQueryVariables) => variables === undefined ? ['unregisterToken'] : ['unregisterToken', variables]; + +export const useSuspenseUnregisterTokenQuery = < + TData = UnregisterTokenQuery, + TError = unknown + >( + variables?: UnregisterTokenQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: variables === undefined ? ['unregisterTokenSuspense'] : ['unregisterTokenSuspense', variables], + queryFn: fetcher(UnregisterTokenDocument, variables), + ...options + } + )}; + +useSuspenseUnregisterTokenQuery.getKey = (variables?: UnregisterTokenQueryVariables) => variables === undefined ? ['unregisterTokenSuspense'] : ['unregisterTokenSuspense', variables]; + + +useUnregisterTokenQuery.fetcher = (variables?: UnregisterTokenQueryVariables, options?: RequestInit['headers']) => fetcher(UnregisterTokenDocument, variables, options); + +export const CheckEmailExistsDocument = ` + query checkEmailExists($input: CheckEmailExistsInput!) { + checkEmailExists(input: $input) +} + `; + +export const useCheckEmailExistsQuery = < + TData = CheckEmailExistsQuery, + TError = unknown + >( + variables: CheckEmailExistsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['checkEmailExists', variables], + queryFn: fetcher(CheckEmailExistsDocument, variables), + ...options + } + )}; + +useCheckEmailExistsQuery.getKey = (variables: CheckEmailExistsQueryVariables) => ['checkEmailExists', variables]; + +export const useSuspenseCheckEmailExistsQuery = < + TData = CheckEmailExistsQuery, + TError = unknown + >( + variables: CheckEmailExistsQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['checkEmailExistsSuspense', variables], + queryFn: fetcher(CheckEmailExistsDocument, variables), + ...options + } + )}; + +useSuspenseCheckEmailExistsQuery.getKey = (variables: CheckEmailExistsQueryVariables) => ['checkEmailExistsSuspense', variables]; + + +useCheckEmailExistsQuery.fetcher = (variables: CheckEmailExistsQueryVariables, options?: RequestInit['headers']) => fetcher(CheckEmailExistsDocument, variables, options); + +export const UpdateAboutDocument = ` + mutation updateAbout($input: UpdateAboutInput!) { + updateAbout(input: $input) { + id + about + } +} + `; + +export const useUpdateAboutMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['updateAbout'], + mutationFn: (variables?: UpdateAboutMutationVariables) => fetcher(UpdateAboutDocument, variables)(), + ...options + } + )}; + +useUpdateAboutMutation.getKey = () => ['updateAbout']; + + +useUpdateAboutMutation.fetcher = (variables: UpdateAboutMutationVariables, options?: RequestInit['headers']) => fetcher(UpdateAboutDocument, variables, options); + +export const UpdateThumbnailDocument = ` + mutation updateThumbnail($input: UpdateThumbnailInput!) { + updateThumbnail(input: $input) { + id + thumbnail + } +} + `; + +export const useUpdateThumbnailMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['updateThumbnail'], + mutationFn: (variables?: UpdateThumbnailMutationVariables) => fetcher(UpdateThumbnailDocument, variables)(), + ...options + } + )}; + +useUpdateThumbnailMutation.getKey = () => ['updateThumbnail']; + + +useUpdateThumbnailMutation.fetcher = (variables: UpdateThumbnailMutationVariables, options?: RequestInit['headers']) => fetcher(UpdateThumbnailDocument, variables, options); + +export const UpdateProfileDocument = ` + mutation updateProfile($input: UpdateProfileInput!) { + updateProfile(input: $input) { + id + display_name + short_bio + } +} + `; + +export const useUpdateProfileMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['updateProfile'], + mutationFn: (variables?: UpdateProfileMutationVariables) => fetcher(UpdateProfileDocument, variables)(), + ...options + } + )}; + +useUpdateProfileMutation.getKey = () => ['updateProfile']; + + +useUpdateProfileMutation.fetcher = (variables: UpdateProfileMutationVariables, options?: RequestInit['headers']) => fetcher(UpdateProfileDocument, variables, options); + +export const UpdateVelogTitleDocument = ` + mutation updateVelogTitle($input: UpdateVelogTitleInput!) { + updateVelogTitle(input: $input) { + id + title + } +} + `; + +export const useUpdateVelogTitleMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['updateVelogTitle'], + mutationFn: (variables?: UpdateVelogTitleMutationVariables) => fetcher(UpdateVelogTitleDocument, variables)(), + ...options + } + )}; + +useUpdateVelogTitleMutation.getKey = () => ['updateVelogTitle']; + + +useUpdateVelogTitleMutation.fetcher = (variables: UpdateVelogTitleMutationVariables, options?: RequestInit['headers']) => fetcher(UpdateVelogTitleDocument, variables, options); + +export const UpdateSocialInfoDocument = ` + mutation updateSocialInfo($input: UpdateSocialInfoInput!) { + updateSocialInfo(input: $input) { + id + profile_links + } +} + `; + +export const useUpdateSocialInfoMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['updateSocialInfo'], + mutationFn: (variables?: UpdateSocialInfoMutationVariables) => fetcher(UpdateSocialInfoDocument, variables)(), + ...options + } + )}; + +useUpdateSocialInfoMutation.getKey = () => ['updateSocialInfo']; + + +useUpdateSocialInfoMutation.fetcher = (variables: UpdateSocialInfoMutationVariables, options?: RequestInit['headers']) => fetcher(UpdateSocialInfoDocument, variables, options); + +export const UpdateEmailRulesDocument = ` + mutation updateEmailRules($input: UpdateEmailRulesInput!) { + updateEmailRules(input: $input) { + email_notification + email_promotion + } +} + `; + +export const useUpdateEmailRulesMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['updateEmailRules'], + mutationFn: (variables?: UpdateEmailRulesMutationVariables) => fetcher(UpdateEmailRulesDocument, variables)(), + ...options + } + )}; + +useUpdateEmailRulesMutation.getKey = () => ['updateEmailRules']; + + +useUpdateEmailRulesMutation.fetcher = (variables: UpdateEmailRulesMutationVariables, options?: RequestInit['headers']) => fetcher(UpdateEmailRulesDocument, variables, options); + +export const UnregisterDocument = ` + mutation unregister($input: UnregisterInput!) { + unregister(input: $input) +} + `; + +export const useUnregisterMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['unregister'], + mutationFn: (variables?: UnregisterMutationVariables) => fetcher(UnregisterDocument, variables)(), + ...options + } + )}; + +useUnregisterMutation.getKey = () => ['unregister']; + + +useUnregisterMutation.fetcher = (variables: UnregisterMutationVariables, options?: RequestInit['headers']) => fetcher(UnregisterDocument, variables, options); + +export const InitiateChangeEmailDocument = ` + mutation initiateChangeEmail($input: InitiateChangeEmailInput!) { + initiateChangeEmail(input: $input) +} + `; + +export const useInitiateChangeEmailMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['initiateChangeEmail'], + mutationFn: (variables?: InitiateChangeEmailMutationVariables) => fetcher(InitiateChangeEmailDocument, variables)(), + ...options + } + )}; + +useInitiateChangeEmailMutation.getKey = () => ['initiateChangeEmail']; + + +useInitiateChangeEmailMutation.fetcher = (variables: InitiateChangeEmailMutationVariables, options?: RequestInit['headers']) => fetcher(InitiateChangeEmailDocument, variables, options); + +export const ConfirmChangeEmailDocument = ` + mutation confirmChangeEmail($input: ConfirmChangeEmailInput!) { + confirmChangeEmail(input: $input) +} + `; + +export const useConfirmChangeEmailMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['confirmChangeEmail'], + mutationFn: (variables?: ConfirmChangeEmailMutationVariables) => fetcher(ConfirmChangeEmailDocument, variables)(), + ...options + } + )}; + +useConfirmChangeEmailMutation.getKey = () => ['confirmChangeEmail']; + + +useConfirmChangeEmailMutation.fetcher = (variables: ConfirmChangeEmailMutationVariables, options?: RequestInit['headers']) => fetcher(ConfirmChangeEmailDocument, variables, options); + +export const LogoutDocument = ` + mutation logout { + logout +} + `; + +export const useLogoutMutation = < + TError = unknown, + TContext = unknown + >(options?: UseMutationOptions) => { + + return useMutation( + { + mutationKey: ['logout'], + mutationFn: (variables?: LogoutMutationVariables) => fetcher(LogoutDocument, variables)(), + ...options + } + )}; + +useLogoutMutation.getKey = () => ['logout']; + + +useLogoutMutation.fetcher = (variables?: LogoutMutationVariables, options?: RequestInit['headers']) => fetcher(LogoutDocument, variables, options); + +export const TrendingWritersDocument = ` + query trendingWriters($input: TrendingWritersInput!) { + trendingWriters(input: $input) { + index + id + user { + id + username + profile { + display_name + thumbnail + } + } + posts { + title + url_slug + } + } +} + `; + +export const useTrendingWritersQuery = < + TData = TrendingWritersQuery, + TError = unknown + >( + variables: TrendingWritersQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseQueryOptions['queryKey'] } + ) => { + + return useQuery( + { + queryKey: ['trendingWriters', variables], + queryFn: fetcher(TrendingWritersDocument, variables), + ...options + } + )}; + +useTrendingWritersQuery.getKey = (variables: TrendingWritersQueryVariables) => ['trendingWriters', variables]; + +export const useSuspenseTrendingWritersQuery = < + TData = TrendingWritersQuery, + TError = unknown + >( + variables: TrendingWritersQueryVariables, + options?: Omit, 'queryKey'> & { queryKey?: UseSuspenseQueryOptions['queryKey'] } + ) => { + + return useSuspenseQuery( + { + queryKey: ['trendingWritersSuspense', variables], + queryFn: fetcher(TrendingWritersDocument, variables), + ...options + } + )}; + +useSuspenseTrendingWritersQuery.getKey = (variables: TrendingWritersQueryVariables) => ['trendingWritersSuspense', variables]; + + +useTrendingWritersQuery.fetcher = (variables: TrendingWritersQueryVariables, options?: RequestInit['headers']) => fetcher(TrendingWritersDocument, variables, options); diff --git a/packages/velog-web/src/graphql/helpers/queryKey.ts b/apps/web/src/graphql/server/helpers/queryKey.ts similarity index 97% rename from packages/velog-web/src/graphql/helpers/queryKey.ts rename to apps/web/src/graphql/server/helpers/queryKey.ts index da4da0ff..60c08213 100644 --- a/packages/velog-web/src/graphql/helpers/queryKey.ts +++ b/apps/web/src/graphql/server/helpers/queryKey.ts @@ -7,7 +7,7 @@ import { TrendingPostsQueryVariables, TrendingWritersQueryVariables, VelogPostsQueryVariables, -} from './generated' +} from '../generated/server' export const infiniteTrendingPostsQueryKey = (variables: TrendingPostsQueryVariables) => [ 'trendingPosts.infinite', diff --git a/packages/velog-web/src/graphql/helpers/fetcher.ts b/apps/web/src/graphql/server/helpers/serverFetcher.ts similarity index 90% rename from packages/velog-web/src/graphql/helpers/fetcher.ts rename to apps/web/src/graphql/server/helpers/serverFetcher.ts index 064aab9b..ee254ef5 100644 --- a/packages/velog-web/src/graphql/helpers/fetcher.ts +++ b/apps/web/src/graphql/server/helpers/serverFetcher.ts @@ -8,7 +8,7 @@ export function fetcher>( ) { return async (): Promise => { const data = await graphqlFetch({ - url: `${ENV.graphqlHost}/graphql`, + url: `${ENV.graphqlServerHost}/graphql`, method: 'POST', body: { query, variables: variables ?? {} }, headers: { diff --git a/packages/velog-web/src/graphql/notification.gql b/apps/web/src/graphql/server/notification.gql similarity index 100% rename from packages/velog-web/src/graphql/notification.gql rename to apps/web/src/graphql/server/notification.gql diff --git a/packages/velog-web/src/graphql/post.gql b/apps/web/src/graphql/server/post.gql similarity index 100% rename from packages/velog-web/src/graphql/post.gql rename to apps/web/src/graphql/server/post.gql diff --git a/packages/velog-web/src/graphql/tag.gql b/apps/web/src/graphql/server/tag.gql similarity index 100% rename from packages/velog-web/src/graphql/tag.gql rename to apps/web/src/graphql/server/tag.gql diff --git a/packages/velog-web/src/graphql/user.gql b/apps/web/src/graphql/server/user.gql similarity index 100% rename from packages/velog-web/src/graphql/user.gql rename to apps/web/src/graphql/server/user.gql diff --git a/packages/velog-web/src/graphql/writer.gql b/apps/web/src/graphql/server/writer.gql similarity index 100% rename from packages/velog-web/src/graphql/writer.gql rename to apps/web/src/graphql/server/writer.gql diff --git a/packages/velog-web/src/hooks/useAdblockDetect.ts b/apps/web/src/hooks/useAdblockDetect.ts similarity index 100% rename from packages/velog-web/src/hooks/useAdblockDetect.ts rename to apps/web/src/hooks/useAdblockDetect.ts diff --git a/packages/velog-web/src/hooks/useCFUpload.ts b/apps/web/src/hooks/useCFUpload.ts similarity index 96% rename from packages/velog-web/src/hooks/useCFUpload.ts rename to apps/web/src/hooks/useCFUpload.ts index 2ab842d2..f5f19ece 100644 --- a/packages/velog-web/src/hooks/useCFUpload.ts +++ b/apps/web/src/hooks/useCFUpload.ts @@ -1,3 +1,5 @@ +'use client' + import { useState } from 'react' import { UploadImageArgs, uploadImage } from '@/lib/api/files' diff --git a/packages/velog-web/src/hooks/useCurrentPath.ts b/apps/web/src/hooks/useCurrentPath.ts similarity index 91% rename from packages/velog-web/src/hooks/useCurrentPath.ts rename to apps/web/src/hooks/useCurrentPath.ts index 37945cc9..89fa0414 100644 --- a/packages/velog-web/src/hooks/useCurrentPath.ts +++ b/apps/web/src/hooks/useCurrentPath.ts @@ -8,7 +8,7 @@ export default function useCurrentPath() { const search = useSearchParams() const currentPath = useMemo(() => { - const query = search.toString() + const query = search?.toString() return `${pathname === '/' ? '' : pathname}${query ? `?${query}` : ''}` }, [pathname, search]) diff --git a/packages/velog-web/src/hooks/useCustomInfiniteQuery.ts b/apps/web/src/hooks/useCustomInfiniteQuery.ts similarity index 91% rename from packages/velog-web/src/hooks/useCustomInfiniteQuery.ts rename to apps/web/src/hooks/useCustomInfiniteQuery.ts index 6ee8b59a..25ecd062 100644 --- a/packages/velog-web/src/hooks/useCustomInfiniteQuery.ts +++ b/apps/web/src/hooks/useCustomInfiniteQuery.ts @@ -1,7 +1,7 @@ 'use client' -import { fetcher } from '@/graphql/helpers/fetcher' -import { Exact } from '@/graphql/helpers/generated' +import { fetcher } from '@/graphql/server/helpers/serverFetcher' +import { Exact } from '@/graphql/server/generated/server' import { InfiniteData, UndefinedInitialDataInfiniteOptions, diff --git a/packages/velog-web/src/hooks/useGtag.ts b/apps/web/src/hooks/useGtag.ts similarity index 100% rename from packages/velog-web/src/hooks/useGtag.ts rename to apps/web/src/hooks/useGtag.ts diff --git a/packages/velog-web/src/hooks/useInfiniteScroll.ts b/apps/web/src/hooks/useInfiniteScroll.ts similarity index 100% rename from packages/velog-web/src/hooks/useInfiniteScroll.ts rename to apps/web/src/hooks/useInfiniteScroll.ts diff --git a/packages/velog-web/src/hooks/useInput.ts b/apps/web/src/hooks/useInput.ts similarity index 100% rename from packages/velog-web/src/hooks/useInput.ts rename to apps/web/src/hooks/useInput.ts diff --git a/packages/velog-web/src/hooks/useInputs.ts b/apps/web/src/hooks/useInputs.ts similarity index 98% rename from packages/velog-web/src/hooks/useInputs.ts rename to apps/web/src/hooks/useInputs.ts index c7a541d5..64818123 100644 --- a/packages/velog-web/src/hooks/useInputs.ts +++ b/apps/web/src/hooks/useInputs.ts @@ -1,3 +1,5 @@ +'use client' + import { useReducer, useCallback, useMemo } from 'react' type UseInputsAction = { diff --git a/packages/velog-web/src/hooks/useOutsideClick.ts b/apps/web/src/hooks/useOutsideClick.ts similarity index 100% rename from packages/velog-web/src/hooks/useOutsideClick.ts rename to apps/web/src/hooks/useOutsideClick.ts diff --git a/packages/velog-web/src/hooks/useSearchPosts.ts b/apps/web/src/hooks/useSearchPosts.ts similarity index 91% rename from packages/velog-web/src/hooks/useSearchPosts.ts rename to apps/web/src/hooks/useSearchPosts.ts index db7c2cd6..dbe8747b 100644 --- a/packages/velog-web/src/hooks/useSearchPosts.ts +++ b/apps/web/src/hooks/useSearchPosts.ts @@ -1,10 +1,12 @@ +'use client' + import { Post, SearchPostsDocument, SearchPostsQuery, SearchPostsQueryVariables, -} from '@/graphql/helpers/generated' -import { infiniteSearchPostsQueryKey } from '@/graphql/helpers/queryKey' +} from '@/graphql/server/generated/server' +import { infiniteSearchPostsQueryKey } from '@/graphql/server/helpers/queryKey' import useCustomInfiniteQuery from '@/hooks/useCustomInfiniteQuery' import { useMemo } from 'react' diff --git a/packages/velog-web/src/hooks/useThemeEffect.ts b/apps/web/src/hooks/useThemeEffect.ts similarity index 100% rename from packages/velog-web/src/hooks/useThemeEffect.ts rename to apps/web/src/hooks/useThemeEffect.ts diff --git a/packages/velog-web/src/hooks/useTimeFormat.ts b/apps/web/src/hooks/useTimeFormat.ts similarity index 86% rename from packages/velog-web/src/hooks/useTimeFormat.ts rename to apps/web/src/hooks/useTimeFormat.ts index 7d0337e4..705fa29b 100644 --- a/packages/velog-web/src/hooks/useTimeFormat.ts +++ b/apps/web/src/hooks/useTimeFormat.ts @@ -3,7 +3,6 @@ import formatDistanceToNow from 'date-fns/formatDistanceToNow' import format from 'date-fns/format' import koLocale from 'date-fns/locale/ko' -import { utcToZonedTime } from 'date-fns-tz' import { useEffect, useState } from 'react' export function useTimeFormat(date: string) { @@ -13,8 +12,8 @@ export function useTimeFormat(date: string) { useEffect(() => { setLoading(true) - const targetDate = utcToZonedTime(new Date(date), 'Asia/Seoul') - const now = utcToZonedTime(new Date(), 'Asia/Seoul') + const targetDate = new Date(date) + const now = new Date() const diff = now.getTime() - targetDate.getTime() const getTimeDescription = () => { diff --git a/packages/velog-web/src/hooks/useToggle.ts b/apps/web/src/hooks/useToggle.ts similarity index 100% rename from packages/velog-web/src/hooks/useToggle.ts rename to apps/web/src/hooks/useToggle.ts diff --git a/packages/velog-web/src/hooks/useUpload.ts b/apps/web/src/hooks/useUpload.ts similarity index 100% rename from packages/velog-web/src/hooks/useUpload.ts rename to apps/web/src/hooks/useUpload.ts diff --git a/packages/velog-web/src/lib/api/apiClient.ts b/apps/web/src/lib/api/apiClient.ts similarity index 100% rename from packages/velog-web/src/lib/api/apiClient.ts rename to apps/web/src/lib/api/apiClient.ts diff --git a/packages/velog-web/src/lib/api/files.ts b/apps/web/src/lib/api/files.ts similarity index 100% rename from packages/velog-web/src/lib/api/files.ts rename to apps/web/src/lib/api/files.ts diff --git a/packages/velog-web/src/lib/api/routes/check.ts b/apps/web/src/lib/api/routes/check.ts similarity index 100% rename from packages/velog-web/src/lib/api/routes/check.ts rename to apps/web/src/lib/api/routes/check.ts diff --git a/packages/velog-web/src/lib/auth.ts b/apps/web/src/lib/auth.ts similarity index 80% rename from packages/velog-web/src/lib/auth.ts rename to apps/web/src/lib/auth.ts index 9aa6f6ba..1ff2d493 100644 --- a/packages/velog-web/src/lib/auth.ts +++ b/apps/web/src/lib/auth.ts @@ -3,8 +3,9 @@ import { RequestCookie } from 'next/dist/compiled/@edge-runtime/cookies' import { cookies } from 'next/headers' -export const getAccessToken = (): RequestCookie | undefined => { +export const getAccessToken = async (): Promise => { const cookieStore = cookies() + const cookie = cookieStore.get('access_token') || cookieStore.get('refresh_token') return cookie } diff --git a/packages/velog-web/src/lib/checkIsHome.ts b/apps/web/src/lib/checkIsHome.ts similarity index 100% rename from packages/velog-web/src/lib/checkIsHome.ts rename to apps/web/src/lib/checkIsHome.ts diff --git a/packages/velog-web/src/lib/graphqlFetch.ts b/apps/web/src/lib/graphqlFetch.ts similarity index 98% rename from packages/velog-web/src/lib/graphqlFetch.ts rename to apps/web/src/lib/graphqlFetch.ts index 04e0dc25..7fdb70eb 100644 --- a/packages/velog-web/src/lib/graphqlFetch.ts +++ b/apps/web/src/lib/graphqlFetch.ts @@ -1,7 +1,7 @@ import { ENV } from '@/env' export default async function graphqlFetch({ - url = `${ENV.graphqlHost}/graphql`, + url = `${ENV.graphqlServerHost}/graphql`, method = 'POST', body, headers = {}, diff --git a/packages/velog-web/src/lib/gtag.ts b/apps/web/src/lib/gtag.ts similarity index 100% rename from packages/velog-web/src/lib/gtag.ts rename to apps/web/src/lib/gtag.ts diff --git a/packages/velog-web/src/lib/includeProtocol.ts b/apps/web/src/lib/includeProtocol.ts similarity index 100% rename from packages/velog-web/src/lib/includeProtocol.ts rename to apps/web/src/lib/includeProtocol.ts diff --git a/packages/velog-web/src/lib/isClientSide.ts b/apps/web/src/lib/isClientSide.ts similarity index 100% rename from packages/velog-web/src/lib/isClientSide.ts rename to apps/web/src/lib/isClientSide.ts diff --git a/packages/velog-web/src/lib/katexWhiteList.ts b/apps/web/src/lib/katexWhiteList.ts similarity index 100% rename from packages/velog-web/src/lib/katexWhiteList.ts rename to apps/web/src/lib/katexWhiteList.ts diff --git a/packages/velog-web/src/lib/remark/embedPlugin.ts b/apps/web/src/lib/remark/embedPlugin.ts similarity index 100% rename from packages/velog-web/src/lib/remark/embedPlugin.ts rename to apps/web/src/lib/remark/embedPlugin.ts diff --git a/packages/velog-web/src/lib/remark/prismPlugin.ts b/apps/web/src/lib/remark/prismPlugin.ts similarity index 100% rename from packages/velog-web/src/lib/remark/prismPlugin.ts rename to apps/web/src/lib/remark/prismPlugin.ts diff --git a/packages/velog-web/src/lib/styles/bindClassNames.ts b/apps/web/src/lib/styles/bindClassNames.ts similarity index 100% rename from packages/velog-web/src/lib/styles/bindClassNames.ts rename to apps/web/src/lib/styles/bindClassNames.ts diff --git a/packages/velog-web/src/lib/styles/grid.module.css b/apps/web/src/lib/styles/grid.module.css similarity index 100% rename from packages/velog-web/src/lib/styles/grid.module.css rename to apps/web/src/lib/styles/grid.module.css diff --git a/apps/web/src/lib/styles/keyframes.module.css b/apps/web/src/lib/styles/keyframes.module.css new file mode 100644 index 00000000..f65a1d39 --- /dev/null +++ b/apps/web/src/lib/styles/keyframes.module.css @@ -0,0 +1,90 @@ +.fadeIn { + animation-name: fade-in; + animation-duration: 0.25s; + animation-fill-mode: forwards; +} + +.fadeOut { + animation-name: fade-out; + animation-duration: 0.25s; + animation-fill-mode: forwards; +} + +.popInFromBottom { + animation-name: pop-in-from-bottom; + animation-duration: 0.4s; + animation-fill-mode: forwards; + animation-timing-function: ease-in-out; + /* animation: popInFromBottom 0.4s forwards ease-in-out; */ +} + +.popOutToBottom { + animation-name: pop-out-to-Bottom; + animation-duration: 0.2s; + animation-fill-mode: forwards; + animation-timing-function: ease-in-out; +} + +.shining { + animation-name: shining; + animation-duration: 1s; + animation-fill-mode: forwards; + animation-timing-function: ease-in-out; + animation-iteration-count: infinite; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +@keyframes pop-in-from-bottom { + 0% { + opacity: 0; + transform: translateY(400px) scale(0.75); + } + 75% { + opacity: 1; + transform: translateY(-16px) scale(1); + } + 100% { + opacity: 1; + transform: translateY(0px); + } +} + +@keyframes pop-out-to-Bottom { + 0% { + opacity: 1; + transform: translateY(0px) scale(1); + } + 100% { + opacity: 0; + transform: translateY(400px) scale(0.75); + } +} + +@keyframes shining { + 0% { + opacity: 0.5; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.5; + } +} diff --git a/packages/velog-web/src/lib/styles/responsive.module.css b/apps/web/src/lib/styles/responsive.module.css similarity index 100% rename from packages/velog-web/src/lib/styles/responsive.module.css rename to apps/web/src/lib/styles/responsive.module.css diff --git a/apps/web/src/lib/styles/revert.module.css b/apps/web/src/lib/styles/revert.module.css new file mode 100644 index 00000000..04aeacb5 --- /dev/null +++ b/apps/web/src/lib/styles/revert.module.css @@ -0,0 +1,131 @@ +.revert { + /* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + + & html, + & body, + & div, + & span, + & applet, + & object, + & iframe, + & h1, + & h2, + & h3, + & h4, + & h5, + & h6, + & p, + & pre, + & a, + & abbr, + & acronym, + & address, + & big, + & cite, + & del, + & dfn, + & em, + & img, + & ins, + & kbd, + & q, + & s, + & samp, + & small, + & strike, + & strong, + & sub, + & sup, + & tt, + & var, + & b, + & u, + & i, + & center, + & dl, + & dt, + & dd, + & ol, + & ul, + & li, + & fieldset, + & form, + & label, + & legend, + & table, + & caption, + & tbody, + & tfoot, + & thead, + & tr, + & th, + & td, + & article, + & aside, + & canvas, + & details, + & embed, + & figure, + & figcaption, + & footer, + & header, + & hgroup, + & menu, + & nav, + & output, + & ruby, + & section, + & summary, + & time, + & mark, + & audio, + & video { + margin: revert; + padding: revert; + /* font: inherit; */ + vertical-align: baseline; + } + /* HTML5 display-role reset for older browsers */ + & article, + & aside, + & details, + & figcaption, + & figure, + & footer, + & header, + & hgroup, + & menu, + & nav, + & section { + display: block; + } + + & ol, + & ul { + list-style: revert; + } + + & blockquote, + & q { + quotes: revert; + } + & blockquote:before, + & blockquote:after, + & q:before, + & q:after { + content: ''; + content: none; + } + + & table { + border-collapse: collapse; + border-spacing: 0; + } + + & a { + text-decoration: none; + } +} diff --git a/packages/velog-web/src/lib/styles/utils.module.css b/apps/web/src/lib/styles/utils.module.css similarity index 100% rename from packages/velog-web/src/lib/styles/utils.module.css rename to apps/web/src/lib/styles/utils.module.css diff --git a/packages/velog-web/src/lib/themeHelpers.ts b/apps/web/src/lib/themeHelpers.ts similarity index 100% rename from packages/velog-web/src/lib/themeHelpers.ts rename to apps/web/src/lib/themeHelpers.ts diff --git a/packages/velog-web/src/lib/utils.ts b/apps/web/src/lib/utils.ts similarity index 100% rename from packages/velog-web/src/lib/utils.ts rename to apps/web/src/lib/utils.ts diff --git a/packages/velog-web/src/lib/validate.ts b/apps/web/src/lib/validate.ts similarity index 100% rename from packages/velog-web/src/lib/validate.ts rename to apps/web/src/lib/validate.ts diff --git a/packages/velog-web/src/middleware.ts b/apps/web/src/middleware.ts similarity index 100% rename from packages/velog-web/src/middleware.ts rename to apps/web/src/middleware.ts diff --git a/packages/velog-web/src/prefetch/getAds.ts b/apps/web/src/prefetch/getAds.ts similarity index 90% rename from packages/velog-web/src/prefetch/getAds.ts rename to apps/web/src/prefetch/getAds.ts index c9a54086..bd9ad20c 100644 --- a/packages/velog-web/src/prefetch/getAds.ts +++ b/apps/web/src/prefetch/getAds.ts @@ -1,4 +1,4 @@ -import { AdsDocument, AdsInput } from '@/graphql/helpers/generated' +import { AdsDocument, AdsInput } from '@/graphql/server/generated/server' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getAds({ type, limit }: AdsInput) { diff --git a/packages/velog-web/src/prefetch/getCurrentUser.ts b/apps/web/src/prefetch/getCurrentUser.ts similarity index 85% rename from packages/velog-web/src/prefetch/getCurrentUser.ts rename to apps/web/src/prefetch/getCurrentUser.ts index 191a5971..e911b923 100644 --- a/packages/velog-web/src/prefetch/getCurrentUser.ts +++ b/apps/web/src/prefetch/getCurrentUser.ts @@ -1,11 +1,12 @@ -import { CurrentUserDocument, User } from '@/graphql/helpers/generated' +import { CurrentUserDocument, User } from '@/graphql/server/generated/server' import { getAccessToken } from '@/lib/auth' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getCurrentUser() { try { const headers = {} - const token = getAccessToken() + const token = await getAccessToken() + if (token) { Object.assign(headers, { authorization: `Bearer ${token.value}` }) } diff --git a/packages/velog-web/src/prefetch/getFeedPosts.ts b/apps/web/src/prefetch/getFeedPosts.ts similarity index 87% rename from packages/velog-web/src/prefetch/getFeedPosts.ts rename to apps/web/src/prefetch/getFeedPosts.ts index a40e5a40..7217a3a6 100644 --- a/packages/velog-web/src/prefetch/getFeedPosts.ts +++ b/apps/web/src/prefetch/getFeedPosts.ts @@ -1,11 +1,11 @@ import { ENV } from '@/env' -import { FeedPostsDocument, Post } from '@/graphql/helpers/generated' +import { FeedPostsDocument, Post } from '@/graphql/server/generated/server' import { getAccessToken } from '@/lib/auth' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getFeedPosts({ limit = ENV.defaultPostLimit }: Args) { const headers = {} - const token = getAccessToken() + const token = await getAccessToken() if (token) { Object.assign(headers, { authorization: `Bearer ${token.value}` }) } diff --git a/packages/velog-web/src/prefetch/getNotificationCount.ts b/apps/web/src/prefetch/getNotificationCount.ts similarity index 88% rename from packages/velog-web/src/prefetch/getNotificationCount.ts rename to apps/web/src/prefetch/getNotificationCount.ts index 92206b2b..d8879fbc 100644 --- a/packages/velog-web/src/prefetch/getNotificationCount.ts +++ b/apps/web/src/prefetch/getNotificationCount.ts @@ -1,4 +1,7 @@ -import { IsLoggedDocument, NotNoticeNotificationCountDocument } from '@/graphql/helpers/generated' +import { + IsLoggedDocument, + NotNoticeNotificationCountDocument, +} from '@/graphql/server/generated/server' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getNotificationCount(): Promise { diff --git a/packages/velog-web/src/prefetch/getPostByUrlSlug.ts b/apps/web/src/prefetch/getPostByUrlSlug.ts similarity index 95% rename from packages/velog-web/src/prefetch/getPostByUrlSlug.ts rename to apps/web/src/prefetch/getPostByUrlSlug.ts index 6a460268..eabf0728 100644 --- a/packages/velog-web/src/prefetch/getPostByUrlSlug.ts +++ b/apps/web/src/prefetch/getPostByUrlSlug.ts @@ -1,4 +1,4 @@ -import { ReadPostInput, ReadPostDocument, Post } from '@/graphql/helpers/generated' +import { ReadPostInput, ReadPostDocument, Post } from '@/graphql/server/generated/server' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getPostByUrlSlug({ username, url_slug }: ReadPostInput) { diff --git a/packages/velog-web/src/prefetch/getRecentPosts.ts b/apps/web/src/prefetch/getRecentPosts.ts similarity index 95% rename from packages/velog-web/src/prefetch/getRecentPosts.ts rename to apps/web/src/prefetch/getRecentPosts.ts index 4c842bda..e99d65e0 100644 --- a/packages/velog-web/src/prefetch/getRecentPosts.ts +++ b/apps/web/src/prefetch/getRecentPosts.ts @@ -1,5 +1,5 @@ import { ENV } from '@/env' -import { Post, RecentPostsDocument, RecentPostsInput } from '@/graphql/helpers/generated' +import { Post, RecentPostsDocument, RecentPostsInput } from '@/graphql/server/generated/server' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getRecentPosts({ limit = ENV.defaultPostLimit }: RecentPostsInput) { diff --git a/packages/velog-web/src/prefetch/getTrendingPosts.ts b/apps/web/src/prefetch/getTrendingPosts.ts similarity index 95% rename from packages/velog-web/src/prefetch/getTrendingPosts.ts rename to apps/web/src/prefetch/getTrendingPosts.ts index 3e028a90..079ea5b5 100644 --- a/packages/velog-web/src/prefetch/getTrendingPosts.ts +++ b/apps/web/src/prefetch/getTrendingPosts.ts @@ -1,7 +1,7 @@ 'use server' import { ENV } from '@/env' -import { Post, TrendingPostsDocument, TrendingPostsInput } from '@/graphql/helpers/generated' +import { Post, TrendingPostsDocument, TrendingPostsInput } from '@/graphql/server/generated/server' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getTrendingPosts({ diff --git a/packages/velog-web/src/prefetch/getTrendingWriters.ts b/apps/web/src/prefetch/getTrendingWriters.ts similarity index 96% rename from packages/velog-web/src/prefetch/getTrendingWriters.ts rename to apps/web/src/prefetch/getTrendingWriters.ts index 155cdd21..7e2a7aaf 100644 --- a/packages/velog-web/src/prefetch/getTrendingWriters.ts +++ b/apps/web/src/prefetch/getTrendingWriters.ts @@ -1,4 +1,4 @@ -import { TrendingWriter, TrendingWritersDocument } from '@/graphql/helpers/generated' +import { TrendingWriter, TrendingWritersDocument } from '@/graphql/server/generated/server' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getTrendingWriters() { diff --git a/packages/velog-web/src/prefetch/getUser.ts b/apps/web/src/prefetch/getUser.ts similarity index 89% rename from packages/velog-web/src/prefetch/getUser.ts rename to apps/web/src/prefetch/getUser.ts index 9ce7352a..ca259727 100644 --- a/packages/velog-web/src/prefetch/getUser.ts +++ b/apps/web/src/prefetch/getUser.ts @@ -1,4 +1,4 @@ -import { GetUserDocument, User } from '@/graphql/helpers/generated' +import { GetUserDocument, User } from '@/graphql/server/generated/server' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getUser(username: string) { diff --git a/packages/velog-web/src/prefetch/getUserFollowInfo.ts b/apps/web/src/prefetch/getUserFollowInfo.ts similarity index 89% rename from packages/velog-web/src/prefetch/getUserFollowInfo.ts rename to apps/web/src/prefetch/getUserFollowInfo.ts index e63438a6..e09ed27e 100644 --- a/packages/velog-web/src/prefetch/getUserFollowInfo.ts +++ b/apps/web/src/prefetch/getUserFollowInfo.ts @@ -1,4 +1,4 @@ -import { GetUserFollowInfoDocument, User } from '@/graphql/helpers/generated' +import { GetUserFollowInfoDocument, User } from '@/graphql/server/generated/server' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getUserFollowInfo(username: string) { diff --git a/packages/velog-web/src/prefetch/getUserTags.ts b/apps/web/src/prefetch/getUserTags.ts similarity index 85% rename from packages/velog-web/src/prefetch/getUserTags.ts rename to apps/web/src/prefetch/getUserTags.ts index 75959e77..f80abf5b 100644 --- a/packages/velog-web/src/prefetch/getUserTags.ts +++ b/apps/web/src/prefetch/getUserTags.ts @@ -1,11 +1,11 @@ -import { UserTags, UserTagsDocument } from '@/graphql/helpers/generated' +import { UserTags, UserTagsDocument } from '@/graphql/server/generated/server' import { getAccessToken } from '@/lib/auth' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getUserTags(username: string) { try { const headers = {} - const token = getAccessToken() + const token = await getAccessToken() if (token) { Object.assign(headers, { authorization: `Bearer ${token.value}` }) } diff --git a/packages/velog-web/src/prefetch/getVelogConfig.ts b/apps/web/src/prefetch/getVelogConfig.ts similarity index 87% rename from packages/velog-web/src/prefetch/getVelogConfig.ts rename to apps/web/src/prefetch/getVelogConfig.ts index b11b823a..97addce2 100644 --- a/packages/velog-web/src/prefetch/getVelogConfig.ts +++ b/apps/web/src/prefetch/getVelogConfig.ts @@ -1,11 +1,11 @@ -import { VelogConfig, VelogConfigDocument } from '@/graphql/helpers/generated' +import { VelogConfig, VelogConfigDocument } from '@/graphql/server/generated/server' import { getAccessToken } from '@/lib/auth' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getVelogConfig({ username }: Args) { try { const headers = {} - const token = getAccessToken() + const token = await getAccessToken() if (token) { Object.assign(headers, { authorization: `Bearer ${token.value}` }) } diff --git a/packages/velog-web/src/prefetch/getVelogPosts.ts b/apps/web/src/prefetch/getVelogPosts.ts similarity index 88% rename from packages/velog-web/src/prefetch/getVelogPosts.ts rename to apps/web/src/prefetch/getVelogPosts.ts index 8bb68573..3c6ff325 100644 --- a/packages/velog-web/src/prefetch/getVelogPosts.ts +++ b/apps/web/src/prefetch/getVelogPosts.ts @@ -1,11 +1,11 @@ -import { Post, VelogPostsDocument } from '@/graphql/helpers/generated' +import { Post, VelogPostsDocument } from '@/graphql/server/generated/server' import { getAccessToken } from '@/lib/auth' import graphqlFetch, { GraphqlRequestBody } from '@/lib/graphqlFetch' export default async function getVelogPosts({ username, tag }: GetVelogPostsArgs) { try { const headers = {} - const token = getAccessToken() + const token = await getAccessToken() if (token) { Object.assign(headers, { authorization: `Bearer ${token.value}` }) } diff --git a/packages/velog-web/src/providers/ConditionalBackgroundProvider.tsx b/apps/web/src/providers/ConditionalBackgroundProvider.tsx similarity index 100% rename from packages/velog-web/src/providers/ConditionalBackgroundProvider.tsx rename to apps/web/src/providers/ConditionalBackgroundProvider.tsx diff --git a/packages/velog-web/src/providers/CoreProvider.tsx b/apps/web/src/providers/CoreProvider.tsx similarity index 100% rename from packages/velog-web/src/providers/CoreProvider.tsx rename to apps/web/src/providers/CoreProvider.tsx diff --git a/packages/velog-web/src/providers/GtagProvider.tsx b/apps/web/src/providers/GtagProvider.tsx similarity index 100% rename from packages/velog-web/src/providers/GtagProvider.tsx rename to apps/web/src/providers/GtagProvider.tsx diff --git a/packages/velog-web/src/providers/InteractiveViewProvider.tsx b/apps/web/src/providers/InteractiveViewProvider.tsx similarity index 100% rename from packages/velog-web/src/providers/InteractiveViewProvider.tsx rename to apps/web/src/providers/InteractiveViewProvider.tsx diff --git a/packages/velog-web/src/providers/JazzbarProvider.tsx b/apps/web/src/providers/JazzbarProvider.tsx similarity index 100% rename from packages/velog-web/src/providers/JazzbarProvider.tsx rename to apps/web/src/providers/JazzbarProvider.tsx diff --git a/packages/velog-web/src/providers/ReactQueryProvider.tsx b/apps/web/src/providers/ReactQueryProvider.tsx similarity index 100% rename from packages/velog-web/src/providers/ReactQueryProvider.tsx rename to apps/web/src/providers/ReactQueryProvider.tsx diff --git a/packages/velog-web/src/providers/SangteContextProvider.tsx b/apps/web/src/providers/SangteContextProvider.tsx similarity index 100% rename from packages/velog-web/src/providers/SangteContextProvider.tsx rename to apps/web/src/providers/SangteContextProvider.tsx diff --git a/packages/velog-web/src/providers/SentryProvider.tsx b/apps/web/src/providers/SentryProvider.tsx similarity index 100% rename from packages/velog-web/src/providers/SentryProvider.tsx rename to apps/web/src/providers/SentryProvider.tsx diff --git a/packages/velog-web/src/providers/ThemeProvider.tsx b/apps/web/src/providers/ThemeProvider.tsx similarity index 100% rename from packages/velog-web/src/providers/ThemeProvider.tsx rename to apps/web/src/providers/ThemeProvider.tsx diff --git a/packages/velog-web/src/state/auth.ts b/apps/web/src/state/auth.ts similarity index 100% rename from packages/velog-web/src/state/auth.ts rename to apps/web/src/state/auth.ts diff --git a/packages/velog-web/src/state/header.ts b/apps/web/src/state/header.ts similarity index 100% rename from packages/velog-web/src/state/header.ts rename to apps/web/src/state/header.ts diff --git a/packages/velog-web/src/state/modal.ts b/apps/web/src/state/modal.ts similarity index 100% rename from packages/velog-web/src/state/modal.ts rename to apps/web/src/state/modal.ts diff --git a/packages/velog-web/src/state/popup.ts b/apps/web/src/state/popup.ts similarity index 100% rename from packages/velog-web/src/state/popup.ts rename to apps/web/src/state/popup.ts diff --git a/packages/velog-web/src/state/theme.ts b/apps/web/src/state/theme.ts similarity index 100% rename from packages/velog-web/src/state/theme.ts rename to apps/web/src/state/theme.ts diff --git a/packages/velog-web/src/styles/global.css b/apps/web/src/styles/global.css similarity index 100% rename from packages/velog-web/src/styles/global.css rename to apps/web/src/styles/global.css diff --git a/packages/velog-web/src/styles/reset.css b/apps/web/src/styles/reset.css similarity index 100% rename from packages/velog-web/src/styles/reset.css rename to apps/web/src/styles/reset.css diff --git a/packages/velog-web/src/types/auth.ts b/apps/web/src/types/auth.ts similarity index 100% rename from packages/velog-web/src/types/auth.ts rename to apps/web/src/types/auth.ts diff --git a/packages/velog-web/src/types/missing..d.ts b/apps/web/src/types/missing.d.ts similarity index 100% rename from packages/velog-web/src/types/missing..d.ts rename to apps/web/src/types/missing.d.ts diff --git a/packages/velog-web/src/types/post.ts b/apps/web/src/types/post.ts similarity index 100% rename from packages/velog-web/src/types/post.ts rename to apps/web/src/types/post.ts diff --git a/packages/velog-web/src/types/series.ts b/apps/web/src/types/series.ts similarity index 62% rename from packages/velog-web/src/types/series.ts rename to apps/web/src/types/series.ts index fe8ea8f3..82f6324c 100644 --- a/packages/velog-web/src/types/series.ts +++ b/apps/web/src/types/series.ts @@ -1,3 +1,3 @@ -import { Series } from '@/graphql/helpers/generated' +import { Series } from '@/graphql/server/generated/server' export type UserSeriesList = Omit diff --git a/packages/velog-web/src/types/user.ts b/apps/web/src/types/user.ts similarity index 100% rename from packages/velog-web/src/types/user.ts rename to apps/web/src/types/user.ts diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 00000000..d87f3c09 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "@packages/tsconfig/next.json", + "compilerOptions": { + "baseUrl": "./", + "rootDir": "./", + "typeRoots": ["node_modules/@types/", "./src/types/", "./node_modules/@packages/**/**/*.d.ts"], + "plugins": [ + { + "name": "next" + }, + { + "name": "typescript-plugin-css-modules" + } + ], + "paths": { + "@/*": ["./src/*"], + "@/public/*": ["./public/*"] + }, + "strictNullChecks": true + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "public/*", + "src/assets/**/svg", + "scripts/*", + "next.config.mjs", + "eslint.config.js", + ".next/types/**/*.ts" + ], + "exclude": ["node_modules", "dist"] +} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..194d04ac --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,18 @@ +import baseConfig from '@packages/eslint-config/base.mjs' +import { resolve } from 'node:path' + +const project = resolve(process.cwd()) + +/** @type {Linter.Config} */ +export default [ + ...baseConfig(project), + { + ignores: [ + 'node_modules', + './apps/*', + './infrastructure/*', + '.lintstagedrc.mjs', + '**/node_modules', + ], + }, +] diff --git a/infrastructure/.eslintignore b/infrastructure/.eslintignore deleted file mode 100644 index 76c180b6..00000000 --- a/infrastructure/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -node_moudles diff --git a/infrastructure/.eslintrc.js b/infrastructure/.eslintrc.js deleted file mode 100644 index 931cfa5b..00000000 --- a/infrastructure/.eslintrc.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: './tsconfig.json', - sourceType: 'module', - tsconfigRootDir: __dirname, - }, - plugins: ['prettier'], - extends: ['plugin:prettier/recommended'], - root: true, - env: { - node: true, - jest: true, - }, - rules: { - 'prettier/prettier': ['error', { semi: false, singleQuote: true }], - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-non-null-assertion': 'off', - '@typescript-eslint/no-empty-interface': 'off', - }, -} diff --git a/infrastructure/Pulumi.production.yaml b/infrastructure/Pulumi.production.yaml index 6ecf24e1..4e7eae1e 100644 --- a/infrastructure/Pulumi.production.yaml +++ b/infrastructure/Pulumi.production.yaml @@ -1,4 +1,4 @@ config: aws:region: ap-northeast-2 velog:DOCKER_ENV: production - velog:target: server + velog:target: cron diff --git a/infrastructure/Pulumi.yaml b/infrastructure/Pulumi.yaml index ed4e61f8..8a7180be 100644 --- a/infrastructure/Pulumi.yaml +++ b/infrastructure/Pulumi.yaml @@ -1,3 +1,6 @@ name: velog -runtime: nodejs +runtime: + name: nodejs + options: + typescript: true description: velog infrastructure diff --git a/infrastructure/eslint.config.js b/infrastructure/eslint.config.js new file mode 100644 index 00000000..4c51ab13 --- /dev/null +++ b/infrastructure/eslint.config.js @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +const { resolve } = require('node:path') +const { FlatCompat } = require('@eslint/eslintrc') +const js = require('@eslint/js') + +const projectPath = resolve(__dirname) +const tsconfigPath = resolve(projectPath, 'tsconfig.json') + +const compat = new FlatCompat({ + baseDirectory: projectPath, + resolvePluginsRelativeTo: projectPath, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}) + +/** @type {import('eslint').Linter.Config} */ +const config = [ + { + ignores: ['node_modules', 'dist'], + }, + ...compat.config({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: tsconfigPath, + sourceType: 'module', + tsconfigRootDir: projectPath, + }, + plugins: ['@typescript-eslint'], + extends: ['plugin:@typescript-eslint/recommended'], + root: true, + env: { + node: true, + jest: true, + }, + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-empty-interface': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'off', + { + ignoreRestSiblings: true, + argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + }, + }), +] + +module.exports = config diff --git a/infrastructure/package-lock.json b/infrastructure/package-lock.json new file mode 100644 index 00000000..bf327558 --- /dev/null +++ b/infrastructure/package-lock.json @@ -0,0 +1,5441 @@ +{ + "name": "infrastructure", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "infrastructure", + "dependencies": { + "@aws-sdk/client-ecr": "^3.413.0", + "@aws-sdk/client-ecs": "^3.418.0", + "@aws-sdk/client-ssm": "^3.379.1", + "@pulumi/aws": "^6.49.1", + "@pulumi/awsx": "^2.14.0", + "@pulumi/docker": "^4.5.5", + "@pulumi/pulumi": "^3.128.0", + "dotenv": "^16.4.5", + "tsx": "^4.7.1", + "zod": "^3.21.4" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.5.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-ecr": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ecr/-/client-ecr-3.758.0.tgz", + "integrity": "sha512-9nTE01CuK5Vq0kPkmw3xdE6lrPDZSXOlX4wRc194GhnhxdppRla5dG6+gvZWTmpkC9a2mGqjXYsbvoSg6kZPHg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-node": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ecs": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ecs/-/client-ecs-3.758.0.tgz", + "integrity": "sha512-FYf7cOVV4UQpHsY072yqJbi28FPFYy50DDhtdFUdKxVtsGaHTZ7+DP+5PQJY9E56lpNpfG0ytrOrSidTXINKVQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-node": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.2", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-ssm": { + "version": "3.759.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.759.0.tgz", + "integrity": "sha512-+h1D1jBi6p2fG+ePxwIn2N4TOZcx7ExzhZJnpPT2actC9bV6vkRbeulGr/2fqNu11/S59DZJLfOHjVDH9X1nWA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-node": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.2", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.758.0.tgz", + "integrity": "sha512-BoGO6IIWrLyLxQG6txJw6RT2urmbtlwfggapNCrNPyYjlXpzTSJhBYjndg7TpDATFd0SXL0zm8y/tXsUXNkdYQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.758.0.tgz", + "integrity": "sha512-0RswbdR9jt/XKemaLNuxi2gGr4xGlHyGxkTdhSQzCyUe9A9OPCoLl3rIESRguQEech+oJnbHk/wuiwHqTuP9sg==", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/core": "^3.1.5", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/signature-v4": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.758.0.tgz", + "integrity": "sha512-N27eFoRrO6MeUNumtNHDW9WOiwfd59LPXPqDrIa3kWL/s+fOKFHb9xIcF++bAwtcZnAxKkgpDCUP+INNZskE+w==", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.758.0.tgz", + "integrity": "sha512-Xt9/U8qUCiw1hihztWkNeIR+arg6P+yda10OuCHX6kFVx3auTlU7+hCqs3UxqniGU4dguHuftf3mRpi5/GJ33Q==", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/property-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.758.0.tgz", + "integrity": "sha512-cymSKMcP5d+OsgetoIZ5QCe1wnp2Q/tq+uIxVdh9MbfdBBEnl9Ecq6dH6VlYS89sp4QKuxHxkWXVnbXU3Q19Aw==", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.758.0", + "@aws-sdk/credential-provider-web-identity": "3.758.0", + "@aws-sdk/nested-clients": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.758.0.tgz", + "integrity": "sha512-+DaMv63wiq7pJrhIQzZYMn4hSarKiizDoJRvyR7WGhnn0oQ/getX9Z0VNCV3i7lIFoLNTb7WMmQ9k7+z/uD5EQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.758.0", + "@aws-sdk/credential-provider-http": "3.758.0", + "@aws-sdk/credential-provider-ini": "3.758.0", + "@aws-sdk/credential-provider-process": "3.758.0", + "@aws-sdk/credential-provider-sso": "3.758.0", + "@aws-sdk/credential-provider-web-identity": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.758.0.tgz", + "integrity": "sha512-AzcY74QTPqcbXWVgjpPZ3HOmxQZYPROIBz2YINF0OQk0MhezDWV/O7Xec+K1+MPGQO3qS6EDrUUlnPLjsqieHA==", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.758.0.tgz", + "integrity": "sha512-x0FYJqcOLUCv8GLLFDYMXRAQKGjoM+L0BG4BiHYZRDf24yQWFCAZsCQAYKo6XZYh2qznbsW6f//qpyJ5b0QVKQ==", + "dependencies": { + "@aws-sdk/client-sso": "3.758.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/token-providers": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.758.0.tgz", + "integrity": "sha512-XGguXhBqiCXMXRxcfCAVPlMbm3VyJTou79r/3mxWddHWF0XbhaQiBIbUz6vobVTD25YQRbWSmSch7VA8kI5Lrw==", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/nested-clients": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.734.0.tgz", + "integrity": "sha512-LW7RRgSOHHBzWZnigNsDIzu3AiwtjeI2X66v+Wn1P1u+eXssy1+up4ZY/h+t2sU4LU36UvEf+jrZti9c6vRnFw==", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.734.0.tgz", + "integrity": "sha512-mUMFITpJUW3LcKvFok176eI5zXAUomVtahb9IQBwLzkqFYOrMJvWAvoV4yuxrJ8TlQBG8gyEnkb9SnhZvjg67w==", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.734.0.tgz", + "integrity": "sha512-CUat2d9ITsFc2XsmeiRQO96iWpxSKYFjxvj27Hc7vo87YUHRnfMfnc8jw1EpxEwMcvBD7LsRa6vDNky6AjcrFA==", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.758.0.tgz", + "integrity": "sha512-iNyehQXtQlj69JCgfaOssgZD4HeYGOwxcaKeG6F+40cwBjTAi0+Ph1yfDwqk2qiBPIRWJ/9l2LodZbxiBqgrwg==", + "dependencies": { + "@aws-sdk/core": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@smithy/core": "^3.1.5", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.758.0.tgz", + "integrity": "sha512-YZ5s7PSvyF3Mt2h1EQulCG93uybprNGbBkPmVuy/HMMfbFTt4iL3SbKjxqvOZelm86epFfj7pvK7FliI2WOEcg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.758.0", + "@aws-sdk/middleware-host-header": "3.734.0", + "@aws-sdk/middleware-logger": "3.734.0", + "@aws-sdk/middleware-recursion-detection": "3.734.0", + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/region-config-resolver": "3.734.0", + "@aws-sdk/types": "3.734.0", + "@aws-sdk/util-endpoints": "3.743.0", + "@aws-sdk/util-user-agent-browser": "3.734.0", + "@aws-sdk/util-user-agent-node": "3.758.0", + "@smithy/config-resolver": "^4.0.1", + "@smithy/core": "^3.1.5", + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/hash-node": "^4.0.1", + "@smithy/invalid-dependency": "^4.0.1", + "@smithy/middleware-content-length": "^4.0.1", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-retry": "^4.0.7", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/protocol-http": "^5.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.7", + "@smithy/util-defaults-mode-node": "^4.0.7", + "@smithy/util-endpoints": "^3.0.1", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.734.0.tgz", + "integrity": "sha512-Lvj1kPRC5IuJBr9DyJ9T9/plkh+EfKLy+12s/mykOy1JaKHDpvj+XGy2YO6YgYVOb8JFtaqloid+5COtje4JTQ==", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.758.0.tgz", + "integrity": "sha512-ckptN1tNrIfQUaGWm/ayW1ddG+imbKN7HHhjFdS4VfItsP0QQOB0+Ov+tpgb4MoNR4JaUghMIVStjIeHN2ks1w==", + "dependencies": { + "@aws-sdk/nested-clients": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.734.0.tgz", + "integrity": "sha512-o11tSPTT70nAkGV1fN9wm/hAIiLPyWX6SuGf+9JyTp7S/rC2cFWhR26MvA69nplcjNaXVzB0f+QFrLXXjOqCrg==", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.743.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.743.0.tgz", + "integrity": "sha512-sN1l559zrixeh5x+pttrnd0A3+r34r0tmPkJ/eaaMaAzXqsmKU/xYre9K3FNnsSS1J1k4PEfk/nHDTVUgFYjnw==", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "@smithy/util-endpoints": "^3.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.723.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.723.0.tgz", + "integrity": "sha512-Yf2CS10BqK688DRsrKI/EO6B8ff5J86NXe4C+VCysK7UOgN0l1zOTeTukZ3H8Q9tYYX3oaF1961o8vRkFm7Nmw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.734.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.734.0.tgz", + "integrity": "sha512-xQTCus6Q9LwUuALW+S76OL0jcWtMOVu14q+GoLnWPUM7QeUw963oQcLhF7oq0CtaLLKyl4GOUfcwc773Zmwwng==", + "dependencies": { + "@aws-sdk/types": "3.734.0", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.758.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.758.0.tgz", + "integrity": "sha512-A5EZw85V6WhoKMV2hbuFRvb9NPlxEErb4HPO6/SPXYY4QrjprIzScHxikqcWv1w4J3apB1wto9LPU3IMsYtfrw==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.758.0", + "@aws-sdk/types": "3.734.0", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.12.6", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.6.tgz", + "integrity": "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q==", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@isaacs/string-locale-compare/-/string-locale-compare-1.1.0.tgz", + "integrity": "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==" + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@logdna/tail-file": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@logdna/tail-file/-/tail-file-2.2.0.tgz", + "integrity": "sha512-XGSsWDweP80Fks16lwkAUIr54ICyBs6PsI4mpfTLQaWgEJRtY9xEV+PeyDpJ+sJEGZxqINlpmAwe/6tS1pP8Ng==", + "engines": { + "node": ">=10.3.0" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/arborist": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-7.5.4.tgz", + "integrity": "sha512-nWtIc6QwwoUORCRNzKx4ypHqCk3drI+5aeYdMTQQiRCcn4lOOgfQh7WyZobGYTxXPSq1VwV53lkpN/BRlRk08g==", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^3.1.1", + "@npmcli/installed-package-contents": "^2.1.0", + "@npmcli/map-workspaces": "^3.0.2", + "@npmcli/metavuln-calculator": "^7.1.1", + "@npmcli/name-from-folder": "^2.0.0", + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.1.0", + "@npmcli/query": "^3.1.0", + "@npmcli/redact": "^2.0.0", + "@npmcli/run-script": "^8.1.0", + "bin-links": "^4.0.4", + "cacache": "^18.0.3", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^7.0.2", + "json-parse-even-better-errors": "^3.0.2", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^10.2.2", + "minimatch": "^9.0.4", + "nopt": "^7.2.1", + "npm-install-checks": "^6.2.0", + "npm-package-arg": "^11.0.2", + "npm-pick-manifest": "^9.0.1", + "npm-registry-fetch": "^17.0.1", + "pacote": "^18.0.6", + "parse-conflict-json": "^3.0.0", + "proc-log": "^4.2.0", + "proggy": "^2.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^10.0.6", + "treeverse": "^3.0.0", + "walk-up-path": "^3.0.1" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/arborist/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/arborist/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/git": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.8.tgz", + "integrity": "sha512-liASfw5cqhjNW9UFd+ruwwdEf/lbOAQjLL2XY2dFW/bkJheXDYZgOyul/4gVvEV4BWkTXjYGmDqMw9uegdbJNQ==", + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "ini": "^4.1.3", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/map-workspaces": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-3.0.6.tgz", + "integrity": "sha512-tkYs0OYnzQm6iIRdfy+LcLBjcKuQCeE5YLb8KnrIlutJfheNaPvPpgoFEyEFgbjzl5PLZ3IA/BWAwRU0eHuQDA==", + "dependencies": { + "@npmcli/name-from-folder": "^2.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/map-workspaces/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/map-workspaces/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/metavuln-calculator": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/metavuln-calculator/-/metavuln-calculator-7.1.1.tgz", + "integrity": "sha512-Nkxf96V0lAx3HCpVda7Vw4P23RILgdi/5K1fmj2tZkWIYLpXAN8k2UVVOsW16TsS5F8Ws2I7Cm+PU1/rsVF47g==", + "dependencies": { + "cacache": "^18.0.0", + "json-parse-even-better-errors": "^3.0.0", + "pacote": "^18.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/metavuln-calculator/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/name-from-folder": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz", + "integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.1.tgz", + "integrity": "sha512-f7zYC6kQautXHvNbLEWgD/uGu1+xCn9izgqBfgItWSx22U0ZDekxN08A1vM8cTxj/cRVe0Q94Ode+tdoYmIOOQ==", + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", + "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/query": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/query/-/query-3.1.0.tgz", + "integrity": "sha512-C/iR0tk7KSKGldibYIB9x8GtO/0Bd0I2mhOaDb8ucQL/bQVTmGoeREaFj64Z5+iCBRf3dQfed0CjJL7I8iTkiQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz", + "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-8.1.0.tgz", + "integrity": "sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "proc-log": "^4.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.55.0.tgz", + "integrity": "sha512-3cpa+qI45VHYcA5c0bHM6VHo9gicv3p5mlLHNG3rLyjQU8b7e0st1rWtrUn3JbZ3DwwCfhKop4eQ9UuYlC6Pkg==", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", + "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/exporter-zipkin": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.30.1.tgz", + "integrity": "sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA==", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/sdk-trace-base": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0" + } + }, + "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.55.0.tgz", + "integrity": "sha512-YDCMlaQRZkziLL3t6TONRgmmGxDx6MyQDXRD0dknkkgUZtOK5+8MWft1OXzmNu6XfBOdT12MKN5rz+jHUkafKQ==", + "dependencies": { + "@opentelemetry/api-logs": "0.55.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-grpc": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.55.0.tgz", + "integrity": "sha512-n2ZH4pRwOy0Vhag/3eKqiyDBwcpUnGgJI9iiIRX7vivE0FMncaLazWphNFezRRaM/LuKwq1TD8pVUvieP68mow==", + "dependencies": { + "@opentelemetry/instrumentation": "0.55.0", + "@opentelemetry/semantic-conventions": "1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-grpc/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@opentelemetry/propagator-b3": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.30.1.tgz", + "integrity": "sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==", + "dependencies": { + "@opentelemetry/core": "1.30.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/propagator-jaeger": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.30.1.tgz", + "integrity": "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==", + "dependencies": { + "@opentelemetry/core": "1.30.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", + "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", + "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.30.1.tgz", + "integrity": "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==", + "dependencies": { + "@opentelemetry/context-async-hooks": "1.30.1", + "@opentelemetry/core": "1.30.1", + "@opentelemetry/propagator-b3": "1.30.1", + "@opentelemetry/propagator-jaeger": "1.30.1", + "@opentelemetry/sdk-trace-base": "1.30.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.30.0.tgz", + "integrity": "sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@pulumi/aws": { + "version": "6.70.0", + "resolved": "https://registry.npmjs.org/@pulumi/aws/-/aws-6.70.0.tgz", + "integrity": "sha512-A1Coq+tCzuqiJXXdX44fI73BoamEI9cmb1iL53wmqzTsaWuCbz1ktD2thponMFfFONjER9I4Bkqthrd0s2Y9VA==", + "dependencies": { + "@pulumi/pulumi": "^3.142.0", + "mime": "^2.0.0" + } + }, + "node_modules/@pulumi/awsx": { + "version": "2.21.0", + "resolved": "https://registry.npmjs.org/@pulumi/awsx/-/awsx-2.21.0.tgz", + "integrity": "sha512-3kUE1J5r+BS25QqL2tQq7mfuCP1CAzGRHUKl+IIl4fQ1K5crI2W7r/FIHfmWResFlBWu5ekS5IfM9fvUHg5ATA==", + "dependencies": { + "@aws-sdk/client-ecs": "^3.405.0", + "@pulumi/aws": "^6.66.2", + "@pulumi/docker": "^4.6.0", + "@pulumi/docker-build": "^0.0.8", + "@pulumi/pulumi": "^3.142.0", + "@types/aws-lambda": "^8.10.23", + "docker-classic": "npm:@pulumi/docker@3.6.1", + "mime": "^2.0.0" + } + }, + "node_modules/@pulumi/docker": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@pulumi/docker/-/docker-4.6.1.tgz", + "integrity": "sha512-csJWLgwq+kAQzJSNbCknXydxDXz9xhss7nI6Crq3hbz/TYifrUImpV22rNSp0dieJz7vGx2QoW+8vxjIBs+HnA==", + "dependencies": { + "@pulumi/pulumi": "^3.142.0", + "semver": "^5.4.0" + } + }, + "node_modules/@pulumi/docker-build": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@pulumi/docker-build/-/docker-build-0.0.8.tgz", + "integrity": "sha512-tS6UUgmDjQ+LVekMXGC/6ND7riY75h3oO9fLiVOrvNafCBumxK+Cjm6ZZ9tUChLYvw6H+rZRExZEhfC8F/SQgQ==", + "hasInstallScript": true, + "dependencies": { + "@pulumi/pulumi": "^3.136.0" + } + }, + "node_modules/@pulumi/pulumi": { + "version": "3.153.1", + "resolved": "https://registry.npmjs.org/@pulumi/pulumi/-/pulumi-3.153.1.tgz", + "integrity": "sha512-NeYLOSOVqsUEhPH3eCRGJNuahtWMHEom2E3NR/LoTccH5SGzlgHeL153lQSjmm2reik/l0B+ZCjPB3rN4HAbqw==", + "dependencies": { + "@grpc/grpc-js": "^1.10.1", + "@logdna/tail-file": "^2.0.6", + "@npmcli/arborist": "^7.3.1", + "@opentelemetry/api": "^1.9", + "@opentelemetry/exporter-zipkin": "^1.28", + "@opentelemetry/instrumentation": "^0.55", + "@opentelemetry/instrumentation-grpc": "^0.55", + "@opentelemetry/resources": "^1.28", + "@opentelemetry/sdk-trace-base": "^1.28", + "@opentelemetry/sdk-trace-node": "^1.28", + "@opentelemetry/semantic-conventions": "^1.28", + "@pulumi/query": "^0.3.0", + "@types/google-protobuf": "^3.15.5", + "@types/semver": "^7.5.6", + "@types/tmp": "^0.2.6", + "execa": "^5.1.0", + "fdir": "^6.1.1", + "google-protobuf": "^3.5.0", + "got": "^11.8.6", + "ini": "^2.0.0", + "js-yaml": "^3.14.0", + "minimist": "^1.2.6", + "normalize-package-data": "^6.0.0", + "picomatch": "^3.0.1", + "pkg-dir": "^7.0.0", + "require-from-string": "^2.0.1", + "semver": "^7.5.2", + "source-map-support": "^0.5.6", + "tmp": "^0.2.1", + "upath": "^1.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ts-node": ">= 7.0.1 < 12", + "typescript": ">= 3.8.3 < 6" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@pulumi/pulumi/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@pulumi/pulumi/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@pulumi/pulumi/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pulumi/pulumi/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/@pulumi/query": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@pulumi/query/-/query-0.3.0.tgz", + "integrity": "sha512-xfo+yLRM2zVjVEA4p23IjQWzyWl1ZhWOGobsBqRpIarzLvwNH/RAGaoehdxlhx4X92302DrpdIFgTICMN4P38w==" + }, + "node_modules/@sigstore/bundle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", + "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", + "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.3.tgz", + "integrity": "sha512-RpacQhBlwpBWd7KEJsRKcBQalbV28fvkxwTOJIqhIuDysMMaJW47V4OqW30iJB9uRpqOSxxEAQFdr8tTattReQ==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", + "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^13.0.1", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", + "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^2.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", + "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.1.tgz", + "integrity": "sha512-fiUIYgIgRjMWznk6iLJz35K2YxSLHzLBA/RC6lBrKfQ8fHbPfvk7Pk9UvpKoHgJjI18MnbPuEju53zcVy6KF1g==", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.0.1.tgz", + "integrity": "sha512-Igfg8lKu3dRVkTSEm98QpZUvKEOa71jDX4vKRcvJVyRc3UgN3j7vFMf0s7xLQhYmKa8kyJGQgUJDOV5V3neVlQ==", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.1.5.tgz", + "integrity": "sha512-HLclGWPkCsekQgsyzxLhCQLa8THWXtB5PxyYN+2O6nkyLt550KQKTlbV2D1/j5dNIQapAZM1+qFnpBFxZQkgCA==", + "dependencies": { + "@smithy/middleware-serde": "^4.0.2", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-stream": "^4.1.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.1.tgz", + "integrity": "sha512-l/qdInaDq1Zpznpmev/+52QomsJNZ3JkTl5yrTl02V6NBgJOQ4LY0SFw/8zsMwj3tLe8vqiIuwF6nxaEwgf6mg==", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.1.tgz", + "integrity": "sha512-3aS+fP28urrMW2KTjb6z9iFow6jO8n3MFfineGbndvzGZit3taZhKWtTorf+Gp5RpFDDafeHlhfsGlDCXvUnJA==", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.1.tgz", + "integrity": "sha512-TJ6oZS+3r2Xu4emVse1YPB3Dq3d8RkZDKcPr71Nj/lJsdAP1c7oFzYqEn1IBc915TsgLl2xIJNuxCz+gLbLE0w==", + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.1.tgz", + "integrity": "sha512-gdudFPf4QRQ5pzj7HEnu6FhKRi61BfH/Gk5Yf6O0KiSbr1LlVhgjThcvjdu658VE6Nve8vaIWB8/fodmS1rBPQ==", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.1.tgz", + "integrity": "sha512-OGXo7w5EkB5pPiac7KNzVtfCW2vKBTZNuCctn++TTSOMpe6RZO/n6WEC1AxJINn3+vWLKW49uad3lo/u0WJ9oQ==", + "dependencies": { + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.0.6.tgz", + "integrity": "sha512-ftpmkTHIFqgaFugcjzLZv3kzPEFsBFSnq1JsIkr2mwFzCraZVhQk2gqN51OOeRxqhbPTkRFj39Qd2V91E/mQxg==", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-serde": "^4.0.2", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "@smithy/url-parser": "^4.0.1", + "@smithy/util-middleware": "^4.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.0.7.tgz", + "integrity": "sha512-58j9XbUPLkqAcV1kHzVX/kAR16GT+j7DUZJqwzsxh1jtz7G82caZiGyyFgUvogVfNTg3TeAOIJepGc8TXF4AVQ==", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/service-error-classification": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-retry": "^4.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.2.tgz", + "integrity": "sha512-Sdr5lOagCn5tt+zKsaW+U2/iwr6bI9p08wOkCp6/eL6iMbgdtc2R5Ety66rf87PeohR0ExI84Txz9GYv5ou3iQ==", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.1.tgz", + "integrity": "sha512-dHwDmrtR/ln8UTHpaIavRSzeIk5+YZTBtLnKwDW3G2t6nAupCiQUvNzNoHBpik63fwUaJPtlnMzXbQrNFWssIA==", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.0.1.tgz", + "integrity": "sha512-8mRTjvCtVET8+rxvmzRNRR0hH2JjV0DFOmwXPrISmTIJEfnCBugpYYGAsCj8t41qd+RB5gbheSQ/6aKZCQvFLQ==", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/shared-ini-file-loader": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.3.tgz", + "integrity": "sha512-dYCLeINNbYdvmMLtW0VdhW1biXt+PPCGazzT5ZjKw46mOtdgToQEwjqZSS9/EN8+tNs/RO0cEWG044+YZs97aA==", + "dependencies": { + "@smithy/abort-controller": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/querystring-builder": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.1.tgz", + "integrity": "sha512-o+VRiwC2cgmk/WFV0jaETGOtX16VNPp2bSQEzu0whbReqE1BMqsP2ami2Vi3cbGVdKu1kq9gQkDAGKbt0WOHAQ==", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.0.1.tgz", + "integrity": "sha512-TE4cpj49jJNB/oHyh/cRVEgNZaoPaxd4vteJNB0yGidOCVR0jCw/hjPVsT8Q8FRmj8Bd3bFZt8Dh7xGCT+xMBQ==", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.1.tgz", + "integrity": "sha512-wU87iWZoCbcqrwszsOewEIuq+SU2mSoBE2CcsLwE0I19m0B2gOJr1MVjxWcDQYOzHbR1xCk7AcOBbGFUYOKvdg==", + "dependencies": { + "@smithy/types": "^4.1.0", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.1.tgz", + "integrity": "sha512-Ma2XC7VS9aV77+clSFylVUnPZRindhB7BbmYiNOdr+CHt/kZNJoPP0cd3QxCnCFyPXC4eybmyE98phEHkqZ5Jw==", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.1.tgz", + "integrity": "sha512-3JNjBfOWpj/mYfjXJHB4Txc/7E4LVq32bwzE7m28GN79+M1f76XHflUaSUkhOriprPDzev9cX/M+dEB80DNDKA==", + "dependencies": { + "@smithy/types": "^4.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.1.tgz", + "integrity": "sha512-hC8F6qTBbuHRI/uqDgqqi6J0R4GtEZcgrZPhFQnMhfJs3MnUTGSnR1NSJCJs5VWlMydu0kJz15M640fJlRsIOw==", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.0.1.tgz", + "integrity": "sha512-nCe6fQ+ppm1bQuw5iKoeJ0MJfz2os7Ic3GBjOkLOPtavbD1ONoyE3ygjBfz2ythFWm4YnRm6OxW+8p/m9uCoIA==", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.1", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.1.6.tgz", + "integrity": "sha512-UYDolNg6h2O0L+cJjtgSyKKvEKCOa/8FHYJnBobyeoeWDmNpXjwOAtw16ezyeu1ETuuLEOZbrynK0ZY1Lx9Jbw==", + "dependencies": { + "@smithy/core": "^3.1.5", + "@smithy/middleware-endpoint": "^4.0.6", + "@smithy/middleware-stack": "^4.0.1", + "@smithy/protocol-http": "^5.0.1", + "@smithy/types": "^4.1.0", + "@smithy/util-stream": "^4.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.1.0.tgz", + "integrity": "sha512-enhjdwp4D7CXmwLtD6zbcDMbo6/T6WtuuKCY49Xxc6OMOmUWlBEBDREsxxgV2LIdeQPW756+f97GzcgAwp3iLw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.1.tgz", + "integrity": "sha512-gPXcIEUtw7VlK8f/QcruNXm7q+T5hhvGu9tl63LsJPZ27exB6dtNwvh2HIi0v7JcXJ5emBxB+CJxwaLEdJfA+g==", + "dependencies": { + "@smithy/querystring-parser": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.7.tgz", + "integrity": "sha512-CZgDDrYHLv0RUElOsmZtAnp1pIjwDVCSuZWOPhIOBvG36RDfX1Q9+6lS61xBf+qqvHoqRjHxgINeQz47cYFC2Q==", + "dependencies": { + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.7.tgz", + "integrity": "sha512-79fQW3hnfCdrfIi1soPbK3zmooRFnLpSx3Vxi6nUlqaaQeC5dm8plt4OTNDNqEEEDkvKghZSaoti684dQFVrGQ==", + "dependencies": { + "@smithy/config-resolver": "^4.0.1", + "@smithy/credential-provider-imds": "^4.0.1", + "@smithy/node-config-provider": "^4.0.1", + "@smithy/property-provider": "^4.0.1", + "@smithy/smithy-client": "^4.1.6", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.1.tgz", + "integrity": "sha512-zVdUENQpdtn9jbpD9SCFK4+aSiavRb9BxEtw9ZGUR1TYo6bBHbIoi7VkrFQ0/RwZlzx0wRBaRmPclj8iAoJCLA==", + "dependencies": { + "@smithy/node-config-provider": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.1.tgz", + "integrity": "sha512-HiLAvlcqhbzhuiOa0Lyct5IIlyIz0PQO5dnMlmQ/ubYM46dPInB+3yQGkfxsk6Q24Y0n3/JmcA1v5iEhmOF5mA==", + "dependencies": { + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.1.tgz", + "integrity": "sha512-WmRHqNVwn3kI3rKk1LsKcVgPBG6iLTBGC1iYOV3GQegwJ3E8yjzHytPt26VNzOWr1qu0xE03nK0Ug8S7T7oufw==", + "dependencies": { + "@smithy/service-error-classification": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.1.2.tgz", + "integrity": "sha512-44PKEqQ303d3rlQuiDpcCcu//hV8sn+u2JBo84dWCE0rvgeiVl0IlLMagbU++o0jCWhYCsHaAt9wZuZqNe05Hw==", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.1", + "@smithy/node-http-handler": "^4.0.3", + "@smithy/types": "^4.1.0", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.2.tgz", + "integrity": "sha512-piUTHyp2Axx3p/kc2CIJkYSv0BAaheBQmbACZgQSSfWUumWNW+R1lL+H9PDBxKJkvOeEX+hKYEFiwO8xagL8AQ==", + "dependencies": { + "@smithy/abort-controller": "^4.0.1", + "@smithy/types": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", + "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.147", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.147.tgz", + "integrity": "sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==" + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/google-protobuf": { + "version": "3.15.12", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.15.12.tgz", + "integrity": "sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==" + }, + "node_modules/@types/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==" + }, + "node_modules/@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bin-links": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-4.0.4.tgz", + "integrity": "sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA==", + "dependencies": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cmd-shim": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-6.0.3.tgz", + "integrity": "sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/common-ancestor-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz", + "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/docker-classic": { + "name": "@pulumi/docker", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@pulumi/docker/-/docker-3.6.1.tgz", + "integrity": "sha512-BZME50QkT556v+LvmTXPT8ssB2xxNkp9+msB5xYFEnUnWcdGAx5yUysQw70RJCb+U0GbkJSbxtlgMJgOQf/now==", + "hasInstallScript": true, + "dependencies": { + "@pulumi/pulumi": "^3.0.0", + "semver": "^5.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com/" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", + "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-protobuf": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==" + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-in-the-middle": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.13.1.tgz", + "integrity": "sha512-k2V9wNm9B+ysuelDTHjI9d5KPc4l8zAZTGqj+pcynvWkypZd857ryzN8jNC7Pg2YZXNMJcHRPpaDyCBbNyVRpA==", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stringify-nice": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/json-stringify-nice/-/json-stringify-nice-1.1.4.tgz", + "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/just-diff": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/just-diff/-/just-diff-6.0.2.tgz", + "integrity": "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==" + }, + "node_modules/just-diff-apply": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/just-diff-apply/-/just-diff-apply-5.5.0.tgz", + "integrity": "sha512-OYTthRfSh55WOItVqwpefPtNt2VdKsq5AnAK6apdtR6yCH8pr0CmSr710J0Mf+WdQy7K/OzMy7K2MgAfdQURDw==" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/long": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz", + "integrity": "sha512-Pp3nFHBThHzVtNY7U6JfPjvT/DTE8+o/4xKsLQtBoU+j2HLsGlhcfzflAoUreaJbNmYnX+LlLi0qjV8kpyO6xQ==", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-packlist": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.1.0.tgz", + "integrity": "sha512-nkc+3pIIhqHVQr085X9d2JzPzLyjzQS96zbruppqC9aZRm/x8xx6xhI98gHtsfELP2bE+loHq8ZaHFHhe+NauA==", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-registry-fetch": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz", + "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==", + "dependencies": { + "@npmcli/redact": "^2.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "node_modules/pacote": { + "version": "18.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", + "integrity": "sha512-+eK3G27SMwsB8kLIuj4h1FUhHtwiEUo21Tw8wNjmvdlpOEr613edv+8FUsTj/4F/VN5ywGE19X18N7CC2EJk6A==", + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/package-json": "^5.1.0", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^8.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^17.0.0", + "proc-log": "^4.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-conflict-json": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/parse-conflict-json/-/parse-conflict-json-3.0.1.tgz", + "integrity": "sha512-01TvEktc68vwbJOtWZluyWeVGWjP+bZwXtPDMQVbBKzbJ/vZBif0L69KH1+cHv1SZ6e0FKLvjyHe8mqsIqYOmw==", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/proggy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/proggy/-/proggy-2.0.0.tgz", + "integrity": "sha512-69agxLtnI8xBs9gUGqEnK26UfiexpHy+KUpBQWabiytQjnn5wFY8rklAi7GRfABIuPNnQ/ik48+LGLkYYJcy4A==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/promise-all-reject-late": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-all-reject-late/-/promise-all-reject-late-1.0.1.tgz", + "integrity": "sha512-vuf0Lf0lOxyQREH7GDIOUMLS7kz+gs8i6B+Yi8dC68a2sychGrHTJYghMBD6k7eUcH0H5P73EckCA48xijWqXw==", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/promise-call-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/promise-call-limit/-/promise-call-limit-3.0.2.tgz", + "integrity": "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-cmd-shim": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz", + "integrity": "sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "optional": true + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/sigstore": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", + "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^2.3.2", + "@sigstore/tuf": "^2.3.4", + "@sigstore/verify": "^1.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==" + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/treeverse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/treeverse/-/treeverse-3.0.0.tgz", + "integrity": "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tuf-js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", + "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "dependencies": { + "@tufjs/models": "2.0.1", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/walk-up-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", + "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==" + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/infrastructure/package.json b/infrastructure/package.json index df9bbea5..316b2d5b 100644 --- a/infrastructure/package.json +++ b/infrastructure/package.json @@ -1,28 +1,23 @@ { - "name": "velog", + "name": "infrastructure", "main": "/src/index.ts", "scripts": { - "ssm": "tsx ./scripts/ssm.ts", - "build": "tsc" + "ssm": "tsx ./scripts/ssm.ts" }, "dependencies": { "@aws-sdk/client-ecr": "^3.413.0", "@aws-sdk/client-ecs": "^3.418.0", - "@aws-sdk/client-ssm": "^3.379.1", - "@pulumi/aws": "^5.42.0", - "@pulumi/awsx": "^1.0.4", - "@pulumi/pulumi": "^3.78.0", - "dotenv": "^16.3.1", - "tsx": "^4.6.2", + "@aws-sdk/client-ssm": "^3.540.0", + "@pulumi/aws": "^6.49.1", + "@pulumi/awsx": "^2.14.0", + "@pulumi/docker": "^4.5.5", + "@pulumi/pulumi": "^3.128.0", + "dotenv": "^16.4.5", + "tsx": "^4.7.1", "zod": "^3.21.4" }, "devDependencies": { - "@types/node": "^20.5.0", - "@typescript-eslint/parser": "^5.59.11", - "eslint": "^8.48.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-prettier": "^5.0.0", - "prettier": "^3.0.2", - "typescript": "^5.3.3" + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.5.0" } } diff --git a/infrastructure/src/packages/cron/index.ts b/infrastructure/src/apps/cron/index.ts similarity index 100% rename from infrastructure/src/packages/cron/index.ts rename to infrastructure/src/apps/cron/index.ts index 7a8d6126..727e48de 100644 --- a/infrastructure/src/packages/cron/index.ts +++ b/infrastructure/src/apps/cron/index.ts @@ -1,8 +1,8 @@ import { ENV } from '../../env' -import { CreateInfraParameter } from '../../type' import { createECSfargateService } from '../../common/ecs' -import { createLoadBalancer } from '../../common/loadBalancer' import { createSecurityGroup } from '../../common/securityGroup' +import { CreateInfraParameter } from '../../type' +import { createLoadBalancer } from '../../common/loadBalancer' export const createCronInfra = ({ vpcId, diff --git a/infrastructure/src/packages/server/index.ts b/infrastructure/src/apps/server/index.ts similarity index 94% rename from infrastructure/src/packages/server/index.ts rename to infrastructure/src/apps/server/index.ts index edcbade8..fcca6d73 100644 --- a/infrastructure/src/packages/server/index.ts +++ b/infrastructure/src/apps/server/index.ts @@ -1,5 +1,5 @@ import { ENV } from '../../env' -import { CreateInfraParameter } from '../../type' +import type { CreateInfraParameter } from '../../type' import { createECSfargateService } from '../../common/ecs' import { createLoadBalancer } from '../../common/loadBalancer' import { createSecurityGroup } from '../../common/securityGroup' diff --git a/infrastructure/src/packages/server/subnet.ts b/infrastructure/src/apps/server/subnet.ts similarity index 87% rename from infrastructure/src/packages/server/subnet.ts rename to infrastructure/src/apps/server/subnet.ts index e9c314e5..3cd90ede 100644 --- a/infrastructure/src/packages/server/subnet.ts +++ b/infrastructure/src/apps/server/subnet.ts @@ -1,6 +1,7 @@ -import * as aws from '@pulumi/aws' -import { withPrefix } from '../../lib/prefix' -import { Input } from '@pulumi/pulumi' +import aws from '@pulumi/aws' + +import type { Input } from '@pulumi/pulumi' +import { withPrefix } from '../../lib/prefix.js' export const createServerSubnet = (vpcId: Input) => { const publicServerSubnet1Name = withPrefix('server-public-subnet-1') diff --git a/infrastructure/src/packages/web/index.ts b/infrastructure/src/apps/web/index.ts similarity index 100% rename from infrastructure/src/packages/web/index.ts rename to infrastructure/src/apps/web/index.ts diff --git a/infrastructure/src/common/ecr.ts b/infrastructure/src/common/ecr.ts index 0a5c1c3b..57103755 100644 --- a/infrastructure/src/common/ecr.ts +++ b/infrastructure/src/common/ecr.ts @@ -1,11 +1,12 @@ -import { PackageType } from './../type.d' import * as clientEcr from '@aws-sdk/client-ecr' -import * as awsx from '@pulumi/awsx' import * as aws from '@pulumi/aws' -import { withPrefix } from '../lib/prefix' -import { ENV } from '../env' import { Repository } from '@pulumi/aws/ecr' import * as pulumi from '@pulumi/pulumi' +import * as docker from '@pulumi/docker' +import { ENV } from '../env' +import { getRandomSHA256Hash } from '../lib/hash' +import { withPrefix } from '../lib/prefix' +import type { PackageType } from '../type' const ecrClient = new clientEcr.ECR({ region: 'ap-northeast-2' }) @@ -72,20 +73,29 @@ const createRepoLifecyclePolicy = (type: PackageType, repo: Repository) => { export const createECRImage = (type: PackageType, repo: Repository): pulumi.Output => { const option = options[type] - const extraOptions = ['--platform', 'linux/amd64', '--build-arg', `DOCKER_ENV=${ENV.dockerEnv}`] - - const image = new awsx.ecr.Image( + const tagName = getRandomSHA256Hash() + const image = new docker.Image( withPrefix(option.imageName), { - repositoryUrl: repo.repositoryUrl, - path: option.path, - extraOptions, + imageName: pulumi.interpolate`${repo.repositoryUrl}:${tagName}`, + skipPush: false, + build: { + context: option.context, + dockerfile: `${option.dockerfile}/Dockerfile`, + platform: 'linux/amd64', + args: { + DOCKER_ENV: `${ENV.dockerEnv}`, + AWS_ACCESS_KEY_ID: `${ENV.awsAccessKeyId}`, + AWS_SECRET_ACCESS_KEY: `${ENV.awsSecretAccessKey}`, + }, + }, }, { retainOnDelete: true, }, ) - return image.imageUri + + return image.imageName } export const getECRImage = (repo: Repository): pulumi.Output => { @@ -109,16 +119,19 @@ const options = { web: { ecrRepoName: ENV.ecrWebRepositoryName, imageName: 'web-image', - path: '../packages/velog-web/', + dockerfile: '../apps/web', + context: '../', }, server: { ecrRepoName: ENV.ecrServerRepositoryName, imageName: 'server-image', - path: '../packages/velog-server/', + dockerfile: '../apps/server', + context: '../', }, cron: { ecrRepoName: ENV.ecrCronRepositoryName, imageName: 'cron-image', - path: '../packages/velog-cron/', + dockerfile: '../apps/cron', + context: '../', }, } diff --git a/infrastructure/src/common/ecs.ts b/infrastructure/src/common/ecs.ts index 7deceaf9..093037d8 100644 --- a/infrastructure/src/common/ecs.ts +++ b/infrastructure/src/common/ecs.ts @@ -1,10 +1,11 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import * as aws from '@pulumi/aws' import * as awsx from '@pulumi/awsx' import * as clientEcs from '@aws-sdk/client-ecs' import { withPrefix } from '../lib/prefix' import { ecsTaskExecutionRole } from './iam' import { ENV } from '../env' -import { CreateECSFargateParams } from '../type' +import type { CreateECSFargateParams } from '../type' import { portMapper } from '../lib/portMapper' import { ecsOption } from '../lib/ecsOptions' @@ -40,14 +41,17 @@ export const createECSfargateService = ({ subnets: subnetIds, }, taskDefinitionArgs: { + // TODO: Ignore changes for taskDefinitionArgs.cpu, memory + // cpu: option.taskCpu, + // memory: option.taskMemory, executionRole: { roleArn: ecsTaskExecutionRole.arn, }, container: { - // name: withPrefix(`${packageType}-container`), + name: withPrefix(`${packageType}-container`), image: imageUri, - cpu: option.cpu, - memory: option.memory, + cpu: option.containerCpu, + memory: option.containerMemory, essential: true, portMappings: [{ targetGroup: targetGroup }], environment: [ @@ -71,7 +75,9 @@ export const createECSfargateService = ({ }, }, }, - { replaceOnChanges: ['taskDefinitionArgs.container.image'] }, + { + replaceOnChanges: ['taskDefinitionArgs.container.image'], + }, ) const resourceId = service.service.id.apply((t) => t.split(':').at(-1)!) diff --git a/infrastructure/src/common/loadBalancer.ts b/infrastructure/src/common/loadBalancer.ts index 0d0c67b7..2675e67a 100644 --- a/infrastructure/src/common/loadBalancer.ts +++ b/infrastructure/src/common/loadBalancer.ts @@ -1,8 +1,8 @@ -import { PackageType } from './../type.d' import * as aws from '@pulumi/aws' +import type { SecurityGroup } from '@pulumi/aws/ec2' import { withPrefix } from '../lib/prefix' -import { SecurityGroup } from '@pulumi/aws/ec2' import { portMapper } from '../lib/portMapper' +import type { PackageType } from '../type' type CreateLoadBalancerParameter = { vpcId: Promise diff --git a/infrastructure/src/common/securityGroup.ts b/infrastructure/src/common/securityGroup.ts index efc04b60..9baf10fa 100644 --- a/infrastructure/src/common/securityGroup.ts +++ b/infrastructure/src/common/securityGroup.ts @@ -1,7 +1,6 @@ import * as aws from '@pulumi/aws' import { withPrefix } from '../lib/prefix' -import { ENV } from '../env' -import { PackageType } from '../type.d' +import { PackageType } from '../type' import { portMapper } from '../lib/portMapper' type CreateSecurityGroupParameter = { diff --git a/infrastructure/src/common/vpc.ts b/infrastructure/src/common/vpc.ts index d87c206e..e56cdc99 100644 --- a/infrastructure/src/common/vpc.ts +++ b/infrastructure/src/common/vpc.ts @@ -1,4 +1,5 @@ -import { createServerSubnet } from '../packages/server/subnet' +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { createServerSubnet } from '../apps/server/subnet' import * as aws from '@pulumi/aws' import { withPrefix } from '../lib/prefix' diff --git a/infrastructure/src/env.ts b/infrastructure/src/env.ts index 68e9482c..f2f93471 100644 --- a/infrastructure/src/env.ts +++ b/infrastructure/src/env.ts @@ -1,7 +1,7 @@ +import { resolve } from 'node:path' +import { existsSync } from 'node:fs' import * as dotenv from 'dotenv' import * as pulumi from '@pulumi/pulumi' -import { resolve } from 'path' -import { existsSync } from 'fs' import { z } from 'zod' const config = new pulumi.Config() diff --git a/infrastructure/src/index.ts b/infrastructure/src/index.ts index 13240ad8..558e9eeb 100644 --- a/infrastructure/src/index.ts +++ b/infrastructure/src/index.ts @@ -1,19 +1,16 @@ -import { CreateInfraParameter, PackageType } from './type.d' -import { createWebInfra } from './packages/web' -import { createServerInfra } from './packages/server' +import { CreateInfraParameter, PackageType } from './type' +import { createWebInfra } from './apps/web' +import { createServerInfra } from './apps/server' +import { createCronInfra } from './apps/cron' import { ENV } from './env' import * as aws from '@pulumi/aws' import * as pulumi from '@pulumi/pulumi' import { createVPC } from './common/vpc' import { getCertificate } from './common/certificate' -import { createCronInfra } from './packages/cron' -import { execCommand } from './lib/execCommand' import { createECRImage, createECRRepository, getECRImage, getECRRepository } from './common/ecr' import { getCluster } from './common/ecs' -execCommand('pnpm -r prisma:copy') - const config = new pulumi.Config() const target = config.get('target') @@ -54,11 +51,11 @@ const createInfraMapper: Record voi export const imageUrls = getCluster().then((cluster) => Object.entries(createInfraMapper).map(async ([pack, func]) => { - let type = pack as PackageType + const type = pack as PackageType let imageUri: pulumi.Output if (targets.includes(type) || target === 'all') { - const newRepo = createECRRepository(type) - imageUri = createECRImage(type, newRepo) + const repo = createECRRepository(type) + imageUri = createECRImage(type, repo) } else { const repo = await getECRRepository(type) imageUri = getECRImage(repo) diff --git a/infrastructure/src/lib/ecsOptions.ts b/infrastructure/src/lib/ecsOptions.ts index d05bb46d..e057586c 100644 --- a/infrastructure/src/lib/ecsOptions.ts +++ b/infrastructure/src/lib/ecsOptions.ts @@ -1,39 +1,93 @@ import { ENV } from '../env' -const serverEcsOption: EcsOption = { - desiredCount: ENV.isProduction ? 2 : 1, - cpu: ENV.isProduction ? 512 : 512, - memory: 1024, - maxCapacity: 12, - minCapacity: ENV.isProduction ? 2 : 1, +// fargate cpu memory combination +// See: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html +// 0.25 vCPU : 0.5 GB ~ 2 GB +// 0.5 vCPU : 1 GB ~ 4 GB +// 1 vCPU : 2 GB ~ 8 GB +// 2 vCPU : 4 GB ~ 16 GB +// 4 vCPU : 8 GB ~ 30 GB + +const webProdEcsOption: EcsBaseOption = { + desiredCount: 2, + cpu: 1, // unit 1024 + memory: 2, // unit 1024 + maxCapacity: 10, + minCapacity: 2, +} + +const webStageEcsOption: EcsBaseOption = { + desiredCount: 1, + cpu: 0.5, // unit 1024 + memory: 1, // unit 1024 + maxCapacity: 10, + minCapacity: 1, } -const webEcsOption: EcsOption = { - desiredCount: ENV.isProduction ? 2 : 1, - cpu: ENV.isProduction ? 1024 : 512, - memory: ENV.isProduction ? 2048 : 1024, +const serverProdEcsOption: EcsBaseOption = { + desiredCount: 2, + cpu: 1, // unit 1024 + memory: 2, // unit 1024 maxCapacity: 12, - minCapacity: ENV.isProduction ? 2 : 1, + minCapacity: 2, } -const cronEcsOption: EcsOption = { +const serverStageEcsOption: EcsBaseOption = { desiredCount: 1, - cpu: 512, - memory: 1024, + cpu: 0.25, // unit 1024 + memory: 1, // unit 1024 + maxCapacity: 10, + minCapacity: 1, +} + +const cronProdEcsOption: EcsBaseOption = { + desiredCount: 1, + cpu: 0.5, // unit 1024 + memory: 1, // unit 1024 maxCapacity: 1, minCapacity: 1, } +const cronStageEcsOption: EcsBaseOption = { + desiredCount: 0, + cpu: 0.25, // unit 1024 + memory: 0.5, // unit 1024 + maxCapacity: 0, + minCapacity: 0, +} + export const ecsOption = { - web: webEcsOption, - server: serverEcsOption, - cron: cronEcsOption, + web: generateEcsOption(ENV.isProduction ? webProdEcsOption : webStageEcsOption), + server: generateEcsOption(ENV.isProduction ? serverProdEcsOption : serverStageEcsOption), + cron: generateEcsOption(ENV.isProduction ? cronProdEcsOption : cronStageEcsOption), } -type EcsOption = { +function generateEcsOption(option: EcsBaseOption): EcsOption { + return { + desiredCount: option.desiredCount, + taskCpu: `${option.cpu} vCPU`, + taskMemory: `${option.memory} GB`, + containerCpu: 1024 * option.cpu, + containerMemory: 1024 * option.memory, + maxCapacity: option.maxCapacity, + minCapacity: option.minCapacity, + } +} + +type EcsBaseOption = { desiredCount: number cpu: number memory: number maxCapacity: number minCapacity: number } + +type EcsOption = { + desiredCount: number + taskCpu: string + taskMemory: string + containerCpu: number + containerMemory: number + maxCapacity: number + minCapacity: number +} diff --git a/infrastructure/src/lib/execCommand.ts b/infrastructure/src/lib/execCommand.ts index 840b606a..895a17cf 100644 --- a/infrastructure/src/lib/execCommand.ts +++ b/infrastructure/src/lib/execCommand.ts @@ -1,4 +1,4 @@ -import { exec } from 'child_process' +import { exec } from 'node:child_process' export const execCommand = (command: string) => { exec(command, (error, stdout, _stderr) => { diff --git a/infrastructure/src/lib/hash.ts b/infrastructure/src/lib/hash.ts new file mode 100644 index 00000000..57a84251 --- /dev/null +++ b/infrastructure/src/lib/hash.ts @@ -0,0 +1,7 @@ +import crypto from 'node:crypto' + +export const getRandomSHA256Hash = () => { + const randomString = crypto.randomBytes(16).toString('hex') + const hash = crypto.createHash('sha256').update(randomString).digest('hex') + return hash +} diff --git a/infrastructure/tsconfig.json b/infrastructure/tsconfig.json index 6ef15682..c80dfb99 100644 --- a/infrastructure/tsconfig.json +++ b/infrastructure/tsconfig.json @@ -1,16 +1,33 @@ { "compilerOptions": { "strict": true, - "outDir": "dist", "target": "es2022", - "module": "commonjs", + "module": "CommonJS", "moduleResolution": "node", "experimentalDecorators": true, "pretty": true, "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "forceConsistentCasingInFileNames": true, - "esModuleInterop": true + "esModuleInterop": true, + "rootDir": "./", + "baseUrl": "./", + "outDir": "./dist", + "paths": { + "@src/*": ["src/*"], + "@common/*": ["src/common/*"], + "@lib/*": ["src/lib/*"], + "@apps/*": ["src/apps/*"] + } }, - "include": ["scripts/**.ts", "src/**/*.ts", ".eslintrc.js"] + "include": [ + "scripts/*.ts", + "src/*", + "src/**/*.ts", + "src/**/*.mts", + "eslint.config.js", + "src/env.ts", + "src/index.ts" + ], + "exclude": ["node_modules", "dist"] } diff --git a/package.json b/package.json index db457856..2afb55f2 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,26 @@ { "name": "velog", - "version": "3.0.0", + "version": "2.4.6", "description": "Velog is a blog platform for developers.", "private": true, "scripts": { - "prepare": "husky install", - "format": "pnpm format --write .", + "format": "pnpm prettier --write \"**/*.{ts,tsx,md}\"", "precommit": "lint-staged", - "dev": "pnpm -r dev" + "dev": "pnpm -r dev", + "build": "turbo build", + "lint": "pnpm -r lint", + "clean": "find . -name \"node_modules\" -type d -prune -exec rm -rf '{}' +", + "prisma:init": "pnpm --filter=@packages/database prisma:generate" }, + "packageManager": "pnpm@9.6.0", "engines": { - "node": ">=18.16", - "pnpm": ">=8.6.0", + "node": ">=20.11.1", + "pnpm": ">=9.1.3", "npm": "please-use-pnpm", "yarn": "please-use-pnpm" }, + "type": "module", "license": "MIT", - "workspaces": [ - "packages/*" - ], "author": { "name": "velopert", "email": "public.velopert@gmail.com" @@ -28,8 +30,25 @@ "blog" ], "devDependencies": { - "husky": "^8.0.0", - "lint-staged": "^13.2.3", - "pnpm": "^8.6.5" + "@eslint/eslintrc": "^3.1.0", + "@packages/eslint-config": "workspace:*", + "@packages/tsconfig": "workspace:*", + "@types/eslint": "^9.6.1", + "@types/eslint__js": "^8.42.3", + "@types/node": "^20.14.0", + "@typescript-eslint/eslint-plugin": "^8.3.0", + "@typescript-eslint/parser": "^8.3.0", + "eslint": "^9.9.0", + "husky": "^9.1.4", + "jiti": "^1.21.6", + "lint-staged": "^15.2.9", + "typescript": "^5.5.4" + }, + "resolutions": { + "graphql": "16.8.1", + "next": "14.2.5" + }, + "dependencies": { + "turbo": "^2.1.1" } } diff --git a/packages/commonjs/eslint.config.mts b/packages/commonjs/eslint.config.mts new file mode 100644 index 00000000..9890b442 --- /dev/null +++ b/packages/commonjs/eslint.config.mts @@ -0,0 +1,13 @@ +import baseConfig from '@packages/eslint-config/base.mjs' +import { resolve } from 'node:path' + +const projectPath = resolve(process.cwd()) + +/** @type {Linter.Config} */ +export default [ + ...baseConfig(projectPath), + {}, + { + ignores: ['node_modules', 'dist'], + }, +] diff --git a/packages/commonjs/package.json b/packages/commonjs/package.json new file mode 100644 index 00000000..c3639d04 --- /dev/null +++ b/packages/commonjs/package.json @@ -0,0 +1,29 @@ +{ + "name": "@packages/commonjs", + "version": "1.0.0", + "description": "", + "exports": { + ".": { + "import": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + } + }, + "files": [ + "dist" + ], + "type": "module", + "license": "MIT", + "scripts": { + "build": "tsc -p tsconfig.build.json && cp ./src/index.d.mts ./dist/index.d.mts" + }, + "dependencies": { + "@graphql-tools/graphql-file-loader": "^8.0.1", + "@graphql-tools/load": "^8.0.2", + "@graphql-tools/merge": "^9.0.6", + "axios": "^1.7.5" + }, + "devDependencies": { + "@types/node": "^20.14.0" + } +} diff --git a/packages/commonjs/src/axios/index.mts b/packages/commonjs/src/axios/index.mts new file mode 100644 index 00000000..7aadd5f4 --- /dev/null +++ b/packages/commonjs/src/axios/index.mts @@ -0,0 +1,7 @@ +import { createRequire } from 'node:module' +import type { Axios } from 'axios' + +const require = createRequire(import.meta.url) +const axios: Axios = require('axios') + +export default axios diff --git a/packages/commonjs/src/graphql-tools/index.mts b/packages/commonjs/src/graphql-tools/index.mts new file mode 100644 index 00000000..732570a8 --- /dev/null +++ b/packages/commonjs/src/graphql-tools/index.mts @@ -0,0 +1,8 @@ +import { createRequire } from 'node:module' +const require = createRequire(import.meta.url) + +const { loadSchemaSync } = require('@graphql-tools/load') +const { mergeResolvers } = require('@graphql-tools/merge') +const { GraphQLFileLoader } = require('@graphql-tools/graphql-file-loader') + +export { loadSchemaSync, mergeResolvers, GraphQLFileLoader } diff --git a/packages/commonjs/src/index.d.mts b/packages/commonjs/src/index.d.mts new file mode 100644 index 00000000..a1b34dde --- /dev/null +++ b/packages/commonjs/src/index.d.mts @@ -0,0 +1,10 @@ +import type { Axios } from 'axios' + +declare module '@packages/commonjs' { + const axios: Axios + const loadSchemaSync: any + const mergeResolvers: any + const GraphQLFileLoader: any + + export { axios, loadSchemaSync, mergeResolvers, GraphQLFileLoader } +} diff --git a/packages/commonjs/src/index.mts b/packages/commonjs/src/index.mts new file mode 100644 index 00000000..79aad2d1 --- /dev/null +++ b/packages/commonjs/src/index.mts @@ -0,0 +1,2 @@ +export { default as axios } from './axios/index.mjs' +export { loadSchemaSync, mergeResolvers, GraphQLFileLoader } from './graphql-tools/index.mjs' diff --git a/packages/commonjs/tsconfig.build.json b/packages/commonjs/tsconfig.build.json new file mode 100644 index 00000000..64ca18b3 --- /dev/null +++ b/packages/commonjs/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": false + }, + "include": ["./src", "./src/index.d.mts"] +} diff --git a/packages/commonjs/tsconfig.json b/packages/commonjs/tsconfig.json new file mode 100644 index 00000000..b7760f19 --- /dev/null +++ b/packages/commonjs/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@packages/tsconfig/base.json", + "compilerOptions": { + "baseUrl": "./", + "rootDir": "./", + "typeRoots": ["./node_modules/@types"], + "outDir": "./dist" + }, + "include": ["./src/*", "./src/*.mts", "./screslint.config.mtsslint.config.js", "./dist"], + "exclude": ["node_modules"] +} diff --git a/packages/velog-prisma/.dockerignore b/packages/database/.dockerignore similarity index 100% rename from packages/velog-prisma/.dockerignore rename to packages/database/.dockerignore diff --git a/packages/velog-prisma/.gitignore b/packages/database/.gitignore similarity index 100% rename from packages/velog-prisma/.gitignore rename to packages/database/.gitignore diff --git a/packages/database/.prettierrc b/packages/database/.prettierrc new file mode 100644 index 00000000..819595c5 --- /dev/null +++ b/packages/database/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "printWidth": 100, + "trailingComma": "all" +} diff --git a/packages/velog-prisma/README.md b/packages/database/README.md similarity index 100% rename from packages/velog-prisma/README.md rename to packages/database/README.md diff --git a/packages/database/env/.env.example b/packages/database/env/.env.example new file mode 100644 index 00000000..9a29dc93 --- /dev/null +++ b/packages/database/env/.env.example @@ -0,0 +1,4 @@ +VELOG_RDS_URL= +VELOG_BOOK_MONGO_URL= +VELOG_REDIS_HOST= +VELOG_REDIS_PORT= \ No newline at end of file diff --git a/packages/database/eslint.config.js b/packages/database/eslint.config.js new file mode 100644 index 00000000..ffd2fde9 --- /dev/null +++ b/packages/database/eslint.config.js @@ -0,0 +1,11 @@ +import baseConfig from '@packages/eslint-config/base.mjs' +import { resolve } from 'node:path' + +const projectPath = resolve(process.cwd()) +/** @type {Linter.Config} */ +export default [ + ...baseConfig(projectPath), + { + ignores: ['node_modules', 'dist'], + }, +] diff --git a/packages/database/package.json b/packages/database/package.json new file mode 100644 index 00000000..51c7fb33 --- /dev/null +++ b/packages/database/package.json @@ -0,0 +1,57 @@ +{ + "name": "@packages/database", + "version": "1.0.0", + "author": { + "name": "velopert", + "email": "public.velopert@gmail.com" + }, + "description": "The database used in the Velog project include pg, mongo, redis, etc", + "exports": { + "./velog-rds": { + "default": "./dist/velog-rds/index.mjs", + "types": "./dist/velog-rds/index.d.ts" + }, + "./velog-book-mongo": { + "default": "./dist/velog-book-mongo/index.mjs", + "types": "./dist/velog-book-mongo/index.d.ts" + }, + "./velog-redis": { + "default": "./dist/velog-redis/index.mjs", + "types": "./dist/velog-redis/index.mjs" + } + }, + "license": "MIT", + "engines": { + "node": ">=20.11.1" + }, + "type": "module", + "scripts": { + "create-mock": "tsx ./scripts/createMock.mts", + "prisma-migrate:dev": "pnpm env:copy --only-dev && pnpm prisma migrate dev --schema=./prisma/velog-rds/schema.prisma --create-only", + "prisma-deploy:rds": "pnpm env:copy && dotenv -e .env -- tsx ./scripts/rdsDeploy.mts", + "prisma-deploy:mongo": "pnpm env:copy && dotenv -e .env -- tsx ./scripts/mongoDeploy.mts", + "prisma:generate": "tsx ./scripts/generatePrisma.mts", + "build": "tsc -p tsconfig.build.json", + "lint": "eslint --fix", + "env:copy": "tsx ./scripts/copyEnv.mts", + "ssm": "tsx ./scripts/ssm.mts" + }, + "dependencies": { + "@packages/scripts": "workspace:*", + "@prisma/client": "^5.17.0", + "dotenv": "^16.4.5", + "dotenv-cli": "^7.2.1", + "inquirer": "^9.2.23", + "ioredis": "^5.3.2", + "prisma": "^5.17.0", + "tsx": "^4.7.1", + "tsyringe": "^4.7.0", + "zod": "^3.22.4" + }, + "devDependencies": { + "@packages/eslint-config": "workspace:*", + "@packages/tsconfig": "workspace:*", + "@types/inquirer": "^9.0.7", + "@types/jest": "^29.5.3" + } +} diff --git a/packages/database/prisma/velog-book-mongo/schema.prisma b/packages/database/prisma/velog-book-mongo/schema.prisma new file mode 100644 index 00000000..9cbebece --- /dev/null +++ b/packages/database/prisma/velog-book-mongo/schema.prisma @@ -0,0 +1,101 @@ +generator client { + provider = "prisma-client-js" + output = "../../node_modules/@prisma/velog-book-mongo/client" +} + +datasource db { + provider = "mongodb" + url = env("VELOG_BOOK_MONGO_URL") +} + +model Writer { + id String @id @default(auto()) @map("_id") @db.ObjectId + fk_user_id String @unique + username String @unique + display_name String @db.String + thumbnail String @db.String + email String @unique + short_bio String? @db.String + last_accessed_at DateTime @default(now()) + books Book[] + images Image[] + + created_at DateTime @default(now()) + + @@map("writers") +} + +model Book { + id String @id @default(auto()) @map("_id") @db.ObjectId + title String @db.String + thumbnail String @db.String + fk_writer_id String @db.ObjectId + is_published Boolean @default(false) + is_private Boolean @default(true) + is_temp Boolean @default(true) + is_deleted Boolean @default(false) + description String @db.String + released_at DateTime? @db.Timestamp + url_slug String @unique // example $@{username}/${escapeUrl(title).toLowerCase()} + published_url String @default("") @db.String + deploy_code String? @default("") @db.String + + created_at DateTime @default(now()) + updated_at DateTime @default(now()) + + writer Writer @relation(fields: [fk_writer_id], references: [id]) + pages Page[] + + @@map("books") +} + +model Page { + id String @id @default(auto()) @map("_id") @db.ObjectId + fk_writer_id String @db.ObjectId + fk_book_id String @db.ObjectId + title String @db.String + body String @db.String + type String @db.String + parent_id String? @db.ObjectId + index Int? @db.Int + url_slug String @unique // example /${escapeUrl(${parent.title}/${title}).toLowerCase()}}-${code} + code String @unique + depth Int @default(1) @db.Int // max depth 3 + is_deleted Boolean @default(false) + + created_at DateTime @default(now()) + updated_at DateTime @default(now()) + + parent Page? @relation("ParentChildren", fields: [parent_id], references: [id], onDelete: NoAction, onUpdate: NoAction) + childrens Page[] @relation("ParentChildren") + book Book @relation(fields: [fk_book_id], references: [id]) + + @@index([url_slug, code]) + @@map("pages") +} + +model PageHistory { + id String @id @default(auto()) @map("_id") @db.ObjectId + fk_writer_id String @db.ObjectId + fk_page_id String @db.String + title String @db.String + body String @db.String + + @@index([fk_page_id]) + @@map("post_histories") +} + +model Image { + id String @id @default(auto()) @map("_id") @db.ObjectId + fk_writer_id String @db.ObjectId + path String? @db.String + filesize Int? @db.Int + type String? @db.String + key String? @default("") @db.String + ref_id String? @db.ObjectId + created_at DateTime @default(now()) + + writer Writer @relation(fields: [fk_writer_id], references: [id]) + + @@map("images") +} diff --git a/packages/velog-prisma/prisma/migrations/0_init/migration.sql b/packages/database/prisma/velog-rds/migrations/0_init/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/0_init/migration.sql rename to packages/database/prisma/velog-rds/migrations/0_init/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230402090409_external_integration/migration.sql b/packages/database/prisma/velog-rds/migrations/20230402090409_external_integration/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230402090409_external_integration/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230402090409_external_integration/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230402090455_add_app_identifier/migration.sql b/packages/database/prisma/velog-rds/migrations/20230402090455_add_app_identifier/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230402090455_add_app_identifier/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230402090455_add_app_identifier/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230402154731_unique_index_for_integration/migration.sql b/packages/database/prisma/velog-rds/migrations/20230402154731_unique_index_for_integration/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230402154731_unique_index_for_integration/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230402154731_unique_index_for_integration/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230402163700_unique_userid_for_profile/migration.sql b/packages/database/prisma/velog-rds/migrations/20230402163700_unique_userid_for_profile/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230402163700_unique_userid_for_profile/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230402163700_unique_userid_for_profile/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230420170509_fk_user_id_not_nullable/migration.sql b/packages/database/prisma/velog-rds/migrations/20230420170509_fk_user_id_not_nullable/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230420170509_fk_user_id_not_nullable/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230420170509_fk_user_id_not_nullable/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230614063534_make_username_unique/migration.sql b/packages/database/prisma/velog-rds/migrations/20230614063534_make_username_unique/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230614063534_make_username_unique/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230614063534_make_username_unique/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230818073753_add_score_field_and_index/migration.sql b/packages/database/prisma/velog-rds/migrations/20230818073753_add_score_field_and_index/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230818073753_add_score_field_and_index/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230818073753_add_score_field_and_index/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230907053730_add_follow_users_table/migration.sql b/packages/database/prisma/velog-rds/migrations/20230907053730_add_follow_users_table/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230907053730_add_follow_users_table/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230907053730_add_follow_users_table/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230911154656_add_last_accessed_at_field_for_profile/migration.sql b/packages/database/prisma/velog-rds/migrations/20230911154656_add_last_accessed_at_field_for_profile/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230911154656_add_last_accessed_at_field_for_profile/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230911154656_add_last_accessed_at_field_for_profile/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230913113525_remove_follow_table/migration.sql b/packages/database/prisma/velog-rds/migrations/20230913113525_remove_follow_table/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230913113525_remove_follow_table/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230913113525_remove_follow_table/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230913113603_recreate_follow_user_table/migration.sql b/packages/database/prisma/velog-rds/migrations/20230913113603_recreate_follow_user_table/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230913113603_recreate_follow_user_table/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230913113603_recreate_follow_user_table/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20230927051803_recreate_feed_table/migration.sql b/packages/database/prisma/velog-rds/migrations/20230927051803_recreate_feed_table/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20230927051803_recreate_feed_table/migration.sql rename to packages/database/prisma/velog-rds/migrations/20230927051803_recreate_feed_table/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20231219044048_create_ads_table/migration.sql b/packages/database/prisma/velog-rds/migrations/20231219044048_create_ads_table/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20231219044048_create_ads_table/migration.sql rename to packages/database/prisma/velog-rds/migrations/20231219044048_create_ads_table/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20231219060038_add_url_column/migration.sql b/packages/database/prisma/velog-rds/migrations/20231219060038_add_url_column/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20231219060038_add_url_column/migration.sql rename to packages/database/prisma/velog-rds/migrations/20231219060038_add_url_column/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20231226223017_add_indexes_feed_table/migration.sql b/packages/database/prisma/velog-rds/migrations/20231226223017_add_indexes_feed_table/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20231226223017_add_indexes_feed_table/migration.sql rename to packages/database/prisma/velog-rds/migrations/20231226223017_add_indexes_feed_table/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20240126061422_create_notification_table/migration.sql b/packages/database/prisma/velog-rds/migrations/20240126061422_create_notification_table/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20240126061422_create_notification_table/migration.sql rename to packages/database/prisma/velog-rds/migrations/20240126061422_create_notification_table/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20240207042213_add_is_noticed_column/migration.sql b/packages/database/prisma/velog-rds/migrations/20240207042213_add_is_noticed_column/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20240207042213_add_is_noticed_column/migration.sql rename to packages/database/prisma/velog-rds/migrations/20240207042213_add_is_noticed_column/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20240214063934_change_read_at_column_type_in_notification/migration.sql b/packages/database/prisma/velog-rds/migrations/20240214063934_change_read_at_column_type_in_notification/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20240214063934_change_read_at_column_type_in_notification/migration.sql rename to packages/database/prisma/velog-rds/migrations/20240214063934_change_read_at_column_type_in_notification/migration.sql diff --git a/packages/velog-prisma/prisma/migrations/20240225113821_add_dynamic_config_items_table/migration.sql b/packages/database/prisma/velog-rds/migrations/20240225113821_create_dynamic_config_items_table/migration.sql similarity index 100% rename from packages/velog-prisma/prisma/migrations/20240225113821_add_dynamic_config_items_table/migration.sql rename to packages/database/prisma/velog-rds/migrations/20240225113821_create_dynamic_config_items_table/migration.sql diff --git a/packages/database/prisma/velog-rds/migrations/20240311221015_add_count_and_last_used_at_to_dynamic_config_item/migration.sql b/packages/database/prisma/velog-rds/migrations/20240311221015_add_count_and_last_used_at_to_dynamic_config_item/migration.sql new file mode 100644 index 00000000..c14d1d7f --- /dev/null +++ b/packages/database/prisma/velog-rds/migrations/20240311221015_add_count_and_last_used_at_to_dynamic_config_item/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "dynamic_config_items" ADD COLUMN "last_used_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "usage_count" INTEGER NOT NULL DEFAULT 0; diff --git a/packages/velog-prisma/prisma/migrations/migration_lock.toml b/packages/database/prisma/velog-rds/migrations/migration_lock.toml similarity index 100% rename from packages/velog-prisma/prisma/migrations/migration_lock.toml rename to packages/database/prisma/velog-rds/migrations/migration_lock.toml diff --git a/packages/velog-prisma/prisma/schema.prisma b/packages/database/prisma/velog-rds/schema.prisma similarity index 98% rename from packages/velog-prisma/prisma/schema.prisma rename to packages/database/prisma/velog-rds/schema.prisma index 8341e0b3..4de43738 100644 --- a/packages/velog-prisma/prisma/schema.prisma +++ b/packages/database/prisma/velog-rds/schema.prisma @@ -1,11 +1,12 @@ generator client { provider = "prisma-client-js" + output = "../../node_modules/@prisma/velog-rds/client" previewFeatures = ["postgresqlExtensions"] } datasource db { provider = "postgresql" - url = env("DATABASE_URL") + url = env("VELOG_RDS_URL") extensions = [uuidOssp(map: "uuid-ossp")] } @@ -642,9 +643,11 @@ model Notification { } model DynamicConfigItem { - id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid - value String @db.VarChar(50) - type String @db.VarChar(50) + id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + value String @db.VarChar(50) + type String @db.VarChar(50) + usage_count Int @default(0) + last_used_at DateTime @default(now()) @db.Timestamptz(6) @@unique([value, type]) @@index([value]) diff --git a/packages/database/scripts/copyEnv.mts b/packages/database/scripts/copyEnv.mts new file mode 100644 index 00000000..28bae0e7 --- /dev/null +++ b/packages/database/scripts/copyEnv.mts @@ -0,0 +1,4 @@ +import { CopyEnvScript } from '@packages/scripts' + +const envScript = new CopyEnvScript() +envScript.execute() diff --git a/packages/database/scripts/env.mts b/packages/database/scripts/env.mts new file mode 100644 index 00000000..e2e1ebd9 --- /dev/null +++ b/packages/database/scripts/env.mts @@ -0,0 +1,34 @@ +import path from 'path' +import { z } from 'zod' +import { existsSync } from 'fs' +import dotenv from 'dotenv' + +// Only used for migration and deployment +if (!process.env.DOCKER_ENV && process.env.NODE_ENV !== undefined) { + console.error( + 'Development environment was initiated, despite the absence of the Docker environment.', + ) +} + +const configPath = path.resolve(process.cwd(), '.env') + +if (!existsSync(configPath)) { + console.log(`Read target: ${configPath}`) + throw new Error('Not found environment file') +} + +dotenv.config({ path: configPath }) + +const env = z.object({ + velogRdsUrl: z.string(), + velogBookMongoUrl: z.string(), + velogRedisHost: z.string(), + velogRedisPort: z.number(), +}) + +export const ENV = env.parse({ + velogRdsUrl: process.env.VELOG_RDS_URL, + velogBookMongoUrl: process.env.VELOG_BOOK_MONGO_URL, + velogRedisHost: process.env.VELOG_REDIS_HOST, + velogRedisPort: Number(process.env.VELOG_REDIS_PORT), +}) diff --git a/packages/database/scripts/generatePrisma.mts b/packages/database/scripts/generatePrisma.mts new file mode 100644 index 00000000..da4c6957 --- /dev/null +++ b/packages/database/scripts/generatePrisma.mts @@ -0,0 +1,32 @@ +import fs from 'fs' +import { exec } from 'child_process' + +function main() { + if (!fs.existsSync('./prisma')) { + fs.mkdirSync('./prisma') + } + + setTimeout(() => { + exec( + `prisma generate --schema='./prisma/velog-rds/schema.prisma'`, + (error, stdout, _stderr) => { + if (error) { + console.log('error', _stderr) + } + }, + ) + }, 0) + + setTimeout(() => { + exec( + `prisma generate --schema='./prisma/velog-book-mongo/schema.prisma'`, + (error, stdout, _stderr) => { + if (error) { + console.log('error', _stderr) + } + }, + ) + }, 0) +} + +main() diff --git a/packages/database/scripts/mongoDeploy.mts b/packages/database/scripts/mongoDeploy.mts new file mode 100644 index 00000000..04e4473b --- /dev/null +++ b/packages/database/scripts/mongoDeploy.mts @@ -0,0 +1,36 @@ +import inquirer from 'inquirer' +import { exec } from 'child_process' +import { ENV } from 'scripts/env.mjs' + +const main = async () => { + const { answer } = await inquirer.prompt([ + { + type: 'list', + name: 'answer', + message: `Target Database Info: ${ENV.velogBookMongoUrl}\n🤔 Are you sure?`, + choices: ['No', 'Yes'], + default: 'No', + }, + ]) + + if (answer === 'No') { + console.info('🚫 db push process stopped by user.') + process.exit(0) + } + + run_exec() +} + +main() + +const run_exec = () => { + exec( + `dotenv -e .env -- npx prisma db push --schema=./prisma/velog-book-mongo/schema.prisma`, + (error, stdout, _stderr) => { + if (error) { + console.log(_stderr) + } + console.log('stdout', stdout) + }, + ) +} diff --git a/packages/velog-prisma/scripts/deploy.ts b/packages/database/scripts/rdsDeploy.mts similarity index 72% rename from packages/velog-prisma/scripts/deploy.ts rename to packages/database/scripts/rdsDeploy.mts index 3d133eb8..e976f940 100644 --- a/packages/velog-prisma/scripts/deploy.ts +++ b/packages/database/scripts/rdsDeploy.mts @@ -1,12 +1,14 @@ -import { PrismaClient } from '@prisma/client' +import { PrismaClient } from '@prisma/velog-rds/client' import path from 'path' import fs from 'fs' import inquirer from 'inquirer' import { exec } from 'child_process' +import { ENV } from 'scripts/env.mjs' -// "prisma-migrate:deploy": "pnpm env:copy && pnpm prisma migrate deploy --schema=./prisma/schema.prisma" +// "prisma-migrate:deploy": "pnpm env:copy && pnpm prisma migrate deploy --schema=./prisma/velog-rds/schema.prisma" -const db = new PrismaClient() +const db = new PrismaClient({ datasourceUrl: ENV.velogRdsUrl }) +const migraionsPath = './prisma/velog-rds/migrations' const main = async () => { const filenames = getMigrationFilenames() @@ -14,7 +16,8 @@ const main = async () => { const diff = filenames.filter((filename) => !appliedMigrationNames.includes(filename)) const message = getMessage(diff) - console.log(`Target Database Info: ${process.env.DATABASE_URL}\n`) + + console.log(`Target Database Info: ${ENV.velogRdsUrl}`) const { answer } = await inquirer.prompt([ { @@ -31,18 +34,21 @@ const main = async () => { process.exit(0) } - exec('pnpm prisma migrate deploy --schema=./prisma/schema.prisma', (error, stdout, _stderr) => { - if (error) { - console.log(_stderr) - } - console.log(stdout) - }) + exec( + 'pnpm prisma migrate deploy --schema=./prisma/velog-rds/schema.prisma', + (error, stdout, _stderr) => { + if (error) { + console.log('_stderr', _stderr) + } + console.log('stdout', stdout) + }, + ) } main() function getMigrationFilenames() { - const dirPath = path.resolve(process.cwd(), './prisma/migrations/') + const dirPath = path.resolve(process.cwd(), migraionsPath) return fs.readdirSync(dirPath).filter((file) => path.extname(file) !== '.toml') } @@ -63,12 +69,7 @@ function getMessage(migrationDiff: string[]): string { } function getMigrationSummary(filename: string) { - const filepath = path.resolve( - process.cwd(), - `./prisma/migrations`, - `${filename}`, - './migration.sql', - ) + const filepath = path.resolve(process.cwd(), migraionsPath, `${filename}`, './migration.sql') const sql = fs.readFileSync(filepath, { encoding: 'utf-8' }) const lines = sql.split('\n') diff --git a/packages/database/scripts/ssm.mts b/packages/database/scripts/ssm.mts new file mode 100644 index 00000000..5e12e8d0 --- /dev/null +++ b/packages/database/scripts/ssm.mts @@ -0,0 +1,4 @@ +import { SSMScript } from '@packages/scripts' + +const ssmScript = new SSMScript({ packageName: 'database' }) +ssmScript.execute() diff --git a/packages/database/src/velog-book-mongo/index.d.ts b/packages/database/src/velog-book-mongo/index.d.ts new file mode 100644 index 00000000..33aaf8e1 --- /dev/null +++ b/packages/database/src/velog-book-mongo/index.d.ts @@ -0,0 +1 @@ +export * from '@prisma/velog-book-mongo/client/index.js' diff --git a/packages/database/src/velog-book-mongo/index.mts b/packages/database/src/velog-book-mongo/index.mts new file mode 100644 index 00000000..26c6a3ed --- /dev/null +++ b/packages/database/src/velog-book-mongo/index.mts @@ -0,0 +1,19 @@ +import type { PrismaClientOptions } from '@prisma/client/runtime/library' +import { PrismaClient as Client } from '@prisma/velog-book-mongo/client/index.js' +export * from '@prisma/velog-book-mongo/client/index.js' + +type Options = Omit + +export class PrismaClient extends Client { + private datasourceUrl: string + constructor(options: Options) { + if (!options.datasourceUrl) throw new Error('velog book datasourceUrl is required') + super(options) + this.datasourceUrl = options.datasourceUrl + } + + public async connection(): Promise { + await super.$connect() + console.info(`INFO: Database connected to "${this.datasourceUrl.split('@')[1]}"`) + } +} diff --git a/packages/database/src/velog-rds/index.d.ts b/packages/database/src/velog-rds/index.d.ts new file mode 100644 index 00000000..e8e8b6ee --- /dev/null +++ b/packages/database/src/velog-rds/index.d.ts @@ -0,0 +1 @@ +export * from '@prisma/velog-rds/client/index.js' diff --git a/packages/database/src/velog-rds/index.mts b/packages/database/src/velog-rds/index.mts new file mode 100644 index 00000000..dc4c8fc9 --- /dev/null +++ b/packages/database/src/velog-rds/index.mts @@ -0,0 +1,19 @@ +import type { PrismaClientOptions } from '@prisma/client/runtime/library' +import { PrismaClient as Client } from '@prisma/velog-rds/client/index.js' +export * from '@prisma/velog-rds/client/index.js' + +type Options = Omit + +export class PrismaClient extends Client { + private datasourceUrl: string + constructor(options: Options) { + if (!options.datasourceUrl) throw new Error('velog rds datasourceUrl is required') + super(options) + this.datasourceUrl = options.datasourceUrl + } + + public async connection(): Promise { + await super.$connect() + console.info(`INFO: Database connected to "${this.datasourceUrl.split('@')[1]}"`) + } +} diff --git a/packages/database/src/velog-redis/index.d.ts b/packages/database/src/velog-redis/index.d.ts new file mode 100644 index 00000000..f91e0dee --- /dev/null +++ b/packages/database/src/velog-redis/index.d.ts @@ -0,0 +1 @@ +export { type RedisService } from './index.mjs' diff --git a/packages/database/src/velog-redis/index.mts b/packages/database/src/velog-redis/index.mts new file mode 100644 index 00000000..28d7bac1 --- /dev/null +++ b/packages/database/src/velog-redis/index.mts @@ -0,0 +1,112 @@ +import Redis from 'ioredis' + +interface Service { + connection(): Promise + get generateKey(): GenerateRedisKey + get queueName(): Record + addToCreateFeedQueue(data: CreateFeedQueueData): Promise + addToCheckPostSpamQueue(data: CheckPostSpamQueueData): Promise + addToScorePostQueue(data: ScorePostQueueData): Promise +} + +type RedisOptions = { + port: number + host: string +} + +export class RedisService extends Redis.default implements Service { + host: string + port: number + constructor({ port, host }: RedisOptions) { + if (!port) throw new Error('redis port is required') + if (!host) throw new Error('redis host is required') + super({ port: port, host }) + this.host = host + this.port = port + } + + public async connection(): Promise { + return new Promise((resolve) => { + super.connect(() => { + resolve() + console.info(`INFO: Redis connected to "${this.host}:${this.port}"`) + }) + }) + } + + public get generateKey(): GenerateRedisKey { + return { + recommendedPost: (postId: string) => `${postId}:recommend`, + postCache: (username: string, postUrlSlug: string) => `ssr:/@${username}/${postUrlSlug}`, + userCache: (username: string) => `ssr:/@${username}`, + postSeries: (username: string, seriesUrlSlug: string) => + `ssr:/@${username}/series/${seriesUrlSlug}`, + changeEmail: (code: string) => `changeEmailCode:${code}`, + trendingWriters: () => `trending:writers`, + existsUser: (userId: string) => `exists:user:${userId}`, + existsWriter: (userId: string) => `exists:writer:${userId}`, + errorMessageCache: (type: string, userId: string) => `error:${type}:${userId}`, + buildBook: (bookId: string) => `book:build:${bookId}`, + deployBook: (bookId: string) => `book:deploy:${bookId}`, + } + } + + public get queueName(): Record { + return { + createFeed: 'queue:feed', + checkPostSpam: 'queue:checkPostSpam', + scorePost: 'queue:scorePost', + } + } + + public async addToCreateFeedQueue(data: CreateFeedQueueData): Promise { + const queueName = this.queueName.createFeed + return await this.lpush(queueName, JSON.stringify(data)) + } + + public async addToCheckPostSpamQueue(data: CheckPostSpamQueueData): Promise { + const queueName = this.queueName.checkPostSpam + return await this.lpush(queueName, JSON.stringify(data)) + } + + public async addToScorePostQueue(data: ScorePostQueueData): Promise { + const queueName = this.queueName.scorePost + return await this.lpush(queueName, JSON.stringify(data)) + } +} + +type GenerateRedisKey = { + recommendedPost: (postId: string) => string + postCache: (username: string, postUrlSlug: string) => string + userCache: (username: string) => string + postSeries: (username: string, seriesUrlSlug: string) => string + changeEmail: (code: string) => string + trendingWriters: () => string + existsUser: (userId: string) => string + existsWriter: (userId: string) => string + errorMessageCache: (type: string, userId: string) => string + buildBook: (bookId: string) => string + deployBook: (bookId: string) => string +} + +type QueueName = 'createFeed' | 'checkPostSpam' | 'scorePost' + +export type ChangeEmailArgs = { + email: string + userId: string +} + +export type CreateFeedQueueData = { + fk_following_id: string + fk_post_id: string +} + +export type CheckPostSpamQueueData = { + post_id: string + user_id: string + ip: string +} + +export type ScorePostQueueData = { + post_id: string +} diff --git a/packages/database/tsconfig.build.json b/packages/database/tsconfig.build.json new file mode 100644 index 00000000..c57911cb --- /dev/null +++ b/packages/database/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true + }, + "include": ["./src"] +} diff --git a/packages/database/tsconfig.json b/packages/database/tsconfig.json new file mode 100644 index 00000000..ce3cba2e --- /dev/null +++ b/packages/database/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@packages/tsconfig/base.json", + "compilerOptions": { + "baseUrl": "./", + "rootDir": "./", + "typeRoots": ["./node_modules/@types"], + "outDir": "./dist" + }, + "include": ["./src", "./scripts/**.mts", "eslint.config.js"], + "exclude": ["node_modules"] +} diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json new file mode 100644 index 00000000..586e8dc1 --- /dev/null +++ b/packages/eslint-config/package.json @@ -0,0 +1,26 @@ +{ + "name": "@packages/eslint-config", + "version": "1.0.0", + "exports": { + "./base.mjs": { + "default": "./dist/base.mjs", + "types": "./dist/base.d.mts" + }, + "./next.mjs": { + "default": "./dist/next.mjs", + "types": "./dist/next.d.mts" + } + }, + "scripts": { + "build": "tsc -p tsconfig.json" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.5.0", + "@packages/tsconfig": "workspace:*", + "@types/node": "^20.14.0", + "eslint-config-prettier": "^9.0.0", + "jiti": "^1.21.6", + "prettier": "^3.2.5" + } +} \ No newline at end of file diff --git a/packages/eslint-config/src/base.mts b/packages/eslint-config/src/base.mts new file mode 100644 index 00000000..f11142c1 --- /dev/null +++ b/packages/eslint-config/src/base.mts @@ -0,0 +1,58 @@ +import { resolve } from 'node:path' +import { FlatCompat } from '@eslint/eslintrc' +import js from '@eslint/js' +import type { Linter } from 'eslint' + +const baseConfig = (projectPath: string) => { + const tsconfigPath = resolve(projectPath, 'tsconfig.json') + + const compat = new FlatCompat({ + baseDirectory: projectPath, + resolvePluginsRelativeTo: projectPath, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, + }) + + return [ + { + ignores: ['node_modules'], + }, + ...compat.config({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: tsconfigPath, + sourceType: 'module', + tsconfigRootDir: projectPath, + }, + plugins: ['@typescript-eslint'], + extends: ['plugin:@typescript-eslint/recommended'], + root: true, + env: { + node: true, + jest: true, + }, + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-empty-interface': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'off', + { + ignoreRestSiblings: true, + argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + }, + }), + ] satisfies Linter.Config[] +} + +export default baseConfig diff --git a/packages/eslint-config/src/next.mts b/packages/eslint-config/src/next.mts new file mode 100644 index 00000000..b4764dab --- /dev/null +++ b/packages/eslint-config/src/next.mts @@ -0,0 +1,52 @@ +import { Linter } from 'eslint' +import { resolve } from 'node:path' +import { FlatCompat } from '@eslint/eslintrc' +import js from '@eslint/js' + +const nextConfig = (projectPath: string) => { + const tsconfigPath = resolve(projectPath, 'tsconfig.json') + + const compat = new FlatCompat({ + baseDirectory: projectPath, + resolvePluginsRelativeTo: projectPath, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, + }) + + return [ + { + ignores: ['node_modules', '.next', '.out'], + }, + ...compat.config({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: tsconfigPath, + sourceType: 'module', + tsconfigRootDir: projectPath, + }, + plugins: ['@typescript-eslint'], + extends: [ + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + // 'plugin:react-hooks/recommended', // not yey support eslint 9 + ], + root: true, + globals: { + React: true, + JSX: true, + }, + env: { + node: true, + browser: true, + }, + rules: { + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@next/next/no-html-link-for-pages': 'off', + '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }], + }, + }), + ] satisfies Linter.Config[] +} + +export default nextConfig diff --git a/packages/eslint-config/tsconfig.json b/packages/eslint-config/tsconfig.json new file mode 100644 index 00000000..40011224 --- /dev/null +++ b/packages/eslint-config/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@packages/tsconfig/base.json", + "compilerOptions": { + "rootDir": "./src", + "baseUrl": "./", + "outDir": "./dist", + "declaration": true, + "allowJs": true, + "typeRoots": ["./node_modules/@types", "./types"] + }, + "include": ["./src", "./types.d.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/eslint-config/types.d.ts b/packages/eslint-config/types.d.ts new file mode 100644 index 00000000..586c463d --- /dev/null +++ b/packages/eslint-config/types.d.ts @@ -0,0 +1,3 @@ +declare module '@eslint/eslintrc' { + export const FlatCompat: any +} diff --git a/packages/library/eslint.config.js b/packages/library/eslint.config.js new file mode 100644 index 00000000..3871f86d --- /dev/null +++ b/packages/library/eslint.config.js @@ -0,0 +1,12 @@ +import baseConfig from '@packages/eslint-config/base.mjs' +import { resolve } from 'node:path' + +const projectPath = resolve(process.cwd()) + +/** @type {Linter.Config} */ +export default [ + ...baseConfig(projectPath), + { + ignores: ['node_modules', 'dist'], + }, +] diff --git a/packages/library/package.json b/packages/library/package.json new file mode 100644 index 00000000..ddebb3d7 --- /dev/null +++ b/packages/library/package.json @@ -0,0 +1,64 @@ +{ + "name": "@packages/library", + "version": "1.0.0", + "description": "", + "type": "module", + "author": { + "name": "velopert", + "email": "public.velopert@gmail.com" + }, + "license": "MIT", + "engines": { + "node": ">=20.11.1" + }, + "exports": { + "./awsS3": { + "default": "./dist/awsS3/AwsS3Service.mjs", + "types": "./dist/awsS3/AwsS3Service.d.mts" + }, + "./discord": { + "default": "./dist/discord/DiscordService.mjs", + "types": "./dist/discord/DiscordService.d.mts" + }, + "./fastifyCookie": { + "default": "./dist/fastifyCookie/FastifyCookieService.mjs", + "types": "./dist/fastifyCookie/FastifyCookieService.d.mts" + }, + "./jwt": { + "default": "./dist/jwt/JwtService.mjs", + "types": "./dist/jwt/JwtService.d.mts" + }, + "./r2": { + "default": "./dist/cloudflare/R2/R2Service.mjs", + "types": "./dist/cloudflare/R2/R2Service.d.mts" + }, + "./utils": { + "default": "./dist/utils/UtilsService.mjs", + "types:": "./dist/utils/UtilsService.d.mts" + } + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "create-service": "tsx ./scripts/createService.mts", + "lint": "eslint --fix" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.587.0", + "@fastify/cookie": "^9.3.1", + "@packages/commonjs": "workspace:*", + "@packages/scripts": "workspace:*", + "discord.js": "^14.14.1", + "fastify": "^4.26.2", + "jsonwebtoken": "^9.0.2", + "nanoid": "^5.0.7", + "nanoid-dictionary": "^4.3.0" + }, + "devDependencies": { + "@packages/eslint-config": "workspace:*", + "@packages/tsconfig": "workspace:*", + "@types/jest": "^29.5.12", + "@types/jsonwebtoken": "^9.0.2", + "@types/nanoid-dictionary": "^4.2.3", + "tsx": "^4.7.2" + } +} diff --git a/packages/library/scripts/createService.mts b/packages/library/scripts/createService.mts new file mode 100644 index 00000000..d2c5096f --- /dev/null +++ b/packages/library/scripts/createService.mts @@ -0,0 +1,7 @@ +import { CreateServiceScript } from '@packages/scripts' + +// const __filename = new URL("", import.meta.url).pathname; +const __dirname = new URL('.', import.meta.url).pathname + +const createServiceScript = new CreateServiceScript({ __dirname, isPackages: true }) +createServiceScript.excute() diff --git a/packages/library/src/awsS3/AwsS3.test.mts b/packages/library/src/awsS3/AwsS3.test.mts new file mode 100644 index 00000000..0284d38c --- /dev/null +++ b/packages/library/src/awsS3/AwsS3.test.mts @@ -0,0 +1,8 @@ +import { AwsS3Service } from './AwsS3Service.mjs' + +describe('AwsS3Service', () => { + const service = new AwsS3Service({ region: 'ap-northeast-2' }) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/packages/library/src/awsS3/AwsS3Service.mts b/packages/library/src/awsS3/AwsS3Service.mts new file mode 100644 index 00000000..b782a6ff --- /dev/null +++ b/packages/library/src/awsS3/AwsS3Service.mts @@ -0,0 +1,130 @@ +import fs from 'fs' +import { + S3Client, + Bucket, + ListBucketsCommand, + PutObjectCommand, + type PutObjectCommandInput, + type PutObjectCommandOutput, + GetObjectCommand, + type GetObjectCommandInput, + type GetObjectCommandOutput, + DeleteObjectCommand, + type DeleteObjectCommandInput, + type DeleteObjectCommandOutput, + ListObjectsV2Command, + DeleteObjectsCommand, +} from '@aws-sdk/client-s3' + +type Params = { + region?: string +} + +interface Service { + getBuckets(): Promise + getObject(args: GetObjectArgs): Promise + deleteObject(args: DeleteObject): Promise + uploadFile(args: UploadFileArgs): Promise +} + +export class AwsS3Service implements Service { + public client!: S3Client + constructor({ region = 'ap-northeast-2' }: Params) { + this.client = new S3Client({ region: region }) + } + public async getBuckets(): Promise { + try { + const command = new ListBucketsCommand({}) + const data = await this.client.send(command) + return data.Buckets ?? [] + } catch (error: any) { + console.error('get buckets error', error) + const { requestId, cfId, extendedRequestId } = error.$metadata + console.log({ requestId, cfId, extendedRequestId }) + return [] + } + } + private async checkBucketExist(bucketName: string) { + const buckets = await this.getBuckets() + const bucket = buckets.find((bucket) => bucket.Name === bucketName) + if (!bucket) { + throw new Error('Bucket not found') + } + } + public async getObject({ bucketName, key }: GetObjectArgs): Promise { + await this.checkBucketExist(bucketName) + const input: GetObjectCommandInput = { + Bucket: bucketName, + Key: key, + } + + const command = new GetObjectCommand(input) + return await this.client.send(command) + } + public async deleteObject({ + bucketName, + key, + }: GetObjectArgs): Promise { + await this.checkBucketExist(bucketName) + const input: DeleteObjectCommandInput = { + Bucket: bucketName, + Key: key, + } + const command = new DeleteObjectCommand(input) + return await this.client.send(command) + } + public async deleteFolder({ bucketName, key }: DeleteFolder) { + await this.checkBucketExist(bucketName) + const listCommand = new ListObjectsV2Command({ + Bucket: bucketName, + Prefix: key, + }) + const listResponse = await this.client.send(listCommand) + + if (listResponse.Contents && listResponse.Contents.length > 0) { + const deleteParams = { + Bucket: bucketName, + Delete: { Objects: listResponse.Contents.map(({ Key }) => ({ Key })) }, + } + const deleteCommand = new DeleteObjectsCommand(deleteParams) + await this.client.send(deleteCommand) + } + } + public async uploadFile({ + bucketName, + key, + body, + ...args + }: UploadFileArgs): Promise { + const buckets = await this.getBuckets() + const bucket = buckets.find((bucket) => bucket.Name === bucketName) + if (!bucket) { + throw new Error('Bucket not found') + } + + const input: PutObjectCommandInput = { + Bucket: bucketName, + Key: key, + Body: body, + ...args, + } + const command = new PutObjectCommand(input) + return await this.client.send(command) + } + public async deleteFile(key: string) {} +} + +type UploadFileArgs = { + bucketName: string + key: string + body: fs.ReadStream | Buffer | string +} & Omit + +type GetObjectArgs = { + bucketName: string + key: string +} + +type DeleteObject = GetObjectArgs + +type DeleteFolder = GetObjectArgs diff --git a/packages/library/src/cloudflare/R2/R2Service.mts b/packages/library/src/cloudflare/R2/R2Service.mts new file mode 100644 index 00000000..6a55c596 --- /dev/null +++ b/packages/library/src/cloudflare/R2/R2Service.mts @@ -0,0 +1,86 @@ +import { HeadBucketCommand, PutObjectCommand, S3Client as R2 } from '@aws-sdk/client-s3' + +interface Service { + exists(): Promise + upload(args: UploadArgs): Promise +} + +export class R2Service implements Service { + private endpoint: string + private r2!: R2 + private uri: string + private name: string + constructor({ accountId, region, accessKeyId, secretAccessKey, bucketName }: ServiceArgs) { + this.endpoint = new URL(`https://${accountId}.r2.cloudflarestorage.com`).origin + this.uri = `${this.endpoint}/${bucketName}` + this.name = bucketName + + this.r2 = new R2({ + endpoint: this.endpoint, + region, + credentials: { + accessKeyId, + secretAccessKey, + }, + }) + } + public async exists(): Promise { + try { + const result = await this.r2.send( + new HeadBucketCommand({ + Bucket: this.name, + }), + ) + return result.$metadata.httpStatusCode === 200 + } catch { + return false + } + } + public async upload({ + contents, + destination, + customMetadata, + mimeType, + }: UploadArgs): Promise { + destination = destination.startsWith('/') ? destination.replace(/^\/+/, '') : destination + + const result = await this.r2.send( + new PutObjectCommand({ + Bucket: this.name, + Key: destination, + Body: contents, + ContentType: mimeType || 'application/octet-stream', + Metadata: customMetadata, + }), + ) + + return { + objectKey: destination, + uri: `${this.uri}/${destination}`, + etag: result.ETag, + versionId: result.VersionId, + } + } +} + +type ServiceArgs = { + accountId: string + region: string + accessKeyId: string + secretAccessKey: string + bucketName: string +} + +type UploadArgs = { + contents: string | Uint8Array | Buffer | ReadableStream + destination: string + customMetadata?: Record + mimeType?: string +} + +type UploadFileResponse = { + objectKey: string + uri: string + etag?: string + versionId?: string +} diff --git a/packages/library/src/cloudflare/turnstile/TurnstileService.mts b/packages/library/src/cloudflare/turnstile/TurnstileService.mts new file mode 100644 index 00000000..36b5942c --- /dev/null +++ b/packages/library/src/cloudflare/turnstile/TurnstileService.mts @@ -0,0 +1,30 @@ +import { axios } from '@packages/commonjs' + +interface Service { + verifyToken(token: string): Promise +} + +export class TurnstileService implements Service { + private turnstileSecretKey: string + private verifyUrl = 'https://challenges.cloudflare.com/turnstile/v0/siteverify' + constructor({ turnstileSecretKey }: ServiceArgs) { + this.turnstileSecretKey = turnstileSecretKey + } + public async verifyToken(token: string): Promise { + try { + const res = await axios.post<{ success: boolean }>(this.verifyUrl, { + secret: this.turnstileSecretKey, + response: token, + }) + + return res.data.success + } catch (error) { + console.log('verifyTurnstileToken error', error) + return false + } + } +} + +type ServiceArgs = { + turnstileSecretKey: string +} diff --git a/packages/library/src/discord/Discord.test.mts b/packages/library/src/discord/Discord.test.mts new file mode 100644 index 00000000..6e4f9132 --- /dev/null +++ b/packages/library/src/discord/Discord.test.mts @@ -0,0 +1,8 @@ +import { DiscordService } from './DiscordService.mjs' + +describe('DiscordService', () => { + const service = new DiscordService() + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/packages/library/src/discord/DiscordService.mts b/packages/library/src/discord/DiscordService.mts new file mode 100644 index 00000000..c31f160a --- /dev/null +++ b/packages/library/src/discord/DiscordService.mts @@ -0,0 +1,75 @@ +import { Client, GatewayIntentBits } from 'discord.js' + +type Channels = Record +type DiscordArgs = { + discordBotToken: string + channels: Channels +} + +export class DiscordService { + private client!: Client + public isSending: boolean = false + private discordBotToken!: string + private channels!: Channels + constructor({ discordBotToken, channels }: DiscordArgs) { + this.discordBotToken = discordBotToken + this.channels = channels + } + + public connection(): Promise { + return new Promise((resolve) => { + this.client = new Client({ + intents: [GatewayIntentBits.MessageContent], + }) + + this.client.on('ready', () => { + console.log('Discord Client ready') + resolve(this.client) + }) + + this.client.login(this.discordBotToken) + }) + } + public async sendMessage(type: ChannelName, message: string) { + this.isSending = true + + const frequentWord: string[] = [] + const isFrequentWordIncluded = frequentWord.some((word) => message.includes(word)) + + if (isFrequentWordIncluded) { + this.isSending = false + console.log('Frequent word included skip sending message') + return + } + + try { + const isReady = this.client.isReady() + + if (!isReady) { + throw new Error('Discord bot is not ready') + } + + const channelId = this.channels[type] + if (!channelId) { + throw new Error('Not found discord channel id') + } + + const channel = await this.client.channels.fetch(channelId as string) + + if (channel?.isTextBased()) { + const chunkSize = 2000 + for (let i = 0; i < message.length; i += chunkSize) { + const chunk = message.slice(i, i + chunkSize) + await channel.send(chunk) + } + } else { + throw new Error('Wrong channel type') + } + } catch (error: any) { + console.log(error) + throw new Error('Failed to send meesage to discord channel') + } finally { + this.isSending = false + } + } +} diff --git a/packages/library/src/fastifyCookie/FastifyCookie.test.mts b/packages/library/src/fastifyCookie/FastifyCookie.test.mts new file mode 100644 index 00000000..705433d6 --- /dev/null +++ b/packages/library/src/fastifyCookie/FastifyCookie.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { FastifyCookieService } from './FastifyCookieService.mjs' + +describe('FastifyCookieService', () => { + const service = container.resolve(FastifyCookieService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/packages/library/src/fastifyCookie/FastifyCookieService.mts b/packages/library/src/fastifyCookie/FastifyCookieService.mts new file mode 100644 index 00000000..f6f7c86a --- /dev/null +++ b/packages/library/src/fastifyCookie/FastifyCookieService.mts @@ -0,0 +1,59 @@ +import type { CookieSerializeOptions } from '@fastify/cookie' +import type { FastifyReply, FastifyRequest } from 'fastify' + +interface Service { + setCookie( + reply: FastifyReply, + name: string, + value: string, + options?: CookieSerializeOptions, + ): void + getCookie(request: FastifyRequest, name: string): string | undefined + clearCookie(reply: FastifyReply, name: string): void +} + +type Params = { + appEnv: 'development' | 'stage' | 'production' +} + +export class FastifyCookieService implements Service { + private appEnv!: 'development' | 'stage' | 'production' + constructor({ appEnv }: Params) { + this.appEnv = appEnv + } + public setCookie( + reply: FastifyReply, + name: string, + value: string, + options?: CookieSerializeOptions, + ): void { + this.domains.forEach((domain) => { + reply.cookie(name, value, { + httpOnly: true, + domain, + path: '/', + ...options, + }) + }) + } + public getCookie(request: FastifyRequest, name: string) { + if (!request.cookies) { + throw new Error('Not Setup @fastify/cookie Plugin') + } + return request.cookies[name] + } + public clearCookie(reply: FastifyReply, name: string): void { + this.domains.forEach((domain) => { + reply.clearCookie(name, { + domain, + maxAge: 0, + httpOnly: true, + }) + }) + } + private get domains() { + const isProduction = this.appEnv !== 'development' + if (isProduction) return ['.velog.io'] + return ['location', undefined] + } +} diff --git a/packages/library/src/jwt/Jwt.test.mts b/packages/library/src/jwt/Jwt.test.mts new file mode 100644 index 00000000..06629765 --- /dev/null +++ b/packages/library/src/jwt/Jwt.test.mts @@ -0,0 +1,9 @@ +import { container } from 'tsyringe' +import { JwtService } from './JwtService.mjs' + +describe('JwtService', () => { + const service = container.resolve(JwtService) + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/packages/library/src/jwt/JwtService.mts b/packages/library/src/jwt/JwtService.mts new file mode 100644 index 00000000..7a885242 --- /dev/null +++ b/packages/library/src/jwt/JwtService.mts @@ -0,0 +1,63 @@ +import jwt, { SignOptions } from 'jsonwebtoken' + +interface Service { + generateToken(payload: string | Buffer | Record, options?: SignOptions): Promise + decodeToken(token: string): Promise +} + +type Params = { + secretKey: string +} + +export class JwtService implements Service { + private secretKey!: string + constructor({ secretKey }: Params) { + this.secretKey = secretKey + } + public generateToken( + payload: string | Buffer | Record, + options?: SignOptions, + ): Promise { + const jwtOptions: SignOptions = { + issuer: 'velog.io', + expiresIn: '7d', + ...options, + } + + if (!jwtOptions.expiresIn) { + // removes expiresIn when expiresIn is given as undefined + delete jwtOptions.expiresIn + } + + return new Promise((resolve, reject) => { + jwt.sign(payload, this.secretKey, jwtOptions, (err, token) => { + if (err) reject(err) + resolve(token as string) + }) + }) + } + public decodeToken(token: string): Promise { + return new Promise((resolve, reject) => { + jwt.verify(token, this.secretKey, (err, decoded) => { + if (err) reject(err) + resolve(decoded as T) + }) + }) + } +} + +type TokenData = { + iat: number + exp: number + sub: string + iss: string +} + +export type AccessTokenData = { + user_id: string +} & TokenData + +export type RefreshTokenData = { + user_id: string + token_id: string +} & TokenData diff --git a/packages/library/src/utils/UtilsService.mts b/packages/library/src/utils/UtilsService.mts new file mode 100644 index 00000000..73c94e71 --- /dev/null +++ b/packages/library/src/utils/UtilsService.mts @@ -0,0 +1,37 @@ +import { customAlphabet } from 'nanoid' +import nanoidDictionary from 'nanoid-dictionary' + +interface Service { + escapeForUrl(text: string): string + randomString(): string + sleep(ms: number): Promise + removeKoreanChars(str: string): string + removeNullBytes(str: string): string +} + +export class UtilsService implements Service { + public escapeForUrl(text: string): string { + return text + .replace( + /[^0-9a-zA-Zㄱ-힣.\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf\u3400-\u4dbf -]/g, + '', + ) + .trim() + .replace(/ /g, '-') + .replace(/--+/g, '-') + .replace(/\.+$/, '') + } + public randomString(size = 10) { + const generateCode = customAlphabet(nanoidDictionary.alphanumeric, size) + return generateCode() + } + public sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) + } + public removeKoreanChars(str: string): string { + return str.replace(/[\uAC00-\uD7A3\u3131-\u3163]/g, '') + } + public removeNullBytes(str: string): string { + return str.replace(/\0/g, '') + } +} diff --git a/packages/library/tsconfig.build.json b/packages/library/tsconfig.build.json new file mode 100644 index 00000000..1e2d0e19 --- /dev/null +++ b/packages/library/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["./src"], + "exclude": ["./scripts", "node_modules/", "src/**/*.test.mts"] +} diff --git a/packages/library/tsconfig.json b/packages/library/tsconfig.json new file mode 100644 index 00000000..1a8bbb45 --- /dev/null +++ b/packages/library/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@packages/tsconfig/base.json", + "compilerOptions": { + "baseUrl": "./", + "rootDir": "./", + "typeRoots": ["./node_modules/@types"], + "outDir": "./dist", + "declaration": true + }, + "include": ["./src", "./scripts", "eslint.config.js"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/markdown-editor/.prettierrc b/packages/markdown-editor/.prettierrc new file mode 100644 index 00000000..215e2c2f --- /dev/null +++ b/packages/markdown-editor/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "printWidth": 100, + "trailingComma": "all", + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/packages/markdown-editor/LICENSE b/packages/markdown-editor/LICENSE new file mode 100644 index 00000000..67344284 --- /dev/null +++ b/packages/markdown-editor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Shu Ding + +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. diff --git a/packages/markdown-editor/README.md b/packages/markdown-editor/README.md new file mode 100644 index 00000000..5976df56 --- /dev/null +++ b/packages/markdown-editor/README.md @@ -0,0 +1,7 @@ +# nextra-theme-docs + +A documentation site theme for [Nextra](https://github.com/shuding/nextra). + +## Example + +[nextra.vercel.app](https://nextra.vercel.app/) diff --git a/packages/velog-web/src/lib/styles/keyframes.module.css b/packages/markdown-editor/css/animations.css similarity index 100% rename from packages/velog-web/src/lib/styles/keyframes.module.css rename to packages/markdown-editor/css/animations.css diff --git a/packages/markdown-editor/css/editor.css b/packages/markdown-editor/css/editor.css new file mode 100644 index 00000000..f823a4b4 --- /dev/null +++ b/packages/markdown-editor/css/editor.css @@ -0,0 +1,6 @@ +.markdown-editor-container { + display: none; + @media (min-width: 1024px) { + display: flex; + } +} diff --git a/packages/markdown-editor/css/hamburger.css b/packages/markdown-editor/css/hamburger.css new file mode 100644 index 00000000..9f739767 --- /dev/null +++ b/packages/markdown-editor/css/hamburger.css @@ -0,0 +1,37 @@ +.nextra-hamburger svg { + g { + @apply nx-origin-center; + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1); + } + path { + opacity: 1; + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1) 0.2s, opacity 0.2s ease 0.2s; + } + + &.open { + path { + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1), opacity 0s ease 0.2s; + } + g { + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1) 0.2s; + } + } + + &.open > { + path { + @apply nx-opacity-0; + } + g:nth-of-type(1) { + @apply nx-rotate-45; + path { + transform: translate3d(0, 6px, 0); + } + } + g:nth-of-type(2) { + @apply -nx-rotate-45; + path { + transform: translate3d(0, -6px, 0); + } + } + } +} diff --git a/packages/markdown-editor/css/missing-tailwind.css b/packages/markdown-editor/css/missing-tailwind.css new file mode 100644 index 00000000..70a6c544 --- /dev/null +++ b/packages/markdown-editor/css/missing-tailwind.css @@ -0,0 +1,25 @@ +/* .nx-text-transparent { + color: transparent; +} + +.nx-w-[\1\/3] { + @apply nx-w-1/3; +} + +.nx-w-[\4\/5] { + @apply nx-w-4/5; +} + +.nx-h-scren { + @apply nx-h-screen; +} + +.nx-top-0 { + top: 0; +} + +.nx-left-0 { + left: 0; +} + +*/ diff --git a/packages/markdown-editor/css/nextra/cards.css b/packages/markdown-editor/css/nextra/cards.css new file mode 100644 index 00000000..48a57e4e --- /dev/null +++ b/packages/markdown-editor/css/nextra/cards.css @@ -0,0 +1,32 @@ +.nextra-cards { + grid-template-columns: repeat( + auto-fill, + minmax(max(250px, calc((100% - 1rem * 2) / var(--rows))), 1fr) + ); +} + +.nextra-card img { + user-select: none; +} + +.nextra-card:hover svg { + color: currentColor; +} + +.nextra-card svg { + width: 1.5rem; + color: #00000033; + transition: color 0.3s ease; +} + +.nextra-card p { + margin-top: 0.5rem; +} + +.dark .nextra-card svg { + color: #ffffff66; +} + +.dark .nextra-card:hover svg { + color: currentColor; +} diff --git a/packages/markdown-editor/css/nextra/code-block.css b/packages/markdown-editor/css/nextra/code-block.css new file mode 100644 index 00000000..bb68670f --- /dev/null +++ b/packages/markdown-editor/css/nextra/code-block.css @@ -0,0 +1,71 @@ +code { + box-decoration-break: slice; + font-feature-settings: + 'rlig' 1, + 'calt' 1, + 'ss01' 1; + + &[data-line-numbers] > .line { + @apply nx-pl-2; + &::before { + counter-increment: line; + content: counter(line); + @apply nx-h-full nx-float-left nx-pr-4 nx-text-right nx-min-w-[2.6rem] nx-text-gray-500; + } + } + + .line { + &.highlighted { + @apply nx-bg-primary-600/10 nx-text-primary-600/50 nx-shadow-[2px_0_currentColor_inset]; + } + .highlighted { + @apply nx-rounded-sm nx-shadow-[0_0_0_2px_rgba(0,0,0,.3)]; + @apply nx-bg-primary-800/10 nx-shadow-primary-800/10; + @apply dark:nx-bg-primary-300/10 dark:nx-shadow-primary-300/10; + } + } +} + +pre { + /* content-visibility: auto; */ + contain: paint; + code { + @apply nx-grid nx-min-w-full nx-rounded-none nx-border-none !nx-bg-transparent !nx-p-0 nx-text-sm nx-leading-5 nx-text-current dark:!nx-bg-transparent; + .line { + @apply nx-px-4; + } + } + + &:not([data-theme]) { + @apply nx-px-4; + } + + html[data-nextra-word-wrap] & { + word-break: break-word; + @apply nx-whitespace-pre-wrap md:nx-whitespace-pre; + .line { + @apply nx-inline-block; + } + } + + .nextra-copy-icon { + animation: fade-in 0.3s ease forwards; + } +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@supports ( + (-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px)) +) { + .nextra-button { + @apply nx-backdrop-blur-md nx-bg-opacity-[.85] dark:nx-bg-opacity-80; + } +} diff --git a/packages/markdown-editor/css/nextra/scrollbar.css b/packages/markdown-editor/css/nextra/scrollbar.css new file mode 100644 index 00000000..d75eacf5 --- /dev/null +++ b/packages/markdown-editor/css/nextra/scrollbar.css @@ -0,0 +1,37 @@ +.markdown-editor-scrollbar { + scrollbar-width: thin; /* Firefox */ + scrollbar-color: oklch(55.55% 0 0 / 40%) transparent; /* Firefox */ + + scrollbar-gutter: stable; + &::-webkit-scrollbar { + @apply nx-h-3 nx-w-3; + } + &::-webkit-scrollbar-track { + @apply nx-bg-transparent; + } + &::-webkit-scrollbar-thumb { + @apply nx-rounded-[10px]; + } + &:hover::-webkit-scrollbar-thumb { + border: 3px solid transparent; + background-color: var(--tw-shadow-color); + background-clip: content-box; + @apply nx-shadow-neutral-500/20 hover:nx-shadow-neutral-500/40; + } + + @media (max-width: 767px) { + .nextra-container & { + scrollbar-gutter: auto; + } + } +} + +/* Hide scrollbar */ +.no-scrollbar { + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE and Edge */ + + &::-webkit-scrollbar { + @apply nx-hidden; /* Chrome, Safari and Opera */ + } +} diff --git a/packages/markdown-editor/css/nextra/steps.css b/packages/markdown-editor/css/nextra/steps.css new file mode 100644 index 00000000..843a186c --- /dev/null +++ b/packages/markdown-editor/css/nextra/steps.css @@ -0,0 +1,11 @@ +.nextra-steps h3 { + counter-increment: step; + + &:before { + @apply nx-absolute nx-w-[33px] nx-h-[33px]; + @apply nx-border-4 nx-border-white nx-bg-gray-100 dark:nx-border-dark dark:nx-bg-neutral-800; + @apply nx-rounded-full nx-text-neutral-400 nx-text-base nx-font-normal nx-text-center -nx-indent-px; + @apply nx-mt-[3px] nx-ml-[-41px]; + content: counter(step); + } +} diff --git a/packages/markdown-editor/css/nextra/subheading-anchor.css b/packages/markdown-editor/css/nextra/subheading-anchor.css new file mode 100644 index 00000000..35dbfb4c --- /dev/null +++ b/packages/markdown-editor/css/nextra/subheading-anchor.css @@ -0,0 +1,17 @@ +.subheading-anchor { + @apply nx-opacity-0 nx-transition-opacity ltr:nx-ml-1 rtl:nx-mr-1; + + :hover > &, + &:focus { + @apply nx-opacity-100; + } + + &:after { + @apply nx-content-['#'] nx-px-1; + @apply nx-text-gray-300 dark:nx-text-neutral-700; + span:target + & { + @apply nx-text-gray-400; + @apply dark:nx-text-neutral-500; + } + } +} diff --git a/packages/markdown-editor/css/nextra/variables.css b/packages/markdown-editor/css/nextra/variables.css new file mode 100644 index 00000000..576438d4 --- /dev/null +++ b/packages/markdown-editor/css/nextra/variables.css @@ -0,0 +1,98 @@ +:root { + --shiki-color-text: oklch(37.53% 0 0); + --shiki-color-background: transparent; + --shiki-token-constant: oklch(56.45% 0.163 253.27); + --shiki-token-string: oklch(54.64% 0.144 147.32); + --shiki-token-comment: oklch(73.8% 0 0); + --shiki-token-keyword: oklch(56.8% 0.2 26.41); + --shiki-token-parameter: oklch(77.03% 0.174 64.05); + --shiki-token-function: oklch(50.15% 0.188 294.99); + --shiki-token-string-expression: var(--shiki-token-string); + --shiki-token-punctuation: oklch(24.78% 0 0); + --shiki-token-link: var(--shiki-token-string); + + /* from github-light */ + --shiki-color-ansi-black: #24292e; + --shiki-color-ansi-black-dim: #24292e80; + --shiki-color-ansi-red: #d73a49; + --shiki-color-ansi-red-dim: #d73a4980; + --shiki-color-ansi-green: #28a745; + --shiki-color-ansi-green-dim: #28a74580; + --shiki-color-ansi-yellow: #dbab09; + --shiki-color-ansi-yellow-dim: #dbab0980; + --shiki-color-ansi-blue: #0366d6; + --shiki-color-ansi-blue-dim: #0366d680; + --shiki-color-ansi-magenta: #5a32a3; + --shiki-color-ansi-magenta-dim: #5a32a380; + --shiki-color-ansi-cyan: #1b7c83; + --shiki-color-ansi-cyan-dim: #1b7c8380; + --shiki-color-ansi-white: #6a737d; + --shiki-color-ansi-white-dim: #6a737d80; + --shiki-color-ansi-bright-black: #959da5; + --shiki-color-ansi-bright-black-dim: #959da580; + --shiki-color-ansi-bright-red: #cb2431; + --shiki-color-ansi-bright-red-dim: #cb243180; + --shiki-color-ansi-bright-green: #22863a; + --shiki-color-ansi-bright-green-dim: #22863a80; + --shiki-color-ansi-bright-yellow: #b08800; + --shiki-color-ansi-bright-yellow-dim: #b0880080; + --shiki-color-ansi-bright-blue: #005cc5; + --shiki-color-ansi-bright-blue-dim: #005cc580; + --shiki-color-ansi-bright-magenta: #5a32a3; + --shiki-color-ansi-bright-magenta-dim: #5a32a380; + --shiki-color-ansi-bright-cyan: #3192aa; + --shiki-color-ansi-bright-cyan-dim: #3192aa80; + --shiki-color-ansi-bright-white: #d1d5da; + --shiki-color-ansi-bright-white-dim: #d1d5da80; + + --opaque-layer: rgba(249, 249, 249, 0.85); +} + +.dark { + --shiki-color-text: oklch(86.07% 0 0); + --shiki-token-constant: oklch(76.85% 0.121 252.34); + --shiki-token-string: oklch(81.11% 0.124 55.08); + --shiki-token-comment: oklch(55.18% 0.017 251.27); + --shiki-token-keyword: oklch(72.14% 0.162 15.49); + /*--shiki-token-parameter: #ff9800; is same as in light mode */ + --shiki-token-function: oklch(72.67% 0.137 299.15); + --shiki-token-string-expression: oklch(69.28% 0.179 143.2); + --shiki-token-punctuation: oklch(79.21% 0 0); + --shiki-token-link: var(--shiki-token-string); + + /* from github-dark */ + --shiki-color-ansi-black: #586069; + --shiki-color-ansi-black-dim: #58606980; + --shiki-color-ansi-red: #ea4a5a; + --shiki-color-ansi-red-dim: #ea4a5a80; + --shiki-color-ansi-green: #34d058; + --shiki-color-ansi-green-dim: #34d05880; + --shiki-color-ansi-yellow: #ffea7f; + --shiki-color-ansi-yellow-dim: #ffea7f80; + --shiki-color-ansi-blue: #2188ff; + --shiki-color-ansi-blue-dim: #2188ff80; + --shiki-color-ansi-magenta: #b392f0; + --shiki-color-ansi-magenta-dim: #b392f080; + --shiki-color-ansi-cyan: #39c5cf; + --shiki-color-ansi-cyan-dim: #39c5cf80; + --shiki-color-ansi-white: #d1d5da; + --shiki-color-ansi-white-dim: #d1d5da80; + --shiki-color-ansi-bright-black: #959da5; + --shiki-color-ansi-bright-black-dim: #959da580; + --shiki-color-ansi-bright-red: #f97583; + --shiki-color-ansi-bright-red-dim: #f9758380; + --shiki-color-ansi-bright-green: #85e89d; + --shiki-color-ansi-bright-green-dim: #85e89d80; + --shiki-color-ansi-bright-yellow: #ffea7f; + --shiki-color-ansi-bright-yellow-dim: #ffea7f80; + --shiki-color-ansi-bright-blue: #79b8ff; + --shiki-color-ansi-bright-blue-dim: #79b8ff80; + --shiki-color-ansi-bright-magenta: #b392f0; + --shiki-color-ansi-bright-magenta-dim: #b392f080; + --shiki-color-ansi-bright-cyan: #56d4dd; + --shiki-color-ansi-bright-cyan-dim: #56d4dd80; + --shiki-color-ansi-bright-white: #fafbfc; + --shiki-color-ansi-bright-white-dim: #fafbfc80; + + --opaque-layer: rgba(0, 0, 0, 0.85); +} diff --git a/packages/markdown-editor/css/styles.css b/packages/markdown-editor/css/styles.css new file mode 100644 index 00000000..001dc35a --- /dev/null +++ b/packages/markdown-editor/css/styles.css @@ -0,0 +1,188 @@ +@import 'http://github.com/tailwindcss/base'; +@import 'http://github.com/tailwindcss/components'; +@import 'http://github.com/tailwindcss/utilities'; +@import 'http://github.com/nextra/variables.css'; +@import 'http://github.com/nextra/code-block.css'; +@import 'http://github.com/nextra/subheading-anchor.css'; +@import 'http://github.com/nextra/scrollbar.css'; +@import 'http://github.com/nextra/steps.css'; +@import 'http://github.com/nextra/cards.css'; +@import 'http://github.com/hamburger.css'; +@import 'http://github.com/typesetting-article.css'; +@import 'http://github.com/editor.css'; +@import 'http://github.com/missing-tailwind.css'; +@import 'http://github.com/animations.css'; + +html { + @apply nx-scroll-pt-[--nextra-navbar-height] nx-text-base nx-antialiased; + font-feature-settings: + 'rlig' 1, + 'calt' 1, + 'ss01' 1; + -webkit-tap-highlight-color: transparent; + /* @apply nx-overflow-hidden; */ +} + +body { + @apply nx-max-h-screen nx-w-full nx-bg-white dark:nx-bg-dark dark:nx-text-gray-100; +} + +#__next { + @apply nx-h-screen nx-overflow-hidden; +} + +a, +summary, +button, +input, +[tabindex]:not([tabindex='-1']) { + @apply nx-outline-none; + &:focus-visible { + @apply nx-ring-2 nx-ring-primary-200 nx-ring-offset-1 nx-ring-offset-primary-300 dark:nx-ring-primary-800 dark:nx-ring-offset-primary-700; + } +} + +a, +summary { + @apply nx-rounded; +} + +.nextra-content { + @apply nx-text-slate-700 dark:nx-text-slate-200; +} + +@media (max-width: 767px) { + .nextra-sidebar-container { + @apply nx-fixed nx-bottom-0 nx-top-0 nx-z-[15] nx-w-full nx-overscroll-contain nx-bg-white nx-pt-[calc(var(--nextra-navbar-height))] dark:nx-bg-dark; + transition: transform 0.8s cubic-bezier(0.52, 0.16, 0.04, 1); + will-change: transform, opacity; + contain: layout style; + backface-visibility: hidden; + + & > .markdown-editor-scrollbar { + mask-image: linear-gradient(to bottom, transparent, #000 20px), + linear-gradient(to left, #000 10px, transparent 10px); + } + } + + .nextra-banner-container ~ div { + .nextra-sidebar-container { + @apply nx-pt-[6.5rem]; + } + &.nextra-header-container { + @apply nx-top-10 md:nx-top-0; + } + } + .nextra-banner-hidden { + .nextra-banner-container ~ div .nextra-sidebar-container { + @apply nx-pt-16; + } + .nextra-header-container { + @apply !nx-top-0; + } + } + .nextra-search .excerpt { + @apply nx-overflow-hidden nx-text-ellipsis; + display: -webkit-box; + line-clamp: 1; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + } +} + +@media (prefers-reduced-motion: reduce) and (max-width: 767px) { + article:before, + .nextra-sidebar-container, + .nextra-sidebar-container.open, + body.resizing .nextra-sidebar-container { + @apply nx-transition-none; + } +} + +/* Content Typography */ +article details > summary { + &::-webkit-details-marker { + @apply nx-hidden; + } + &::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='h-5 w-5' viewBox='0 0 20 20' fill='currentColor'%3E%3Cpath fill-rule='evenodd' d='M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z' clip-rule='evenodd' /%3E%3C/svg%3E"); + height: 1.2em; + width: 1.2em; + vertical-align: -4px; + } +} + +@media (min-width: 768px) { + .nextra-toc > .div, + .nextra-sidebar-container { + mask-image: linear-gradient(to bottom, transparent, #000 20px), + linear-gradient(to left, #000 10px, transparent 10px); + } +} + +@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) { + .nextra-search ul { + @apply nx-bg-white/70 nx-backdrop-blur-lg dark:nx-bg-dark/80; + } + .nextra-header-container-blur { + @apply nx-bg-white/[.85] nx-backdrop-blur-md dark:!nx-bg-dark/80; + } +} + +input[type='search'] { + &::-webkit-search-decoration, + &::-webkit-search-cancel-button, + &::-webkit-search-results-button, + &::-webkit-search-results-decoration { + -webkit-appearance: none; + } +} + +.contains-task-list { + @apply nx-ml-0 nx-list-none; + input[type='checkbox'] { + @apply nx-mr-1; + } +} + +.nextra-banner-hidden .nextra-banner-container { + @apply nx-hidden; +} + +.nextra-sidebar-container { + [data-toggle-animation='show'] button { + opacity: 0; + animation: nextra-fadein 1s ease 0.2s forwards; + } + [data-toggle-animation='hide'] button { + opacity: 0; + animation: nextra-fadein2 1s ease 0.2s forwards; + } +} + +.footnotes a[data-footnote-backref] { + font-family: initial; +} + +.markdown-editor-codemirror { + transition: none; + will-change: auto; +} + +@keyframes nextra-fadein { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes nextra-fadein2 { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} diff --git a/packages/markdown-editor/css/typesetting-article.css b/packages/markdown-editor/css/typesetting-article.css new file mode 100644 index 00000000..2bb2950f --- /dev/null +++ b/packages/markdown-editor/css/typesetting-article.css @@ -0,0 +1,26 @@ +article.nextra-body-typesetting-article { + font-size: 17px; + font-feature-settings: 'rlig' 1, 'calt' 1; + h1 { + @apply nx-mt-6 nx-mb-4 nx-text-center; + font-size: 2.5rem; + } + h2 { + @apply nx-border-none; + } + a { + @apply nx-no-underline hover:nx-underline; + } + p { + @apply nx-leading-8; + } + code { + @apply nx-border-none dark:nx-bg-neutral-700; + } + pre code { + @apply dark:nx-bg-transparent; + } + .subheading-anchor + a { + @apply nx-no-underline hover:nx-no-underline after:nx-hidden; + } +} diff --git a/packages/markdown-editor/env/.env.example b/packages/markdown-editor/env/.env.example new file mode 100644 index 00000000..d5d9a1e5 --- /dev/null +++ b/packages/markdown-editor/env/.env.example @@ -0,0 +1 @@ +NEXT_PUBLIC_GRAPHQL_BOOK_SERVER_HOST= \ No newline at end of file diff --git a/packages/markdown-editor/eslint.config.js b/packages/markdown-editor/eslint.config.js new file mode 100644 index 00000000..634d62ec --- /dev/null +++ b/packages/markdown-editor/eslint.config.js @@ -0,0 +1,17 @@ +import nextConfig from '@packages/eslint-config/next.mjs' +import { resolve } from 'node:path' + +const projectPath = resolve(process.cwd()) + +/** @type {import("eslint").Linter.Config} */ +export default [ + ...nextConfig(projectPath), + { + ignores: ['node_modules', 'dist', 'style.css'], + }, + { + rules: { + '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }], + }, + }, +] diff --git a/packages/markdown-editor/package.json b/packages/markdown-editor/package.json new file mode 100644 index 00000000..91c5757a --- /dev/null +++ b/packages/markdown-editor/package.json @@ -0,0 +1,135 @@ +{ + "name": "@packages/markdown-editor", + "version": "1.0.0", + "description": "A Velog markdown editor with codemirrro markdown editor", + "repository": "https://github.com/velog-io/velog", + "author": "Winverse ", + "license": "MIT", + "exports": { + "./style.css": "./style.css", + ".": { + "import": "./dist/index.js", + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "type": "module", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "style.css" + ], + "scripts": { + "build": "tsup && pnpm build:tailwind", + "build:tailwind": "pnpm postcss css/styles.css -o style.css --verbose", + "clean": "rimraf ./dist ./style.css", + "dev": "concurrently \"pnpm dev:layout\" \"pnpm dev:tailwind\"", + "dev:layout": "tsup --watch", + "dev:tailwind": "TAILWIND_MODE=watch pnpm postcss css/styles.css -o style.css --watch", + "prepublishOnly": "pnpm build", + "test": "echo ❗ No tests, previous tests were moved to the `nextra` package", + "types": "tsup --dts-only", + "types:check": "tsc --noEmit", + "lint": "eslint --fix", + "ssm": "tsx ./scripts/ssm.mts" + }, + "peerDependencies": { + "react": ">=16.13.1", + "react-dom": ">=16.13.1" + }, + "dependencies": { + "@codemirror/autocomplete": "^6.16.0", + "@codemirror/commands": "^6.5.0", + "@codemirror/lang-markdown": "^6.2.5", + "@codemirror/language": "^6.10.2", + "@codemirror/language-data": "^6.5.1", + "@codemirror/state": "^6.4.1", + "@codemirror/theme-one-dark": "^6.1.2", + "@codemirror/view": "^6.26.3", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/modifiers": "^7.0.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@headlessui/react": "^1.7.17", + "@lezer/highlight": "^1.2.0", + "@mdx-js/mdx": "3.0.1", + "@mdx-js/react": "2.3.0", + "@popperjs/core": "^2.11.8", + "@shikijs/rehype": "^1.4.0", + "@theguild/remark-mermaid": "0.0.6", + "@theguild/remark-npm2yarn": "0.2.0", + "@uiw/codemirror-extensions-basic-setup": "^4.22.1", + "@uiw/codemirror-extensions-hyper-link": "^4.22.1", + "@uiw/codemirror-themes": "^4.22.1", + "@uiw/codemirror-themes-all": "^4.22.1", + "axios": "^1.4.0", + "clsx": "^2.0.0", + "codemirror": "6.0.1", + "escape-string-regexp": "^5.0.0", + "flexsearch": "^0.7.31", + "focus-visible": "^5.2.0", + "git-url-parse": "^13.1.0", + "github-slugger": "^2.0.0", + "gray-matter": "4.0.3", + "intersection-observer": "^0.12.2", + "lucide-react": "^0.379.0", + "match-sorter": "^6.3.1", + "next": "14.2.5", + "next-mdx-remote": "5.0.0", + "next-seo": "^6.0.0", + "next-themes": "^0.2.1", + "react-hot-toast": "^2.4.1", + "rehype-katex": "^7.0.0", + "rehype-pretty-code": "0.9.11", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.0", + "remark-gfm": "^4.0.0", + "remark-math": "^6.0.0", + "remark-mdx": "^3.0.1", + "remark-parse": "11.0.0", + "remark-reading-time": "^2.0.1", + "remark-rehype": "^11.1.0", + "remark-smartypants": "^2.1.0", + "scroll-into-view-if-needed": "^3.1.0", + "shiki": "^0.14.3", + "slash": "3.0.0", + "throttle-debounce": "^5.0.0", + "title": "3.5.3", + "tsx": "^4.11.2", + "unist-util-remove": "^4.0.0", + "unist-util-visit": "^5.0.0", + "use-debounce": "^10.0.1", + "zod": "^3.22.3" + }, + "devDependencies": { + "@packages/eslint-config": "workspace:*", + "@packages/scripts": "workspace:*", + "@tailwindcss/nesting": "^0.0.0-insiders.565cd3e", + "@testing-library/react": "^14.0.0", + "@types/flexsearch": "^0.7.3", + "@types/git-url-parse": "^9.0.1", + "@types/mdast": "3.0.12", + "@types/react": "^18.2.21", + "@types/react-dom": "^18.2.7", + "@types/throttle-debounce": "^5.0.1", + "@types/title": "^3.4.3", + "@vitejs/plugin-react": "^4.1.0", + "concurrently": "^8.0.0", + "jsdom": "^23.0.0", + "postcss": "^8.4.31", + "postcss-cli": "^10.1.0", + "postcss-import": "^15.1.0", + "postcss-lightningcss": "^1.0.0", + "prettier": "^3.2.5", + "prettier-plugin-tailwindcss": "^0.5.14", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwindcss": "^3.4.3", + "tsup": "7.2.0", + "unified": "^10.1.2", + "vitest": "^0.34.0" + }, + "sideEffects": [ + "./src/polyfill.ts" + ] +} diff --git a/packages/markdown-editor/postcss.config.cjs b/packages/markdown-editor/postcss.config.cjs new file mode 100644 index 00000000..c077b74b --- /dev/null +++ b/packages/markdown-editor/postcss.config.cjs @@ -0,0 +1,11 @@ +/** @type {import('postcss').Postcss} */ +module.exports = { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + 'postcss-lightningcss': { + browsers: '>= .25%' + } + } +} diff --git a/packages/markdown-editor/scripts/ssm.mts b/packages/markdown-editor/scripts/ssm.mts new file mode 100644 index 00000000..b155e9ee --- /dev/null +++ b/packages/markdown-editor/scripts/ssm.mts @@ -0,0 +1,4 @@ +import { SSMScript } from '@packages/scripts' + +const ssmScript = new SSMScript({ packageName: 'markdown-editor' }) +ssmScript.execute() diff --git a/packages/markdown-editor/src/api/apiClient.ts b/packages/markdown-editor/src/api/apiClient.ts new file mode 100644 index 00000000..75c0e684 --- /dev/null +++ b/packages/markdown-editor/src/api/apiClient.ts @@ -0,0 +1,9 @@ +import { ENV } from '@/env' +import axios from 'axios' + +const apiClient = axios.create({ + baseURL: ENV.graphqlBookServerHost, + withCredentials: true, +}) + +export default apiClient diff --git a/packages/markdown-editor/src/api/routes/files.ts b/packages/markdown-editor/src/api/routes/files.ts new file mode 100644 index 00000000..8f34a6f5 --- /dev/null +++ b/packages/markdown-editor/src/api/routes/files.ts @@ -0,0 +1,30 @@ +import { AxiosProgressEvent } from 'axios' +import apiClient from '../apiClient' + +export interface PreuploadInfo { + image_path: string + signed_url: string +} + +export async function uploadImage({ file, info, onUploadProgress }: UploadImageArgs) { + const formData = new FormData() + formData.append('image', file) + formData.append('type', info.type) + if (info.refId) { + formData.append('ref_id', info.refId) + } + const response = await apiClient.post<{ path: string }>('/api/files/v3/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + onUploadProgress, + }) + + return response.data +} + +export type UploadImageArgs = { + file: File + info: { type: 'book'; refId?: string } + onUploadProgress?: (event: AxiosProgressEvent) => void +} diff --git a/packages/markdown-editor/src/components/404.tsx b/packages/markdown-editor/src/components/404.tsx new file mode 100644 index 00000000..b1673547 --- /dev/null +++ b/packages/markdown-editor/src/components/404.tsx @@ -0,0 +1,30 @@ +import { useMounted } from '../nextra/hooks' +import type { ReactElement } from 'react' +import { useConfig } from '../contexts' +import { getGitIssueUrl, renderComponent } from '../utils' +import { Anchor } from './anchor' + +export function NotFoundPage(): ReactElement | null { + const config = useConfig() + const mounted = useMounted() + const { content, labels } = config.notFound + if (!content) { + return null + } + + return ( +

    + + {renderComponent(content)} + +

    + ) +} diff --git a/packages/markdown-editor/src/components/500.tsx b/packages/markdown-editor/src/components/500.tsx new file mode 100644 index 00000000..f05229db --- /dev/null +++ b/packages/markdown-editor/src/components/500.tsx @@ -0,0 +1,30 @@ +import { useMounted } from '../nextra/hooks' +import type { ReactElement } from 'react' +import { useConfig } from '../contexts' +import { getGitIssueUrl, renderComponent } from '../utils' +import { Anchor } from './anchor' + +export function ServerSideErrorPage(): ReactElement | null { + const config = useConfig() + const mounted = useMounted() + const { content, labels } = config.serverSideError + if (!content) { + return null + } + + return ( +

    + + {renderComponent(content)} + +

    + ) +} diff --git a/packages/markdown-editor/src/components/anchor.tsx b/packages/markdown-editor/src/components/anchor.tsx new file mode 100644 index 00000000..5cab04ab --- /dev/null +++ b/packages/markdown-editor/src/components/anchor.tsx @@ -0,0 +1,29 @@ +import NextLink from 'next/link' +import type { ComponentProps, ReactElement } from 'react' +import { forwardRef } from 'react' + +export type AnchorProps = Omit, 'ref'> & { + newWindow?: boolean +} + +export const Anchor = forwardRef(function ( + { href = '', children, newWindow, ...props }, + // ref is used in + forwardedRef, +): ReactElement { + if (!href) { + return ( + + {children} + + ) + } + + return ( + + {children} + + ) +}) + +Anchor.displayName = 'Anchor' diff --git a/packages/markdown-editor/src/components/back-to-top.tsx b/packages/markdown-editor/src/components/back-to-top.tsx new file mode 100644 index 00000000..f268717a --- /dev/null +++ b/packages/markdown-editor/src/components/back-to-top.tsx @@ -0,0 +1,35 @@ +import cn from 'clsx' +import { ArrowRightIcon } from '../nextra/icons' +import type { ReactElement } from 'react' +import { useEffect, useRef } from 'react' + +function scrollToTop() { + window.scrollTo({ top: 0, behavior: 'smooth' }) +} + +export function BackToTop({ className }: { className?: string }): ReactElement { + const ref = useRef(null) + useEffect(() => { + function toggleVisible() { + const { scrollTop } = document.documentElement + ref.current?.classList.toggle('nx-opacity-0', scrollTop < 300) + } + + window.addEventListener('scroll', toggleVisible) + return () => { + window.removeEventListener('scroll', toggleVisible) + } + }, []) + + return ( + + ) +} diff --git a/packages/markdown-editor/src/components/banner.tsx b/packages/markdown-editor/src/components/banner.tsx new file mode 100644 index 00000000..282593c2 --- /dev/null +++ b/packages/markdown-editor/src/components/banner.tsx @@ -0,0 +1,50 @@ +import cn from 'clsx' +import { XIcon } from '../nextra/icons' +import type { ReactElement } from 'react' +import { useConfig } from '../contexts' +import { renderComponent } from '../utils' + +export function Banner(): ReactElement | null { + const { banner } = useConfig() + if (!banner.text) { + return null + } + const hideBannerScript = `try{if(localStorage.getItem(${JSON.stringify( + banner.key, + )})==='0'){document.body.classList.add('nextra-banner-hidden')}}catch(e){}` + + return ( + <> +