@san-siva/blogkit-md 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc.json +9 -0
- package/LICENSE +21 -0
- package/README.md +236 -0
- package/app/layout.tsx +44 -0
- package/app/page.tsx +27 -0
- package/components/BlogPost.tsx +54 -0
- package/eslint/common.config.ts +29 -0
- package/eslint/react.config.ts +20 -0
- package/eslint/utilities.ts +236 -0
- package/eslint.config.ts +8 -0
- package/hooks/readMarkdownFile.ts +44 -0
- package/index.ts +1 -0
- package/next.config.ts +5 -0
- package/package.json +54 -0
- package/scripts/dev.ts +50 -0
- package/tsconfig.json +29 -0
- package/utils/extractText.ts +11 -0
- package/utils/groupSections.ts +114 -0
- package/utils/parseMarkdown.ts +9 -0
- package/utils/renderMarkdown.tsx +163 -0
- package/utils/renderPhrasingContent.tsx +98 -0
- package/website/app/data.ts +46 -0
- package/website/app/layout.tsx +74 -0
- package/website/app/page.tsx +30 -0
- package/website/next.config.ts +9 -0
- package/website/package-lock.json +3273 -0
- package/website/package.json +25 -0
- package/website/tsconfig.json +30 -0
package/.prettierrc.json
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Santhosh Siva
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# blogkit-md
|
|
2
|
+
|
|
3
|
+
A Next.js tool that converts standard markdown files into rendered blog posts for [`@san-siva/blogkit`](https://blogkit.santhoshsiva.dev).
|
|
4
|
+
|
|
5
|
+
> Note: [This](https://blogkit-md.santhoshsiva.dev) blog post is translated from the `README.md` file in the [`blogkit-md`](https://github.com/san-siva/blogkit-md) repository.
|
|
6
|
+
|
|
7
|
+
## Getting started
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/san-siva/blogkit-md.git
|
|
11
|
+
npm install
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Dev server
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm run dev # uses data/test.md by default
|
|
18
|
+
npm run dev -- --file=data/my-post.md # specify a markdown file
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The dev server watches the markdown file for changes and auto-reloads the browser via HMR.
|
|
22
|
+
|
|
23
|
+
### Build
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm run build
|
|
27
|
+
npm run start
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Lint
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm run lint # check
|
|
34
|
+
npm run fix # auto-fix
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Using BlogPost in your own Next.js app
|
|
38
|
+
|
|
39
|
+
`blogkit-md` exposes a `BlogPost` server component you can drop into any Next.js project.
|
|
40
|
+
|
|
41
|
+
### Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install @san-siva/blogkit-md
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or as a local package:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
"@san-siva/blogkit-md": "file:../blogkit-md"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Add it to `transpilePackages` in your `next.config.ts`:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const nextConfig = {
|
|
57
|
+
transpilePackages: ['@san-siva/blogkit-md'],
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Usage
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { BlogPost } from '@san-siva/blogkit-md';
|
|
65
|
+
|
|
66
|
+
export default function Page() {
|
|
67
|
+
return (
|
|
68
|
+
<BlogPost
|
|
69
|
+
filePath="content/my-post.md"
|
|
70
|
+
jsonLd={{
|
|
71
|
+
'@context': 'https://schema.org',
|
|
72
|
+
'@type': 'BlogPosting',
|
|
73
|
+
headline: 'My Post',
|
|
74
|
+
description: 'Post description',
|
|
75
|
+
datePublished: '2026-01-01',
|
|
76
|
+
author: { '@type': 'Person', name: 'Your Name' },
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Props
|
|
84
|
+
|
|
85
|
+
| Prop | Type | Required | Description |
|
|
86
|
+
| :--------- | :------------------- | :------: | :--------------------------------------------------------------------------- |
|
|
87
|
+
| `filePath` | `string` | Yes | Path to the markdown file. Relative paths are resolved from `process.cwd()`. |
|
|
88
|
+
| `jsonLd` | `WithContext<Thing>` | No | Optional JSON-LD schema passed to `<Blog>` for structured data / SEO. |
|
|
89
|
+
|
|
90
|
+
## Supported markdown features
|
|
91
|
+
|
|
92
|
+
| Feature | Syntax |
|
|
93
|
+
| --------------- | ---------------------------------- |
|
|
94
|
+
| Headings | `# H1` `## H2` `### H3` `#### H4` |
|
|
95
|
+
| Paragraph | Plain text |
|
|
96
|
+
| Hard line break | Two spaces at end of line |
|
|
97
|
+
| Bold | `**bold**` |
|
|
98
|
+
| Italic | `_italic_` |
|
|
99
|
+
| Inline code | `` `code` `` |
|
|
100
|
+
| Link | `[text](url)` |
|
|
101
|
+
| Image | `` |
|
|
102
|
+
| Ordered list | `1. item` |
|
|
103
|
+
| Unordered list | `- item` |
|
|
104
|
+
| Table | GFM table syntax |
|
|
105
|
+
| Code block | ` ```lang ` |
|
|
106
|
+
| Mermaid diagram | ` ```mermaid ` |
|
|
107
|
+
| Thematic break | `---` |
|
|
108
|
+
| Blockquote | `> text` — renders as info callout |
|
|
109
|
+
|
|
110
|
+
## Philosophy
|
|
111
|
+
|
|
112
|
+
### Not Your Average Markdown Viewer
|
|
113
|
+
|
|
114
|
+
If you're looking for a strictly standard, 1:1 markdown renderer, `blogkit-md` might not be what you expect.
|
|
115
|
+
|
|
116
|
+
<mark>Instead of building just another plain document viewer, intentional design liberties have been taken to render markdown as **beautiful, engaging blog posts**.</mark>
|
|
117
|
+
|
|
118
|
+
Documentation shouldn't be a wall of boring text. The goal of this tool is to make reading technical docs, articles, and guides an exciting and visually pleasing experience.
|
|
119
|
+
|
|
120
|
+
### Key Differences
|
|
121
|
+
|
|
122
|
+
| | blogkit-md | Plain markdown renderer |
|
|
123
|
+
| -------------- | -------------------------------------------------------- | ----------------------- |
|
|
124
|
+
| **Output** | Styled blog post | Raw document |
|
|
125
|
+
| **Typography** | Optimized for long-form reading | Unstyled |
|
|
126
|
+
| **Ecosystem** | Built for [Blogkit](https://github.com/san-siva/blogkit) | Generic |
|
|
127
|
+
|
|
128
|
+
## Architecture
|
|
129
|
+
|
|
130
|
+
The markdown file is parsed into an AST using `remark-parse` + `remark-gfm`, then transformed into React components from `@san-siva/blogkit`.
|
|
131
|
+
|
|
132
|
+
```mermaid
|
|
133
|
+
flowchart LR
|
|
134
|
+
A[Markdown file] --> B[Parse AST]
|
|
135
|
+
B --> C[Group sections]
|
|
136
|
+
C --> D[Render to React]
|
|
137
|
+
D --> E[Blog page]
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## How Markdown Translates to Blog Sections
|
|
141
|
+
|
|
142
|
+
### Headings as Layout Triggers
|
|
143
|
+
|
|
144
|
+
In `blogkit-md`, headings aren't just for changing font sizes—**they are the architectural blueprint for your post**. Each heading level acts as a layout trigger, directly controlling how `BlogSection` components are generated, nested, or promoted.
|
|
145
|
+
|
|
146
|
+
| Markdown | Layout Behavior |
|
|
147
|
+
| :----------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
148
|
+
| `# H1` | **Page Title.** Sets the main article title. Does not generate a structural section block. |
|
|
149
|
+
| `## H2` | **Main Section.** Creates a new, top-level `BlogSection`. |
|
|
150
|
+
| `### H3` | **Subsection.** Nests cleanly within the currently active `H2` section. |
|
|
151
|
+
| `#### H4` | **Section Break.** Renders as a bold line, but acts as a layout trigger: it forces the _next_ `H3` to break out and become a brand-new, top-level section. |
|
|
152
|
+
| `##### H5` & `###### H6` | **Inline Emphasis.** Renders as a bold line within the current section or subsection without altering the page layout. |
|
|
153
|
+
|
|
154
|
+
> Standard content—such as paragraphs, lists, and code blocks—automatically flows into the most recently opened section or subsection.
|
|
155
|
+
|
|
156
|
+
### Special Layout Rules
|
|
157
|
+
|
|
158
|
+
Because `blogkit-md` is optimized for blog readability, it includes smart fallbacks to ensure your layout looks great even in edge cases:
|
|
159
|
+
|
|
160
|
+
| Rule | Behavior |
|
|
161
|
+
| ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
162
|
+
| Strictly Deepening Hierarchy | Headings within a section must always go deeper (e.g., `H2 → H3 → H4`). If the hierarchy reverses—like an `H3` appearing after an `H4`—the nesting breaks, and the `H3` is promoted to a brand-new top-level section. |
|
|
163
|
+
| `H1` loses structural significance if not at the top | If an `H1` appears anywhere other than the very top of the document, it does not create a page title. Instead, it is treated as stylized text and rendered as a section break. |
|
|
164
|
+
| Intro section | Any text written before the first heading—or directly beneath the `# H1` page title—is automatically grouped into an untitled, top-level BlogSection |
|
|
165
|
+
|
|
166
|
+
### Visualizing the Structure
|
|
167
|
+
|
|
168
|
+
Let's put those layout rules into practice. Here is how a standard markdown document translates into a blog layout:
|
|
169
|
+
|
|
170
|
+
```markdown
|
|
171
|
+
# My Awesome Blog Post
|
|
172
|
+
|
|
173
|
+
This text becomes the Preamble (an untitled, top-level section).
|
|
174
|
+
|
|
175
|
+
## The Setup
|
|
176
|
+
|
|
177
|
+
Some content goes here.
|
|
178
|
+
|
|
179
|
+
### Prerequisites
|
|
180
|
+
|
|
181
|
+
Nested content belongs here.
|
|
182
|
+
|
|
183
|
+
## The Execution
|
|
184
|
+
|
|
185
|
+
#### Note on performance:
|
|
186
|
+
|
|
187
|
+
### The Results
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Here is how the parser breaks the above document down into isolated React components:
|
|
191
|
+
|
|
192
|
+
##### Intro section
|
|
193
|
+
|
|
194
|
+
```markdown
|
|
195
|
+
This text becomes an introductory, untitled section.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
##### Section 1
|
|
199
|
+
|
|
200
|
+
```markdown
|
|
201
|
+
## The Setup
|
|
202
|
+
|
|
203
|
+
Some content goes here.
|
|
204
|
+
|
|
205
|
+
#### Prerequisites
|
|
206
|
+
|
|
207
|
+
Nested content belongs here.
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
##### Section 2
|
|
211
|
+
|
|
212
|
+
```markdown
|
|
213
|
+
## The Execution
|
|
214
|
+
|
|
215
|
+
#### Note on performance:
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
##### Section 3
|
|
219
|
+
|
|
220
|
+
```markdown
|
|
221
|
+
## The Results
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Want more customization?
|
|
225
|
+
|
|
226
|
+
`blogkit-md` is just one piece of the puzzle. If you want to customize the underlying React components, tweak the UI, or take full control over your blog's layout, dive into the official [Blogkit documentation](https://blogkit.santhoshsiva.dev/).
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
`blogkit-md` is open source software licensed under the [MIT license](https://github.com/san-siva/blogkit-md/blob/main/LICENSE).
|
|
231
|
+
Contributions are welcome!
|
|
232
|
+
|
|
233
|
+
## About
|
|
234
|
+
|
|
235
|
+
- **Author:** [Santhosh Siva](https://www.santhoshsiva.dev)
|
|
236
|
+
- **License:** [MIT](https://github.com/san-siva/blogkit-md/blob/main/LICENSE)
|
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import { JetBrains_Mono, Montserrat, Rubik } from 'next/font/google';
|
|
3
|
+
|
|
4
|
+
import '@san-siva/stylekit/styles/globals.scss';
|
|
5
|
+
import '@san-siva/blogkit/styles.css';
|
|
6
|
+
import styles from '@san-siva/stylekit/styles/index.module.scss';
|
|
7
|
+
|
|
8
|
+
const montserrat = Montserrat({
|
|
9
|
+
subsets: ['latin'] as const,
|
|
10
|
+
weight: ['400', '500', '600', '700', '800'] as const,
|
|
11
|
+
style: ['normal', 'italic'] as const,
|
|
12
|
+
variable: '--font-montserrat',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const rubik = Rubik({
|
|
16
|
+
subsets: ['latin'] as const,
|
|
17
|
+
weight: ['300', '400', '500', '600', '700', '800', '900'] as const,
|
|
18
|
+
style: ['normal', 'italic'] as const,
|
|
19
|
+
variable: '--font-rubik',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const jetbrainsMono = JetBrains_Mono({
|
|
23
|
+
subsets: ['latin'] as const,
|
|
24
|
+
weight: ['400', '500', '600', '700'] as const,
|
|
25
|
+
style: ['normal', 'italic'] as const,
|
|
26
|
+
variable: '--font-jetbrains-mono',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const metadata: Metadata = {
|
|
30
|
+
title: 'Blogkit MD',
|
|
31
|
+
description: 'Markdown to JSX blog post renderer',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const RootLayout = ({ children }: { children: React.ReactNode }) => (
|
|
35
|
+
<html lang="en">
|
|
36
|
+
<body
|
|
37
|
+
className={`${montserrat.variable} ${rubik.variable} ${jetbrainsMono.variable}`}
|
|
38
|
+
>
|
|
39
|
+
<div className={`${styles.page}`}>{children}</div>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export default RootLayout;
|
package/app/page.tsx
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Blog, BlogHeader, Callout } from '@san-siva/blogkit';
|
|
2
|
+
|
|
3
|
+
import { readMarkdownFile } from '@/hooks/readMarkdownFile';
|
|
4
|
+
import { MarkdownSections } from '@/utils/renderMarkdown';
|
|
5
|
+
|
|
6
|
+
const Page = async () => {
|
|
7
|
+
const result = await readMarkdownFile(process.env.MARKDOWN_FILE);
|
|
8
|
+
|
|
9
|
+
if (!result.success) {
|
|
10
|
+
return (
|
|
11
|
+
<Blog>
|
|
12
|
+
<Callout type="warning">{result.error}</Callout>
|
|
13
|
+
</Blog>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { rendered } = result;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Blog>
|
|
21
|
+
{rendered.pageTitle && <BlogHeader title={[rendered.pageTitle]} desc={[]} />}
|
|
22
|
+
<MarkdownSections rendered={rendered} />
|
|
23
|
+
</Blog>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default Page;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Blog, BlogHeader, Callout } from '@san-siva/blogkit';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type { Thing, WithContext } from 'schema-dts';
|
|
5
|
+
|
|
6
|
+
import { parseMarkdown } from '../utils/parseMarkdown';
|
|
7
|
+
import { MarkdownSections, renderMarkdownAst } from '../utils/renderMarkdown';
|
|
8
|
+
|
|
9
|
+
type BlogPostProperties = {
|
|
10
|
+
filePath: string;
|
|
11
|
+
jsonLd?: WithContext<Thing>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const BlogPost = async ({ filePath, jsonLd }: BlogPostProperties) => {
|
|
15
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
16
|
+
? filePath
|
|
17
|
+
: path.join(process.cwd(), filePath);
|
|
18
|
+
|
|
19
|
+
let content: string;
|
|
20
|
+
try {
|
|
21
|
+
content = await readFile(absolutePath, 'utf8');
|
|
22
|
+
} catch {
|
|
23
|
+
return (
|
|
24
|
+
<Blog>
|
|
25
|
+
<Callout type="warning">
|
|
26
|
+
Could not read file: "{filePath}". Make sure the path is
|
|
27
|
+
correct and the file exists.
|
|
28
|
+
</Callout>
|
|
29
|
+
</Blog>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!content.trim()) {
|
|
34
|
+
return (
|
|
35
|
+
<Blog>
|
|
36
|
+
<Callout type="warning">File "{filePath}" is empty.</Callout>
|
|
37
|
+
</Blog>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ast = parseMarkdown(content);
|
|
42
|
+
const rendered = renderMarkdownAst(ast);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Blog jsonLd={jsonLd}>
|
|
46
|
+
{rendered.pageTitle && (
|
|
47
|
+
<BlogHeader title={[rendered.pageTitle]} desc={[]} />
|
|
48
|
+
)}
|
|
49
|
+
<MarkdownSections rendered={rendered} />
|
|
50
|
+
</Blog>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default BlogPost;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import tseslint, { configs as tseslintConfigs } from 'typescript-eslint';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
defaultExtends,
|
|
5
|
+
defaultPlugins,
|
|
6
|
+
defaultRules,
|
|
7
|
+
defaultSettings,
|
|
8
|
+
languageOptions,
|
|
9
|
+
testFiles,
|
|
10
|
+
} from './utilities';
|
|
11
|
+
|
|
12
|
+
export default tseslint.config(
|
|
13
|
+
{
|
|
14
|
+
ignores: ['node_modules/**', 'dist/**', '.next/**', 'next-env.d.ts'],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
files: ['**/*.ts', '**/*.js'],
|
|
18
|
+
ignores: testFiles,
|
|
19
|
+
plugins: defaultPlugins,
|
|
20
|
+
extends: defaultExtends,
|
|
21
|
+
rules: defaultRules,
|
|
22
|
+
settings: defaultSettings,
|
|
23
|
+
languageOptions,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
files: ['eslint.config.ts', 'eslint/**/*.ts'],
|
|
27
|
+
extends: [tseslintConfigs.disableTypeChecked],
|
|
28
|
+
}
|
|
29
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import tseslint from 'typescript-eslint';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
languageOptions,
|
|
5
|
+
reactExtends,
|
|
6
|
+
reactPlugins,
|
|
7
|
+
reactRules,
|
|
8
|
+
reactSettings,
|
|
9
|
+
testFiles,
|
|
10
|
+
} from './utilities';
|
|
11
|
+
|
|
12
|
+
export default tseslint.config({
|
|
13
|
+
files: ['**/*.tsx', '**/*.jsx'],
|
|
14
|
+
ignores: testFiles,
|
|
15
|
+
plugins: reactPlugins,
|
|
16
|
+
extends: reactExtends,
|
|
17
|
+
rules: reactRules,
|
|
18
|
+
settings: reactSettings,
|
|
19
|
+
languageOptions,
|
|
20
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import eslint from '@eslint/js';
|
|
2
|
+
import type { Linter } from 'eslint';
|
|
3
|
+
import eslintConfigPrettier from 'eslint-config-prettier';
|
|
4
|
+
import importPlugin from 'eslint-plugin-import';
|
|
5
|
+
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
|
6
|
+
import { configs as noAwaitInPromiseConfigs } from 'eslint-plugin-no-await-in-promise';
|
|
7
|
+
import react from 'eslint-plugin-react';
|
|
8
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
9
|
+
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
|
10
|
+
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
|
|
11
|
+
import { configs as tseslintConfigs } from 'typescript-eslint';
|
|
12
|
+
|
|
13
|
+
export const defaultExtends = [
|
|
14
|
+
eslint.configs.recommended,
|
|
15
|
+
eslintPluginUnicorn.configs.recommended,
|
|
16
|
+
eslintConfigPrettier,
|
|
17
|
+
noAwaitInPromiseConfigs.recommended,
|
|
18
|
+
...tseslintConfigs.recommended,
|
|
19
|
+
importPlugin.flatConfigs.recommended,
|
|
20
|
+
importPlugin.flatConfigs.typescript,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export const defaultPlugins = {
|
|
24
|
+
'simple-import-sort': simpleImportSort,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const tsRules: Linter.RulesRecord = {
|
|
28
|
+
'@typescript-eslint/no-explicit-any': 1,
|
|
29
|
+
'@typescript-eslint/no-floating-promises': 2,
|
|
30
|
+
'@typescript-eslint/no-unused-vars': 1,
|
|
31
|
+
'@typescript-eslint/naming-convention': [
|
|
32
|
+
1,
|
|
33
|
+
{
|
|
34
|
+
selector: 'import',
|
|
35
|
+
format: ['camelCase', 'PascalCase', 'snake_case', 'UPPER_CASE'],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
selector: 'enumMember',
|
|
39
|
+
format: ['PascalCase'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
selector: 'default',
|
|
43
|
+
format: ['camelCase'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
selector: 'variable',
|
|
47
|
+
format: ['camelCase', 'UPPER_CASE', 'PascalCase'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
selector: 'variable',
|
|
51
|
+
modifiers: ['destructured'],
|
|
52
|
+
format: ['camelCase', 'snake_case', 'UPPER_CASE', 'PascalCase'],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
selector: 'function',
|
|
56
|
+
format: ['camelCase', 'PascalCase'],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
selector: 'parameter',
|
|
60
|
+
format: [],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
selector: 'memberLike',
|
|
64
|
+
modifiers: ['private'],
|
|
65
|
+
format: ['camelCase'],
|
|
66
|
+
leadingUnderscore: 'require',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
selector: 'typeLike',
|
|
70
|
+
format: ['PascalCase'],
|
|
71
|
+
},
|
|
72
|
+
{ selector: 'property', format: [] },
|
|
73
|
+
{
|
|
74
|
+
selector: 'method',
|
|
75
|
+
format: ['PascalCase', 'camelCase', 'UPPER_CASE'],
|
|
76
|
+
leadingUnderscore: 'allowDouble',
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
'@typescript-eslint/no-use-before-define': 2,
|
|
80
|
+
'@typescript-eslint/no-unused-expressions': [
|
|
81
|
+
'error',
|
|
82
|
+
{
|
|
83
|
+
allowShortCircuit: true,
|
|
84
|
+
allowTernary: true,
|
|
85
|
+
allowTaggedTemplates: true,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const defaultRules: Linter.RulesRecord = {
|
|
91
|
+
...tsRules,
|
|
92
|
+
'unicorn/filename-case': [
|
|
93
|
+
'error',
|
|
94
|
+
{
|
|
95
|
+
cases: {
|
|
96
|
+
camelCase: true,
|
|
97
|
+
kebabCase: true,
|
|
98
|
+
pascalCase: true,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
'unicorn/prevent-abbreviations': [1, { allowList: { dev: true } }],
|
|
103
|
+
'unicorn/no-array-reduce': [1],
|
|
104
|
+
'unicorn/catch-error-name': [0],
|
|
105
|
+
'unicorn/no-for-loop': [1],
|
|
106
|
+
'unicorn/no-null': [0],
|
|
107
|
+
'unicorn/prefer-string-raw': [1],
|
|
108
|
+
'unicorn/prefer-string-slice': [1],
|
|
109
|
+
'unicorn/no-negated-condition': [1],
|
|
110
|
+
'unicorn/no-array-for-each': [0],
|
|
111
|
+
'unicorn/no-array-callback-reference': [0],
|
|
112
|
+
'unicorn/no-object-as-default-parameter': [1],
|
|
113
|
+
'unicorn/prefer-math-min-max': [1],
|
|
114
|
+
'unicorn/no-process-exit': [1],
|
|
115
|
+
'unicorn/prefer-top-level-await': [1],
|
|
116
|
+
'unicorn/prefer-number-properties': [1],
|
|
117
|
+
'unicorn/consistent-function-scoping': [1],
|
|
118
|
+
'arrow-body-style': [1, 'as-needed'],
|
|
119
|
+
camelcase: 0,
|
|
120
|
+
'default-param-last': 0,
|
|
121
|
+
'import/order': 0,
|
|
122
|
+
'simple-import-sort/imports': [
|
|
123
|
+
1,
|
|
124
|
+
{
|
|
125
|
+
groups: [
|
|
126
|
+
['^react$', '^react-.*'],
|
|
127
|
+
['next', 'next-.*'],
|
|
128
|
+
[String.raw`^@?\w`],
|
|
129
|
+
[String.raw`^\.\.(?!/?$)`, String.raw`^\.\./?$`],
|
|
130
|
+
[
|
|
131
|
+
String.raw`^\./(?=.*/)(?!/?$)`,
|
|
132
|
+
String.raw`^\.(?!/?$)`,
|
|
133
|
+
String.raw`^\./?$`,
|
|
134
|
+
],
|
|
135
|
+
[String.raw`^.+\.s?css$`],
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
'simple-import-sort/exports': 1,
|
|
140
|
+
'import/no-extraneous-dependencies': 0,
|
|
141
|
+
'import/extensions': 0,
|
|
142
|
+
'import/no-cycle': [2, { ignoreExternal: true }],
|
|
143
|
+
'import/no-unresolved': 0,
|
|
144
|
+
'import/prefer-default-export': 0,
|
|
145
|
+
'max-len': 0,
|
|
146
|
+
'no-console': 0,
|
|
147
|
+
'no-delete-var': 2,
|
|
148
|
+
'no-shadow': 0,
|
|
149
|
+
'no-underscore-dangle': [2, { allow: ['_id'] }],
|
|
150
|
+
'no-unused-expressions': 2,
|
|
151
|
+
'no-unused-labels': 2,
|
|
152
|
+
'no-use-before-define': 0,
|
|
153
|
+
semi: 1,
|
|
154
|
+
quotes: [
|
|
155
|
+
1,
|
|
156
|
+
'single',
|
|
157
|
+
{
|
|
158
|
+
avoidEscape: true,
|
|
159
|
+
allowTemplateLiterals: true,
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const languageOptions = {
|
|
165
|
+
ecmaVersion: 2022 as const,
|
|
166
|
+
parserOptions: {
|
|
167
|
+
projectService: true,
|
|
168
|
+
defaultProject: '../tsconfig.json',
|
|
169
|
+
tsconfigRootDir: import.meta.dirname,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const testFiles = [
|
|
174
|
+
'**/*.test.ts',
|
|
175
|
+
'**/*.test.tsx',
|
|
176
|
+
'**/*.test.js',
|
|
177
|
+
'**/*.test.jsx',
|
|
178
|
+
'**/*.spec.ts',
|
|
179
|
+
'**/*.spec.tsx',
|
|
180
|
+
'**/*.spec.js',
|
|
181
|
+
'**/*.spec.jsx',
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
export const defaultSettings = {
|
|
185
|
+
'import/resolver': {
|
|
186
|
+
typescript: {
|
|
187
|
+
project: '../tsconfig.json',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const reactPlugins = {
|
|
193
|
+
...defaultPlugins,
|
|
194
|
+
react,
|
|
195
|
+
'react-hooks': reactHooks,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const reactSettings = {
|
|
199
|
+
...defaultSettings,
|
|
200
|
+
react: {
|
|
201
|
+
version: 'detect',
|
|
202
|
+
pragma: 'React',
|
|
203
|
+
fragment: 'Fragment',
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const reactRules: Linter.RulesRecord = {
|
|
208
|
+
...defaultRules,
|
|
209
|
+
'react/jsx-uses-react': 0,
|
|
210
|
+
'react/react-in-jsx-scope': 0,
|
|
211
|
+
'react-hooks/rules-of-hooks': 2,
|
|
212
|
+
'react-hooks/exhaustive-deps': 1,
|
|
213
|
+
'eol-last': ['error', 'always'],
|
|
214
|
+
'jsx-a11y/alt-text': 0,
|
|
215
|
+
'jsx-a11y/click-events-have-key-events': 0,
|
|
216
|
+
'jsx-a11y/control-has-associated-label': 0,
|
|
217
|
+
'jsx-a11y/no-noninteractive-element-interactions': 0,
|
|
218
|
+
'jsx-a11y/no-static-element-interactions': 0,
|
|
219
|
+
'jsx-a11y/label-has-associated-control': 0,
|
|
220
|
+
'react/jsx-filename-extension': [1, { extensions: ['.tsx'] }],
|
|
221
|
+
'react/jsx-props-no-spreading': 0,
|
|
222
|
+
'react/no-unstable-nested-components': ['error', { allowAsProps: true }],
|
|
223
|
+
'react/prop-types': 0,
|
|
224
|
+
'react/require-default-props': 0,
|
|
225
|
+
'react/function-component-definition': [
|
|
226
|
+
2,
|
|
227
|
+
{ namedComponents: 'arrow-function' },
|
|
228
|
+
],
|
|
229
|
+
'react/no-unknown-property': 1,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export const reactExtends = [
|
|
233
|
+
react.configs.flat.recommended,
|
|
234
|
+
jsxA11y.flatConfigs?.recommended,
|
|
235
|
+
...defaultExtends,
|
|
236
|
+
];
|