@beforesemicolon/builder 1.0.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/.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
|
+
}
|