├── src ├── css │ ├── rule-config.css │ ├── fixed.css │ ├── split-panel.css │ ├── editor.css │ ├── header.css │ └── app.css ├── states │ ├── index.ts │ └── state.ts ├── components │ ├── App │ │ ├── index.ts │ │ ├── App.css │ │ └── App.tsx │ ├── SplitPanel │ │ ├── index.tsx │ │ ├── SplitPanel.css │ │ └── SplitPanel.tsx │ ├── ESTreeASTViewer │ │ ├── index.ts │ │ └── ESTreeASTViewer.tsx │ ├── ErrorMessage.tsx │ ├── Fixed.tsx │ ├── CodeEditor.tsx │ ├── RuleConfig.tsx │ ├── BaseEditor.tsx │ ├── Header.tsx │ ├── LintMessages.tsx │ └── ParserOptions.tsx ├── modules │ ├── globby.js │ └── resolve-from.js ├── index.tsx ├── lib │ ├── files.ts │ ├── __tests__ │ │ └── parser-options-converter.test.ts │ ├── source-files.ts │ ├── parser-options-converter.ts │ ├── compiler-host.ts │ ├── create-ast-program.ts │ ├── linter.ts │ └── parser.ts ├── shared │ ├── debounce.ts │ └── query-params-state.ts ├── constants.ts └── index.html ├── .prettierignore ├── assets ├── favicon.png └── share.png ├── .travis.yml ├── jest.config.js ├── scripts ├── rm-rf.ts └── gen-libs.ts ├── .babelrc ├── webpack.dev.config.js ├── .gitignore ├── tsconfig.json ├── README.md ├── index.html ├── .eslintrc.js ├── webpack.prod.config.js ├── LICENSE.md ├── webpack.base.config.js ├── package.json └── CODE_OF_CONDUCT.md /src/css/rule-config.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/states/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./state"; 2 | -------------------------------------------------------------------------------- /src/components/App/index.ts: -------------------------------------------------------------------------------- 1 | export { App } from "./App"; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | dist 4 | src/lib/ts-libs 5 | -------------------------------------------------------------------------------- /src/components/SplitPanel/index.tsx: -------------------------------------------------------------------------------- 1 | export { SplitPanel } from "./SplitPanel"; 2 | -------------------------------------------------------------------------------- /src/components/ESTreeASTViewer/index.ts: -------------------------------------------------------------------------------- 1 | export { ESTreeASTViewer } from "./ESTreeASTViewer"; 2 | -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeonjuan/typescript-eslint-demo/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeonjuan/typescript-eslint-demo/HEAD/assets/share.png -------------------------------------------------------------------------------- /src/modules/globby.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sync() { 3 | return ["./tsconfig.json"]; 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | branches: 5 | only: 6 | - master 7 | script: 8 | - npm run test:all 9 | -------------------------------------------------------------------------------- /src/css/fixed.css: -------------------------------------------------------------------------------- 1 | .fixed-code { 2 | background: #f5f5f5; 3 | border: 1px solid #ccc; 4 | border-radius: 4px; 5 | padding: 10px; 6 | } 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | moduleNameMapper: { 5 | "@/(.*)$": "/src/$1", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/components/ESTreeASTViewer/ESTreeASTViewer.tsx: -------------------------------------------------------------------------------- 1 | import ReactJson from "react-json-view"; 2 | 3 | export const ESTreeASTViewer = (ast: any) => { 4 | return ; 5 | }; 6 | -------------------------------------------------------------------------------- /src/css/split-panel.css: -------------------------------------------------------------------------------- 1 | .divider { 2 | background-color: #ddd; 3 | } 4 | 5 | .divider.horizontal { 6 | width: 5px; 7 | } 8 | 9 | .divider.vertical { 10 | height: 5px; 11 | } 12 | -------------------------------------------------------------------------------- /scripts/rm-rf.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | 4 | (function rmrf([dirPath]: string[]) { 5 | try { 6 | fs.rmdirSync(path.resolve(process.cwd(), dirPath), { recursive: true }); 7 | } catch {} 8 | })(process.argv.slice(2)); 9 | -------------------------------------------------------------------------------- /src/css/editor.css: -------------------------------------------------------------------------------- 1 | .CodeMirror { 2 | /* border: 1px solid #ccc; 3 | border-radius: 4px; */ 4 | height: 100%; 5 | } 6 | 7 | .editor-error { 8 | text-decoration: underline dashed red; 9 | } 10 | 11 | .editor { 12 | height: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "chrome": 79 8 | } 9 | } 10 | ], 11 | "@babel/preset-typescript", 12 | "@babel/preset-react" 13 | ], 14 | "ignore": ["**/__tests__"] 15 | } 16 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDom from "react-dom"; 3 | import { App } from "./components/App"; 4 | import { RecoilRoot } from "recoil"; 5 | 6 | ReactDom.render( 7 | 8 | 9 | , 10 | document.getElementById("app") 11 | ); 12 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("lodash"); 2 | const baseConfig = require("./webpack.base.config"); 3 | 4 | module.exports = merge(baseConfig, { 5 | mode: "development", 6 | devtool: "cheap-eval-source-map", 7 | devServer: { 8 | open: true, 9 | hot: true, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/css/header.css: -------------------------------------------------------------------------------- 1 | header { 2 | padding: 10px; 3 | } 4 | 5 | .version-badge { 6 | margin-left: 10px; 7 | } 8 | 9 | .gh-btn { 10 | margin-left: 10px; 11 | } 12 | 13 | ul { 14 | margin-bottom: 0; 15 | } 16 | li { 17 | list-style: none; 18 | } 19 | .version-list { 20 | margin-bottom: 0; 21 | } 22 | .version { 23 | font-size: 10px; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/resolve-from.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const resolveFrom = (fromDir, moduleId, silent) => { 3 | return { 4 | id: "id", 5 | filename: "filename", 6 | paths: "test", 7 | }; 8 | }; 9 | 10 | module.exports = (fromDir, moduleId) => resolveFrom(fromDir, moduleId); 11 | module.exports.silent = (fromDir, moduleId) => 12 | resolveFrom(fromDir, moduleId, true); 13 | -------------------------------------------------------------------------------- /src/components/SplitPanel/SplitPanel.css: -------------------------------------------------------------------------------- 1 | .divider { 2 | background-color: #ccc; 3 | } 4 | 5 | .divider.horizontal { 6 | width: 3px; 7 | } 8 | 9 | .horizontal:hover { 10 | background-color: #888; 11 | cursor: col-resize; 12 | } 13 | 14 | .vertical:hover { 15 | background-color: #888; 16 | cursor: row-resize; 17 | } 18 | 19 | .divider.vertical { 20 | height: 3px; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Alert } from "react-bootstrap"; 3 | import type { FC } from "react"; 4 | 5 | interface Props { 6 | origin: string; 7 | error: Error; 8 | } 9 | 10 | export const ErrorMessage: FC = (props) => ( 11 | 12 | {props.origin} 13 | {`${props.error}`} 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/components/App/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100vh; 5 | max-height: 100vh; 6 | font-size: 13px; 7 | } 8 | 9 | .top { 10 | width: 100%; 11 | } 12 | 13 | .bottom { 14 | width: 100%; 15 | } 16 | 17 | .tab-content { 18 | height: calc(100% - 47px); 19 | } 20 | 21 | .tab-pane { 22 | height: 100%; 23 | } 24 | 25 | .rule-editor { 26 | height: 100%; 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | /dist 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /src/lib/files.ts: -------------------------------------------------------------------------------- 1 | import libs from "@/ts-libs"; 2 | 3 | export interface Files { 4 | [name: string]: string; 5 | } 6 | 7 | export const LIB_FILES = Object.entries(libs).reduce( 8 | (files, [name, text]) => ({ 9 | ...files, 10 | [name]: text, 11 | }), 12 | {} as Files 13 | ); 14 | 15 | export function createFilesIncludeLibs(name: string, text: string): Files { 16 | return Object.assign(LIB_FILES, { [name]: text }); 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/debounce.ts: -------------------------------------------------------------------------------- 1 | type Func = (...args: T[]) => void; 2 | 3 | export function debounce(func: Func, delayMs: number): Func { 4 | let timer: null | ReturnType = null; 5 | return function call(...args: Parameters) { 6 | if (timer) { 7 | clearTimeout(timer); 8 | } 9 | timer = setTimeout(() => { 10 | timer = null; 11 | func(...args); 12 | }, delayMs); 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Fixed.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRecoilValueLoadable } from "recoil"; 3 | import * as states from "@/states"; 4 | import type { FC } from "react"; 5 | import "@/css/fixed.css"; 6 | 7 | export const Fixed: FC = () => { 8 | const lintResult = useRecoilValueLoadable(states.lintResultState); 9 | let code = ""; 10 | if (lintResult.state === "hasValue") { 11 | code = lintResult.contents.fixed; 12 | } 13 | return
{code}
; 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "allowJs": false, 6 | "jsx": "preserve", 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "baseUrl": "./", 11 | "paths": { 12 | "@/*": ["./src/*"] 13 | }, 14 | "esModuleInterop": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build](https://travis-ci.org/yeonjuan/typescript-eslint-demo.svg?branch=master) 2 | 3 | > ⚠️ Deprecation Notice ⚠️ 4 | > 5 | > This project is archived because TypeScript ESLint's official playground is served now. 6 | 7 | # TypeScript ESLint Demo 8 | 9 | An online demo for [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) plugin. - [🚀 Online Demo](https://yeonjuan.github.io/typescript-eslint-demo/) 10 | 11 | ## Development 12 | 13 | - `npm run dev` : run webpack dev server on http://localhost:8080 14 | - `npm run format` : format code with prettier 15 | - `npm run test`: run test 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TypeScript-ESLint Demo 6 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | plugins: ["@typescript-eslint"], 5 | extends: [ 6 | "plugin:@typescript-eslint/recommended", 7 | "prettier", 8 | "prettier/@typescript-eslint", 9 | ], 10 | ignorePatterns: ["*.js"], 11 | rules: { 12 | "@typescript-eslint/ban-ts-comment": "off", 13 | }, 14 | overrides: [ 15 | { 16 | files: ["*.html"], 17 | plugins: ["@html-eslint"], 18 | parser: "@html-eslint/parser", 19 | rules: { 20 | "@html-eslint/require-lang": "error", 21 | "@html-eslint/require-title": "error", 22 | "@html-eslint/no-multiple-h1": "error", 23 | }, 24 | }, 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /src/css/app.css: -------------------------------------------------------------------------------- 1 | main { 2 | padding-right: 15px; 3 | padding-left: 15px; 4 | margin-right: auto; 5 | margin-left: auto; 6 | } 7 | .row { 8 | margin-top: 20px; 9 | } 10 | .bottom-col { 11 | overflow: auto; 12 | } 13 | .versions { 14 | float: right; 15 | } 16 | .parser-options { 17 | padding: 10px; 18 | } 19 | .parser-options h5 { 20 | margin-top: 10px; 21 | margin-bottom: 10px; 22 | } 23 | .checkbox { 24 | margin-left: 20px; 25 | margin-right: 10px; 26 | } 27 | .checkbox input[type="checkbox"] { 28 | margin-left: 10px; 29 | margin-right: 10px; 30 | } 31 | .select { 32 | margin-left: 27px; 33 | } 34 | 35 | .tab-content { 36 | height: calc(100% - 47px); 37 | } 38 | 39 | .tab-pane { 40 | height: 100%; 41 | } 42 | -------------------------------------------------------------------------------- /src/shared/query-params-state.ts: -------------------------------------------------------------------------------- 1 | import JSON5 from "json5"; 2 | import type { Linter } from "eslint"; 3 | import type { ParserOptions } from "@typescript-eslint/parser"; 4 | 5 | interface QueryParamsState { 6 | code?: string; 7 | rules?: Linter.RulesRecord; 8 | parserOptions?: ParserOptions; 9 | } 10 | 11 | export const queryParamsState = { 12 | get(): QueryParamsState { 13 | try { 14 | const decoded = decodeURIComponent( 15 | escape(atob(location.hash.replace("#", ""))) 16 | ); 17 | return JSON5.parse(decoded); 18 | } catch {} 19 | return {}; 20 | }, 21 | set(state: QueryParamsState): void { 22 | const encoded = btoa(unescape(encodeURIComponent(JSON5.stringify(state)))); 23 | location.hash = encoded; 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/lib/__tests__/parser-options-converter.test.ts: -------------------------------------------------------------------------------- 1 | import * as converter from "@/lib/parser-options-converter"; 2 | import { ParserOptions } from "@typescript-eslint/parser"; 3 | import { JsxEmit } from "typescript"; 4 | 5 | const JSX_PARSER_OPTIONS: ParserOptions = { 6 | ecmaVersion: 2019, 7 | ecmaFeatures: { 8 | jsx: true, 9 | globalReturn: true, 10 | }, 11 | sourceType: "module", 12 | }; 13 | 14 | describe("parser-options-converter", () => { 15 | it("toTSESTOptions", () => { 16 | expect(converter.toTSESTOptions(JSX_PARSER_OPTIONS)).toMatchObject({ 17 | jsx: true, 18 | useJSXTextNode: true, 19 | }); 20 | }); 21 | 22 | it("toCompilerOptions", () => { 23 | expect(converter.toCompilerOptions(JSX_PARSER_OPTIONS)).toMatchObject({ 24 | jsx: JsxEmit.Preserve, 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const { merge } = require("lodash"); 2 | const baseConfig = require("./webpack.base.config"); 3 | const TerserPlugin = require("terser-webpack-plugin"); 4 | 5 | module.exports = merge(baseConfig, { 6 | mode: "production", 7 | optimization: { 8 | minimize: true, 9 | minimizer: [ 10 | new TerserPlugin({ 11 | terserOptions: { 12 | comments: false, 13 | }, 14 | extractComments: false, 15 | }), 16 | ], 17 | splitChunks: { 18 | chunks: "all", 19 | maxInitialRequests: Infinity, 20 | minSize: 0, 21 | cacheGroups: { 22 | vendor: { 23 | test: /[\\/]node_modules[\\/]/, 24 | }, 25 | }, 26 | }, 27 | }, 28 | output: { 29 | filename: "[name].[hash].js", 30 | chunkFilename: "[name].[hash].js", 31 | publicPath: "./", 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/lib/source-files.ts: -------------------------------------------------------------------------------- 1 | import { LIB_FILES } from "@/lib/files"; 2 | import { ScriptTarget, createSourceFile, ScriptKind } from "typescript"; 3 | import type { SourceFile } from "typescript"; 4 | 5 | export interface SourceFiles { 6 | [name: string]: SourceFile; 7 | } 8 | 9 | function createTSSourceFile( 10 | name: string, 11 | code: string, 12 | scriptKind: ScriptKind 13 | ) { 14 | return createSourceFile(name, code, ScriptTarget.Latest, true, scriptKind); 15 | } 16 | 17 | const LIB_SOURCE_FILES = Object.entries(LIB_FILES).reduce( 18 | (sourceFiles, [name, code]) => ({ 19 | ...sourceFiles, 20 | [name]: createTSSourceFile(name, code, ScriptKind.TS), 21 | }) 22 | ); 23 | 24 | export function createSourceFilesIncludeLibs( 25 | name: string, 26 | code: string, 27 | scriptKind: ScriptKind 28 | ): SourceFiles { 29 | return Object.assign(LIB_SOURCE_FILES, { 30 | [name]: createTSSourceFile(name, code, scriptKind), 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 YeonJuan 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 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import type { Linter } from "eslint"; 2 | import type { ParserOptions } from "@typescript-eslint/parser"; 3 | 4 | export const DEMO_FILE_NAME = "/demo.tsx"; 5 | 6 | export const EDITING_TIMEOUT = 300; 7 | 8 | export const TS_ESLINT_SCOPE = "@typescript-eslint"; 9 | 10 | export const BOOLEAN_ECMA_FEATURES = ["jsx", "globalReturn"] as const; 11 | 12 | export const ECMA_VERSIONS = [2015, 2016, 2017, 2018, 2019, 2020] as const; 13 | 14 | export const SOURCE_TYPES = ["module", "script"] as const; 15 | 16 | // https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#configuration 17 | export const DEFAULT_PARSER_OPTIONS: ParserOptions = { 18 | ecmaFeatures: { 19 | jsx: false, 20 | globalReturn: false, 21 | }, 22 | ecmaVersion: 2018, 23 | project: ["./tsconfig.json"], 24 | sourceType: "script", // https://eslint.org/docs/user-guide/configuring#specifying-parser-options 25 | }; 26 | 27 | export const DEFAULT_CODE = ` 28 | async function invalidInTryCatch1() { 29 | try { 30 | return Promise.resolve('try'); 31 | } catch (e) {} 32 | } 33 | `; 34 | 35 | export const DEFAULT_RULE_CONFIG: Linter.RulesRecord = { 36 | "@typescript-eslint/return-await": "error", 37 | }; 38 | -------------------------------------------------------------------------------- /src/lib/parser-options-converter.ts: -------------------------------------------------------------------------------- 1 | import type { ParserOptions } from "@typescript-eslint/types"; 2 | import type { AnalyzeOptions } from "@typescript-eslint/scope-manager"; 3 | import type { TSESTreeOptions } from "@typescript-eslint/typescript-estree"; 4 | import { CompilerOptions, ScriptTarget, JsxEmit, ModuleKind } from "typescript"; 5 | 6 | export function toAnalyzeOptions(parserOptions: ParserOptions): AnalyzeOptions { 7 | return { 8 | ecmaVersion: parserOptions.ecmaVersion, 9 | globalReturn: parserOptions.ecmaFeatures?.globalReturn ?? false, 10 | sourceType: parserOptions.sourceType ?? "script", 11 | }; 12 | } 13 | 14 | export function toTSESTOptions(parserOptions: ParserOptions): TSESTreeOptions { 15 | return Object.assign({}, parserOptions, { 16 | jsx: parserOptions.ecmaFeatures?.jsx ?? false, 17 | useJSXTextNode: true, 18 | projectFolderIgnoreList: [], 19 | }); 20 | } 21 | 22 | export function toCompilerOptions( 23 | parserOptions: ParserOptions 24 | ): CompilerOptions { 25 | return { 26 | noResolve: true, 27 | strict: true, 28 | target: ScriptTarget.Latest, 29 | jsx: parserOptions.ecmaFeatures?.jsx ? JsxEmit.Preserve : undefined, 30 | module: ModuleKind.ES2015, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/compiler-host.ts: -------------------------------------------------------------------------------- 1 | import { getDefaultLibFileName } from "typescript"; 2 | import type { 3 | CompilerHost as ICompilerHost, 4 | CompilerOptions, 5 | } from "typescript"; 6 | import type { Files } from "@/lib/files"; 7 | import type { SourceFiles } from "@/lib/source-files"; 8 | 9 | class CompilerHost implements ICompilerHost { 10 | constructor(private files: Files, private sourceFiles: SourceFiles) {} 11 | 12 | fileExists(name: string) { 13 | return !!this.files[name]; 14 | } 15 | getCanonicalFileName(name: string) { 16 | return name; 17 | } 18 | getCurrentDirectory() { 19 | return "/"; 20 | } 21 | getDirectories() { 22 | return []; 23 | } 24 | getDefaultLibFileName(options: CompilerOptions) { 25 | return "/" + getDefaultLibFileName(options); 26 | } 27 | getNewLine() { 28 | return "\n"; 29 | } 30 | useCaseSensitiveFileNames() { 31 | return true; 32 | } 33 | writeFile() { 34 | return null; 35 | } 36 | readFile(name: string) { 37 | return this.files[name]; 38 | } 39 | getSourceFile(name: string) { 40 | return this.sourceFiles[name]; 41 | } 42 | } 43 | 44 | export function createCompilerHost( 45 | files: Files, 46 | sourceFiles: SourceFiles 47 | ): CompilerHost { 48 | return new CompilerHost(files, sourceFiles); 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/create-ast-program.ts: -------------------------------------------------------------------------------- 1 | import { createProgram, ScriptKind } from "typescript"; 2 | import type { SourceFile, Program } from "typescript"; 3 | import { createCompilerHost } from "./compiler-host"; 4 | import { createFilesIncludeLibs } from "./files"; 5 | import { createSourceFilesIncludeLibs } from "./source-files"; 6 | import { toCompilerOptions } from "@/lib/parser-options-converter"; 7 | import { DEMO_FILE_NAME } from "@/constants"; 8 | import type { ParserOptions } from "@typescript-eslint/types"; 9 | 10 | interface ASTandProgram { 11 | ast?: SourceFile; 12 | program: Program; 13 | } 14 | 15 | export function createAstAndProgram( 16 | code: string, 17 | parserOptions: ParserOptions 18 | ): ASTandProgram { 19 | const files = createFilesIncludeLibs(DEMO_FILE_NAME, code); 20 | const sourceFiles = createSourceFilesIncludeLibs( 21 | DEMO_FILE_NAME, 22 | code, 23 | parserOptions?.ecmaFeatures?.jsx ? ScriptKind.TSX : ScriptKind.TS 24 | ); 25 | 26 | const compilierHost = createCompilerHost(files, sourceFiles); 27 | const compilerOptions = toCompilerOptions(parserOptions); 28 | compilerOptions.jsx; 29 | const program = createProgram( 30 | Object.keys(files), 31 | compilerOptions, 32 | compilierHost 33 | ); 34 | const ast = program.getSourceFile(DEMO_FILE_NAME); 35 | return { ast, program }; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BaseEditor } from "@/components/BaseEditor"; 3 | import * as states from "@/states"; 4 | import { useRecoilValueLoadable, useRecoilState } from "recoil"; 5 | import type { Marker } from "@/components/BaseEditor"; 6 | import type { FC } from "react"; 7 | import type { Linter } from "eslint"; 8 | 9 | function toMarkerPos(pos: number): number { 10 | return pos - 1; 11 | } 12 | 13 | function messageToMarker(message: Linter.LintMessage): Marker { 14 | const from = { 15 | line: toMarkerPos(message.line), 16 | ch: toMarkerPos(message.column), 17 | }; 18 | const to = { 19 | line: toMarkerPos(message.endLine || message.line), 20 | ch: toMarkerPos(message.endColumn || message.column), 21 | }; 22 | return [from, to]; 23 | } 24 | 25 | export const CodeEditor: FC = () => { 26 | const [code, setCode] = useRecoilState(states.codeState); 27 | const lintResultLoadable = useRecoilValueLoadable(states.lintResultState); 28 | 29 | let markers: Marker[] = []; 30 | if (lintResultLoadable.state === "hasValue") { 31 | markers = lintResultLoadable.contents.messages.map((message) => 32 | messageToMarker(message) 33 | ); 34 | } 35 | 36 | return ( 37 |
38 | 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /scripts/gen-libs.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import * as ts from "typescript"; 3 | import path from "path"; 4 | import { createMinifier } from "dts-minify"; 5 | 6 | (function generateLibFiles() { 7 | const ROOT = path.resolve(__dirname, "../"); 8 | const TS_LIB_PATH = path.resolve(ROOT, "node_modules/typescript/lib"); 9 | const OUTPUT_PATH = path.resolve(ROOT, "src/ts-libs"); 10 | const minifiler = createMinifier(ts); 11 | 12 | function isLibFile(fileName: string): boolean { 13 | return /^lib[\w\.]*\.(ts)$/.test(fileName); 14 | } 15 | 16 | function reolveToLibs(relPath: string): string { 17 | return path.resolve(TS_LIB_PATH, relPath); 18 | } 19 | 20 | function resolveToOutput(relPath: string): string { 21 | return path.resolve(OUTPUT_PATH, relPath); 22 | } 23 | 24 | function toProperty(fileName: string, text: string) { 25 | return `["${fileName}"]: \`${minifiler 26 | .minify(text) 27 | .replace(/\r?\n/g, "\n")}\`,\n`; 28 | } 29 | 30 | const libFiles = fs.readdirSync(TS_LIB_PATH).filter(isLibFile); 31 | const outPath = resolveToOutput("index.ts"); 32 | let propertiesCode = ""; 33 | 34 | libFiles.forEach((fileName) => { 35 | const srcPath = reolveToLibs(fileName); 36 | 37 | if (fs.existsSync(outPath)) { 38 | fs.unlinkSync(outPath); 39 | } 40 | propertiesCode += toProperty( 41 | `/${fileName}`, 42 | fs.readFileSync(srcPath).toString() 43 | ); 44 | }); 45 | 46 | const code = ` 47 | // Below code in module is copyright Microsoft and under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0). 48 | export default { 49 | ${propertiesCode} 50 | } as {[name: string]: string}; 51 | `; 52 | fs.writeFileSync(outPath, code); 53 | })(); 54 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 27 | 31 | TypeScript-ESLint Demo 32 | 48 | 49 | 50 | 51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /src/components/RuleConfig.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import CodeMirror from "codemirror"; 3 | import { EDITING_TIMEOUT } from "@/constants"; 4 | import { debounce } from "@/shared/debounce"; 5 | import JSON5 from "json5"; 6 | import * as states from "@/states"; 7 | import { useRecoilState } from "recoil"; 8 | import type { FC } from "react"; 9 | import "codemirror/lib/codemirror.css"; 10 | import "codemirror/mode/javascript/javascript.js"; 11 | import "@/css/editor.css"; 12 | 13 | const CODE_MIRROR_OPTIONS = { 14 | mode: "application/ld+json", 15 | lineNumbers: true, 16 | showCursorWhenSelecting: true, 17 | matchBrackets: true, 18 | } as const; 19 | 20 | export const RuleConfig: FC = () => { 21 | const [rulesConfig, setRulesConfig] = useRecoilState(states.rulesConfigState); 22 | const [text, setText] = useState( 23 | JSON5.stringify(rulesConfig.rules, null, 2) 24 | ); 25 | const ref = useRef(null); 26 | 27 | useEffect(() => { 28 | if (ref.current) { 29 | const codeMirror = CodeMirror.fromTextArea( 30 | ref.current, 31 | CODE_MIRROR_OPTIONS 32 | ); 33 | codeMirror.on( 34 | "change", 35 | debounce(() => { 36 | const value = codeMirror.getValue(); 37 | try { 38 | const rules = JSON5.parse(value); 39 | setRulesConfig({ 40 | rules, 41 | error: null, 42 | }); 43 | } catch (error) { 44 | setRulesConfig({ 45 | ...rulesConfig, 46 | error, 47 | }); 48 | } 49 | 50 | setText(value); 51 | }, EDITING_TIMEOUT) 52 | ); 53 | } 54 | }, []); 55 | 56 | return ( 57 |
58 |