@beforesemicolon/builder 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.eslintignore +1 -0
- package/.eslintrc.cjs +13 -0
- package/.prettierignore +2 -0
- package/.prettierrc +6 -0
- package/package.json +65 -0
- package/src/build-browser.ts +35 -0
- package/src/build-modules.ts +53 -0
- package/src/docs/renderer/code.ts +15 -0
- package/src/docs/renderer/heading.ts +13 -0
- package/src/docs/renderer/index.ts +10 -0
- package/src/docs/renderer/link.ts +7 -0
- package/src/docs/run.ts +212 -0
- package/src/docs/templates/default.ts +19 -0
- package/src/docs/types.ts +16 -0
- package/tsconfig.json +24 -0
package/.eslintignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
src/**/*.spec.ts
|
package/.eslintrc.cjs
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
module.exports = {
|
2
|
+
extends: [
|
3
|
+
'eslint:recommended',
|
4
|
+
'plugin:@typescript-eslint/recommended',
|
5
|
+
'prettier',
|
6
|
+
],
|
7
|
+
parser: '@typescript-eslint/parser',
|
8
|
+
plugins: ['@typescript-eslint'],
|
9
|
+
root: true,
|
10
|
+
rules: {
|
11
|
+
'no-prototype-builtins': ['off'],
|
12
|
+
},
|
13
|
+
}
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
package/package.json
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
{
|
2
|
+
"name": "@beforesemicolon/builder",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "Utilities to build npm packages and documentation website",
|
5
|
+
"engines": {
|
6
|
+
"node": ">=18.16.0"
|
7
|
+
},
|
8
|
+
"type": "module",
|
9
|
+
"scripts": {
|
10
|
+
"build": "rm -rf dist && npm-run-all lint && tsc",
|
11
|
+
"lint": "eslint ./src && prettier --check .",
|
12
|
+
"format": "eslint ./src --fix && prettier --write ."
|
13
|
+
},
|
14
|
+
"author": "Elson Correia",
|
15
|
+
"license": "BSD-3-Clause",
|
16
|
+
"repository": {
|
17
|
+
"url": "https://github.com/beforesemicolon/builder",
|
18
|
+
"type": "git"
|
19
|
+
},
|
20
|
+
"keywords": [
|
21
|
+
"builder",
|
22
|
+
"build npm",
|
23
|
+
"docs"
|
24
|
+
],
|
25
|
+
"funding": {
|
26
|
+
"url": "https://github.com/sponsors/beforesemicolon",
|
27
|
+
"type": "github"
|
28
|
+
},
|
29
|
+
"devDependencies": {
|
30
|
+
"@gjsify/esbuild-plugin-transform-ext": "0.0.4",
|
31
|
+
"@types/jest": "^29.5.11",
|
32
|
+
"@types/jsdom": "^21.1.6",
|
33
|
+
"@types/jsdom-global": "^3.0.7",
|
34
|
+
"@types/node": "^20.10.5",
|
35
|
+
"@types/node-sass": "^4.11.7",
|
36
|
+
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
37
|
+
"@typescript-eslint/parser": "^6.15.0",
|
38
|
+
"core-js": "^3.34.0",
|
39
|
+
"esbuild": "^0.19.10",
|
40
|
+
"esbuild-plugin-text-replace": "^1.3.0",
|
41
|
+
"eslint": "^8.56.0",
|
42
|
+
"eslint-config-prettier": "^9.1.0",
|
43
|
+
"eslint-config-standard": "^17.1.0",
|
44
|
+
"eslint-plugin-import": "^2.29.1",
|
45
|
+
"eslint-plugin-n": "^16.5.0",
|
46
|
+
"eslint-plugin-prettier": "^5.1.2",
|
47
|
+
"eslint-plugin-promise": "^6.1.1",
|
48
|
+
"front-matter": "^4.0.2",
|
49
|
+
"global-jsdom": "^9.2.0",
|
50
|
+
"highlight.js": "^11.10.0",
|
51
|
+
"install": "^0.13.0",
|
52
|
+
"isomorphic-dompurify": "^2.16.0",
|
53
|
+
"jsdom": "^23.0.1",
|
54
|
+
"marked": "^14.1.2",
|
55
|
+
"marked-highlight": "^2.1.4",
|
56
|
+
"nodemon": "^3.1.7",
|
57
|
+
"npm": "^10.8.3",
|
58
|
+
"npm-run-all": "^4.1.5",
|
59
|
+
"prettier": "3.1.1",
|
60
|
+
"tinybench": "^2.8.0",
|
61
|
+
"ts-jest": "^29.1.1",
|
62
|
+
"tsx": "^4.7.0",
|
63
|
+
"typescript": "^5.3.3"
|
64
|
+
}
|
65
|
+
}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import esbuild, { Plugin } from 'esbuild'
|
2
|
+
|
3
|
+
// the Doc exported with @beforesemicolon/html-parser is not used so no need
|
4
|
+
// to include it in the final package
|
5
|
+
const emptyParserDoc = {
|
6
|
+
name: 'empty-parser-Doc-plugin',
|
7
|
+
setup(build) {
|
8
|
+
build.onResolve({ filter: /Doc$/ }, (args) => {
|
9
|
+
if (args.importer.includes('@beforesemicolon/html-parser')) {
|
10
|
+
return {
|
11
|
+
path: args.path,
|
12
|
+
namespace: 'empty-Doc',
|
13
|
+
}
|
14
|
+
}
|
15
|
+
})
|
16
|
+
build.onLoad({ filter: /.*/, namespace: 'empty-Doc' }, () => {
|
17
|
+
return {
|
18
|
+
contents: '',
|
19
|
+
}
|
20
|
+
})
|
21
|
+
},
|
22
|
+
} as Plugin
|
23
|
+
|
24
|
+
esbuild
|
25
|
+
.build({
|
26
|
+
entryPoints: ['src/client.ts'],
|
27
|
+
outfile: 'dist/client.js',
|
28
|
+
bundle: true,
|
29
|
+
keepNames: true,
|
30
|
+
sourcemap: true,
|
31
|
+
target: 'esnext',
|
32
|
+
minify: true,
|
33
|
+
plugins: [emptyParserDoc],
|
34
|
+
})
|
35
|
+
.catch(console.error)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import esbuild from 'esbuild'
|
2
|
+
import path from 'path'
|
3
|
+
import fs from 'fs'
|
4
|
+
import { transformExtPlugin } from '@gjsify/esbuild-plugin-transform-ext'
|
5
|
+
|
6
|
+
function readFilesRecursively(directoryPath: string) {
|
7
|
+
const files: string[] = []
|
8
|
+
|
9
|
+
function readDirectory(currentPath: string) {
|
10
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
11
|
+
|
12
|
+
for (const entry of entries) {
|
13
|
+
const fullPath = path.join(currentPath, entry.name)
|
14
|
+
|
15
|
+
if (entry.isDirectory()) {
|
16
|
+
readDirectory(fullPath)
|
17
|
+
} else {
|
18
|
+
files.push(fullPath)
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
readDirectory(directoryPath)
|
24
|
+
return files
|
25
|
+
}
|
26
|
+
|
27
|
+
const directoryPath = path.join(process.cwd(), 'src')
|
28
|
+
const allFiles = readFilesRecursively(directoryPath).filter(
|
29
|
+
(file) => !file.endsWith('.spec.ts') && !file.endsWith('/client.ts')
|
30
|
+
)
|
31
|
+
|
32
|
+
Promise.all([
|
33
|
+
esbuild.build({
|
34
|
+
entryPoints: allFiles,
|
35
|
+
outdir: 'dist/esm',
|
36
|
+
target: 'esnext',
|
37
|
+
minify: true,
|
38
|
+
format: 'esm',
|
39
|
+
platform: 'node',
|
40
|
+
keepNames: true,
|
41
|
+
plugins: [transformExtPlugin({ outExtension: { '.ts': '.js' } })],
|
42
|
+
}),
|
43
|
+
esbuild.build({
|
44
|
+
entryPoints: allFiles,
|
45
|
+
outdir: 'dist/cjs',
|
46
|
+
target: 'esnext',
|
47
|
+
minify: true,
|
48
|
+
format: 'cjs',
|
49
|
+
platform: 'node',
|
50
|
+
keepNames: true,
|
51
|
+
plugins: [transformExtPlugin({ outExtension: { '.ts': '.js' } })],
|
52
|
+
}),
|
53
|
+
]).catch(console.error)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import { Tokens } from 'marked'
|
2
|
+
|
3
|
+
export default function code({ lang, text }: Tokens.Code) {
|
4
|
+
return `<div class="code-snippet">
|
5
|
+
${lang ? `<div class="label ${lang.toLowerCase()}">${lang}</div>` : ''}
|
6
|
+
<div class="content">
|
7
|
+
<pre>
|
8
|
+
<code class="hljs language-javascript">${text}</code>
|
9
|
+
</pre>
|
10
|
+
</div>
|
11
|
+
<button type="button" class="code-copy-btn" style="visibility: hidden">
|
12
|
+
copy
|
13
|
+
</button>
|
14
|
+
</div>`
|
15
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { Tokens } from 'marked'
|
2
|
+
|
3
|
+
export default function heading({ tokens, depth }: Tokens.Heading) {
|
4
|
+
// @ts-expect-error this
|
5
|
+
const text = this.parser.parseInline(tokens)
|
6
|
+
const escapedText = text
|
7
|
+
.toLowerCase()
|
8
|
+
.replace(/[^\w]+/g, ' ')
|
9
|
+
.trim()
|
10
|
+
.replace(/\s/g, '-')
|
11
|
+
|
12
|
+
return `<h${depth} id="${escapedText}"><a href="#${escapedText}">${text}</a></h${depth}>`
|
13
|
+
}
|
package/src/docs/run.ts
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
import { Marked } from 'marked'
|
2
|
+
import { cp, mkdir, readdir, readFile, writeFile } from 'fs/promises'
|
3
|
+
import path from 'path'
|
4
|
+
import fm from 'front-matter'
|
5
|
+
import DOMPurify from 'isomorphic-dompurify'
|
6
|
+
import { markedHighlight } from 'marked-highlight'
|
7
|
+
import hljs from 'highlight.js'
|
8
|
+
import defaultTemp from './templates/default.js'
|
9
|
+
import { CustomOptions, PageProps, SiteMap } from './types.js'
|
10
|
+
import renderer from './renderer/index.js'
|
11
|
+
|
12
|
+
const layouts: Map<string, (props: PageProps) => string> = new Map()
|
13
|
+
|
14
|
+
layouts.set('default', defaultTemp)
|
15
|
+
|
16
|
+
let currentFileMeta: {
|
17
|
+
attributes: CustomOptions
|
18
|
+
siteMap: SiteMap
|
19
|
+
}
|
20
|
+
|
21
|
+
const marked = new Marked(
|
22
|
+
markedHighlight({
|
23
|
+
langPrefix: 'hljs language-',
|
24
|
+
highlight(code, lang) {
|
25
|
+
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
|
26
|
+
return hljs.highlight(code, { language }).value
|
27
|
+
},
|
28
|
+
})
|
29
|
+
)
|
30
|
+
|
31
|
+
marked.use({ renderer })
|
32
|
+
|
33
|
+
const docsDir = path.resolve(process.cwd(), 'docs')
|
34
|
+
const docsSiteDir = path.resolve(process.cwd(), 'website')
|
35
|
+
const docsLayoutsDir = path.resolve(process.cwd(), 'docs/_layouts')
|
36
|
+
const docsAssetsDir = path.resolve(process.cwd(), 'docs/assets')
|
37
|
+
const docsStylesheetsDir = path.resolve(process.cwd(), 'docs/stylesheets')
|
38
|
+
const docsScriptsDir = path.resolve(process.cwd(), 'docs/scripts')
|
39
|
+
|
40
|
+
const traverseDirectory = async (dir: string) => {
|
41
|
+
const items = await readdir(dir)
|
42
|
+
const files: string[] = []
|
43
|
+
|
44
|
+
for (const item of items) {
|
45
|
+
const ext = path.extname(item)
|
46
|
+
|
47
|
+
if (/^[._]/.test(item)) continue
|
48
|
+
|
49
|
+
if (ext) {
|
50
|
+
files.push(path.join(dir, item))
|
51
|
+
} else if (!/(stylesheets|assets|scripts)/.test(item)) {
|
52
|
+
files.push(...(await traverseDirectory(path.join(dir, item))))
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
return files
|
57
|
+
}
|
58
|
+
|
59
|
+
marked.use({
|
60
|
+
hooks: {
|
61
|
+
postprocess(html: string) {
|
62
|
+
const { layout = 'default', ...options } =
|
63
|
+
currentFileMeta.attributes
|
64
|
+
|
65
|
+
html = DOMPurify.sanitize(html)
|
66
|
+
|
67
|
+
const tableOfContent = [
|
68
|
+
...html.matchAll(
|
69
|
+
/<h([0-6])\sid="[^"]+".*?>.*?<a\s+href="([^"]+)".*?>([^<]+)<\/a>/gm
|
70
|
+
),
|
71
|
+
].map((m) => ({ path: m[2], label: m[3], level: m[1] }))
|
72
|
+
|
73
|
+
return (
|
74
|
+
layouts.get(layout)?.({
|
75
|
+
...options,
|
76
|
+
content: html,
|
77
|
+
siteMap: currentFileMeta.siteMap,
|
78
|
+
tableOfContent,
|
79
|
+
}) || html
|
80
|
+
)
|
81
|
+
},
|
82
|
+
},
|
83
|
+
})
|
84
|
+
;(async () => {
|
85
|
+
// create website dir
|
86
|
+
await mkdir(docsSiteDir, { recursive: true })
|
87
|
+
|
88
|
+
try {
|
89
|
+
await cp(docsAssetsDir, docsAssetsDir.replace(docsDir, docsSiteDir), {
|
90
|
+
recursive: true,
|
91
|
+
})
|
92
|
+
} catch (e) {
|
93
|
+
// ignore
|
94
|
+
}
|
95
|
+
|
96
|
+
try {
|
97
|
+
await cp(
|
98
|
+
docsStylesheetsDir,
|
99
|
+
docsStylesheetsDir.replace(docsDir, docsSiteDir),
|
100
|
+
{ recursive: true }
|
101
|
+
)
|
102
|
+
} catch (e) {
|
103
|
+
// ignore
|
104
|
+
}
|
105
|
+
|
106
|
+
try {
|
107
|
+
await cp(docsScriptsDir, docsScriptsDir.replace(docsDir, docsSiteDir), {
|
108
|
+
recursive: true,
|
109
|
+
})
|
110
|
+
} catch (e) {
|
111
|
+
// ignore
|
112
|
+
}
|
113
|
+
|
114
|
+
try {
|
115
|
+
// import the layouts first
|
116
|
+
const layoutPaths = await traverseDirectory(docsLayoutsDir)
|
117
|
+
|
118
|
+
for (const layoutPath of layoutPaths) {
|
119
|
+
if (/\.(j|t)s$/.test(layoutPath)) {
|
120
|
+
const fileName = path
|
121
|
+
.basename(layoutPath)
|
122
|
+
.replace(path.extname(layoutPath), '')
|
123
|
+
const handler = await import(layoutPath)
|
124
|
+
|
125
|
+
layouts.set(fileName, handler.default)
|
126
|
+
}
|
127
|
+
}
|
128
|
+
} catch (e) {
|
129
|
+
// ignore
|
130
|
+
}
|
131
|
+
|
132
|
+
const filePaths = await traverseDirectory(docsDir)
|
133
|
+
|
134
|
+
const siteMap: SiteMap = new Map()
|
135
|
+
|
136
|
+
;(
|
137
|
+
await Promise.all(
|
138
|
+
filePaths
|
139
|
+
.filter((filePath) => filePath.endsWith('.md'))
|
140
|
+
.map(async (filePath) => {
|
141
|
+
const content = await readFile(filePath, 'utf-8')
|
142
|
+
return {
|
143
|
+
filePath,
|
144
|
+
// @ts-expect-error call unknown
|
145
|
+
...(fm(content) as {
|
146
|
+
attributes: CustomOptions
|
147
|
+
body: string
|
148
|
+
}),
|
149
|
+
}
|
150
|
+
})
|
151
|
+
)
|
152
|
+
)
|
153
|
+
.sort((a, b) => a.attributes.order - b.attributes.order)
|
154
|
+
.map(({ attributes, body, filePath }) => {
|
155
|
+
const pathname = filePath
|
156
|
+
.replace(docsDir, '')
|
157
|
+
.replace(/\.md/, '.html')
|
158
|
+
const fileWebsitePath = filePath
|
159
|
+
.replace(docsDir, docsSiteDir)
|
160
|
+
.replace('.md', '.html')
|
161
|
+
const fileDirWebsitePath = fileWebsitePath.replace(
|
162
|
+
path.basename(fileWebsitePath),
|
163
|
+
''
|
164
|
+
)
|
165
|
+
|
166
|
+
const fileName = path.basename(pathname) || '/'
|
167
|
+
const dir = pathname.replace(fileName, '').replace(/\/$/, '')
|
168
|
+
|
169
|
+
let currentDir = siteMap
|
170
|
+
|
171
|
+
dir.split('/')
|
172
|
+
.filter(Boolean)
|
173
|
+
.forEach((d) => {
|
174
|
+
if (!currentDir.has(d)) {
|
175
|
+
currentDir.set(d, new Map())
|
176
|
+
}
|
177
|
+
|
178
|
+
currentDir = currentDir.get(d) as SiteMap
|
179
|
+
})
|
180
|
+
|
181
|
+
const attrs = {
|
182
|
+
...attributes,
|
183
|
+
path: pathname,
|
184
|
+
} as CustomOptions
|
185
|
+
|
186
|
+
if (!currentDir.has(fileName)) {
|
187
|
+
currentDir.set(fileName, attrs)
|
188
|
+
}
|
189
|
+
|
190
|
+
return {
|
191
|
+
attributes: attrs,
|
192
|
+
fileWebsitePath,
|
193
|
+
fileDirWebsitePath,
|
194
|
+
body,
|
195
|
+
siteMap,
|
196
|
+
}
|
197
|
+
})
|
198
|
+
.forEach(
|
199
|
+
async ({
|
200
|
+
attributes,
|
201
|
+
body,
|
202
|
+
fileDirWebsitePath,
|
203
|
+
fileWebsitePath,
|
204
|
+
siteMap,
|
205
|
+
}) => {
|
206
|
+
currentFileMeta = { attributes, siteMap }
|
207
|
+
const contentMd = await marked.parse(body)
|
208
|
+
await mkdir(fileDirWebsitePath, { recursive: true })
|
209
|
+
await writeFile(fileWebsitePath, contentMd)
|
210
|
+
}
|
211
|
+
)
|
212
|
+
})()
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import { PageProps } from '../types.js'
|
2
|
+
|
3
|
+
export default ({ title, description, content }: PageProps) => `
|
4
|
+
<!doctype html>
|
5
|
+
<html lang="en">
|
6
|
+
<head>
|
7
|
+
<meta charset="UTF-8" />
|
8
|
+
<meta
|
9
|
+
name="viewport"
|
10
|
+
content="width=device-width, initial-scale=1.0"
|
11
|
+
/>
|
12
|
+
<meta name="description" content="${description}" />
|
13
|
+
<title>${title}</title>
|
14
|
+
</head>
|
15
|
+
<body>
|
16
|
+
${content}
|
17
|
+
</body>
|
18
|
+
</html>
|
19
|
+
`
|
@@ -0,0 +1,16 @@
|
|
1
|
+
export interface PageProps {
|
2
|
+
name: string
|
3
|
+
path: string
|
4
|
+
order: number
|
5
|
+
title: string
|
6
|
+
description: string
|
7
|
+
content: string
|
8
|
+
siteMap: SiteMap
|
9
|
+
tableOfContent: { path: string; label: string }[]
|
10
|
+
}
|
11
|
+
|
12
|
+
export interface CustomOptions extends Omit<PageProps, 'content'> {
|
13
|
+
layout: string
|
14
|
+
}
|
15
|
+
|
16
|
+
export type SiteMap = Map<string, CustomOptions | SiteMap>
|
package/tsconfig.json
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"target": "es2015",
|
4
|
+
"types": ["node"],
|
5
|
+
"moduleResolution": "nodenext",
|
6
|
+
"lib": ["decorators", "es5", "scripthost", "es2015.promise"],
|
7
|
+
"outDir": "./dist",
|
8
|
+
"module": "nodenext",
|
9
|
+
"declaration": true,
|
10
|
+
"declarationMap": false,
|
11
|
+
"sourceMap": true,
|
12
|
+
"removeComments": true,
|
13
|
+
"downlevelIteration": true,
|
14
|
+
"strict": true,
|
15
|
+
"alwaysStrict": true,
|
16
|
+
"resolveJsonModule": true,
|
17
|
+
"allowSyntheticDefaultImports": true,
|
18
|
+
"esModuleInterop": true,
|
19
|
+
"skipLibCheck": true,
|
20
|
+
"forceConsistentCasingInFileNames": true,
|
21
|
+
"allowJs": true
|
22
|
+
},
|
23
|
+
"include": ["./src/**/*"]
|
24
|
+
}
|