diff --git a/react-app-internal/public/markdown-search.json b/react-app-internal/public/markdown-search.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/react-app-internal/public/markdown-search.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/react-app-internal/src/App.jsx b/react-app-internal/src/App.jsx
index 456f420..0c0dcda 100644
--- a/react-app-internal/src/App.jsx
+++ b/react-app-internal/src/App.jsx
@@ -1,41 +1,42 @@
import './App.css'
-import {initImageFileMap, initMarkdownFileMap, initTocMap} from "./utils/file/fileUtils.js";
-import {useEffect, useState} from "react";
-import MarkdownContent from "./components/MarkdownContent.jsx";
-import {Route, Routes} from "react-router-dom";
-import LoadingScreen from "./components/LoadingScreen.jsx";
-import {WorkspaceContainer} from "./containers/WorkspaceContainer";
-
-function App() {
- const [isInitialized, setIsInitialized] = useState(false);
- useEffect(() => {
- async function initialize() {
+import {initImageFileMap, initMarkdownFileMap, initMarkdownSearchMap, initTocMap} from "./utils/file/fileUtils.js";
+import {useEffect, useState} from "react";
+import MarkdownContent from "./components/MarkdownContent.jsx";
+import {Route, Routes} from "react-router-dom";
+import LoadingScreen from "./components/LoadingScreen.jsx";
+import {WorkspaceContainer} from "./containers/WorkspaceContainer";
+
+function App() {
+ const [isInitialized, setIsInitialized] = useState(false);
+ useEffect(() => {
+ async function initialize() {
await initMarkdownFileMap();
+ await initMarkdownSearchMap();
await initImageFileMap();
await initTocMap();
- setIsInitialized(true);
- }
- initialize();
- }, []);
-
- if (!isInitialized) {
- return ();
- }
-
- return (
-
-
-
-
-
- }
- >
-
-
-
- );
-}
-
-export default App;
\ No newline at end of file
+ setIsInitialized(true);
+ }
+ initialize();
+ }, []);
+
+ if (!isInitialized) {
+ return ();
+ }
+
+ return (
+
+
+
+
+
+ }
+ >
+
+
+
+ );
+}
+
+export default App;
diff --git a/react-app-internal/src/App.test.jsx b/react-app-internal/src/App.test.jsx
index 900f2d4..1e255ed 100644
--- a/react-app-internal/src/App.test.jsx
+++ b/react-app-internal/src/App.test.jsx
@@ -1,44 +1,61 @@
import {render, screen, waitFor} from '@testing-library/react';
import {expect, vi} from 'vitest';
import App from './App';
-import {initImageFileMap, initMarkdownFileMap, initTocMap} from './utils/file/fileUtils';
+import {initImageFileMap, initMarkdownFileMap, initMarkdownSearchMap, initTocMap} from './utils/file/fileUtils';
import {MemoryRouter} from "react-router-dom";
-
+
beforeAll(() => {
+ vi.stubGlobal('fetch', vi.fn(async () => ({
+ text: async () => '',
+ })));
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation(() => ({
+ matches: false,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ })),
+ });
+
vi.mock('./utils/file/fileUtils', () => ({
initMarkdownFileMap: vi.fn(),
+ initMarkdownSearchMap: vi.fn(),
initImageFileMap: vi.fn(),
initTocMap: vi.fn(),
+ getTocMap: vi.fn(() => ({})),
+ getIndexFilePath: vi.fn(() => ''),
getMarkdownFileSet: vi.fn(() => new Set()),
}));
-})
-
+})
+
afterAll(() => {
+ vi.unstubAllGlobals();
vi.clearAllMocks();
})
-
-describe('App 컴포넌트 렌더링 시', () => {
+
+describe('App 컴포넌트 렌더링 시', () => {
it('초기화 메서드가 호출된다.', async () => {
- render();
+ render();
await waitFor(() => {
expect(initMarkdownFileMap).toHaveBeenCalled();
+ expect(initMarkdownSearchMap).toHaveBeenCalled();
expect(initImageFileMap).toHaveBeenCalled();
expect(initTocMap).toHaveBeenCalled();
});
});
it('아직 초기화 되지 않았을 때는 로딩 컴포넌트가 렌더링된다.', () => {
- render();
-
- expect(screen.getByTestId('init-loading-screen')).toBeInTheDocument();
- });
-
+ render();
+
+ expect(screen.getByTestId('init-loading-screen')).toBeInTheDocument();
+ });
+
it('초기화가 완료되면 App이 렌더링 된다.', async () => {
- render();
-
- await waitFor(() => {
- expect(screen.queryByTestId('init-loading-screen')).not.toBeInTheDocument();
- });
- });
-});
\ No newline at end of file
+ render();
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('init-loading-screen')).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/react-app-internal/src/components/TreeContainer.jsx b/react-app-internal/src/components/TreeContainer.jsx
index 391c5a3..adc5466 100644
--- a/react-app-internal/src/components/TreeContainer.jsx
+++ b/react-app-internal/src/components/TreeContainer.jsx
@@ -1,55 +1,56 @@
import {createContext, useMemo, useRef} from 'react';
-import {initTree, markUsedPaths, renderTree} from "../utils/treeUtils.jsx";
-
-export const TreeContainerContext = createContext({});
-
-const directoryFirstFn = ([key1, value1], [key2, value2]) => {
- if (value1.isFile !== value2.isFile) {
- return value1.isFile ? 1 : -1;
- }
- return key1.localeCompare(key2);
-};
-
-const TreeContainer = ({
- datatype,
- searchKeyword = '',
- forceExpandDirectories = false,
- showSearchInput = false,
- onSearchKeywordChange = (_nextKeyword) => {},
-}) => {
- const tree = useMemo(() => initTree(undefined, searchKeyword), [searchKeyword]);
- const shouldRenderTree = !showSearchInput || searchKeyword.trim().length > 0;
- const usedPaths = useRef(markUsedPaths());
- return (
-
-
-
-
- {showSearchInput && (
-
- onSearchKeywordChange(event.target.value)}
- style={{
- width: '100%',
- boxSizing: 'border-box',
- padding: '6px 8px',
- }}
- />
-
- )}
-
-
- {shouldRenderTree ? renderTree(tree.children, '', directoryFirstFn) : null}
-
-
-
-
-
- );
-};
-
-export default TreeContainer;
+import {initTree, markUsedPaths, MIN_SEARCH_KEYWORD_LENGTH, renderTree} from "../utils/treeUtils.jsx";
+
+export const TreeContainerContext = createContext({});
+
+const directoryFirstFn = ([key1, value1], [key2, value2]) => {
+ if (value1.isFile !== value2.isFile) {
+ return value1.isFile ? 1 : -1;
+ }
+ return key1.localeCompare(key2);
+};
+
+const TreeContainer = ({
+ datatype,
+ searchKeyword = '',
+ forceExpandDirectories = false,
+ showSearchInput = false,
+ onSearchKeywordChange = (_nextKeyword) => {},
+}) => {
+ const tree = useMemo(() => initTree(undefined, searchKeyword), [searchKeyword]);
+ const normalizedSearchLength = searchKeyword.normalize('NFC').trim().length;
+ const shouldRenderTree = !showSearchInput || normalizedSearchLength >= MIN_SEARCH_KEYWORD_LENGTH;
+ const usedPaths = useRef(markUsedPaths());
+ return (
+
+
+
+
+ {showSearchInput && (
+
+ onSearchKeywordChange(event.target.value)}
+ style={{
+ width: '100%',
+ boxSizing: 'border-box',
+ padding: '6px 8px',
+ }}
+ />
+
+ )}
+
+
+ {shouldRenderTree ? renderTree(tree.children, '', directoryFirstFn) : null}
+
+
+
+
+
+ );
+};
+
+export default TreeContainer;
diff --git a/react-app-internal/src/components/TreeContainer.test.jsx b/react-app-internal/src/components/TreeContainer.test.jsx
index 62a2920..b99e1ba 100644
--- a/react-app-internal/src/components/TreeContainer.test.jsx
+++ b/react-app-internal/src/components/TreeContainer.test.jsx
@@ -1,74 +1,75 @@
-import {fireEvent, render, screen} from "@testing-library/react";
-import TreeContainer from "./TreeContainer.jsx";
-import {initTree, markUsedPaths, renderTree} from "../utils/treeUtils.jsx";
-import {afterEach, beforeEach, describe, expect, it, vi} from "vitest";
-import {MemoryRouter} from "react-router-dom";
-
-let tree = {
- children: {}
-};
-describe('트리 컨테이너 컴포넌트 렌더링 시', () => {
- beforeEach(() => {
+import {fireEvent, render, screen} from "@testing-library/react";
+import TreeContainer from "./TreeContainer.jsx";
+import {initTree, markUsedPaths, renderTree} from "../utils/treeUtils.jsx";
+import {afterEach, beforeEach, describe, expect, it, vi} from "vitest";
+import {MemoryRouter} from "react-router-dom";
+
+let tree = {
+ children: {}
+};
+describe('트리 컨테이너 컴포넌트 렌더링 시', () => {
+ beforeEach(() => {
vi.mock('../utils/treeUtils.jsx', () => {
return {
+ MIN_SEARCH_KEYWORD_LENGTH: 2,
initTree: vi.fn(() => tree),
renderTree: vi.fn(),
markUsedPaths: vi.fn()
}
})
- })
-
- afterEach(() => {
- vi.clearAllMocks();
- })
-
- it('트리 초기화 로직이 호출된다.', () => {
- render()
- expect(initTree).toHaveBeenCalled();
- });
-
- it('초기화한 트리 정보를 사용해 renderTree 함수가 호출된다.', () => {
- tree = {
- children: {
- a: { children: {}}
- }
- }
-
- render()
-
- expect(renderTree).toHaveBeenCalled()
- expect(renderTree.mock.calls[0][0]).toStrictEqual(tree.children);
- });
-
- it('markUsedPaths가 호출된다', () => {
- render()
- expect(markUsedPaths).toHaveBeenCalled();
- });
-
- it('검색어가 주어지면 트리 초기화 시 검색어를 함께 전달한다.', () => {
- render()
- expect(initTree).toHaveBeenCalledWith(undefined, 'react');
- });
-
- it('검색 탭 모드면 검색 input이 트리 컨테이너 내부 상단에 렌더링된다.', () => {
- render();
- expect(screen.getByLabelText('Search query')).toBeInTheDocument();
- });
-
- it('검색 input 값 변경 시 콜백이 호출된다.', () => {
- const onSearchKeywordChange = vi.fn();
- render(
-
-
-
- );
-
- fireEvent.change(screen.getByLabelText('Search query'), {target: {value: 'react'}});
- expect(onSearchKeywordChange).toHaveBeenCalledWith('react');
- });
-
- it('검색 탭에서 검색어가 비어있으면 트리를 렌더링하지 않는다.', () => {
- render();
- expect(renderTree).not.toHaveBeenCalled();
- });
-});
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ })
+
+ it('트리 초기화 로직이 호출된다.', () => {
+ render()
+ expect(initTree).toHaveBeenCalled();
+ });
+
+ it('초기화한 트리 정보를 사용해 renderTree 함수가 호출된다.', () => {
+ tree = {
+ children: {
+ a: { children: {}}
+ }
+ }
+
+ render()
+
+ expect(renderTree).toHaveBeenCalled()
+ expect(renderTree.mock.calls[0][0]).toStrictEqual(tree.children);
+ });
+
+ it('markUsedPaths가 호출된다', () => {
+ render()
+ expect(markUsedPaths).toHaveBeenCalled();
+ });
+
+ it('검색어가 주어지면 트리 초기화 시 검색어를 함께 전달한다.', () => {
+ render()
+ expect(initTree).toHaveBeenCalledWith(undefined, 'react');
+ });
+
+ it('검색 탭 모드면 검색 input이 트리 컨테이너 내부 상단에 렌더링된다.', () => {
+ render();
+ expect(screen.getByLabelText('Search query')).toBeInTheDocument();
+ });
+
+ it('검색 input 값 변경 시 콜백이 호출된다.', () => {
+ const onSearchKeywordChange = vi.fn();
+ render(
+
+
+
+ );
+
+ fireEvent.change(screen.getByLabelText('Search query'), {target: {value: 'react'}});
+ expect(onSearchKeywordChange).toHaveBeenCalledWith('react');
+ });
+
+ it('검색 탭에서 검색어가 비어있으면 트리를 렌더링하지 않는다.', () => {
+ render();
+ expect(renderTree).not.toHaveBeenCalled();
+ });
+});
diff --git a/react-app-internal/src/utils/file/fileUtils.js b/react-app-internal/src/utils/file/fileUtils.js
index 9b89452..7cb2417 100644
--- a/react-app-internal/src/utils/file/fileUtils.js
+++ b/react-app-internal/src/utils/file/fileUtils.js
@@ -1,168 +1,240 @@
-import fs from "fs";
-import path from "path";
-import mime from "mime";
-import data from "../../stores/data.json";
-
+import fs from "fs";
+import path from "path";
+import mime from "mime";
+import data from "../../stores/data.json";
+
const sourceDir = 'public/sources';
const imageJsonFilePath = 'image-files.json';
const markdownJsonFilePath = 'markdown-files.json';
-
-const addToFileMap = (fileMap, key, value) => {
- if (!fileMap[key]) {
- fileMap[key] = [value];
- } else {
- fileMap[key].push(value);
- }
-}
-
-function traverseImageRecursively(dir, fileMap) {
- const files = fs.readdirSync(dir, { encoding: 'utf-8' });
- const directories = [];
- files.forEach((file) => {
- const filePath = path.join(dir, file);
- const mimeType = mime.getType(file);
- if (fs.statSync(filePath).isDirectory()) {
- directories.push(file);
- } else {
- if (mimeType && mimeType.startsWith('image/')) {
- const fileNameKey = file.normalize('NFC');
- const relativePath = path.relative(sourceDir, filePath).normalize('NFC')
-
- addToFileMap(fileMap, fileNameKey, relativePath);
- if (fileNameKey !== relativePath) {
- addToFileMap(fileMap, relativePath, relativePath);
- }
- }
- }
- });
-
- directories.forEach((file) => {
- const filePath = path.join(dir, file);
- traverseImageRecursively(filePath, fileMap);
- })
- return fileMap;
-}
-
+const markdownSearchJsonFilePath = 'markdown-search.json';
+
+const addToFileMap = (fileMap, key, value) => {
+ if (!fileMap[key]) {
+ fileMap[key] = [value];
+ } else {
+ fileMap[key].push(value);
+ }
+}
+
+function traverseImageRecursively(dir, fileMap) {
+ const files = fs.readdirSync(dir, { encoding: 'utf-8' });
+ const directories = [];
+ files.forEach((file) => {
+ const filePath = path.join(dir, file);
+ const mimeType = mime.getType(file);
+ if (fs.statSync(filePath).isDirectory()) {
+ directories.push(file);
+ } else {
+ if (mimeType && mimeType.startsWith('image/')) {
+ const fileNameKey = file.normalize('NFC');
+ const relativePath = path.relative(sourceDir, filePath).normalize('NFC')
+
+ addToFileMap(fileMap, fileNameKey, relativePath);
+ if (fileNameKey !== relativePath) {
+ addToFileMap(fileMap, relativePath, relativePath);
+ }
+ }
+ }
+ });
+
+ directories.forEach((file) => {
+ const filePath = path.join(dir, file);
+ traverseImageRecursively(filePath, fileMap);
+ })
+ return fileMap;
+}
+
function traverseFilesRecursively(dir, fileMap) {
- const files = fs.readdirSync(dir, { encoding: 'utf-8' });
- const directories = [];
- files.forEach((file) => {
- const filePath = path.join(dir, file);
- if (fs.statSync(filePath).isDirectory()) {
- directories.push(file);
- } else if (file.endsWith('.md')) {
- const fileNameKey = file.replace('.md', '').normalize('NFC');
- const relativePath = path.relative(sourceDir, filePath).normalize('NFC')
- const relativePathKey = relativePath.replace('.md', '')
-
- addToFileMap(fileMap, fileNameKey, relativePath);
- if (relativePathKey !== fileNameKey) {
- addToFileMap(fileMap, relativePathKey, relativePath);
- }
- }
- });
-
- directories.forEach((file) => {
- const filePath = path.join(dir, file);
- traverseFilesRecursively(filePath, fileMap);
- })
+ const files = fs.readdirSync(dir, { encoding: 'utf-8' });
+ const directories = [];
+ files.forEach((file) => {
+ const filePath = path.join(dir, file);
+ if (fs.statSync(filePath).isDirectory()) {
+ directories.push(file);
+ } else if (file.endsWith('.md')) {
+ const fileNameKey = file.replace('.md', '').normalize('NFC');
+ const relativePath = path.relative(sourceDir, filePath).normalize('NFC')
+ const relativePathKey = relativePath.replace('.md', '')
+
+ addToFileMap(fileMap, fileNameKey, relativePath);
+ if (relativePathKey !== fileNameKey) {
+ addToFileMap(fileMap, relativePathKey, relativePath);
+ }
+ }
+ });
+
+ directories.forEach((file) => {
+ const filePath = path.join(dir, file);
+ traverseFilesRecursively(filePath, fileMap);
+ })
return fileMap;
}
-let tocMap = {};
-export async function initTocMap() {
- try {
- const response = await fetch("/toc.json");
- tocMap = await response.json();
- } catch (error) {
- console.error(error);
+function normalizeSearchText(markdownText = '') {
+ return markdownText.normalize('NFC').replace(/\s+/g, ' ').trim();
+}
+
+function traverseMarkdownSearchRecursively(dir, searchMap) {
+ const files = fs.readdirSync(dir, { encoding: 'utf-8' });
+ for (const file of files) {
+ const filePath = path.join(dir, file);
+ if (fs.statSync(filePath).isDirectory()) {
+ traverseMarkdownSearchRecursively(filePath, searchMap);
+ continue;
+ }
+
+ if (!file.endsWith('.md')) {
+ continue;
+ }
+
+ const relativePath = path.relative(sourceDir, filePath).normalize('NFC');
+ const markdown = fs.readFileSync(filePath, 'utf-8');
+ searchMap[relativePath] = normalizeSearchText(markdown);
}
+ return searchMap;
}
-export function getTocMap() {
- return tocMap;
+async function buildMarkdownSearchMapFromMarkdownFiles() {
+ const fallbackSearchMap = {};
+ const markdownPathSet = new Set();
+ for (const fileKey in getMarkdownFileMap()) {
+ const markdownPaths = markdownFileMap[fileKey] || [];
+ markdownPaths.forEach((markdownPath) => markdownPathSet.add(markdownPath));
+ }
+
+ const tasks = [...markdownPathSet].map(async (markdownPath) => {
+ try {
+ const response = await fetch('/sources/' + markdownPath);
+ const markdownText = await response.text();
+ fallbackSearchMap[markdownPath] = normalizeSearchText(markdownText);
+ } catch (error) {
+ console.log('Failed to fetch markdown source for search map.', markdownPath, error);
+ }
+ });
+
+ await Promise.all(tasks);
+ return fallbackSearchMap;
}
-
-
+
+let tocMap = {};
+export async function initTocMap() {
+ try {
+ const response = await fetch("/toc.json");
+ tocMap = await response.json();
+ } catch (error) {
+ console.error(error);
+ }
+}
+
+export function getTocMap() {
+ return tocMap;
+}
+
+
let markdownFileMap = {};
export async function initMarkdownFileMap() {
- try {
- const response = await fetch("/" + markdownJsonFilePath)
- markdownFileMap = await response.json();
- } catch (error) {
- console.log('Failed to initialize markdown file map.', error);
- }
+ try {
+ const response = await fetch("/" + markdownJsonFilePath)
+ markdownFileMap = await response.json();
+ } catch (error) {
+ console.log('Failed to initialize markdown file map.', error);
+ }
}
-
+
export function getMarkdownFileMap() {
return markdownFileMap;
}
-export function normalizeIndexFilePath(indexFilePath, sourceDir) {
- if (!indexFilePath) {
- return '';
- }
-
- const normalizedIndexPath = indexFilePath.normalize('NFC');
- if (!sourceDir) {
- return normalizedIndexPath;
- }
-
- const normalizedSourceDir = sourceDir.normalize('NFC').replace(/^\/+|\/+$/g, '');
- const sourcePrefix = `${normalizedSourceDir}/`;
- if (normalizedIndexPath.startsWith(sourcePrefix)) {
- return normalizedIndexPath.substring(sourcePrefix.length);
- }
-
- return normalizedIndexPath;
-}
-
-export function getIndexFilePath() {
- return normalizeIndexFilePath(data.indexFilePath || '', data.sourceDir || '');
-}
-
-let markdownFileSet = null;
-export function getMarkdownFileSet() {
- if (!markdownFileSet) {
- const result = new Set();
- for (const fileKey in getMarkdownFileMap()) {
- markdownFileMap[fileKey].forEach(file => result.add(file));
- }
- markdownFileSet = result;
- }
- return markdownFileSet;
-}
-
-let imageFileMap = {};
-export async function initImageFileMap() {
+let markdownSearchMap = {};
+export async function initMarkdownSearchMap() {
try {
- const response = await fetch("/" + imageJsonFilePath)
- imageFileMap = await response.json();
+ const response = await fetch('/' + markdownSearchJsonFilePath);
+ markdownSearchMap = await response.json();
} catch (error) {
- console.log('Failed to initialize image file map.', error);
+ console.log('Failed to initialize markdown search map.', error);
+ if (Object.keys(markdownFileMap).length === 0) {
+ await initMarkdownFileMap();
+ }
+ markdownSearchMap = await buildMarkdownSearchMapFromMarkdownFiles();
}
}
-export function getImageFileMap() {
- return imageFileMap;
+export function getMarkdownSearchMap() {
+ return markdownSearchMap;
}
-
-export function createImageMapToJson() {
- const fileMap = {};
- traverseImageRecursively(sourceDir, fileMap);
- fs.writeFileSync('public/' + imageJsonFilePath, JSON.stringify(fileMap, null, 2), { encoding: 'utf-8' });
- imageFileMap = fileMap;
-}
-
+
+export function normalizeIndexFilePath(indexFilePath, sourceDir) {
+ if (!indexFilePath) {
+ return '';
+ }
+
+ const normalizedIndexPath = indexFilePath.normalize('NFC');
+ if (!sourceDir) {
+ return normalizedIndexPath;
+ }
+
+ const normalizedSourceDir = sourceDir.normalize('NFC').replace(/^\/+|\/+$/g, '');
+ const sourcePrefix = `${normalizedSourceDir}/`;
+ if (normalizedIndexPath.startsWith(sourcePrefix)) {
+ return normalizedIndexPath.substring(sourcePrefix.length);
+ }
+
+ return normalizedIndexPath;
+}
+
+export function getIndexFilePath() {
+ return normalizeIndexFilePath(data.indexFilePath || '', data.sourceDir || '');
+}
+
+let markdownFileSet = null;
+export function getMarkdownFileSet() {
+ if (!markdownFileSet) {
+ const result = new Set();
+ for (const fileKey in getMarkdownFileMap()) {
+ markdownFileMap[fileKey].forEach(file => result.add(file));
+ }
+ markdownFileSet = result;
+ }
+ return markdownFileSet;
+}
+
+let imageFileMap = {};
+export async function initImageFileMap() {
+ try {
+ const response = await fetch("/" + imageJsonFilePath)
+ imageFileMap = await response.json();
+ } catch (error) {
+ console.log('Failed to initialize image file map.', error);
+ }
+}
+
+export function getImageFileMap() {
+ return imageFileMap;
+}
+
+export function createImageMapToJson() {
+ const fileMap = {};
+ traverseImageRecursively(sourceDir, fileMap);
+ fs.writeFileSync('public/' + imageJsonFilePath, JSON.stringify(fileMap, null, 2), { encoding: 'utf-8' });
+ imageFileMap = fileMap;
+}
+
export function createFileMapToJson() {
- const fileMap = {};
- traverseFilesRecursively(sourceDir, fileMap);
- fs.writeFileSync('public/' + markdownJsonFilePath, JSON.stringify(fileMap, null, 2), { encoding: 'utf-8' });
- markdownFileMap = fileMap;
+ const fileMap = {};
+ traverseFilesRecursively(sourceDir, fileMap);
+ fs.writeFileSync('public/' + markdownJsonFilePath, JSON.stringify(fileMap, null, 2), { encoding: 'utf-8' });
+ markdownFileMap = fileMap;
}
-export function initSourceDirectory() {
- if (!fs.existsSync(sourceDir)) {
- fs.mkdirSync(sourceDir, { recursive: true });
- }
+export function createMarkdownSearchMapToJson() {
+ const searchMap = {};
+ traverseMarkdownSearchRecursively(sourceDir, searchMap);
+ fs.writeFileSync('public/' + markdownSearchJsonFilePath, JSON.stringify(searchMap, null, 2), { encoding: 'utf-8' });
+ markdownSearchMap = searchMap;
}
+
+export function initSourceDirectory() {
+ if (!fs.existsSync(sourceDir)) {
+ fs.mkdirSync(sourceDir, { recursive: true });
+ }
+}
diff --git a/react-app-internal/src/utils/file/fileUtils.test.js b/react-app-internal/src/utils/file/fileUtils.test.js
index b3c1c68..a60c0d3 100644
--- a/react-app-internal/src/utils/file/fileUtils.test.js
+++ b/react-app-internal/src/utils/file/fileUtils.test.js
@@ -1,331 +1,412 @@
-import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
+import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import {
createFileMapToJson,
createImageMapToJson,
+ createMarkdownSearchMapToJson,
getImageFileMap,
getIndexFilePath,
getMarkdownFileMap,
+ getMarkdownSearchMap,
initImageFileMap,
initMarkdownFileMap,
+ initMarkdownSearchMap,
normalizeIndexFilePath,
} from "./fileUtils.js";
-import fs from "fs";
-
+import fs from "fs";
+
const sourceDir = 'public/sources';
const imageJsonFilePath = 'image-files.json';
const markdownJsonFilePath = 'markdown-files.json'
-
-const options = { encoding: 'utf-8' };
-const jsonOf = (expectedFileList) => {return JSON.stringify(expectedFileList, null, 2)};
-
-let mockFiles;
-let expectedFileMap;
-
-beforeAll(() => {
- vi.mock('fs')
- vi.mock('fetch')
- fs.readdirSync.mockImplementation((dir) => {
- return mockFiles[dir] || [];
- })
- fs.statSync.mockImplementation((filePath) => ({
- isDirectory: () => mockFiles[filePath] !== undefined,
- }));
- fs.writeFileSync.mockImplementation(() => {});
- fs.readFileSync.mockImplementation(() => {});
-})
-
-afterAll(() => {
- vi.clearAllMocks();
-})
-
-describe('이미지 맵 JSON 생성 요청 시', () => {
-
- it('이미지 타입의 파일만 맵에 포함된다.', () => {
- // given
- mockFiles = {
- [sourceDir] : ['file1.png', 'file2.txt', 'file3.md', 'file4.pngpng', 'file5.avi'],
- };
- expectedFileMap = {
- 'file1.png' : ['file1.png']
- };
-
- // when
- createImageMapToJson();
-
- // then
- expect(fs.writeFileSync).toHaveBeenCalledWith(
- 'public/' + imageJsonFilePath,
- jsonOf(expectedFileMap),
- options
- );
- });
-
- it('재귀적으로 이미지를 탐색해 맵에 담는다.', () => {
- // given
- mockFiles = {
- [sourceDir] : ['file1.png', 'dir1'],
- [sourceDir + '/dir1']: ['file2.png', 'dir2'],
- [sourceDir + '/dir1/dir2']: ['file3.jpeg', 'dir3'],
- [sourceDir + '/dir1/dir2/dir3']: ['file4.gif'],
- };
- expectedFileMap = {
- 'file1.png': ['file1.png'],
- 'file2.png' : ['dir1/file2.png'],
- 'dir1/file2.png': ['dir1/file2.png'],
- 'file3.jpeg': ['dir1/dir2/file3.jpeg'],
- 'dir1/dir2/file3.jpeg': ['dir1/dir2/file3.jpeg'],
- 'file4.gif': ['dir1/dir2/dir3/file4.gif'],
- 'dir1/dir2/dir3/file4.gif': ['dir1/dir2/dir3/file4.gif'],
- };
-
- // when
- createImageMapToJson()
-
- // then
- expect(fs.writeFileSync).toHaveBeenCalledWith(
- 'public/' + imageJsonFilePath,
- jsonOf(expectedFileMap),
- options
- );
- });
-
- it('파일명이 중복되면 엔트리의 리스트에 삽입된다.', () => {
- // given
- mockFiles = {
- [sourceDir]: ['file1.png', 'dir1'],
- [sourceDir + '/dir1']: ['file1.png'],
- };
- expectedFileMap = {
- 'file1.png' : ['file1.png', 'dir1/file1.png'],
- 'dir1/file1.png' : ['dir1/file1.png'],
- };
-
- // when
- createImageMapToJson();
-
- // then
- expect(fs.writeFileSync).toHaveBeenCalledWith(
- 'public/' + imageJsonFilePath,
- jsonOf(expectedFileMap),
- options
- );
- });
- it('이미지 맵을 업데이트한다.', () => {
- mockFiles = {
- [sourceDir] : ['file1.png'],
- };
- expectedFileMap = {
- 'file1.png' : ['file1.png']
- };
-
- createImageMapToJson();
-
- expect(getImageFileMap()).toStrictEqual(expectedFileMap);
- });
-})
-
-describe('파일 목록 JSON 생성 요청 시', () => {
- it('md 확장자를 가진 파일만 목록에 포함된다.', () => {
- // given
- mockFiles = {
- [sourceDir] : ['file1.md', 'file2.txt', 'file3.png', 'file4.mdmd', 'file5.md6'],
- };
- expectedFileMap = {
- 'file1' : ['file1.md']
- };
-
- // when
- createFileMapToJson();
-
- // then
- expect(fs.writeFileSync).toHaveBeenCalledWith(
- 'public/' + markdownJsonFilePath,
- jsonOf(expectedFileMap),
- options
- );
- });
-
- it('재귀적으로 파일을 탐색해 목록에 포함한다.', () => {
- // given
- mockFiles = {
- [sourceDir]: ['file1.md', 'dir1'],
- [sourceDir + '/dir1']: ['file2.md', 'dir2'],
- [sourceDir + '/dir1/dir2']: ['file3.md', 'dir3'],
- [sourceDir + '/dir1/dir2/dir3']: ['file4.md'],
- };
- expectedFileMap = {
- 'file1' : ['file1.md'],
- 'file2' : ['dir1/file2.md'],
- 'dir1/file2' : ['dir1/file2.md'],
- 'file3' : ['dir1/dir2/file3.md'],
- 'dir1/dir2/file3' : ['dir1/dir2/file3.md'],
- 'file4' : ['dir1/dir2/dir3/file4.md'],
- 'dir1/dir2/dir3/file4' : ['dir1/dir2/dir3/file4.md'],
- };
-
- // when
- createFileMapToJson();
-
- // then
- expect(fs.writeFileSync).toHaveBeenCalledWith(
- 'public/' + markdownJsonFilePath,
- jsonOf(expectedFileMap),
- options
- );
- });
-
- it('파일명이 중복되면 엔트리의 리스트에 삽입된다.', () => {
- // given
- mockFiles = {
- [sourceDir]: ['file1.md', 'dir1'],
- [sourceDir + '/dir1']: ['file1.md'],
- };
- expectedFileMap = {
- 'file1' : ['file1.md', 'dir1/file1.md'],
- 'dir1/file1' : ['dir1/file1.md'],
- };
-
- // when
- createFileMapToJson();
-
- // then
- expect(fs.writeFileSync).toHaveBeenCalledWith(
- 'public/' + markdownJsonFilePath,
- jsonOf(expectedFileMap),
- options
- );
- });
- it('마크다운 맵을 업데이트한다.', () => {
- mockFiles = {
- [sourceDir] : ['file1.md'],
- };
- expectedFileMap = {
- 'file1' : ['file1.md']
- };
-
- createFileMapToJson();
-
- expect(getMarkdownFileMap()).toStrictEqual(expectedFileMap);
- });
-});
-
+const markdownSearchJsonFilePath = 'markdown-search.json'
+
+const options = { encoding: 'utf-8' };
+const jsonOf = (expectedFileList) => {return JSON.stringify(expectedFileList, null, 2)};
+
+let mockFiles;
+let expectedFileMap;
+
+beforeAll(() => {
+ vi.mock('fs')
+ vi.mock('fetch')
+ fs.readdirSync.mockImplementation((dir) => {
+ return mockFiles[dir] || [];
+ })
+ fs.statSync.mockImplementation((filePath) => ({
+ isDirectory: () => mockFiles[filePath] !== undefined,
+ }));
+ fs.writeFileSync.mockImplementation(() => {});
+ fs.readFileSync.mockImplementation(() => {});
+})
+
+afterAll(() => {
+ vi.clearAllMocks();
+})
+
+describe('이미지 맵 JSON 생성 요청 시', () => {
+
+ it('이미지 타입의 파일만 맵에 포함된다.', () => {
+ // given
+ mockFiles = {
+ [sourceDir] : ['file1.png', 'file2.txt', 'file3.md', 'file4.pngpng', 'file5.avi'],
+ };
+ expectedFileMap = {
+ 'file1.png' : ['file1.png']
+ };
+
+ // when
+ createImageMapToJson();
+
+ // then
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ 'public/' + imageJsonFilePath,
+ jsonOf(expectedFileMap),
+ options
+ );
+ });
+
+ it('재귀적으로 이미지를 탐색해 맵에 담는다.', () => {
+ // given
+ mockFiles = {
+ [sourceDir] : ['file1.png', 'dir1'],
+ [sourceDir + '/dir1']: ['file2.png', 'dir2'],
+ [sourceDir + '/dir1/dir2']: ['file3.jpeg', 'dir3'],
+ [sourceDir + '/dir1/dir2/dir3']: ['file4.gif'],
+ };
+ expectedFileMap = {
+ 'file1.png': ['file1.png'],
+ 'file2.png' : ['dir1/file2.png'],
+ 'dir1/file2.png': ['dir1/file2.png'],
+ 'file3.jpeg': ['dir1/dir2/file3.jpeg'],
+ 'dir1/dir2/file3.jpeg': ['dir1/dir2/file3.jpeg'],
+ 'file4.gif': ['dir1/dir2/dir3/file4.gif'],
+ 'dir1/dir2/dir3/file4.gif': ['dir1/dir2/dir3/file4.gif'],
+ };
+
+ // when
+ createImageMapToJson()
+
+ // then
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ 'public/' + imageJsonFilePath,
+ jsonOf(expectedFileMap),
+ options
+ );
+ });
+
+ it('파일명이 중복되면 엔트리의 리스트에 삽입된다.', () => {
+ // given
+ mockFiles = {
+ [sourceDir]: ['file1.png', 'dir1'],
+ [sourceDir + '/dir1']: ['file1.png'],
+ };
+ expectedFileMap = {
+ 'file1.png' : ['file1.png', 'dir1/file1.png'],
+ 'dir1/file1.png' : ['dir1/file1.png'],
+ };
+
+ // when
+ createImageMapToJson();
+
+ // then
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ 'public/' + imageJsonFilePath,
+ jsonOf(expectedFileMap),
+ options
+ );
+ });
+ it('이미지 맵을 업데이트한다.', () => {
+ mockFiles = {
+ [sourceDir] : ['file1.png'],
+ };
+ expectedFileMap = {
+ 'file1.png' : ['file1.png']
+ };
+
+ createImageMapToJson();
+
+ expect(getImageFileMap()).toStrictEqual(expectedFileMap);
+ });
+})
+
+describe('파일 목록 JSON 생성 요청 시', () => {
+ it('md 확장자를 가진 파일만 목록에 포함된다.', () => {
+ // given
+ mockFiles = {
+ [sourceDir] : ['file1.md', 'file2.txt', 'file3.png', 'file4.mdmd', 'file5.md6'],
+ };
+ expectedFileMap = {
+ 'file1' : ['file1.md']
+ };
+
+ // when
+ createFileMapToJson();
+
+ // then
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ 'public/' + markdownJsonFilePath,
+ jsonOf(expectedFileMap),
+ options
+ );
+ });
+
+ it('재귀적으로 파일을 탐색해 목록에 포함한다.', () => {
+ // given
+ mockFiles = {
+ [sourceDir]: ['file1.md', 'dir1'],
+ [sourceDir + '/dir1']: ['file2.md', 'dir2'],
+ [sourceDir + '/dir1/dir2']: ['file3.md', 'dir3'],
+ [sourceDir + '/dir1/dir2/dir3']: ['file4.md'],
+ };
+ expectedFileMap = {
+ 'file1' : ['file1.md'],
+ 'file2' : ['dir1/file2.md'],
+ 'dir1/file2' : ['dir1/file2.md'],
+ 'file3' : ['dir1/dir2/file3.md'],
+ 'dir1/dir2/file3' : ['dir1/dir2/file3.md'],
+ 'file4' : ['dir1/dir2/dir3/file4.md'],
+ 'dir1/dir2/dir3/file4' : ['dir1/dir2/dir3/file4.md'],
+ };
+
+ // when
+ createFileMapToJson();
+
+ // then
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ 'public/' + markdownJsonFilePath,
+ jsonOf(expectedFileMap),
+ options
+ );
+ });
+
+ it('파일명이 중복되면 엔트리의 리스트에 삽입된다.', () => {
+ // given
+ mockFiles = {
+ [sourceDir]: ['file1.md', 'dir1'],
+ [sourceDir + '/dir1']: ['file1.md'],
+ };
+ expectedFileMap = {
+ 'file1' : ['file1.md', 'dir1/file1.md'],
+ 'dir1/file1' : ['dir1/file1.md'],
+ };
+
+ // when
+ createFileMapToJson();
+
+ // then
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ 'public/' + markdownJsonFilePath,
+ jsonOf(expectedFileMap),
+ options
+ );
+ });
+ it('마크다운 맵을 업데이트한다.', () => {
+ mockFiles = {
+ [sourceDir] : ['file1.md'],
+ };
+ expectedFileMap = {
+ 'file1' : ['file1.md']
+ };
+
+ createFileMapToJson();
+
+ expect(getMarkdownFileMap()).toStrictEqual(expectedFileMap);
+ });
+});
+
describe('파일 맵 초기화 요청 시', () => {
+ beforeEach(() => {
+ global.fetch = vi.fn(() => {
+ return Promise.resolve({
+ json: () => Promise.resolve(expectedFileMap)
+ });
+ }
+ )
+ })
+
+ it('fetch를 통해 마크다운 JSON 파일을 로드한다.', async () => {
+ expectedFileMap = {'file1': 'file1'};
+ await initMarkdownFileMap()
+ expect(fetch).toHaveBeenCalledWith('/' + markdownJsonFilePath)
+ });
+
+ it('fetch에 성공하면 내부 객체를 업데이트한다.', async () => {
+ expectedFileMap = {'file2': 'file2'};
+ await initMarkdownFileMap()
+ const markdownFileMap = getMarkdownFileMap()
+ expect(markdownFileMap).toEqual(expectedFileMap)
+ });
+
+ it('fetch에 실패하면 내부 객체를 업데이트하지 않는다.', async () => {
+ expectedFileMap = {'file3': 'file3'};
+ global.fetch = vi.fn(() => Promise.reject(new Error('Fetch error')));
+ await initMarkdownFileMap()
+ const markdownFileMap = getMarkdownFileMap()
+ expect(markdownFileMap).not.toBe(expectedFileMap)
+ });
+});
+
+describe('검색 맵 초기화 요청 시', () => {
beforeEach(() => {
- global.fetch = vi.fn(() => {
+ global.fetch = vi.fn((url) => {
+ if (url === '/' + markdownSearchJsonFilePath) {
+ return Promise.resolve({
+ json: () => Promise.resolve({'doc.md': 'cached index'})
+ });
+ }
+ if (url === '/doc.md') {
+ return Promise.resolve({
+ text: () => Promise.resolve('apple and 안녕하세요')
+ });
+ }
return Promise.resolve({
- json: () => Promise.resolve(expectedFileMap)
- });
- }
- )
- })
-
- it('fetch를 통해 마크다운 JSON 파일을 로드한다.', async () => {
- expectedFileMap = {'file1': 'file1'};
- await initMarkdownFileMap()
- expect(fetch).toHaveBeenCalledWith('/' + markdownJsonFilePath)
+ json: () => Promise.resolve(expectedFileMap),
+ text: () => Promise.resolve(''),
+ });
+ });
});
-
- it('fetch에 성공하면 내부 객체를 업데이트한다.', async () => {
- expectedFileMap = {'file2': 'file2'};
- await initMarkdownFileMap()
- const markdownFileMap = getMarkdownFileMap()
- expect(markdownFileMap).toEqual(expectedFileMap)
- });
-
- it('fetch에 실패하면 내부 객체를 업데이트하지 않는다.', async () => {
- expectedFileMap = {'file3': 'file3'};
- global.fetch = vi.fn(() => Promise.reject(new Error('Fetch error')));
- await initMarkdownFileMap()
- const markdownFileMap = getMarkdownFileMap()
- expect(markdownFileMap).not.toBe(expectedFileMap)
- });
-});
-describe('이미지 맵 초기화 요청 시', () => {
- let expectedImageMap = {'default': 'default'};
- beforeEach(() => {
- global.fetch = vi.fn(() => {
- return Promise.resolve({
- json: () => Promise.resolve(expectedImageMap)
- });
- }
- )
- })
-
- it('fetch를 통해 이미지 JSON 파일을 로드한다.', async () => {
- expectedImageMap = {'image1': 'image1'};
- await initImageFileMap()
- expect(fetch).toHaveBeenCalledWith('/' + imageJsonFilePath)
+ it('검색 맵 JSON fetch 성공 시 내부 검색 맵을 업데이트한다.', async () => {
+ await initMarkdownSearchMap();
+ expect(getMarkdownSearchMap()).toEqual({'doc.md': 'cached index'});
});
-
- it('fetch에 성공하면 내부 객체를 업데이트한다.', async () => {
- expectedImageMap = {'image2': 'image2'};
- await initImageFileMap()
- const imageFileMap = getImageFileMap()
- expect(imageFileMap).toEqual(expectedImageMap)
- });
-
- it('fetch에 실패하면 내부 객체를 업데이트하지 않는다.', async () => {
- expectedImageMap = {'image3': 'image3'};
- global.fetch = vi.fn(() => Promise.reject(new Error('Fetch error')));
- await initImageFileMap()
- const imageFileMap = getImageFileMap()
- expect(imageFileMap).not.toBe(expectedFileMap)
- });
-});
-describe('파일 셋 조회 요청 시', () => {
- beforeEach(() => {
+ it('검색 맵 JSON fetch 실패 시 마크다운 파일 본문 fetch로 검색 맵을 구성한다.', async () => {
vi.resetModules();
- global.fetch = vi.fn(() => {
+ global.fetch = vi.fn((url) => {
+ if (url === '/' + markdownSearchJsonFilePath) {
+ return Promise.reject(new Error('missing index'));
+ }
+ if (url === '/' + markdownJsonFilePath) {
return Promise.resolve({
- json: () => Promise.resolve(expectedFileMap)
+ json: () => Promise.resolve({doc: ['doc.md']})
});
}
- )
- })
-
- let expectedFileSet;
- it('파일 셋이 초기화되지 않은 경우 파일 맵을 전체 순회하며 파일 셋에 담는다.', async () => {
- const { getMarkdownFileSet, initMarkdownFileMap } = await import('./fileUtils.js')
- expectedFileMap = {'file1': ['file1', 'dir/file1'], 'dir/file1': ['dir/file1']}
- expectedFileSet = new Set(['file1', 'dir/file1'])
- await initMarkdownFileMap()
-
- const resultSet = getMarkdownFileSet()
-
- expect(resultSet).toStrictEqual(expectedFileSet);
- });
-
- it('파일 셋이 초기화된 경우 캐시된 파일 셋을 반환한다.', async () => {
- const { getMarkdownFileSet, initMarkdownFileMap } = await import('./fileUtils.js')
- expectedFileMap = {}
- await initMarkdownFileMap()
- const initSet = getMarkdownFileSet()
+ if (url === '/sources/doc.md') {
+ return Promise.resolve({
+ text: () => Promise.resolve('apple and 안녕하세요')
+ });
+ }
+ return Promise.resolve({
+ json: () => Promise.resolve({}),
+ text: () => Promise.resolve(''),
+ });
+ });
- expectedFileMap = {'file2': ['file2', 'dir/file2'], 'dir/file2': ['dir/file2']}
- expectedFileSet = new Set(['file2', 'dir/file2'])
- await initMarkdownFileMap()
-
- const cachedSet = getMarkdownFileSet()
-
- expect(cachedSet).not.toStrictEqual(expectedFileSet);
- expect(cachedSet).toStrictEqual(initSet);
+ const {initMarkdownFileMap, initMarkdownSearchMap, getMarkdownSearchMap} = await import('./fileUtils.js');
+ await initMarkdownFileMap();
+ await initMarkdownSearchMap();
+
+ expect(getMarkdownSearchMap()).toEqual({'doc.md': 'apple and 안녕하세요'});
});
});
-describe('index 파일 경로 정규화 요청 시', () => {
- it('sourceDir를 포함한 index 경로를 source 기준 상대 경로로 정규화한다.', () => {
- const normalized = normalizeIndexFilePath('!new-blog/index.md', '!new-blog');
- expect(normalized).toBe('index.md');
- });
+describe('검색 맵 JSON 생성 요청 시', () => {
+ it('재귀적으로 마크다운 파일 본문을 탐색해 검색 맵 JSON을 생성한다.', () => {
+ mockFiles = {
+ [sourceDir]: ['doc.md'],
+ };
+ fs.readFileSync.mockImplementation((filePath) => {
+ if (filePath === sourceDir + '/doc.md') {
+ return ' apple\n\n안녕하세요 ';
+ }
+ return '';
+ });
- it('이미 source 기준 상대 경로면 그대로 반환한다.', () => {
- const normalized = normalizeIndexFilePath('index.md', '!new-blog');
- expect(normalized).toBe('index.md');
- });
+ createMarkdownSearchMapToJson();
- it('설정 데이터에 index 경로가 없으면 빈 값을 반환한다.', () => {
- expect(getIndexFilePath()).toBe('');
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
+ 'public/' + markdownSearchJsonFilePath,
+ jsonOf({'doc.md': 'apple 안녕하세요'}),
+ options,
+ );
});
});
+
+describe('이미지 맵 초기화 요청 시', () => {
+ let expectedImageMap = {'default': 'default'};
+ beforeEach(() => {
+ global.fetch = vi.fn(() => {
+ return Promise.resolve({
+ json: () => Promise.resolve(expectedImageMap)
+ });
+ }
+ )
+ })
+
+ it('fetch를 통해 이미지 JSON 파일을 로드한다.', async () => {
+ expectedImageMap = {'image1': 'image1'};
+ await initImageFileMap()
+ expect(fetch).toHaveBeenCalledWith('/' + imageJsonFilePath)
+ });
+
+ it('fetch에 성공하면 내부 객체를 업데이트한다.', async () => {
+ expectedImageMap = {'image2': 'image2'};
+ await initImageFileMap()
+ const imageFileMap = getImageFileMap()
+ expect(imageFileMap).toEqual(expectedImageMap)
+ });
+
+ it('fetch에 실패하면 내부 객체를 업데이트하지 않는다.', async () => {
+ expectedImageMap = {'image3': 'image3'};
+ global.fetch = vi.fn(() => Promise.reject(new Error('Fetch error')));
+ await initImageFileMap()
+ const imageFileMap = getImageFileMap()
+ expect(imageFileMap).not.toBe(expectedFileMap)
+ });
+});
+
+describe('파일 셋 조회 요청 시', () => {
+ beforeEach(() => {
+ vi.resetModules();
+ global.fetch = vi.fn(() => {
+ return Promise.resolve({
+ json: () => Promise.resolve(expectedFileMap)
+ });
+ }
+ )
+ })
+
+ let expectedFileSet;
+ it('파일 셋이 초기화되지 않은 경우 파일 맵을 전체 순회하며 파일 셋에 담는다.', async () => {
+ const { getMarkdownFileSet, initMarkdownFileMap } = await import('./fileUtils.js')
+ expectedFileMap = {'file1': ['file1', 'dir/file1'], 'dir/file1': ['dir/file1']}
+ expectedFileSet = new Set(['file1', 'dir/file1'])
+ await initMarkdownFileMap()
+
+ const resultSet = getMarkdownFileSet()
+
+ expect(resultSet).toStrictEqual(expectedFileSet);
+ });
+
+ it('파일 셋이 초기화된 경우 캐시된 파일 셋을 반환한다.', async () => {
+ const { getMarkdownFileSet, initMarkdownFileMap } = await import('./fileUtils.js')
+ expectedFileMap = {}
+ await initMarkdownFileMap()
+ const initSet = getMarkdownFileSet()
+
+ expectedFileMap = {'file2': ['file2', 'dir/file2'], 'dir/file2': ['dir/file2']}
+ expectedFileSet = new Set(['file2', 'dir/file2'])
+ await initMarkdownFileMap()
+
+ const cachedSet = getMarkdownFileSet()
+
+ expect(cachedSet).not.toStrictEqual(expectedFileSet);
+ expect(cachedSet).toStrictEqual(initSet);
+ });
+});
+
+describe('index 파일 경로 정규화 요청 시', () => {
+ it('sourceDir를 포함한 index 경로를 source 기준 상대 경로로 정규화한다.', () => {
+ const normalized = normalizeIndexFilePath('!new-blog/index.md', '!new-blog');
+ expect(normalized).toBe('index.md');
+ });
+
+ it('이미 source 기준 상대 경로면 그대로 반환한다.', () => {
+ const normalized = normalizeIndexFilePath('index.md', '!new-blog');
+ expect(normalized).toBe('index.md');
+ });
+
+ it('설정 데이터에 index 경로가 없으면 빈 값을 반환한다.', () => {
+ expect(getIndexFilePath()).toBe('');
+ });
+});
diff --git a/react-app-internal/src/utils/treeUtils.jsx b/react-app-internal/src/utils/treeUtils.jsx
index 1f92612..caffb88 100644
--- a/react-app-internal/src/utils/treeUtils.jsx
+++ b/react-app-internal/src/utils/treeUtils.jsx
@@ -1,109 +1,116 @@
import React from 'react'
-import {getIndexFilePath, getMarkdownFileSet} from "./file/fileUtils.js";
+import {getIndexFilePath, getMarkdownFileSet, getMarkdownSearchMap} from "./file/fileUtils.js";
import TreeItem from "../components/TreeItem.jsx";
-const calculateCounts = (node) => {
- let total = 0;
- Object.values(node).forEach((child) => {
- if (child.children && !child.isFile) {
- child.count += calculateCounts(child.children);
- }
- total += child.count;
- });
- return total;
-};
-
-const buildTree = (paths) => {
- const root = {};
- paths.forEach((path) => {
- const parts = path.split('/');
- let current = root;
- parts.forEach((part, index) => {
- if (!current[part]) {
- current[part] = {
- children: {},
- count: 0,
- isFile: index === parts.length - 1,
- };
- }
- if (index === parts.length - 1) {
- current[part].count += 1;
- }
- current = current[part].children;
- });
- });
-
- const tree = {};
- tree.children = root;
- tree.count = calculateCounts(root);
- return tree;
-};
-
+export const MIN_SEARCH_KEYWORD_LENGTH = 2;
+
+const calculateCounts = (node) => {
+ let total = 0;
+ Object.values(node).forEach((child) => {
+ if (child.children && !child.isFile) {
+ child.count += calculateCounts(child.children);
+ }
+ total += child.count;
+ });
+ return total;
+};
+
+const buildTree = (paths) => {
+ const root = {};
+ paths.forEach((path) => {
+ const parts = path.split('/');
+ let current = root;
+ parts.forEach((part, index) => {
+ if (!current[part]) {
+ current[part] = {
+ children: {},
+ count: 0,
+ isFile: index === parts.length - 1,
+ };
+ }
+ if (index === parts.length - 1) {
+ current[part].count += 1;
+ }
+ current = current[part].children;
+ });
+ });
+
+ const tree = {};
+ tree.children = root;
+ tree.count = calculateCounts(root);
+ return tree;
+};
+
export const filterPathsByKeyword = (paths, keyword = '') => {
const normalizedKeyword = keyword.normalize('NFC').trim().toLowerCase();
if (!normalizedKeyword) {
return new Set(paths);
}
+ if (normalizedKeyword.length < MIN_SEARCH_KEYWORD_LENGTH) {
+ return new Set();
+ }
+ const markdownSearchMap = getMarkdownSearchMap() || {};
const result = new Set();
for (const path of paths) {
const normalizedPath = path.normalize('NFC').toLowerCase();
- if (normalizedPath.includes(normalizedKeyword)) {
+ const markdownContent = (markdownSearchMap[path] || '').normalize('NFC').toLowerCase();
+ if (normalizedPath.includes(normalizedKeyword) || markdownContent.includes(normalizedKeyword)) {
result.add(path);
}
}
- return result;
-}
-
-export const initTree = (indexMarkdownPath = getIndexFilePath(), searchKeyword = '') => {
- const fileSet = new Set(getMarkdownFileSet());
- const indexFilePath = (indexMarkdownPath || '').normalize('NFC');
- if (indexFilePath) {
- fileSet.delete(indexFilePath);
- }
-
- const filteredPaths = filterPathsByKeyword(fileSet, searchKeyword);
- return buildTree(filteredPaths)
-}
-
-export const renderTree = (nodes, basePath = '', compareFn = () => {}) => {
- const sortedNodes = Object.entries(nodes).sort(compareFn)
- return (
- <>
- {/* TODO 아마 크기 조절용인듯? 크기 조절 기능은 나중에 추가*/}
-
- {
- sortedNodes.map(([key, value]) => {
- const path = `${basePath}/${key}`;
- return (
-
- {!value.isFile ? (
-
- {renderTree(value.children, path, compareFn)}
-
- ) : (
-
- )}
-
- );
- }
- )
- }
- >
- )
-};
-
-export const markUsedPaths = (path = location.pathname) => {
- const usedPaths = {};
- const currentPath = decodeURIComponent(path);
- if (currentPath) {
- const pathParts = currentPath.split('/').filter(Boolean);
- let subPath = '';
- pathParts.forEach((part) => {
- subPath += `/${part}`;
- usedPaths[subPath] = true;
- });
- return usedPaths
- }
- return usedPaths
-}
+ return result;
+}
+
+export const initTree = (indexMarkdownPath = getIndexFilePath(), searchKeyword = '') => {
+ const fileSet = new Set(getMarkdownFileSet());
+ const indexFilePath = (indexMarkdownPath || '').normalize('NFC');
+ if (indexFilePath) {
+ fileSet.delete(indexFilePath);
+ }
+
+ const filteredPaths = filterPathsByKeyword(fileSet, searchKeyword);
+ return buildTree(filteredPaths)
+}
+
+export const renderTree = (nodes, basePath = '', compareFn = () => {}) => {
+ const sortedNodes = Object.entries(nodes).sort(compareFn)
+ return (
+ <>
+ {/* TODO 아마 크기 조절용인듯? 크기 조절 기능은 나중에 추가*/}
+
+ {
+ sortedNodes.map(([key, value]) => {
+ const path = `${basePath}/${key}`;
+ return (
+
+ {!value.isFile ? (
+
+ {renderTree(value.children, path, compareFn)}
+
+ ) : (
+
+ )}
+
+ );
+ }
+ )
+ }
+ >
+ )
+};
+
+export const markUsedPaths = (path = location.pathname) => {
+ const usedPaths = {};
+ const currentPath = decodeURIComponent(path);
+ if (currentPath) {
+ const pathParts = currentPath.split('/').filter(Boolean);
+ let subPath = '';
+ pathParts.forEach((part) => {
+ subPath += `/${part}`;
+ usedPaths[subPath] = true;
+ });
+ return usedPaths
+ }
+ return usedPaths
+}
diff --git a/react-app-internal/src/utils/treeUtils.test.jsx b/react-app-internal/src/utils/treeUtils.test.jsx
index e851b26..aa4a396 100644
--- a/react-app-internal/src/utils/treeUtils.test.jsx
+++ b/react-app-internal/src/utils/treeUtils.test.jsx
@@ -1,133 +1,136 @@
-import {afterAll, beforeAll, describe, expect, it, vi} from "vitest";
+import {afterAll, beforeAll, describe, expect, it, vi} from "vitest";
import {initTree, markUsedPaths, renderTree} from "./treeUtils.jsx";
-import {render, screen} from "@testing-library/react";
-import {MemoryRouter} from "react-router-dom";
-
+import {render, screen} from "@testing-library/react";
+import {MemoryRouter} from "react-router-dom";
+
let fileSet;
+let searchContentMap;
let expectedTree;
-
-beforeAll(() => {
+
+beforeAll(() => {
vi.mock('./file/fileUtils.js', () => {
return {
getMarkdownFileSet: () => fileSet,
+ getMarkdownSearchMap: () => searchContentMap,
getIndexFilePath: () => '',
}
})
- vi.mock('../components/TreeItem', () => ({
- default: ({title, children}) => }))
-});
-
-afterAll(() => {
- vi.clearAllMocks();
-})
-
-describe('initTree 호출 시', () => {
- it('조회한 파일 셋의 내용으로 트리를 구축한다.', () => {
- fileSet = new Set(['file1']);
- expectedTree = {
- children: {
- 'file1' : {children: {}, count: 1, isFile: true},
- },
- count: fileSet.size,
- }
- const tree = initTree();
- expect(tree.count).toBe(fileSet.size);
- expect(tree).toStrictEqual(expectedTree)
- });
-
- it('/를 기준으로 path 정보를 분할해 트리를 구성한다.', () => {
- fileSet = new Set(['dir/file1']);
- expectedTree = {
- children: {
- 'dir': {
- children: {
- 'file1' : {children: {}, count: 1, isFile: true},
- },
- count: 1,
- isFile: false
- },
- },
- count: fileSet.size,
- }
- const tree = initTree();
- expect(tree.count).toBe(fileSet.size);
- expect(tree).toStrictEqual(expectedTree)
- });
-
- it('트리 노드가 디렉토리면 자식 정보와 자식의 수가 포함된다.', () => {
- fileSet = new Set(['dir/file2', 'dir/file3']);
- expectedTree = {
- children: {
- 'dir': {
- children: {
- 'file2' : {children: {}, count: 1, isFile: true},
- 'file3' : {children: {}, count: 1, isFile: true}
- },
- count: 2,
- isFile: false
- },
- },
- count: fileSet.size,
- }
- const tree = initTree();
- expect(tree.count).toBe(fileSet.size);
- expect(tree).toStrictEqual(expectedTree)
- });
-
- it('자식의 수를 계산할 때 중첩된 자식의 수까지 모두 계산한다.', () => {
- fileSet = new Set([
- 'dir1/file1',
- 'dir1/dir2/file2',
- ]);
- expectedTree = {
- children: {
-
- 'dir1': {
- children: {
- 'dir2': {
- children: {
- 'file2': {children: {}, count: 1, isFile: true},
- },
- count: 1, isFile: false,
- },
- 'file1': {children: {}, count: 1, isFile: true},
- },
- count: 2,
- isFile: false,
- },
- },
- 'count': fileSet.size,
- };
- const tree = initTree();
- expect(tree.count).toBe(fileSet.size);
- expect(tree).toStrictEqual(expectedTree)
- });
-
- it('index 파일 경로가 전달되면 해당 파일은 트리에서 제외한다.', () => {
- fileSet = new Set(['index.md', 'posts/post-1.md']);
- expectedTree = {
- children: {
- posts: {
- children: {
- 'post-1.md': {children: {}, count: 1, isFile: true},
- },
- count: 1,
- isFile: false,
- },
- },
- count: 1,
- };
-
- const tree = initTree('index.md');
- expect(tree.count).toBe(1);
- expect(tree).toStrictEqual(expectedTree);
- });
-
- it('검색어가 전달되면 해당 키워드를 포함한 경로만 트리에 포함한다.', () => {
+ vi.mock('../components/TreeItem', () => ({
+ default: ({title, children}) => }))
+});
+
+afterAll(() => {
+ vi.clearAllMocks();
+})
+
+describe('initTree 호출 시', () => {
+ it('조회한 파일 셋의 내용으로 트리를 구축한다.', () => {
+ fileSet = new Set(['file1']);
+ expectedTree = {
+ children: {
+ 'file1' : {children: {}, count: 1, isFile: true},
+ },
+ count: fileSet.size,
+ }
+ const tree = initTree();
+ expect(tree.count).toBe(fileSet.size);
+ expect(tree).toStrictEqual(expectedTree)
+ });
+
+ it('/를 기준으로 path 정보를 분할해 트리를 구성한다.', () => {
+ fileSet = new Set(['dir/file1']);
+ expectedTree = {
+ children: {
+ 'dir': {
+ children: {
+ 'file1' : {children: {}, count: 1, isFile: true},
+ },
+ count: 1,
+ isFile: false
+ },
+ },
+ count: fileSet.size,
+ }
+ const tree = initTree();
+ expect(tree.count).toBe(fileSet.size);
+ expect(tree).toStrictEqual(expectedTree)
+ });
+
+ it('트리 노드가 디렉토리면 자식 정보와 자식의 수가 포함된다.', () => {
+ fileSet = new Set(['dir/file2', 'dir/file3']);
+ expectedTree = {
+ children: {
+ 'dir': {
+ children: {
+ 'file2' : {children: {}, count: 1, isFile: true},
+ 'file3' : {children: {}, count: 1, isFile: true}
+ },
+ count: 2,
+ isFile: false
+ },
+ },
+ count: fileSet.size,
+ }
+ const tree = initTree();
+ expect(tree.count).toBe(fileSet.size);
+ expect(tree).toStrictEqual(expectedTree)
+ });
+
+ it('자식의 수를 계산할 때 중첩된 자식의 수까지 모두 계산한다.', () => {
+ fileSet = new Set([
+ 'dir1/file1',
+ 'dir1/dir2/file2',
+ ]);
+ expectedTree = {
+ children: {
+
+ 'dir1': {
+ children: {
+ 'dir2': {
+ children: {
+ 'file2': {children: {}, count: 1, isFile: true},
+ },
+ count: 1, isFile: false,
+ },
+ 'file1': {children: {}, count: 1, isFile: true},
+ },
+ count: 2,
+ isFile: false,
+ },
+ },
+ 'count': fileSet.size,
+ };
+ const tree = initTree();
+ expect(tree.count).toBe(fileSet.size);
+ expect(tree).toStrictEqual(expectedTree)
+ });
+
+ it('index 파일 경로가 전달되면 해당 파일은 트리에서 제외한다.', () => {
+ fileSet = new Set(['index.md', 'posts/post-1.md']);
+ expectedTree = {
+ children: {
+ posts: {
+ children: {
+ 'post-1.md': {children: {}, count: 1, isFile: true},
+ },
+ count: 1,
+ isFile: false,
+ },
+ },
+ count: 1,
+ };
+
+ const tree = initTree('index.md');
+ expect(tree.count).toBe(1);
+ expect(tree).toStrictEqual(expectedTree);
+ });
+
+ it('검색어가 전달되면 해당 키워드를 포함한 경로만 트리에 포함한다.', () => {
fileSet = new Set([
'Development/Frontend/React/useState.md',
'Development/Backend/Java/ArrayList.md',
]);
+ searchContentMap = {};
const tree = initTree('', 'react');
expect(tree.count).toBe(1);
@@ -135,101 +138,139 @@ describe('initTree 호출 시', () => {
expect(tree.children.Development.children.Backend).toBeUndefined();
});
-});
+ it('검색어 길이가 2글자 미만이면 검색 결과를 표시하지 않는다.', () => {
+ fileSet = new Set([
+ 'Development/Frontend/React/useState.md',
+ 'Development/Backend/Java/ArrayList.md',
+ ]);
+ searchContentMap = {};
-describe('renderTree 호출 시', () => {
- let inputTree;
- it('인자로 넘겨진 트리 노드가 컴포넌트로 렌더링된다.', () => {
- const nodeTitle1 = 'file1';
- const nodeTitle2 = 'file2';
- inputTree = {
- [nodeTitle1] : {children: {}, count: 1, isFile: true},
- [nodeTitle2] : {children: {}, count: 1, isFile: true},
- }
- render({renderTree(inputTree)})
- expect(screen.getByText(nodeTitle1)).toBeInTheDocument();
- expect(screen.getByText(nodeTitle2)).toBeInTheDocument();
- });
-
- it('트리 노드가 파일 노드가 아니면 자식의 수가 타이틀에 표시된다.', () => {
- const nodeKey = 'dir'
- const nodeCount = 3;
- inputTree = {
- [nodeKey] : {
- children: {},
- count: nodeCount,
- isFile: false
- },
- }
- const expectedTitle = `${nodeKey} (${nodeCount})`
- render({renderTree(inputTree)})
- expect(screen.getByText(expectedTitle)).toBeInTheDocument();
- });
-
-
- it('트리 노드가 파일 노드가 아니면 재귀적으로 렌더링된다.', () => {
- inputTree = {
- 'dir1' : {
- children: {
- 'dir2': {
- children: {
- 'file2': {children: {}, count: 1, isFile: true},
- },
- count: 1,
- isFile: false
- },
- 'file1': {children: {}, count: 1, isFile: true},
- },
- count: 1,
- isFile: false
- },
- }
-
- render({renderTree(inputTree)})
- const dir1Children = screen.getByText('dir1 (1)').nextSibling;
- const file1 = screen.getByText('file1');
- const dir2 = screen.getByText('dir2 (1)');
- const dir2Children = dir2.nextSibling;
- const file2 = screen.getByText('file2');
- expect(dir1Children).toContainElement(file1);
- expect(dir1Children).toContainElement(dir2);
- expect(dir1Children).toContainElement(file2);
- expect(dir2Children).toContainElement(file2);
- expect(dir2Children).not.toContainElement(file1);
- });
-
- it('인자로 전달된 정렬 기준으로 정렬된 트리로 렌더링한다.', () => {
- inputTree = {
- '1' : {children: {}, count: 1, isFile: true},
- '2' : {children: {}, count: 1, isFile: true},
- }
- render({renderTree(inputTree, '', ([k1], [k2]) => {
- return k2.localeCompare(k1);
- })})
- const el1 = screen.getByText('1');
- const el2 = screen.getByText('2');
- const el1Parent = el1.parentElement;
- const el2Parent = el2.parentElement;
- const ancestor = el1Parent.parentElement;
- expect(ancestor.children[1]).toBe(el2Parent)
- expect(ancestor.children[2]).toBe(el1Parent)
- });
-});
+ const tree = initTree('', 'r');
+ expect(tree.count).toBe(0);
+ expect(tree.children).toStrictEqual({});
+ });
+
+ it('검색어가 문서 본문에 부분 일치하면 해당 문서를 검색 결과에 포함한다.', () => {
+ fileSet = new Set(['posts/greeting.md', 'posts/other.md']);
+ searchContentMap = {
+ 'posts/greeting.md': '오늘의 인사말은 안녕하세요 입니다.',
+ 'posts/other.md': '테스트 문서입니다.',
+ };
+
+ const tree = initTree('', '하세');
+ expect(tree.count).toBe(1);
+ expect(tree.children.posts.children['greeting.md']).toBeDefined();
+ expect(tree.children.posts.children['other.md']).toBeUndefined();
+ });
+
+ it('영문 검색어가 문서 본문에 부분 일치하면 해당 문서를 검색 결과에 포함한다.', () => {
+ fileSet = new Set(['posts/doc-1.md', 'posts/doc-2.md']);
+ searchContentMap = {
+ 'posts/doc-1.md': 'I like green apple pie.',
+ 'posts/doc-2.md': 'Banana bread recipe.',
+ };
+
+ const tree = initTree('', 'pl');
+ expect(tree.count).toBe(1);
+ expect(tree.children.posts.children['doc-1.md']).toBeDefined();
+ expect(tree.children.posts.children['doc-2.md']).toBeUndefined();
+ });
-describe('markUsedPaths 호출 시', () => {
- it('현재 경로가 비어있다면 빈 오브젝트를 반환한다', () => {
- const result = markUsedPaths('');
- expect(result).toStrictEqual({})
- });
-
- it('현재 경로가 비어있지 않다면 모든 서브 경로를 담은 오브젝트를 반환한다', () => {
- const path = '/A/B/C';
- const expectedObject = {
- '/A': true,
- '/A/B': true,
- '/A/B/C': true
- }
- const result = markUsedPaths(path);
- expect(result).toStrictEqual(expectedObject)
- });
});
+
+describe('renderTree 호출 시', () => {
+ let inputTree;
+ it('인자로 넘겨진 트리 노드가 컴포넌트로 렌더링된다.', () => {
+ const nodeTitle1 = 'file1';
+ const nodeTitle2 = 'file2';
+ inputTree = {
+ [nodeTitle1] : {children: {}, count: 1, isFile: true},
+ [nodeTitle2] : {children: {}, count: 1, isFile: true},
+ }
+ render({renderTree(inputTree)})
+ expect(screen.getByText(nodeTitle1)).toBeInTheDocument();
+ expect(screen.getByText(nodeTitle2)).toBeInTheDocument();
+ });
+
+ it('트리 노드가 파일 노드가 아니면 자식의 수가 타이틀에 표시된다.', () => {
+ const nodeKey = 'dir'
+ const nodeCount = 3;
+ inputTree = {
+ [nodeKey] : {
+ children: {},
+ count: nodeCount,
+ isFile: false
+ },
+ }
+ const expectedTitle = `${nodeKey} (${nodeCount})`
+ render({renderTree(inputTree)})
+ expect(screen.getByText(expectedTitle)).toBeInTheDocument();
+ });
+
+
+ it('트리 노드가 파일 노드가 아니면 재귀적으로 렌더링된다.', () => {
+ inputTree = {
+ 'dir1' : {
+ children: {
+ 'dir2': {
+ children: {
+ 'file2': {children: {}, count: 1, isFile: true},
+ },
+ count: 1,
+ isFile: false
+ },
+ 'file1': {children: {}, count: 1, isFile: true},
+ },
+ count: 1,
+ isFile: false
+ },
+ }
+
+ render({renderTree(inputTree)})
+ const dir1Children = screen.getByText('dir1 (1)').nextSibling;
+ const file1 = screen.getByText('file1');
+ const dir2 = screen.getByText('dir2 (1)');
+ const dir2Children = dir2.nextSibling;
+ const file2 = screen.getByText('file2');
+ expect(dir1Children).toContainElement(file1);
+ expect(dir1Children).toContainElement(dir2);
+ expect(dir1Children).toContainElement(file2);
+ expect(dir2Children).toContainElement(file2);
+ expect(dir2Children).not.toContainElement(file1);
+ });
+
+ it('인자로 전달된 정렬 기준으로 정렬된 트리로 렌더링한다.', () => {
+ inputTree = {
+ '1' : {children: {}, count: 1, isFile: true},
+ '2' : {children: {}, count: 1, isFile: true},
+ }
+ render({renderTree(inputTree, '', ([k1], [k2]) => {
+ return k2.localeCompare(k1);
+ })})
+ const el1 = screen.getByText('1');
+ const el2 = screen.getByText('2');
+ const el1Parent = el1.parentElement;
+ const el2Parent = el2.parentElement;
+ const ancestor = el1Parent.parentElement;
+ expect(ancestor.children[1]).toBe(el2Parent)
+ expect(ancestor.children[2]).toBe(el1Parent)
+ });
+});
+
+describe('markUsedPaths 호출 시', () => {
+ it('현재 경로가 비어있다면 빈 오브젝트를 반환한다', () => {
+ const result = markUsedPaths('');
+ expect(result).toStrictEqual({})
+ });
+
+ it('현재 경로가 비어있지 않다면 모든 서브 경로를 담은 오브젝트를 반환한다', () => {
+ const path = '/A/B/C';
+ const expectedObject = {
+ '/A': true,
+ '/A/B': true,
+ '/A/B/C': true
+ }
+ const result = markUsedPaths(path);
+ expect(result).toStrictEqual(expectedObject)
+ });
+});
diff --git a/react-app-internal/vite.config.js b/react-app-internal/vite.config.js
index c7e9efa..c88b990 100644
--- a/react-app-internal/vite.config.js
+++ b/react-app-internal/vite.config.js
@@ -1,33 +1,39 @@
import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react'
import generateHtmlFiles from "./src/generateHtml.js";
-import {createFileMapToJson, createImageMapToJson, initSourceDirectory,} from "./src/utils/file/fileUtils.js";
-import {viteStaticCopy} from "vite-plugin-static-copy";
-
-export default defineConfig({
- plugins: [
- react(),
- viteStaticCopy({
- targets: [
- {
- src: 'public/*',
- dest: '',
- }
- ],
- }),
- {
- name: 'pre-build-plugin',
- async buildStart() {
+import {
+ createFileMapToJson,
+ createImageMapToJson,
+ createMarkdownSearchMapToJson,
+ initSourceDirectory,
+} from "./src/utils/file/fileUtils.js";
+import {viteStaticCopy} from "vite-plugin-static-copy";
+
+export default defineConfig({
+ plugins: [
+ react(),
+ viteStaticCopy({
+ targets: [
+ {
+ src: 'public/*',
+ dest: '',
+ }
+ ],
+ }),
+ {
+ name: 'pre-build-plugin',
+ async buildStart() {
initSourceDirectory()
createFileMapToJson()
+ createMarkdownSearchMapToJson()
createImageMapToJson()
await generateHtmlFiles()
}
- }
- ],
- test: {
- globals: true,
- environment: 'jsdom',
- setupFiles: './setupTests.js',
- }
-})
+ }
+ ],
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: './setupTests.js',
+ }
+})