@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 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
+ }
@@ -0,0 +1,2 @@
1
+ src/**/*.spec.ts
2
+ src/docs/**/*.html
package/.prettierrc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "trailingComma": "es5",
3
+ "tabWidth": 4,
4
+ "semi": false,
5
+ "singleQuote": true
6
+ }
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
+ }
@@ -0,0 +1,10 @@
1
+ import heading from './heading.js'
2
+ import code from './code.js'
3
+ import link from './link.js'
4
+ import { RendererObject } from 'marked'
5
+
6
+ export default {
7
+ heading,
8
+ code,
9
+ link,
10
+ } as unknown as RendererObject
@@ -0,0 +1,7 @@
1
+ import { Tokens } from 'marked'
2
+
3
+ export default function link({ href, text }: Tokens.Link) {
4
+ return `<a href="${href
5
+ .replace(/\.md/, '.html')
6
+ .replace(/index\.html/, '')}">${text}</a>`
7
+ }
@@ -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
+ }