@limcpf/everything-is-a-markdown 0.5.1 → 0.5.3
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/README.ko.md +25 -0
- package/README.md +25 -0
- package/package.json +1 -1
- package/src/build.ts +91 -1
- package/src/config.ts +45 -2
- package/src/dev.ts +12 -1
- package/src/runtime/app.css +49 -10
- package/src/types.ts +2 -0
package/README.ko.md
CHANGED
|
@@ -40,6 +40,31 @@ bun run blog [build|dev|clean] [options]
|
|
|
40
40
|
- `--exclude <glob>`: 제외 패턴 추가 (반복 가능)
|
|
41
41
|
- `--port <n>`: 개발 서버 포트 (기본 `3000`)
|
|
42
42
|
|
|
43
|
+
## 설정 파일 (`blog.config.ts`)
|
|
44
|
+
|
|
45
|
+
SEO/UI/정적 파일 설정은 config 파일에서 관리할 수 있습니다.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const config = {
|
|
49
|
+
vaultDir: "./vault",
|
|
50
|
+
outDir: "./dist",
|
|
51
|
+
staticPaths: ["assets", "public/favicon.ico"],
|
|
52
|
+
seo: {
|
|
53
|
+
siteUrl: "https://example.com",
|
|
54
|
+
defaultOgImage: "/assets/og.png",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default config;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`staticPaths`:
|
|
62
|
+
|
|
63
|
+
- 볼트 기준 상대 경로 배열
|
|
64
|
+
- 폴더와 파일 모두 지정 가능
|
|
65
|
+
- 지정한 경로의 파일들을 `dist`에 같은 상대 경로로 복사
|
|
66
|
+
- 예: 볼트 `assets/og.png` -> `dist/assets/og.png`
|
|
67
|
+
|
|
43
68
|
예시:
|
|
44
69
|
|
|
45
70
|
```bash
|
package/README.md
CHANGED
|
@@ -40,6 +40,31 @@ Common options:
|
|
|
40
40
|
- `--exclude <glob>`: Add exclude pattern (repeatable)
|
|
41
41
|
- `--port <n>`: Dev server port (default `3000`)
|
|
42
42
|
|
|
43
|
+
## Config File (`blog.config.ts`)
|
|
44
|
+
|
|
45
|
+
Use a config file for SEO/UI/static assets.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const config = {
|
|
49
|
+
vaultDir: "./vault",
|
|
50
|
+
outDir: "./dist",
|
|
51
|
+
staticPaths: ["assets", "public/favicon.ico"],
|
|
52
|
+
seo: {
|
|
53
|
+
siteUrl: "https://example.com",
|
|
54
|
+
defaultOgImage: "/assets/og.png",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default config;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`staticPaths`:
|
|
62
|
+
|
|
63
|
+
- Array of vault-relative paths
|
|
64
|
+
- Supports both folders and files
|
|
65
|
+
- Copies all matched files into the same relative location in `dist`
|
|
66
|
+
- Example: `assets/og.png` in vault becomes `dist/assets/og.png`
|
|
67
|
+
|
|
43
68
|
Examples:
|
|
44
69
|
|
|
45
70
|
```bash
|
package/package.json
CHANGED
package/src/build.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import matter from "gray-matter";
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
removeEmptyParents,
|
|
17
18
|
removeFileIfExists,
|
|
18
19
|
stripMdExt,
|
|
20
|
+
toPosixPath,
|
|
19
21
|
toDocId,
|
|
20
22
|
toRoute,
|
|
21
23
|
} from "./utils";
|
|
@@ -655,7 +657,7 @@ function isNewByFrontmatterDate(date: string | undefined, newThreshold: number):
|
|
|
655
657
|
return publishedAt != null && publishedAt >= newThreshold;
|
|
656
658
|
}
|
|
657
659
|
|
|
658
|
-
function getRecentSortEpochMs(doc: DocRecord): number {
|
|
660
|
+
function getRecentSortEpochMs(doc: DocRecord): number | null {
|
|
659
661
|
return parseDateToEpochMs(doc.updatedDate) ?? parseDateToEpochMs(doc.date);
|
|
660
662
|
}
|
|
661
663
|
|
|
@@ -924,6 +926,92 @@ async function writeOutputIfChanged(
|
|
|
924
926
|
await Bun.write(outputPath, content);
|
|
925
927
|
}
|
|
926
928
|
|
|
929
|
+
async function copyOutputFileIfChanged(
|
|
930
|
+
context: OutputWriteContext,
|
|
931
|
+
relOutputPath: string,
|
|
932
|
+
sourcePath: string,
|
|
933
|
+
): Promise<void> {
|
|
934
|
+
const bytes = new Uint8Array(await Bun.file(sourcePath).arrayBuffer());
|
|
935
|
+
const outputHash = crypto.createHash("sha1").update(bytes).digest("hex");
|
|
936
|
+
context.nextHashes[relOutputPath] = outputHash;
|
|
937
|
+
|
|
938
|
+
const outputPath = path.join(context.outDir, relOutputPath);
|
|
939
|
+
const unchanged = context.previousHashes[relOutputPath] === outputHash;
|
|
940
|
+
if (unchanged && (await Bun.file(outputPath).exists())) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
await ensureDir(path.dirname(outputPath));
|
|
945
|
+
await Bun.write(outputPath, bytes);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
async function listFilesRecursively(baseDir: string): Promise<string[]> {
|
|
949
|
+
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
950
|
+
entries.sort((left, right) => left.name.localeCompare(right.name, "ko-KR"));
|
|
951
|
+
|
|
952
|
+
const files: string[] = [];
|
|
953
|
+
for (const entry of entries) {
|
|
954
|
+
const absolutePath = path.join(baseDir, entry.name);
|
|
955
|
+
if (entry.isDirectory()) {
|
|
956
|
+
const children = await listFilesRecursively(absolutePath);
|
|
957
|
+
for (const child of children) {
|
|
958
|
+
files.push(path.join(entry.name, child));
|
|
959
|
+
}
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (entry.isFile()) {
|
|
964
|
+
files.push(entry.name);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return files;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async function copyStaticPaths(context: OutputWriteContext, options: BuildOptions): Promise<void> {
|
|
972
|
+
for (const staticPath of options.staticPaths) {
|
|
973
|
+
const sourcePath = path.resolve(options.vaultDir, staticPath);
|
|
974
|
+
|
|
975
|
+
let sourceStat;
|
|
976
|
+
try {
|
|
977
|
+
sourceStat = await fs.stat(sourcePath);
|
|
978
|
+
} catch {
|
|
979
|
+
console.warn(`[static] path not found: ${staticPath}`);
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (sourceStat.isDirectory()) {
|
|
984
|
+
const files = await listFilesRecursively(sourcePath);
|
|
985
|
+
for (const file of files) {
|
|
986
|
+
const relFilePath = toPosixPath(file);
|
|
987
|
+
const relOutputPath = path.posix.join(staticPath, relFilePath);
|
|
988
|
+
const filePath = path.join(sourcePath, file);
|
|
989
|
+
await copyOutputFileIfChanged(context, relOutputPath, filePath);
|
|
990
|
+
}
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (sourceStat.isFile()) {
|
|
995
|
+
await copyOutputFileIfChanged(context, staticPath, sourcePath);
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
console.warn(`[static] unsupported path type, skipped: ${staticPath}`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
async function removeStaleOutputs(context: OutputWriteContext): Promise<void> {
|
|
1004
|
+
for (const previousPath of Object.keys(context.previousHashes)) {
|
|
1005
|
+
if (Object.hasOwn(context.nextHashes, previousPath)) {
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const outputPath = path.join(context.outDir, previousPath);
|
|
1010
|
+
await removeFileIfExists(outputPath);
|
|
1011
|
+
await removeEmptyParents(path.dirname(outputPath), context.outDir);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
927
1015
|
function toRelativeAssetPath(fromOutputPath: string, assetOutputPath: string): string {
|
|
928
1016
|
const fromDir = path.posix.dirname(fromOutputPath);
|
|
929
1017
|
const relative = path.posix.relative(fromDir, assetOutputPath);
|
|
@@ -1242,6 +1330,7 @@ export async function buildSite(options: BuildOptions): Promise<BuildResult> {
|
|
|
1242
1330
|
nextHashes: {},
|
|
1243
1331
|
};
|
|
1244
1332
|
const runtimeAssets = await writeRuntimeAssets(outputContext);
|
|
1333
|
+
await copyStaticPaths(outputContext, options);
|
|
1245
1334
|
|
|
1246
1335
|
const tree = buildTree(docs, options);
|
|
1247
1336
|
const manifest = buildManifest(docs, tree, options);
|
|
@@ -1313,6 +1402,7 @@ export async function buildSite(options: BuildOptions): Promise<BuildResult> {
|
|
|
1313
1402
|
|
|
1314
1403
|
await writeShellPages(outputContext, docs, manifest, options, runtimeAssets, contentByDocId);
|
|
1315
1404
|
await writeSeoArtifacts(outputContext, docs, options);
|
|
1405
|
+
await removeStaleOutputs(outputContext);
|
|
1316
1406
|
|
|
1317
1407
|
await writeCache(cachePath, nextCache);
|
|
1318
1408
|
|
package/src/config.ts
CHANGED
|
@@ -132,6 +132,45 @@ function normalizePinnedMenu(raw: unknown, errorPrefix = "[config]"): PinnedMenu
|
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
function normalizeStaticPaths(raw: unknown, errorPrefix = "[config]"): string[] {
|
|
136
|
+
if (raw == null) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(raw)) {
|
|
140
|
+
throw new Error(`${errorPrefix} "staticPaths" must be an array of strings`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const normalized = new Set<string>();
|
|
144
|
+
for (const [index, value] of raw.entries()) {
|
|
145
|
+
if (typeof value !== "string") {
|
|
146
|
+
throw new Error(`${errorPrefix} "staticPaths[${index}]" must be a string`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const cleaned = value
|
|
150
|
+
.trim()
|
|
151
|
+
.replace(/\\/g, "/")
|
|
152
|
+
.replace(/^\.\/+/, "")
|
|
153
|
+
.replace(/\/+$/, "");
|
|
154
|
+
|
|
155
|
+
if (
|
|
156
|
+
!cleaned ||
|
|
157
|
+
cleaned === "." ||
|
|
158
|
+
cleaned === ".." ||
|
|
159
|
+
cleaned.startsWith("../") ||
|
|
160
|
+
cleaned.startsWith("/") ||
|
|
161
|
+
path.isAbsolute(value.trim())
|
|
162
|
+
) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`${errorPrefix} "staticPaths[${index}]" must be a non-empty vault-relative path (for example: "assets")`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
normalized.add(cleaned);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return Array.from(normalized);
|
|
172
|
+
}
|
|
173
|
+
|
|
135
174
|
export async function loadPinnedMenuConfig(
|
|
136
175
|
configPath: string | undefined,
|
|
137
176
|
cwd = process.cwd(),
|
|
@@ -166,9 +205,12 @@ export function resolveBuildOptions(
|
|
|
166
205
|
pinnedMenu: PinnedMenuOption | null,
|
|
167
206
|
cwd = process.cwd(),
|
|
168
207
|
): BuildOptions {
|
|
208
|
+
const vaultDir = path.resolve(cwd, cli.vaultDir ?? userConfig.vaultDir ?? DEFAULTS.vaultDir);
|
|
209
|
+
const outDir = path.resolve(cwd, cli.outDir ?? userConfig.outDir ?? DEFAULTS.outDir);
|
|
169
210
|
const cfgExclude = userConfig.exclude ?? [];
|
|
170
211
|
const cliExclude = cli.exclude ?? [];
|
|
171
212
|
const mergedExclude = Array.from(new Set([...DEFAULTS.exclude, ...cfgExclude, ...cliExclude]));
|
|
213
|
+
const staticPaths = normalizeStaticPaths(userConfig.staticPaths, "[config]");
|
|
172
214
|
const seo = normalizeSeoConfig(userConfig.seo);
|
|
173
215
|
const siteTitleRaw = userConfig.seo?.siteName ?? userConfig.seo?.defaultTitle;
|
|
174
216
|
const siteTitle =
|
|
@@ -179,9 +221,10 @@ export function resolveBuildOptions(
|
|
|
179
221
|
const resolvedPinnedMenu = pinnedMenu ?? configPinnedMenu;
|
|
180
222
|
|
|
181
223
|
return {
|
|
182
|
-
vaultDir
|
|
183
|
-
outDir
|
|
224
|
+
vaultDir,
|
|
225
|
+
outDir,
|
|
184
226
|
exclude: mergedExclude,
|
|
227
|
+
staticPaths,
|
|
185
228
|
newWithinDays: cli.newWithinDays ?? userConfig.ui?.newWithinDays ?? DEFAULTS.newWithinDays,
|
|
186
229
|
recentLimit: cli.recentLimit ?? userConfig.ui?.recentLimit ?? DEFAULTS.recentLimit,
|
|
187
230
|
siteTitle,
|
package/src/dev.ts
CHANGED
|
@@ -8,6 +8,10 @@ interface DevOptions {
|
|
|
8
8
|
port: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function isStaticPathMatch(relPath: string, staticPath: string): boolean {
|
|
12
|
+
return relPath === staticPath || relPath.startsWith(`${staticPath}/`);
|
|
13
|
+
}
|
|
14
|
+
|
|
11
15
|
async function resolveOutputFile(outDir: string, pathname: string): Promise<string | null> {
|
|
12
16
|
const decoded = decodeURIComponent(pathname);
|
|
13
17
|
const clean = decoded.replace(/^\/+/, "");
|
|
@@ -89,7 +93,14 @@ export async function runDev(options: BuildOptions, devOptions: DevOptions): Pro
|
|
|
89
93
|
};
|
|
90
94
|
|
|
91
95
|
watcher.on("all", (_event, changedPath) => {
|
|
92
|
-
|
|
96
|
+
const relPath = path.relative(options.vaultDir, changedPath).split(path.sep).join("/");
|
|
97
|
+
if (!relPath || relPath.startsWith("..")) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const isMarkdownChange = /\.md$/i.test(changedPath);
|
|
102
|
+
const isStaticChange = options.staticPaths.some((staticPath) => isStaticPathMatch(relPath, staticPath));
|
|
103
|
+
if (!isMarkdownChange && !isStaticChange) {
|
|
93
104
|
return;
|
|
94
105
|
}
|
|
95
106
|
scheduleRebuild();
|
package/src/runtime/app.css
CHANGED
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
--badge-new-fg: #ffffff;
|
|
65
65
|
--mobile-toggle-bg: var(--latte-mauve);
|
|
66
66
|
--mobile-toggle-fg: #ffffff;
|
|
67
|
+
--content-heading-accent-soft: rgba(136, 57, 239, 0.32);
|
|
68
|
+
--content-heading-subtle: var(--latte-subtext1);
|
|
67
69
|
--desktop-sidebar-default: 420px;
|
|
68
70
|
--desktop-sidebar-min: 320px;
|
|
69
71
|
--desktop-viewer-min: 680px;
|
|
@@ -111,6 +113,8 @@
|
|
|
111
113
|
--badge-new-fg: var(--mocha-crust);
|
|
112
114
|
--mobile-toggle-bg: var(--mocha-mauve);
|
|
113
115
|
--mobile-toggle-fg: var(--mocha-crust);
|
|
116
|
+
--content-heading-accent-soft: rgba(203, 166, 247, 0.34);
|
|
117
|
+
--content-heading-subtle: var(--mocha-subtext0);
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
* {
|
|
@@ -884,20 +888,55 @@ body.mobile-toggle-left .mobile-menu-toggle {
|
|
|
884
888
|
color: var(--latte-subtext1);
|
|
885
889
|
}
|
|
886
890
|
|
|
891
|
+
.viewer-content > :first-child {
|
|
892
|
+
margin-top: 0;
|
|
893
|
+
}
|
|
894
|
+
|
|
887
895
|
.viewer-content h1,
|
|
888
896
|
.viewer-content h2,
|
|
889
897
|
.viewer-content h3,
|
|
890
898
|
.viewer-content h4 {
|
|
891
899
|
color: var(--latte-text);
|
|
892
|
-
font-weight:
|
|
893
|
-
line-height: 1.
|
|
894
|
-
|
|
895
|
-
|
|
900
|
+
font-weight: 650;
|
|
901
|
+
line-height: 1.33;
|
|
902
|
+
letter-spacing: -0.015em;
|
|
903
|
+
text-wrap: balance;
|
|
904
|
+
margin-top: 2.2rem;
|
|
905
|
+
margin-bottom: 0.95rem;
|
|
906
|
+
scroll-margin-top: 20px;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.viewer-content h1 {
|
|
910
|
+
font-size: 2.08rem;
|
|
911
|
+
font-weight: 760;
|
|
912
|
+
letter-spacing: -0.02em;
|
|
913
|
+
margin-top: 2.8rem;
|
|
914
|
+
margin-bottom: 1.1rem;
|
|
915
|
+
padding-bottom: 0.4rem;
|
|
916
|
+
border-bottom: 1px solid var(--latte-surface0);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.viewer-content h2 {
|
|
920
|
+
font-size: 1.62rem;
|
|
921
|
+
margin-top: 2.6rem;
|
|
922
|
+
margin-bottom: 1.04rem;
|
|
923
|
+
padding: 0.1rem 0 0.18rem 0.55rem;
|
|
924
|
+
border-left: 3px solid var(--content-heading-accent-soft);
|
|
896
925
|
}
|
|
897
926
|
|
|
898
|
-
.viewer-content
|
|
899
|
-
|
|
900
|
-
|
|
927
|
+
.viewer-content h3 {
|
|
928
|
+
font-size: 1.34rem;
|
|
929
|
+
color: var(--content-heading-subtle);
|
|
930
|
+
margin-top: 2.15rem;
|
|
931
|
+
margin-bottom: 0.92rem;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.viewer-content h4 {
|
|
935
|
+
font-size: 1.14rem;
|
|
936
|
+
color: var(--content-heading-subtle);
|
|
937
|
+
margin-top: 1.85rem;
|
|
938
|
+
margin-bottom: 0.8rem;
|
|
939
|
+
}
|
|
901
940
|
|
|
902
941
|
.viewer-content p {
|
|
903
942
|
margin-bottom: 1.25rem;
|
|
@@ -1318,15 +1357,15 @@ body.mobile-toggle-left .mobile-menu-toggle {
|
|
|
1318
1357
|
}
|
|
1319
1358
|
|
|
1320
1359
|
.viewer-content h1 {
|
|
1321
|
-
font-size: 1.
|
|
1360
|
+
font-size: 1.82rem;
|
|
1322
1361
|
}
|
|
1323
1362
|
|
|
1324
1363
|
.viewer-content h2 {
|
|
1325
|
-
font-size: 1.
|
|
1364
|
+
font-size: 1.42rem;
|
|
1326
1365
|
}
|
|
1327
1366
|
|
|
1328
1367
|
.viewer-content h3 {
|
|
1329
|
-
font-size: 1.
|
|
1368
|
+
font-size: 1.22rem;
|
|
1330
1369
|
}
|
|
1331
1370
|
|
|
1332
1371
|
.viewer-content table {
|
package/src/types.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface UserConfig {
|
|
|
39
39
|
vaultDir?: string;
|
|
40
40
|
outDir?: string;
|
|
41
41
|
exclude?: string[];
|
|
42
|
+
staticPaths?: string[];
|
|
42
43
|
pinnedMenu?: {
|
|
43
44
|
label?: string;
|
|
44
45
|
sourceDir: string;
|
|
@@ -63,6 +64,7 @@ export interface BuildOptions {
|
|
|
63
64
|
vaultDir: string;
|
|
64
65
|
outDir: string;
|
|
65
66
|
exclude: string[];
|
|
67
|
+
staticPaths: string[];
|
|
66
68
|
newWithinDays: number;
|
|
67
69
|
recentLimit: number;
|
|
68
70
|
siteTitle?: string;
|