├── .vscode ├── extensions.json └── settings.json ├── jest.config.js ├── .prettierrc ├── src ├── login.spec.ts ├── utils.ts ├── login.ts ├── index.ts ├── crawl.ts └── crawl.spec.ts ├── .eslintrc.json ├── LICENSE ├── .github └── workflows │ └── pipeline.yml ├── package.json ├── README.md ├── .gitignore └── tsconfig.json /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | setupFilesAfterEnv: [], 6 | testMatch: ['**/*.spec.ts'], 7 | }; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /src/login.spec.ts: -------------------------------------------------------------------------------- 1 | import { login } from './login'; 2 | 3 | describe('Login', () => { 4 | xit('should login', async () => { 5 | const cookie = await login('id', 'password'); 6 | 7 | expect(cookie).toBeDefined(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier", 6 | "plugin:prettier/recommended" 7 | ], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": 2020, 11 | "sourceType": "module" 12 | }, 13 | "plugins": ["@typescript-eslint", "prettier"], 14 | "rules": { 15 | "prettier/prettier": "error" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Wonshik Alex Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.renderWhitespace": "all", 4 | "editor.formatOnPaste": true, 5 | "editor.insertSpaces": true, 6 | "editor.renderControlCharacters": true, 7 | "editor.formatOnSave": true, 8 | "files.trimTrailingWhitespace": true, 9 | "files.trimFinalNewlines": true, 10 | "files.insertFinalNewline": true, 11 | "files.exclude": { 12 | "**/.git": true, 13 | "**/.DS_Store": true, 14 | "**/node_modules": true, 15 | "**/*.d.ts": true, 16 | "lib": true, 17 | "coverage": true, 18 | }, 19 | "search.exclude": { 20 | "**/node_modules": true, 21 | "lib": true 22 | }, 23 | "typescript.tsdk": "node_modules/typescript/lib", 24 | "npm.exclude": "**/extensions/**", 25 | "npm.packageManager": "npm", 26 | "emmet.excludeLanguages": [], 27 | "typescript.preferences.importModuleSpecifier": "relative", 28 | "typescript.preferences.quoteStyle": "single", 29 | "[plaintext]": { 30 | "files.insertFinalNewline": false 31 | }, 32 | "[typescript]": { 33 | "editor.defaultFormatter": "esbenp.prettier-vscode", 34 | "editor.formatOnSave": true 35 | }, 36 | "[javascript]": { 37 | "editor.defaultFormatter": "esbenp.prettier-vscode", 38 | "editor.formatOnSave": true 39 | }, 40 | "json.maxItemsComputed": 5000 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | include: 17 | - os: ubuntu-latest 18 | target: linux-x64-14.15.3 19 | output: clien-archiver-linux-x64 20 | - os: macos-latest 21 | target: mac-x64-14.15.3 22 | output: clien-archiver-macos-x64 23 | - os: windows-latest 24 | target: windows-x64-14.15.3 25 | output: clien-archiver-win-x64 26 | 27 | steps: 28 | - uses: actions/checkout@v3 29 | 30 | - name: Use Node.js 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: '14.x' 34 | 35 | - name: Install dependencies 36 | run: npm install 37 | 38 | - name: Build 39 | run: | 40 | npm run build 41 | npx nexe lib/index.js --target ${{ matrix.target }} -o ${{ matrix.output }} 42 | 43 | - name: Upload Release Asset 44 | uses: actions/upload-release-asset@v1 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | with: 48 | upload_url: ${{ github.event.release.upload_url }} 49 | asset_path: ./${{ matrix.output }}${{ startsWith(matrix.output, 'clien-archiver-win') && '.exe' || '' }} 50 | asset_name: ${{ matrix.output }}-${{ github.ref_name }}${{ startsWith(matrix.output, 'clien-archiver-win') && '.exe' || '' }} 51 | asset_content_type: application/octet-stream 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clien-archiver", 3 | "description": "Clien-Archiver is an open-source project that allows users to backup and manage posts from the Clien.net website in a local environment. It also offers various plugins to export backed-up data to other platforms, such as Damoang, Reddit, and more, providing flexibility and convenience for users.", 4 | "main": "lib/index.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "start": "ts-node src/index.ts", 8 | "lint": "eslint 'src/**/*.ts'", 9 | "lint:fix": "eslint 'src/**/*.ts' --fix", 10 | "prettier": "prettier --write 'src/**/*.ts'", 11 | "test": "jest", 12 | "test:cov": "jest --coverage" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/wokim/clien-archiver.git" 17 | }, 18 | "keywords": [ 19 | "clien" 20 | ], 21 | "author": "Wonshik Alex Kim ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/wokim/clien-archiver/issues" 25 | }, 26 | "homepage": "https://github.com/wokim/clien-archiver#readme", 27 | "devDependencies": { 28 | "@types/jest": "^29.5.12", 29 | "@types/node": "^20.11.30", 30 | "@typescript-eslint/eslint-plugin": "^7.4.0", 31 | "@typescript-eslint/parser": "^7.4.0", 32 | "eslint": "^8.57.0", 33 | "eslint-config-prettier": "^9.1.0", 34 | "eslint-plugin-prettier": "^5.1.3", 35 | "jest": "^29.7.0", 36 | "nexe": "^4.0.0-rc.4", 37 | "ts-jest": "^29.1.2", 38 | "ts-node": "^10.9.2", 39 | "typescript": "^5.4.3" 40 | }, 41 | "dependencies": { 42 | "axios": "^1.6.8", 43 | "cheerio": "^1.0.0-rc.12", 44 | "commander": "^12.0.0", 45 | "moment-timezone": "^0.5.45", 46 | "querystring": "^0.2.0" 47 | }, 48 | "version": "1.0.0" 49 | } 50 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { load } from 'cheerio'; 3 | 4 | export const getMyArticleUrls = async (cookie: string, type: string, sk: string) => { 5 | let po = 0; 6 | const myArticleList: { title: string; url: string }[] = []; 7 | 8 | // eslint-disable-next-line no-constant-condition 9 | while (true) { 10 | console.info(`${po + 1}번째 내 글 목록 리스트를 가져옵니다.`); 11 | const myArticleUrl = `https://www.clien.net/service/mypage/myArticle?&type=${type}&sk=${sk}&sv=&po=${po}`; 12 | const response = await axios.get(myArticleUrl, { 13 | headers: { 14 | Cookie: cookie, 15 | 'Sec-Fetch-Dest': 'document', 16 | 'Sec-Fetch-Mode': 'navigate', 17 | 'Sec-Fetch-Site': 'same-origin', 18 | 'Sec-Fetch-User': '?1', 19 | 'Upgrade-InSecure-Requests': '1', 20 | 'User-Agent': 21 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', 22 | 'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', 23 | 'sec-ch-ua-mobile': '?0', 24 | 'sec-ch-ua-platform': '"macOS"', 25 | }, 26 | }); 27 | console.log(`${po + 1}번째 내 글 목록 리스트를 가져와서 필요한 내용을 추출합니다.`); 28 | 29 | const $ = load(response.data); 30 | const subjectList = $('.list_subject'); 31 | subjectList.each((index, element) => { 32 | if (!element.attribs.title || !element.attribs.href) return; 33 | 34 | myArticleList.push({ 35 | title: element.attribs.title, 36 | url: `https:/www.clien.net${element.attribs.href}`, 37 | }); 38 | }); 39 | 40 | if (subjectList.length === 0) break; 41 | 42 | po++; 43 | 44 | await new Promise(resolve => setTimeout(resolve, 1000)); 45 | } 46 | 47 | return myArticleList; 48 | }; 49 | -------------------------------------------------------------------------------- /src/login.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const login = async (userId: string, userPassword: string) => { 4 | const response = await axios.get('https://www.clien.net/service', { 5 | headers: { 6 | 'User-Agent': 7 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', 8 | 'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', 9 | 'Sec-Ch-Ua-Mobile': '?0', 10 | 'Sec-Ch-Ua-Platform': '"macOS"', 11 | 'Sec-Fetch-Dest': 'document', 12 | 'Sec-Fetch-Site': 'none', 13 | 'Sec-Fetch-User': '?1', 14 | 'Upgrade-Insecure-Requests': '1', 15 | }, 16 | }); 17 | 18 | const clienCookie: Map = new Map(); 19 | if (!response) throw new Error('Failed to get CSRF token'); 20 | if (!response.headers) throw new Error('Failed to get CSRF token'); 21 | 22 | let cookies = response.headers['set-cookie']; 23 | 24 | if (cookies && Array.isArray(cookies)) { 25 | cookies 26 | .map(cookie => cookie.split(';')[0]) 27 | .forEach(cookie => clienCookie.set(cookie.split('=')[0], cookie.split('=')[1])); 28 | } 29 | 30 | const csrf = response.data.match(/ `${key}=${clienCookie.get(key)}`) 47 | .join('; '), 48 | Origin: 'https://www.clien.net', 49 | Referer: 'https://www.clien.net/service/', 50 | 'Sec-Fetch-Dest': 'document', 51 | 'Sec-Fetch-Mode': 'navigate', 52 | 'Sec-Fetch-Site': 'same-origin', 53 | 'Sec-Fetch-User': '?1', 54 | 'Upgrade-Insecure-Requested': '1', 55 | 'User-Agent': 56 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', 57 | 'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', 58 | 'sec-ch-ua-mobile': '?0', 59 | 'sec-ch-ua-platform': '"macOS"', 60 | }, 61 | }, 62 | ); 63 | 64 | cookies = response.headers['set-cookie']; 65 | if (cookies && Array.isArray(cookies)) { 66 | cookies 67 | .map(cookie => cookie.split(';')[0]) 68 | .forEach(cookie => clienCookie.set(cookie.split('=')[0], cookie.split('=')[1])); 69 | } 70 | 71 | console.info(`로그인 성공`); 72 | } catch (error) { 73 | if (error instanceof Error) console.error(error.message); 74 | throw error; 75 | } 76 | 77 | return clienCookie; 78 | }; 79 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { program } from 'commander'; 2 | import { crawlPost, writeFile } from './crawl'; 3 | import { login } from './login'; 4 | import { getMyArticleUrls } from './utils'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | 8 | const DEALY_IN_MS = 1000; 9 | 10 | async function main() { 11 | program 12 | .command('archive') 13 | .description('주어진 URL의 게시글을 저장합니다.') 14 | .option('--url ', '게시글 URL') 15 | .option('--urls ', '개행 문자로 구분된 URL 목록이 저장된 파일 경로') 16 | .option('--dry-run', '실제로 백업하지 않고 URL만 출력합니다.') 17 | .action(async options => { 18 | const { url, urls, dryRun } = options; 19 | 20 | if (url) { 21 | const sanitizedUrl = url.split('?')[0]; 22 | console.log(`Archiving URL: ${sanitizedUrl}`); 23 | if (!dryRun) { 24 | const post = await crawlPost(sanitizedUrl); 25 | if (post) { 26 | await writeFile(post); 27 | } 28 | } 29 | } else if (urls) { 30 | const filePath = path.resolve(urls); 31 | const urlData = fs.readFileSync(filePath, 'utf-8'); 32 | const urlList = urlData.split(/\r?\n/).map(url => url.trim()); 33 | 34 | console.log(`Archiving URLs from following file: ${filePath}`); 35 | for (const url of urlList) { 36 | const sanitizedUrl = url.split('?')[0]; 37 | console.log(`Archiving URL: ${sanitizedUrl}`); 38 | if (!dryRun) { 39 | const post = await crawlPost(sanitizedUrl); 40 | if (post) { 41 | await writeFile(post); 42 | } 43 | // wait for 0.5 second 44 | await new Promise(resolve => setTimeout(resolve, DEALY_IN_MS)); 45 | } 46 | } 47 | 48 | if (dryRun) { 49 | urls ? console.log(`Total ${urlList.length} URLs found.`) : console.log('URL found.'); 50 | } 51 | } else { 52 | console.log('Please provide either --url or --urls option.'); 53 | } 54 | }); 55 | 56 | program 57 | .command('backup') 58 | .description('내가 작성한 모든 게시글을 백업합니다. 아이디와 비밀번호는 절대 저장하지 않습니다.') 59 | .option('--id ', '클리앙 아이디') 60 | .option('--password ', '비밀번호. 2차 인증을 비활성화 해야 합니다.') 61 | .option('--dry-run', '실제로 백업하지 않고 URL만 출력합니다.') 62 | .action(async options => { 63 | const { id, password, dryRun } = options; 64 | 65 | if (id && password) { 66 | const clienCookie = await login(id, password); 67 | const articles = await getMyArticleUrls( 68 | Array.from(clienCookie.keys()) 69 | .map(key => `${key}=${clienCookie.get(key)}`) 70 | .join('; '), 71 | 'articles', // 나의 게시글 72 | 'title', 73 | ); 74 | 75 | for (const article of articles) { 76 | console.log(`Archiving URL: ${article.url}`); 77 | if (!dryRun) { 78 | const post = await crawlPost(article.url); 79 | if (post) { 80 | await writeFile(post); 81 | } 82 | // wait for 0.5 second 83 | await new Promise(resolve => setTimeout(resolve, DEALY_IN_MS)); 84 | } 85 | } 86 | 87 | if (dryRun) { 88 | console.log(`Total ${articles.length} articles found.`); 89 | 90 | // Save URLs to a file 91 | const filePath = path.join(process.cwd(), 'my-articles.txt'); 92 | fs.writeFileSync(filePath, articles.map(article => article.url).join('\n')); 93 | } 94 | } else { 95 | console.log('Please provide --id and --password option.'); 96 | } 97 | }); 98 | 99 | program.parse(process.argv); 100 | } 101 | 102 | main(); 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clien Archiver 2 | 3 | Clien-Archiver는 [클리앙]((https://www.clien.net) 웹사이트의 게시물을 로컬 환경에 백업하고 관리할 수 있는 CLI 도구입니다. 4 | 5 | ## 기능 6 | 7 | - 내가 작성한 게시물을 로컬 디스크에 저장할 수 있습니다. 8 | - 단일 게시물 URL 또는 여러 개의 게시물 URL을 파일에서 읽어와 백업할 수 있습니다. 9 | - 게시물의 제목, 작성자, 카테고리, 내용, 작성일, 댓글 등의 정보를 저장합니다. 10 | - 게시물에 포함된 이미지를 로컬 디렉토리에 다운로드하여 저장합니다. 11 | - 저장된 데이터는 JSON 형식으로 저장됩니다. 12 | 13 | ## 알려진 문제 14 | 15 | > 댓글의 이미지와 작성자는 저장하지 않으며, 댓글의 답글이나 사용자 본인이 작성한 댓글도 특별히 구별하지 않습니다. 16 | 17 | ## 내보내기 플러그인 (예정) 18 | 19 | 향후 백업된 데이터를 다른 플랫폼으로 내보낼 수 있는 내보내기 플러그인 기능을 개발할 계획입니다. 초기에는 [다모앙](https://damoang.net/)과 [레딧 모공](https://new.reddit.com/r/Mogong/)을 지원할 예정입니다. 20 | 21 | ## 설치 22 | 23 | [여기](https://github.com/wokim/clien-archiver/releases)에서 사전 빌드 된 바이너리를 다운로드 하세요. 24 | 25 | ```sh 26 | ./clien-archiver-- --help 27 | 28 | # For example, 29 | ./client-archiver-macos-x64 --help 30 | ``` 31 | 32 | ### Building from Source 33 | 34 | ```sh 35 | # Clone the project: 36 | $ git clone https://github.com/yourusername/clien-archiver.git 37 | 38 | # Navigate to the project directory: 39 | $ cd clien-archiver 40 | 41 | # Install the required dependencies: 42 | $ npm install 43 | 44 | # Build the project: 45 | $ npm run build 46 | 47 | # Run the tool: 48 | node lib/index.js --help 49 | 50 | ``` 51 | 52 | ## 사용법 53 | 54 | ### 단일 게시물 백업 55 | 56 | 주어진 게시물을 백업합니다. 내가 작성한 게시물이 아니어도 상관 없습니다. 57 | 58 | ```sh 59 | $ clien-archiver archive --url 60 | 61 | # 예시: 62 | $ clien-archiver archive --url https://www.clien.net/service/board/park/18680440 63 | 64 | # dry-run 명령어를 추가하면 실제로 게시물을 저장하지 않고, 처리 결과만을 확인할 수 있습니다. 65 | $ clien-archiver archive --url https://www.clien.net/service/board/park/18680440 --dry-run 66 | Archiving URL: https://www.clien.net/service/board/park/18680440 67 | ``` 68 | 69 | ### 여러 게시물 백업 70 | 71 | 1. 백업할 게시물 URL을 urls.txt 파일에 개행 문자로 구분하여 저장합니다. 72 | 2. 다음 명령을 실행합니다. 73 | 74 | ```sh 75 | $ clien-archiver archive --urls urls.txt 76 | 77 | # urls.txt 78 | # URL이 개행 문자로 구분되어 있습니다 79 | $ cat urls.txt 80 | https://www.clien.net/service/board/park/18680440 81 | https://www.clien.net/service/board/park/18680441 82 | https://www.clien.net/service/board/park/18680442 83 | ... 84 | 85 | # # dry-run 명령어를 추가하면 실제로 게시물을 저장하지 않고, 처리 결과만을 확인할 수 있습니다. 86 | $ clien-archiver archive --urls urls.txt --dry-run 87 | ``` 88 | 89 | ### 사용자 글 백업 90 | 91 | 주어진 사용자 아이디와 비밀번호로 로그인하여 내가 작성한 게시글 목록을 가져옵니다. 반드시 **2단계 인증은 사용안함**으로 설정되어 있어야 합니다. `dry-run` 명령어를 사용한 경우 현재 경로의 `my-articles.txt` 파일에 내가 작성한 모든 게시글의 경로를 저장합니다. 92 | 93 | > 아이디와 비밀번호는 절대 저장하지 않습니다. 소스코드가 공개되어 있으므로 확인 가능합니다. 94 | 95 | ```sh 96 | $ clien-archiver backup --id <사용자 아이디> --password <비밀번호> 97 | 98 | # 예시: 99 | $ clien-archiver backup --id myusername --password mypassword 100 | 101 | # dry-run 명령어를 추가하면 실제로 게시물을 저장하지 않고, 처리 결과만을 확인할 수 있습니다. 102 | # 현재 경로에 my-articles.txt 라는 파일로 저장합니다. 103 | $ clien-archiver backup --id myusername --password mypassword --dry-run 104 | ``` 105 | 106 | ### 도움말 107 | 108 | ```sh 109 | clien-archiver --help 110 | ``` 111 | 112 | ## 파일 구조 113 | 114 | 백업된 데이터는 다음과 같은 구조로 저장됩니다: 115 | 116 | ```sh 117 | # articles 폴더를 잘 보관해주세요. 118 | articles/ 119 | Board Name/ 120 | Post ID.json 121 | images/ 122 | Image File1.jpg 123 | Image File2.png 124 | ... 125 | ``` 126 | 127 | - `articles` 디렉토리 내에 각 게시판 이름으로 하위 디렉토리가 생성됩니다. 128 | - 각 게시물은 `게시물 ID.json` 파일로 저장되며, JSON 형식으로 게시물 정보가 포함됩니다. 129 | - 게시물에 포함된 이미지는 `images` 디렉토리에 저장됩니다. 130 | 131 | ## Contributing 132 | 133 | This project is open-source, and contributions are welcome. You can contribute to the project in various ways, such as reporting bugs, requesting features, or improving the code. 134 | 135 | ## License 136 | 137 | This project is licensed under the MIT License. 138 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,node,git 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,node,git 3 | 4 | ### Git ### 5 | # Created by git for backups. To disable backups in Git: 6 | # $ git config --global mergetool.keepBackup false 7 | *.orig 8 | 9 | # Created by git when using merge tools for conflicts 10 | *.BACKUP.* 11 | *.BASE.* 12 | *.LOCAL.* 13 | *.REMOTE.* 14 | *_BACKUP_*.txt 15 | *_BASE_*.txt 16 | *_LOCAL_*.txt 17 | *_REMOTE_*.txt 18 | 19 | ### Node ### 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | lerna-debug.log* 27 | .pnpm-debug.log* 28 | 29 | # Diagnostic reports (https://nodejs.org/api/report.html) 30 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 31 | 32 | # Runtime data 33 | pids 34 | *.pid 35 | *.seed 36 | *.pid.lock 37 | 38 | # Directory for instrumented libs generated by jscoverage/JSCover 39 | lib-cov 40 | 41 | # Coverage directory used by tools like istanbul 42 | coverage 43 | *.lcov 44 | 45 | # nyc test coverage 46 | .nyc_output 47 | 48 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 49 | .grunt 50 | 51 | # Bower dependency directory (https://bower.io/) 52 | bower_components 53 | 54 | # node-waf configuration 55 | .lock-wscript 56 | 57 | # Compiled binary addons (https://nodejs.org/api/addons.html) 58 | build/Release 59 | 60 | # Dependency directories 61 | node_modules/ 62 | jspm_packages/ 63 | 64 | # Snowpack dependency directory (https://snowpack.dev/) 65 | web_modules/ 66 | 67 | # TypeScript cache 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | .npm 72 | 73 | # Optional eslint cache 74 | .eslintcache 75 | 76 | # Optional stylelint cache 77 | .stylelintcache 78 | 79 | # Microbundle cache 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | .node_repl_history 87 | 88 | # Output of 'npm pack' 89 | *.tgz 90 | 91 | # Yarn Integrity file 92 | .yarn-integrity 93 | 94 | # dotenv environment variable files 95 | .env 96 | .env.development.local 97 | .env.test.local 98 | .env.production.local 99 | .env.local 100 | 101 | # parcel-bundler cache (https://parceljs.org/) 102 | .cache 103 | .parcel-cache 104 | 105 | # Next.js build output 106 | .next 107 | out 108 | 109 | # Nuxt.js build / generate output 110 | .nuxt 111 | dist 112 | 113 | # Gatsby files 114 | .cache/ 115 | # Comment in the public line in if your project uses Gatsby and not Next.js 116 | # https://nextjs.org/blog/next-9-1#public-directory-support 117 | # public 118 | 119 | # vuepress build output 120 | .vuepress/dist 121 | 122 | # vuepress v2.x temp and cache directory 123 | .temp 124 | 125 | # Docusaurus cache and generated files 126 | .docusaurus 127 | 128 | # Serverless directories 129 | .serverless/ 130 | 131 | # FuseBox cache 132 | .fusebox/ 133 | 134 | # DynamoDB Local files 135 | .dynamodb/ 136 | 137 | # TernJS port file 138 | .tern-port 139 | 140 | # Stores VSCode versions used for testing VSCode extensions 141 | .vscode-test 142 | 143 | # yarn v2 144 | .yarn/cache 145 | .yarn/unplugged 146 | .yarn/build-state.yml 147 | .yarn/install-state.gz 148 | .pnp.* 149 | 150 | ### Node Patch ### 151 | # Serverless Webpack directories 152 | .webpack/ 153 | 154 | # Optional stylelint cache 155 | 156 | # SvelteKit build / generate output 157 | .svelte-kit 158 | 159 | ### VisualStudioCode ### 160 | .vscode/* 161 | !.vscode/settings.json 162 | !.vscode/tasks.json 163 | !.vscode/launch.json 164 | !.vscode/extensions.json 165 | !.vscode/*.code-snippets 166 | 167 | # Local History for Visual Studio Code 168 | .history/ 169 | 170 | # Built Visual Studio Code Extensions 171 | *.vsix 172 | 173 | ### VisualStudioCode Patch ### 174 | # Ignore all local history of files 175 | .history 176 | .ionide 177 | 178 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,node,git 179 | 180 | lib 181 | bin 182 | -------------------------------------------------------------------------------- /src/crawl.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { load } from 'cheerio'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import moment from 'moment-timezone'; 6 | 7 | export interface Comment { 8 | html: string; 9 | text: string; 10 | } 11 | 12 | export interface Post { 13 | boardName: string; 14 | category: string; 15 | title: string; 16 | author?: string; // first version does not have author 17 | createdAt: string; 18 | postId: string; 19 | content: string; 20 | comments?: Comment[]; // first version does not have comments 21 | } 22 | 23 | export interface Metadata { 24 | directory: string; 25 | filePath: string; 26 | imageDirectory: string; 27 | imageUrls: { url: string; path: string }[]; 28 | } 29 | 30 | export async function crawlPost(url: string): Promise<{ metadata: Metadata; body: Post } | undefined> { 31 | try { 32 | const response = await axios.get(url); 33 | const $ = load(response.data); 34 | 35 | const boardName = $('.board_head .board_name h2').text().trim(); 36 | const category = $('.post_subject .post_category').text().trim(); 37 | const title = $('.post_subject > span:not(.post_category)').text().trim(); 38 | const createdAtRaw = $('.post_view .post_author span:first-child').text().trim(); 39 | const createdAt = moment.tz(createdAtRaw, 'YYYY-MM-DD HH:mm:ss', 'Asia/Seoul').toISOString(); 40 | const postId = url.split('/').pop()!.split('?')[0]; 41 | const authorElement = $('.content_view .post_info .post_contact .contact_name .nickname'); 42 | let author = ''; 43 | 44 | if (authorElement.length > 0) { 45 | const childElement = authorElement.children('img, span'); 46 | if (childElement.length > 0) { 47 | author = childElement.attr('title') || ''; 48 | } 49 | } 50 | 51 | let content = $('.post_view .post_content article').html() || ''; 52 | 53 | const comments: Comment[] = []; 54 | $('.post_comment [data-role="comment"]').each((_, commentElement) => { 55 | const commentRows = $(commentElement).find('> div'); 56 | 57 | commentRows.each((_, rowElement) => { 58 | const rowClass = $(rowElement).attr('class'); 59 | if (rowClass && rowClass.includes('comment_row') && !rowClass.includes('blocked')) { 60 | const commentContent = $(rowElement).find('.comment_content .comment_view'); 61 | comments.push({ html: commentContent.html() || '', text: commentContent.text().trim() }); 62 | } 63 | }); 64 | }); 65 | 66 | const directory = path.join('articles', boardName); 67 | const filePath = path.join(directory, `${postId}.json`); 68 | const imageDirectory = path.join(directory, 'images'); 69 | 70 | const imageUrls = $('img', content) 71 | .map((_, img) => $(img).attr('src')) 72 | .get() 73 | .map(imageUrl => { 74 | const url = new URL(imageUrl || ''); 75 | const imagePath = path.join(directory, 'images', url.pathname); 76 | 77 | // 콘텐츠의 이미지 경로 변경 78 | if (content) { 79 | content = content.replace(imageUrl, imagePath.replace(directory, '').replace(/\\/g, '/')); 80 | } 81 | 82 | return { url: imageUrl, path: imagePath }; 83 | }); 84 | 85 | return { 86 | metadata: { 87 | directory, 88 | filePath, 89 | imageDirectory, 90 | imageUrls, 91 | }, 92 | body: { 93 | boardName, 94 | category, 95 | title, 96 | author, 97 | createdAt, 98 | postId, 99 | content, 100 | comments, 101 | }, 102 | }; 103 | } catch (error) { 104 | if (error instanceof Error) { 105 | console.error(`크롤링 에러: ${error.message}`); 106 | } 107 | } 108 | } 109 | 110 | export async function writeFile(post: { metadata: Metadata; body: Post }) { 111 | try { 112 | const { directory, imageUrls, filePath } = post.metadata; 113 | 114 | // 디렉토리 생성 115 | fs.mkdirSync(directory, { recursive: true }); 116 | 117 | // 이미지 다운로드 및 저장 118 | const imageDirectory = path.join(directory, 'images'); 119 | fs.mkdirSync(imageDirectory, { recursive: true }); 120 | 121 | for (const imageUrl of imageUrls) { 122 | const imageDirectory = path.dirname(imageUrl.path); 123 | fs.mkdirSync(imageDirectory, { recursive: true }); 124 | 125 | const imageResponse = await axios.get(imageUrl.url, { responseType: 'stream' }); 126 | imageResponse.data.pipe(fs.createWriteStream(imageUrl.path)); 127 | } 128 | 129 | // JSON 파일 생성 130 | fs.writeFileSync(filePath, JSON.stringify(post.body, null, 2)); 131 | console.log(`게시물 저장 완료: ${filePath}`); 132 | } catch (error) { 133 | if (error instanceof Error) { 134 | console.error(`파일을 저장하는 중에 에러가 발생하였습니다: ${error.message}`); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/crawl.spec.ts: -------------------------------------------------------------------------------- 1 | import { crawlPost } from './crawl'; 2 | 3 | describe('Crawl', () => { 4 | xit('should crawl a website with external CDN images', async () => { 5 | const post = await crawlPost('https://www.clien.net/service/board/lecture/13750265'); 6 | 7 | expect(post?.body.boardName).toBe('팁과강좌'); 8 | expect(post?.body.category).toBe('기타'); 9 | expect(post?.body.title).toBe('입사 전, 반드시 확인해야 할 연봉 및 처우 내역! - 현명한 연봉 조율'); 10 | expect(post?.body.author).toBe('stillcalm'); 11 | expect(post?.body.createdAt).toBe('2019-07-25T02:01:19.000Z'); 12 | expect(post?.body.postId).toBe('13750265'); 13 | expect(post?.metadata.imageUrls).toEqual( 14 | expect.arrayContaining([ 15 | { 16 | url: 'https://k.kakaocdn.net/dn/bDbCnN/btqv2J013Kd/D4JqPz6OPO28hl2D2Oaye0/img.jpg', 17 | path: 'articles/팁과강좌/images/dn/bDbCnN/btqv2J013Kd/D4JqPz6OPO28hl2D2Oaye0/img.jpg', 18 | }, 19 | { 20 | url: 'https://k.kakaocdn.net/dn/t0xj9/btqv2IHMziC/beoY9pAWdkEaIwROfiK64k/img.jpg', 21 | path: 'articles/팁과강좌/images/dn/t0xj9/btqv2IHMziC/beoY9pAWdkEaIwROfiK64k/img.jpg', 22 | }, 23 | { 24 | url: 'https://k.kakaocdn.net/dn/TUSR7/btqv0xVAHOf/4PDRdEUY6rVQ0r8CyUn63k/img.jpg', 25 | path: 'articles/팁과강좌/images/dn/TUSR7/btqv0xVAHOf/4PDRdEUY6rVQ0r8CyUn63k/img.jpg', 26 | }, 27 | { 28 | url: 'https://k.kakaocdn.net/dn/X3hTh/btqv3tdDY9n/D8UYSPkSLpTxBZGMwpZ0h0/img.jpg', 29 | path: 'articles/팁과강좌/images/dn/X3hTh/btqv3tdDY9n/D8UYSPkSLpTxBZGMwpZ0h0/img.jpg', 30 | }, 31 | ]), 32 | ); 33 | }); 34 | 35 | xit('should crawl a website', async () => { 36 | const post = await crawlPost('https://www.clien.net/service/board/park/18676865'); 37 | 38 | console.log(post); 39 | console.log(JSON.stringify(post?.body.comments, null, 2)); 40 | 41 | expect(post?.body.boardName).toBe('모두의공원'); 42 | expect(post?.body.category).toBe(''); 43 | expect(post?.body.title).toBe('운영자입니다. 추가 설명 올립니다.'); 44 | expect(post?.body.author).toBe('운영자'); 45 | expect(post?.body.postId).toBe('18676865'); 46 | 47 | expect(post?.metadata.directory).toBe('articles/모두의공원'); 48 | expect(post?.metadata.filePath).toBe('articles/모두의공원/18676865.json'); 49 | expect(post?.metadata.imageDirectory).toBe('articles/모두의공원/images'); 50 | expect(post?.metadata.imageUrls.length).toBe(0); 51 | }); 52 | 53 | xit('should crawl a website with images', async () => { 54 | const post = await crawlPost('https://www.clien.net/service/board/lecture/18668326'); 55 | 56 | console.log(post); 57 | 58 | expect(post?.body.boardName).toBe('팁과강좌'); 59 | expect(post?.body.category).toBe('PC/모바일'); 60 | expect(post?.body.title).toBe('웹브라우저에서 광고 차단하는 방법'); 61 | expect(post?.body.author).toBe('에스까르고'); 62 | expect(post?.body.createdAt).toBe('2024-03-27T01:27:33.000Z'); 63 | expect(post?.body.postId).toBe('18668326'); 64 | 65 | expect(post?.metadata.directory).toBe('articles/팁과강좌'); 66 | expect(post?.metadata.filePath).toBe('articles/팁과강좌/18668326.json'); 67 | expect(post?.metadata.imageDirectory).toBe('articles/팁과강좌/images'); 68 | expect(post?.metadata.imageUrls).toEqual( 69 | expect.arrayContaining([ 70 | { 71 | url: 'https://edgio.clien.net/F01/15015025/395348d1c9ad58.png?scale=width[740],options[limit]', 72 | path: 'articles/팁과강좌/images/F01/15015025/395348d1c9ad58.png', 73 | }, 74 | { 75 | url: 'https://edgio.clien.net/F01/15015026/3952e44612118e.png?scale=width[740],options[limit]', 76 | path: 'articles/팁과강좌/images/F01/15015026/3952e44612118e.png', 77 | }, 78 | { 79 | url: 'https://edgio.clien.net/F01/15015027/395348e3564890.png?scale=width[740],options[limit]', 80 | path: 'articles/팁과강좌/images/F01/15015027/395348e3564890.png', 81 | }, 82 | { 83 | url: 'https://edgio.clien.net/F01/15015030/3952ed86a5c3ce.png?scale=width[740],options[limit]', 84 | path: 'articles/팁과강좌/images/F01/15015030/3952ed86a5c3ce.png', 85 | }, 86 | { 87 | url: 'https://edgio.clien.net/F01/15015031/39535223a3e4af.png?scale=width[740],options[limit]', 88 | path: 'articles/팁과강좌/images/F01/15015031/39535223a3e4af.png', 89 | }, 90 | { 91 | url: 'https://edgio.clien.net/F01/15015032/3952ed9a0093e2.png?scale=width[740],options[limit]', 92 | path: 'articles/팁과강좌/images/F01/15015032/3952ed9a0093e2.png', 93 | }, 94 | { 95 | url: 'https://edgio.clien.net/F01/15015033/3953590040c834.png?scale=width[740],options[limit]', 96 | path: 'articles/팁과강좌/images/F01/15015033/3953590040c834.png', 97 | }, 98 | { 99 | url: 'https://edgio.clien.net/F01/15015034/3952f4746dc002.png?scale=width[740],options[limit]', 100 | path: 'articles/팁과강좌/images/F01/15015034/3952f4746dc002.png', 101 | }, 102 | { 103 | url: 'https://edgio.clien.net/F01/15015035/39535910baa799.png?scale=width[740],options[limit]', 104 | path: 'articles/팁과강좌/images/F01/15015035/39535910baa799.png', 105 | }, 106 | { 107 | url: 'https://edgio.clien.net/F01/15015054/39531778d17057.png?scale=width[740],options[limit]', 108 | path: 'articles/팁과강좌/images/F01/15015054/39531778d17057.png', 109 | }, 110 | { 111 | url: 'https://edgio.clien.net/F01/15015055/39537c18b37176.png?scale=width[740],options[limit]', 112 | path: 'articles/팁과강좌/images/F01/15015055/39537c18b37176.png', 113 | }, 114 | { 115 | url: 'https://edgio.clien.net/F01/15015059/39532a5407bda2.png?scale=width[740],options[limit]', 116 | path: 'articles/팁과강좌/images/F01/15015059/39532a5407bda2.png', 117 | }, 118 | { 119 | url: 'https://edgio.clien.net/F01/15015060/39538ef1f7336e.png?scale=width[740],options[limit]', 120 | path: 'articles/팁과강좌/images/F01/15015060/39538ef1f7336e.png', 121 | }, 122 | { 123 | url: 'https://edgio.clien.net/F01/15015063/395333ef4c12e3.png?scale=width[740],options[limit]', 124 | path: 'articles/팁과강좌/images/F01/15015063/395333ef4c12e3.png', 125 | }, 126 | { 127 | url: 'https://edgio.clien.net/F01/15015064/3953988f5265e2.png?scale=width[740],options[limit]', 128 | path: 'articles/팁과강좌/images/F01/15015064/3953988f5265e2.png', 129 | }, 130 | ]), 131 | ); 132 | expect(post!.body.comments!.length).not.toBe(0); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./lib", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": ["src/**/*"], 110 | "exclude": ["node_modules", "lib"] 111 | } 112 | --------------------------------------------------------------------------------