website: add search feature. #32

This commit is contained in:
jaywcjlove 2022-11-20 03:26:06 +08:00
parent 9290c65fdc
commit 3bd4114c63
12 changed files with 516 additions and 84 deletions

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 18 18">
<path fill="currentColor" d="M17.71,16.29 L14.31,12.9 C15.4069846,11.5024547 16.0022094,9.77665502 16,8 C16,3.581722 12.418278,0 8,0 C3.581722,0 0,3.581722 0,8 C0,12.418278 3.581722,16 8,16 C9.77665502,16.0022094 11.5024547,15.4069846 12.9,14.31 L16.29,17.71 C16.4777666,17.8993127 16.7333625,18.0057983 17,18.0057983 C17.2666375,18.0057983 17.5222334,17.8993127 17.71,17.71 C17.8993127,17.5222334 18.0057983,17.2666375 18.0057983,17 C18.0057983,16.7333625 17.8993127,16.4777666 17.71,16.29 Z M2,8 C2,4.6862915 4.6862915,2 8,2 C11.3137085,2 14,4.6862915 14,8 C14,11.3137085 11.3137085,14 8,14 C4.6862915,14 2,11.3137085 2,8 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 723 B

View File

@ -7,13 +7,13 @@ import { getCodeString } from 'rehype-rewrite';
import rehypeSlug from 'rehype-slug';
import { htmlTagAddAttri } from './nodes/htmlTagAddAttri.mjs';
import { footer } from './nodes/footer.mjs';
import { search } from './nodes/search.mjs';
import { header } from './nodes/header.mjs';
import { rehypeUrls } from './utils/rehypeUrls.mjs';
import { tooltips } from './utils/tooltips.mjs';
import { homeCardIcons } from './utils/homeCardIcons.mjs';
import { getTocsTree, getTocsTitleNode, getTocsTitleNodeWarpper, addTocsInWarp } from './utils/getTocsTree.mjs';
import { rehypeTitle } from './utils/rehypeTitle.mjs';
import { anchorPoint } from './utils/anchorPoint.mjs';
import { rehypePreviewHTML } from './utils/rehypePreviewHTML.mjs';
const favicon = `data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%221em%22%20width%3D%221em%22%3E%20%3Cpath%20d%3D%22m21.66%2010.44-.98%204.18c-.84%203.61-2.5%205.07-5.62%204.77-.5-.04-1.04-.13-1.62-.27l-1.68-.4c-4.17-.99-5.46-3.05-4.48-7.23l.98-4.19c.2-.85.44-1.59.74-2.2%201.17-2.42%203.16-3.07%206.5-2.28l1.67.39c4.19.98%205.47%203.05%204.49%207.23Z%22%20fill%3D%22%23c9d1d9%22%2F%3E%20%3Cpath%20d%3D%22M15.06%2019.39c-.62.42-1.4.77-2.35%201.08l-1.58.52c-3.97%201.28-6.06.21-7.35-3.76L2.5%2013.28c-1.28-3.97-.22-6.07%203.75-7.35l1.58-.52c.41-.13.8-.24%201.17-.31-.3.61-.54%201.35-.74%202.2l-.98%204.19c-.98%204.18.31%206.24%204.48%207.23l1.68.4c.58.14%201.12.23%201.62.27Zm2.43-8.88c-.06%200-.12-.01-.19-.02l-4.85-1.23a.75.75%200%200%201%20.37-1.45l4.85%201.23a.748.748%200%200%201-.18%201.47Z%22%20fill%3D%22%23228e6c%22%20%2F%3E%20%3Cpath%20d%3D%22M14.56%2013.89c-.06%200-.12-.01-.19-.02l-2.91-.74a.75.75%200%200%201%20.37-1.45l2.91.74c.4.1.64.51.54.91-.08.34-.38.56-.72.56Z%22%20fill%3D%22%23228e6c%22%20%2F%3E%20%3C%2Fsvg%3E`;
@ -92,6 +92,7 @@ export function create(str = '', options = {}) {
if (!options.isHome) {
const tocsMenus = getTocsTitleNode([...tocsData]);
node.children = addTocsInWarp([...tocsData], getTocsTitleNodeWarpper(tocsMenus));
// 生成搜索数据
tocsMenus.forEach((menu, idx) => {
const level = menu?.properties['data-num'];
if (idx + 1 === tocsMenus.length && level === 2) {
@ -110,7 +111,8 @@ export function create(str = '', options = {}) {
}
node.children.unshift(header(options));
node.children.push(footer(options));
node.children.push(anchorPoint());
// node.children.push(search(options));
node.children = node.children.concat(search(options));
}
}
},

View File

@ -7,6 +7,7 @@ export const OUTOUT = path.resolve(process.cwd(), 'dist');
export const DOCS = path.resolve(process.cwd(), 'docs');
/** 搜索数据路径 */
export const SEARCH_DATA = path.resolve(OUTOUT, 'data.json');
export const SEARCH_DATA_JS = path.resolve(OUTOUT, 'data.js');
export const SEARCH_DATA_CACHE = path.resolve(process.cwd(), 'node_modules/.cache/reference/data.json');
export async function createHTML(files = [], num = 0) {
@ -49,12 +50,21 @@ export async function createHTML(files = [], num = 0) {
.map((name) => searchData[name])
.filter((item) => typeof item !== 'string');
await fs.writeJSON(SEARCH_DATA, resultSearchData);
await fs.writeFile(SEARCH_DATA_JS, `const REFS_DATA = ${JSON.stringify(resultSearchData)}`);
}
await fs.writeFile(outputHTMLPath, html);
console.log(`♻️ \x1b[32;1m ${path.relative(OUTOUT, outputHTMLPath)} \x1b[0m`);
createHTML(files, num);
}
export async function copyCSSFile() {
await fs.copy(path.resolve(process.cwd(), 'scripts/style'), path.resolve(OUTOUT, 'style'));
}
export async function copyJSFile() {
await fs.copy(path.resolve(process.cwd(), 'scripts/js'), path.resolve(OUTOUT, 'js'));
}
export async function run() {
try {
await fs.ensureDir(OUTOUT);
@ -63,7 +73,8 @@ export async function run() {
await fs.ensureFile(SEARCH_DATA_CACHE);
await fs.writeFile(SEARCH_DATA_CACHE, '{}');
await fs.writeFile(SEARCH_DATA, '[]');
await fs.copy(path.resolve(process.cwd(), 'scripts/style'), path.resolve(OUTOUT, 'style'));
await copyCSSFile();
await copyJSFile();
const files = await recursiveReaddirFiles(process.cwd(), {
ignored: /\/(node_modules|\.git)/,
exclude: /(\.json|\.mjs|CONTRIBUTING\.md)$/,

9
scripts/js/fuse.min.js vendored Normal file

File diff suppressed because one or more lines are too long

153
scripts/js/main.js Normal file
View File

@ -0,0 +1,153 @@
/** ==========dark theme============== */
const LOCAL_NANE = '_dark_mode_theme_'
const rememberedValue = localStorage.getItem(LOCAL_NANE);
if (rememberedValue && ['light', 'dark'].includes(rememberedValue)) {
document.documentElement.setAttribute('data-color-mode', rememberedValue);
}
const button = document.querySelector('#darkMode');
button.onclick = () => {
const theme = document.documentElement.dataset.colorMode;
const mode = theme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-color-mode', mode);
localStorage.setItem(LOCAL_NANE, mode);
}
/** ==========anchor============== */
if(('onhashchange' in window) && ((typeof document.documentMode==='undefined') || document.documentMode==8)) {
window.onhashchange = function () {
anchorPoint()
updateAnchor()
};
}
function anchorPoint() {
const hash = window.location.hash?.replace(/^#/, '') || '';
const elm = document.getElementById(decodeURIComponent(hash));
Array.from(document.querySelectorAll('.h2wrap-body .wrap')).forEach((elm) => elm.classList.remove('active'))
if (elm?.tagName === 'H3') {
elm?.parentElement?.parentElement?.classList.add('active');
}
}
anchorPoint();
function updateAnchor(element) {
const anchorContainer = document.querySelectorAll('.menu-tocs .menu-modal a.tocs-link');
anchorContainer.forEach((tocanchor) => {
tocanchor.classList.remove('is-active-link');
});
const anchor = element || document.querySelector(`a.tocs-link[href='${decodeURIComponent(window.location.hash)}']`);
if (anchor) {
anchor.classList.add('is-active-link');
}
}
// toc 定位
updateAnchor()
const anchorAll = document.querySelectorAll('.menu-tocs .menu-modal a.tocs-link');
anchorAll.forEach((item) => {
item.addEventListener('click', (e) => {
updateAnchor()
})
})
/** ==========search============== */
const fuse = new Fuse(REFS_DATA, {
includeScore: !1,
shouldSort: !0,
includeMatches: !0,
matchEmptyQuery: !0,
threshold: .1,
keys: [
{ name: "title", weight: 12 },
{ name: 'intro', weight: 2 },
{ name: 'sections.t', weight: 5 }
],
});
const searchBtn = document.getElementById('searchbtn');
const searchBox = document.getElementById('mysearch');
const searchInput = document.getElementById('mysearch-input');
const closeBtn = document.getElementById('mysearch-close');
const searchMenu = document.getElementById('mysearch-menu');
const searchContent = document.getElementById('mysearch-content');
const isHome = document.body.classList.contains('home');
searchBtn.addEventListener('click', (ev) => {
ev.preventDefault();
showSearch();
});
closeBtn.addEventListener('click', hideSearch);
searchBox.addEventListener('click', hideSearch);
searchBox.firstChild.addEventListener('click', (ev) => ev.stopPropagation());
searchInput.addEventListener('input', (evn) => searchResult(evn.target.value));
document.addEventListener('keydown', (ev) => {
if (ev.metaKey && ev.key.toLocaleLowerCase() === 'k') {
searchBox.classList.contains('show') ? hideSearch() : showSearch();
}
});
function showSearch() {
document.body.classList.add('search');
searchBox.classList.add('show');
searchInput.focus();
}
function hideSearch() {
document.body.classList.remove('search');
searchBox.classList.remove('show');
}
let result = []
let inputValue = '';
function searchResult(value) {
inputValue = value;
result = fuse.search(value);
let menuHTML = '';
result.forEach((item, idx) => {
const label = item.item.title.replace(new RegExp(value, 'ig'), (txt) => {
return `<mark>${txt}</mark>`
})
const href = isHome ? item.item.path : item.item.path.replace('docs/', '');
if (idx === 0) {
menuHTML += `<a href="${href}" class="active">${label}</a>`;
} else {
menuHTML += `<a href="${href}">${label}</a>`;
}
});
searchMenu.innerHTML = menuHTML;
searchSectionsResult();
const data = Array.from(searchMenu.children)
data.forEach((anchor, idx) => {
anchor.onmouseenter = (evn) => {
data.forEach(item => item.classList.remove('active'));
evn.target.classList.add('active');
searchSectionsResult(idx);
}
});
const anchorData = searchContent.querySelectorAll('a');
Array.from(anchorData).forEach((item) => {
item.addEventListener('click', hideSearch);
})
}
function searchSectionsResult(idx = 0) {
const data = result[idx] || [];
const title = (data.item?.intro || '').replace(new RegExp(inputValue, 'ig'), (txt) => {
return `<mark>${txt}</mark>`
});
let sectionHTML = `<h3>${title}</h3><ol>`;
if (data && data.item && data.item.sections) {
data.item.sections.forEach((item, idx) => {
const label = item.t.replace(new RegExp(inputValue, 'ig'), (txt) => {
return `<mark>${txt}</mark>`
})
const href = isHome ? data.item.path : data.item.path.replace('docs/', '');
if (item.l < 3) {
sectionHTML += `<li><a href="${href + item.a}">${label}</a><div>`
} else {
sectionHTML += `<a href="${href + item.a}">${label}</a>`
}
if (data.item.sections.length === idx + 1) {
sectionHTML += `</div></li>`
}
})
}
searchContent.innerHTML = sectionHTML;
}

View File

@ -1,8 +1,8 @@
import formatter from '@uiw/formatter';
export function footer(options = {}) {
export function footer({ isHome } = {}) {
let footerText = '© 2022 Kenny Wang.';
if (options.isHome) {
if (isHome) {
const now = new Date();
const utc = now.getTime() + now.getTimezoneOffset() * 60000;
const cst = new Date(utc + 3600000 * 8);

View File

@ -4,9 +4,45 @@ import { getSVGNode } from '../utils/getSVGNode.mjs';
import { darkMode } from '../utils/darkMode.mjs';
const ICONS_PATH = path.resolve(process.cwd(), 'scripts/assets/quickreference.svg');
const ICONS_SEARCH_PATH = path.resolve(process.cwd(), 'scripts/assets/search.svg');
export function header({ homePath, githubURL = '' }) {
const svgNode = getSVGNode(ICONS_PATH);
const svgSearchNode = getSVGNode(ICONS_SEARCH_PATH);
const data = [
{
menu: true,
href: 'javascript:void(0);',
class: ['searchbtn'],
id: 'searchbtn',
children: [
...svgSearchNode,
{
type: 'element',
tagName: 'span',
children: [
{
type: 'text',
value: '搜索',
},
],
},
{
type: 'element',
tagName: 'span',
children: [
{
type: 'text',
value: '⌘',
},
{
type: 'text',
value: 'K',
},
],
},
],
},
{
menu: true,
href: githubURL,

107
scripts/nodes/search.mjs Normal file
View File

@ -0,0 +1,107 @@
import path from 'path';
import { getSVGNode } from '../utils/getSVGNode.mjs';
const ICONS_SEARCH_PATH = path.resolve(process.cwd(), 'scripts/assets/search.svg');
export function search({ homePath = '', isHome } = {}) {
const relativePath = homePath.replace(/\/?index.html$/, isHome ? '' : '/');
const fuseJSUrl = relativePath + 'js/fuse.min.js';
const manJSUrl = relativePath + 'js/main.js';
const dataJSUrl = relativePath + 'data.js';
const svgSearchNode = getSVGNode(ICONS_SEARCH_PATH);
return [
{
type: 'element',
tagName: 'script',
properties: {
src: dataJSUrl,
defer: true,
},
},
{
type: 'element',
tagName: 'script',
properties: {
src: fuseJSUrl,
defer: true,
},
},
{
type: 'element',
tagName: 'script',
properties: {
src: manJSUrl,
defer: true,
},
},
{
type: 'element',
tagName: 'div',
properties: {
id: 'mysearch',
},
children: [
{
type: 'element',
tagName: 'div',
properties: {
class: ['mysearch-box'],
},
children: [
{
type: 'element',
tagName: 'div',
properties: { class: ['mysearch-input'] },
children: [
{
type: 'element',
tagName: 'div',
properties: {},
children: [
...svgSearchNode,
{
type: 'element',
tagName: 'input',
properties: { id: ['mysearch-input'], type: 'search' },
children: [],
},
{
type: 'element',
tagName: 'div',
properties: { class: ['mysearch-clear'] },
},
],
},
{
type: 'element',
tagName: 'button',
properties: { id: ['mysearch-close'], type: 'button' },
children: [{ type: 'text', value: '取消' }],
},
],
},
{
type: 'element',
tagName: 'div',
properties: { class: ['mysearch-result'] },
children: [
{
type: 'element',
tagName: 'div',
properties: { id: 'mysearch-menu' },
children: [],
},
{
type: 'element',
tagName: 'div',
properties: { id: 'mysearch-content' },
children: [],
},
],
},
],
},
],
},
];
}

View File

@ -80,6 +80,7 @@ body {
--color-attention-subtle: #fff8c5;
--color-danger-fg: #cf222e;
--box-shadow: 109 109 109;
--primary-color: #228e6c;
}
[data-color-mode*='dark'],
@ -128,6 +129,7 @@ body {
--color-attention-subtle: rgba(187, 128, 9, 0.15);
--color-danger-fg: #f85149;
--box-shadow: 0 0 0;
--primary-color: #228e6c;
}
body {
@ -540,6 +542,21 @@ a.text-grey {
.header-nav .menu a > span {
font-size: 0.9rem;
}
.header-nav .menu .searchbtn {
text-decoration-color: transparent;
gap: 0.75rem;
}
.header-nav .menu .searchbtn span:last-child {
transition: all 0.3s;
border: 1px solid var(--color-border-default);
border-radius: 3px;
padding: 1px 1px 1px 3px;
letter-spacing: 3px;
}
.header-nav .menu .searchbtn:hover span:last-child {
border-color: var(--primary-color);
color: var(--primary-color);
}
.header-nav .menu button {
font-family: inherit;
font-size: 100%;
@ -1199,6 +1216,158 @@ body:not(.home) .h2wrap-body > .wrap:hover .h3wrap > h3 a::after {
font-size: 0.75rem;
}
body.search {
overflow: hidden;
}
#mysearch {
transition: all 0.3s;
display: none;
}
#mysearch.show .mysearch-box {
background-color: var(--color-canvas-default);
box-shadow: 0 0 #0000, 0 0 #0000, 0 0 #0000, 0 0 #0000, 0 35px 60px -15px rgba(0, 0, 0, 0.3);
border-radius: 0.5rem;
display: flex;
flex-direction: column;
max-width: 1024px;
width: 100%;
height: 100%;
}
#mysearch.show .mysearch-result > * {
width: 50%;
overflow-y: auto;
padding: 0.6rem;
}
#mysearch.show .mysearch-result > :last-child {
background-color: var(--color-neutral-muted);
border-bottom-right-radius: 0.5rem;
}
#mysearch.show .mysearch-result {
display: flex;
flex: 1;
height: calc(100% - 3.5rem);
}
#mysearch.show {
background-color: var(--color-neutral-muted);
height: 100vh;
left: 0;
position: fixed;
top: 0;
width: 100vw;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
}
.mysearch-input {
height: 3.5rem;
display: flex;
flex-direction: row;
border-bottom: 1px solid var(--color-neutral-muted);
}
.mysearch-input > :first-child {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
}
.mysearch-input > :first-child svg {
margin-left: 1rem;
font-size: 1.3rem;
position: absolute;
color: var(--primary-color);
}
.mysearch-input > :first-child input {
flex: 1;
height: 100%;
padding-left: 2.9rem;
font-size: 1.6rem;
color: var(--color-fg-default);
border: 0;
font-weight: 800;
background: transparent;
outline: 0;
}
#mysearch-close:hover {
color: var(--primary-color);
}
#mysearch-close {
background-color: transparent;
color: var(--color-fg-default);
border: 0;
padding: 0 1.6rem;
cursor: pointer;
font-size: 1.1rem;
transition: all 0.3s;
}
#mysearch-menu a + a {
margin: 0.2rem 0;
}
#mysearch-menu a {
display: flex;
padding-top: 0.625rem;
padding-bottom: 0.625rem;
padding-left: 0.875rem;
padding-right: 0.875rem;
transition: all 0.3s;
white-space: pre-wrap;
text-decoration: none;
color: var(--color-fg-default);
}
#mysearch-menu a:hover,
#mysearch-menu a.active {
background-color: var(--color-neutral-muted);
border-radius: 0.5rem;
}
#mysearch-content ol li div a:hover {
background-color: var(--primary-color);
color: #fff;
}
#mysearch-content ol li div a {
padding: 0.125rem 0.5rem;
border-radius: 100px;
margin: 0.1rem 0.2rem;
color: var(--color-fg-subtle);
}
#mysearch-content ol li div {
margin-left: -1.54rem;
padding-top: 0.82rem;
}
#mysearch-content ol li > a:hover {
text-decoration: underline;
}
#mysearch-content ol li > a {
font-weight: bold;
}
#mysearch-content ol li a {
font-size: 0.85rem;
white-space: nowrap;
display: inline-block;
text-decoration: none;
color: var(--color-fg-default);
transition: all 0.3s;
}
#mysearch-content ol li {
word-break: break-all;
white-space: pre-wrap;
padding-bottom: 1.56rem;
}
#mysearch-content ol {
list-style: auto;
padding-left: 1.75rem;
}
#mysearch-content h3 {
padding-bottom: 1.3rem;
text-align: center;
padding-top: 1.5rem;
color: var(--color-fg-subtle);
max-width: 23rem;
margin: 0 auto;
font-size: 0.85rem;
}
@media (min-width: 1024px) {
.h2wrap-body {
display: grid;
@ -1207,6 +1376,10 @@ body:not(.home) .h2wrap-body > .wrap:hover .h3wrap > h3 a::after {
.h2wrap-body > .wrap {
margin-bottom: 0;
}
#mysearch.show {
padding-bottom: 5rem;
padding-top: 4rem;
}
}
@media (375px <= width <= 1024px) {
.header-nav .title {

View File

@ -1,49 +0,0 @@
const scripts = `
if(('onhashchange' in window) && ((typeof document.documentMode==='undefined') || document.documentMode==8)) {
window.onhashchange = function () {
anchorPoint()
updateAnchor()
};
}
function anchorPoint() {
const hash = window.location.hash?.replace(/^#/, '') || '';
const elm = document.getElementById(decodeURIComponent(hash));
Array.from(document.querySelectorAll('.h2wrap-body .wrap')).forEach((elm) => elm.classList.remove('active'))
if (elm?.tagName === 'H3') {
elm?.parentElement?.parentElement?.classList.add('active');
}
}
anchorPoint();
function updateAnchor(element) {
const anchorContainer = document.querySelectorAll('.menu-tocs .menu-modal a.tocs-link');
anchorContainer.forEach((tocanchor) => {
tocanchor.classList.remove('is-active-link');
});
const anchor = element || document.querySelector(\`a.tocs-link[href='\${decodeURIComponent(window.location.hash)}']\`);
if (anchor) {
anchor.classList.add('is-active-link');
}
}
// toc 定位
updateAnchor()
const anchor = document.querySelectorAll('.menu-tocs .menu-modal a.tocs-link');
anchor.forEach((item) => {
item.addEventListener('click', (e) => {
updateAnchor()
})
})
`;
export function anchorPoint() {
return {
type: 'element',
tagName: 'script',
children: [
{
type: 'text',
value: scripts,
},
],
};
}

View File

@ -1,21 +1,6 @@
import path from 'path';
import { getSVGNode } from './getSVGNode.mjs';
const scripts = `
const LOCAL_NANE = '_dark_mode_theme_'
const rememberedValue = localStorage.getItem(LOCAL_NANE);
if (rememberedValue && ['light', 'dark'].includes(rememberedValue)) {
document.documentElement.setAttribute('data-color-mode', rememberedValue);
}
const button = document.querySelector('#darkMode');
button.onclick = () => {
const theme = document.documentElement.dataset.colorMode;
const mode = theme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-color-mode', mode);
localStorage.setItem(LOCAL_NANE, mode);
}
`;
const ICONS_PATH = path.resolve(process.cwd(), 'scripts/assets');
export function darkMode() {
@ -33,15 +18,5 @@ export function darkMode() {
},
children: [...sunNode, ...moonNode],
},
{
type: 'element',
tagName: 'script',
children: [
{
type: 'text',
value: scripts,
},
],
},
];
}

View File

@ -1,20 +1,32 @@
import path from 'path';
import chokidar from 'chokidar';
import { getStat } from 'recursive-readdir-files';
import { run, DOCS, createHTML } from './index.mjs';
import { run, DOCS, createHTML, copyCSSFile, copyJSFile } from './index.mjs';
(async () => {
await run();
const homeMdPath = path.relative(process.cwd(), 'README.md');
const watcher = chokidar.watch([DOCS, homeMdPath], {
const cssDirPath = path.relative(process.cwd(), 'scripts/style');
const jsDirPath = path.relative(process.cwd(), 'scripts/js');
const watcher = chokidar.watch([DOCS, homeMdPath, cssDirPath, jsDirPath], {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true,
});
watcher
.on('change', async (path) => {
const stats = await getStat(path);
createHTML([stats]);
.on('change', async (filepath) => {
if (filepath.endsWith('.md')) {
const stats = await getStat(filepath);
createHTML([stats]);
}
if (filepath.endsWith('.css')) {
copyCSSFile(filepath);
console.log(`♻️ \x1b[32;1m ${path.relative(cssDirPath, filepath)} \x1b[0m`);
}
if (filepath.endsWith('.js')) {
copyJSFile(filepath);
console.log(`♻️ \x1b[32;1m ${path.relative(jsDirPath, filepath)} \x1b[0m`);
}
})
.on('error', (error) => console.log(`Watcher error: ${error}`));
})();