From 66d7147de9f1021f3b14053bb06a326fcc660515 Mon Sep 17 00:00:00 2001 From: gitgitWi Date: Thu, 10 Sep 2026 02:47:46 +0900 Subject: [PATCH 1/9] =?UTF-8?q?docs(phase-2):=20=EC=8A=A4=ED=83=9D=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=9C=BC=EB=A1=9C=20PLAN=20=EC=8A=B9?= =?UTF-8?q?=EC=9D=B8=20=ED=95=AD=EB=AA=A9=EC=9D=84=20=EA=B3=A0=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구경로 shim과 wiki 공개안전 선별을 채택한다. Co-authored-by: Cursor --- .tasks/phase-2-content-routes/LOG.md | 11 +++++++++++ .tasks/phase-2-content-routes/PLAN.md | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 .tasks/phase-2-content-routes/LOG.md diff --git a/.tasks/phase-2-content-routes/LOG.md b/.tasks/phase-2-content-routes/LOG.md new file mode 100644 index 0000000..418e7f5 --- /dev/null +++ b/.tasks/phase-2-content-routes/LOG.md @@ -0,0 +1,11 @@ +# Phase 2 LOG + +- Harness: cursor +- Started: 2026-09-10 +- Status: PLAN-READY (스택 진행 지시로 shim·공개안전 선별 채택). base `feat/phase-1.5-visual` (#58 ready, 미머지). + +## Notes + +- 콘텐츠 원본: `legacy-next/pages/posts/**`, `legacy-next/pages/study/**`. +- `llm-wiki-template`는 이 레포에 없음. 필요 시 GitHub에서 읽기 전용 clone, 통과분만 `src/content/til/`에 복사. clone 자체는 커밋하지 않는다. +- 스택: `feat/phase-1.5-visual` ← `feat/phase-2-content`. 머지는 사람. Phase 1(#56) → 1.5(#58) 먼저. diff --git a/.tasks/phase-2-content-routes/PLAN.md b/.tasks/phase-2-content-routes/PLAN.md index 808eb51..970adef 100644 --- a/.tasks/phase-2-content-routes/PLAN.md +++ b/.tasks/phase-2-content-routes/PLAN.md @@ -28,5 +28,5 @@ ## 승인 요청 -- [ ] 구경로 shim 방식 동의? (vs 404 안내) -- [ ] llm-wiki 선별 기준(공개안전) 동의? +- [x] 구경로 shim 방식 동의? (vs 404 안내) — 2026-09-10 스택 진행 지시. PLAN 기본안(shim + canonical) 채택. +- [x] llm-wiki 선별 기준(공개안전) 동의? — 동. 회사정보·개인정보·저작권 불통과분은 이식하지 않는다. `raw/notes` 금지. From d446ef769a505330b3f9b3284dacc35fd7b58827 Mon Sep 17 00:00:00 2001 From: gitgitWi Date: Thu, 10 Sep 2026 02:54:06 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat(phase-2):=20Content=20Collections?= =?UTF-8?q?=EC=99=80=20MDX=20=ED=8C=8C=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit articles/til 스키마, remark/rehype 플러그인, colorway 팔레트 토큰을 정의한다. --- astro.config.mjs | 19 ++- bun.lock | 282 ++++++++++++++++++++++++++++++++---- package.json | 9 ++ src/content.config.ts | 45 ++++++ src/styles/themes.ts | 5 +- src/styles/tokens.stylex.ts | 26 ++++ 6 files changed, 358 insertions(+), 28 deletions(-) create mode 100644 src/content.config.ts diff --git a/astro.config.mjs b/astro.config.mjs index 2595ba4..fd9228e 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,13 +1,28 @@ import { defineConfig } from "astro/config"; import stylex from "@stylexjs/unplugin"; - import preact from "@astrojs/preact"; +import mdx from "@astrojs/mdx"; +import rehypeAutolinkHeadings from "rehype-autolink-headings"; +import rehypeExternalLinks from "rehype-external-links"; +import rehypeSlug from "rehype-slug"; +import remarkGfm from "remark-gfm"; +import remarkSmartypants from "remark-smartypants"; + +const markdownPlugins = { + remarkPlugins: [remarkGfm, remarkSmartypants], + rehypePlugins: [ + rehypeSlug, + [rehypeAutolinkHeadings, { behavior: "wrap" }], + [rehypeExternalLinks, { target: "_blank", rel: ["noopener", "noreferrer"] }], + ], +}; export default defineConfig({ site: "https://gitgitwi.github.io", markdown: { syntaxHighlight: "prism", + ...markdownPlugins, }, vite: { @@ -22,5 +37,5 @@ export default defineConfig({ ], }, - integrations: [preact()], + integrations: [preact(), mdx(markdownPlugins)], }); diff --git a/bun.lock b/bun.lock index 5aed757..87c2266 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,9 @@ "": { "name": "gh-blog", "dependencies": { + "@astrojs/markdown-remark": "^7.3.1", + "@astrojs/markdown-satteri": "^0.4.1", + "@astrojs/mdx": "^8.0.1", "@astrojs/preact": "^6.0.5", "@astrojs/prism": "^4.0.2", "@fontsource/caveat": "^5.3.0", @@ -15,6 +18,11 @@ "astro": "7.3.2", "preact": "^10.29.8", "prismjs": "^1.30.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-external-links": "^3.0.0", + "rehype-slug": "^6.0.0", + "remark-gfm": "^4.0.1", + "remark-smartypants": "^3.0.3", "sharp": "0.35.4", "three": "^0.186.0", }, @@ -68,8 +76,12 @@ "@astrojs/language-server": ["@astrojs/language-server@2.16.16", "", { "dependencies": { "@astrojs/compiler": "^2.13.1", "@astrojs/yaml2ts": "^0.2.4", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.16", "volar-service-css": "0.0.71", "volar-service-emmet": "0.0.71", "volar-service-html": "0.0.71", "volar-service-prettier": "0.0.71", "volar-service-typescript": "0.0.71", "volar-service-typescript-twoslash-queries": "0.0.71", "volar-service-yaml": "0.0.71", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "./bin/nodeServer.js" } }, "sha512-KuS17AOOqH/M5mHXPmKvtp8L+NNteARC7Xjf395+9R9dtJOBndqdStwU4V+OKzYZpgZ+hliV13knzx/oMtFl7Q=="], + "@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.3.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.11.0", "@astrojs/prism": "4.0.2", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "estree-util-visit": "^2.0.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-html": "^9.0.5", "hast-util-to-text": "^4.0.2", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "source-map": "^0.7.6", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-QCjsjZQl0+Cgb91+T3Ip9hyUe1r5aLm4udi584rML9YO3syupbAYE+jWpAxYg+z4lFLNUugPrylFxZ+8qFTClg=="], + "@astrojs/markdown-satteri": ["@astrojs/markdown-satteri@0.4.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.11.0", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "satteri": "^0.10.3" } }, "sha512-EniHbFNa6SQHDih7JnpPos3NWXO6hdt4raBcOosfGmlfc3gP9CI/sESA3VOf1PgJZ7SFfewlZW9Livn0JugEZg=="], + "@astrojs/mdx": ["@astrojs/mdx@8.0.1", "", { "dependencies": { "@astrojs/internal-helpers": "0.11.0", "@astrojs/markdown-satteri": "0.4.1", "es-module-lexer": "^2.0.0" }, "peerDependencies": { "@astrojs/markdown-remark": "^7.3.0", "astro": "^7.2.6" }, "optionalPeers": ["@astrojs/markdown-remark"] }, "sha512-VKodp/f3+XE6L0950K+ZOAolY47vdt0R78Ao+L2RdJ1hfAdYffL7IEjc/MsoyuS7CPVvCsd5/I3RP0ZyAIewdQ=="], + "@astrojs/preact": ["@astrojs/preact@6.0.5", "", { "dependencies": { "@astrojs/internal-helpers": "0.11.0", "@preact/preset-vite": "^2.10.5", "@preact/signals": "^2.8.2", "devalue": "^5.8.1", "preact-render-to-string": "^6.6.6", "vite": "^8.0.13" }, "peerDependencies": { "preact": "^10.6.5" } }, "sha512-ONNVrPUM3PxEGJ0Z1MseRRXn0MCirTxEzH84ZUphD8WoR2WOvgHAf4C/0RVc4dmJigytHG3UFg+hsZd/oFz03w=="], "@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="], @@ -332,6 +344,8 @@ "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="], "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], @@ -590,6 +604,8 @@ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="], @@ -604,6 +620,10 @@ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/mdx": ["@types/mdx@2.0.14", "", {}, "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="], "@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], @@ -612,7 +632,7 @@ "@types/three": ["@types/three@0.185.4", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "fflate": "~0.8.2", "meshoptimizer": "~1.1.1" } }, "sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA=="], - "@types/unist": ["@types/unist@2.0.6", "", {}, "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], @@ -682,10 +702,14 @@ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "ast-types": ["ast-types@0.16.3", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA=="], + "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + "astro": ["astro@7.3.2", "", { "dependencies": { "@astrojs/compiler-rs": "^0.4.0", "@astrojs/internal-helpers": "0.11.0", "@astrojs/markdown-satteri": "0.4.1", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^9.0.0", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "find-proc": "0.1.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.3.0", "jsonc-parser": "^3.3.1", "magic-string": "^1.0.0", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.5", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.5.4" }, "optionalDependencies": { "sharp": "^0.35.4" }, "peerDependencies": { "@astrojs/markdown-remark": "^7.3.0" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-ysTcdpGP61XZpHoMRYC/CK19DQ94qkBvzXMtXEo0gEqPNkmOU/Tv++CtWmzaI4nF2Ck8RXFdiWvdlTWa2oOr9w=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], @@ -716,10 +740,14 @@ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], @@ -730,6 +758,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], @@ -762,6 +792,8 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], @@ -810,6 +842,10 @@ "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + "esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -832,6 +868,18 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-B0np3dcdxqILX5e9nEi5/Fr4K7gL4oYFVPV1zRa2e9wRCbQoZZNWOZFYyoInvXUPJXBXjss+QXlWLJChDEHDkA=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], @@ -896,10 +944,34 @@ "hashery": ["hashery@1.5.1", "", { "dependencies": { "hookified": "^1.15.0" } }, "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ=="], + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + + "hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="], + + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + + "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + + "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + + "hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], + + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], "hono": ["hono@4.13.7", "", {}, "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ=="], @@ -920,16 +992,28 @@ "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], "iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="], + "is-absolute-url": ["is-absolute-url@4.0.1", "", {}, "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + "is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="], "is-extglob": ["is-extglob@2.1.1", "", {}, "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], @@ -992,6 +1076,8 @@ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], @@ -1004,18 +1090,114 @@ "magicast": ["magicast@0.5.4", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w=="], + "markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + + "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "mdast-util-definitions": ["mdast-util-definitions@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], "meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], @@ -1084,8 +1266,14 @@ "package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "parse-latin": ["parse-latin@7.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="], + "parse-srcset": ["parse-srcset@1.0.2", "", {}, "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -1134,6 +1322,14 @@ "recast": ["recast@0.23.21", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw=="], + "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + + "recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], @@ -1142,14 +1338,44 @@ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "rehype-autolink-headings": ["rehype-autolink-headings@7.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-is-element": "^3.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw=="], + + "rehype-external-links": ["rehype-external-links@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-is-element": "^3.0.0", "is-absolute-url": "^4.0.0", "space-separated-tokens": "^2.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw=="], + + "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], + + "rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "rehype-slug": ["rehype-slug@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "github-slugger": "^2.0.0", "hast-util-heading-rank": "^3.0.0", "hast-util-to-string": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A=="], + + "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], + + "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-smartypants": ["remark-smartypants@3.0.3", "", { "dependencies": { "retext": "^9.0.0", "retext-smartypants": "^6.0.0", "unified": "^11.0.4", "unist-util-visit": "^5.0.0" } }, "sha512-gCaK+ndZ0hYezlqFegHFCVh2CQemsi0Npdh1qVM9bxlUFknjkbP6VmojWhddOCrbK0PbbacmYLWfTULRiT1eWA=="], + + "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + "request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], + + "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], + "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="], + "retext-stringify": ["retext-stringify@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unified": "^11.0.0" } }, "sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA=="], + "rolldown": ["rolldown@1.2.8", "", { "dependencies": { "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.8", "@rolldown/binding-android-arm64": "1.2.8", "@rolldown/binding-darwin-arm64": "1.2.8", "@rolldown/binding-darwin-x64": "1.2.8", "@rolldown/binding-freebsd-x64": "1.2.8", "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", "@rolldown/binding-linux-arm64-gnu": "1.2.8", "@rolldown/binding-linux-arm64-musl": "1.2.8", "@rolldown/binding-linux-ppc64-gnu": "1.2.8", "@rolldown/binding-linux-s390x-gnu": "1.2.8", "@rolldown/binding-linux-x64-gnu": "1.2.8", "@rolldown/binding-linux-x64-musl": "1.2.8", "@rolldown/binding-openharmony-arm64": "1.2.8", "@rolldown/binding-win32-arm64-msvc": "1.2.8", "@rolldown/binding-win32-x64-msvc": "1.2.8" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ=="], "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], @@ -1178,7 +1404,7 @@ "smol-toml": ["smol-toml@1.8.0", "", {}, "sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ=="], - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -1200,6 +1426,10 @@ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "styleq": ["styleq@0.2.1", "", {}, "sha512-L0TR0NQb+X4/ktDEKmjWyp27gla+LUYi/by5k5SjKXf6/pvZP7wbwEB5J+tqxdFVPgzbsuz+d4RTScO/QZquBw=="], "svgo": ["svgo@4.1.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^6.0.0", "css-tree": "^3.0.1", "css-what": "^7.0.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "1.6.1" }, "bin": { "svgo": "bin/svgo.js" } }, "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q=="], @@ -1258,14 +1488,24 @@ "unifont": ["unifont@0.7.5", "", { "dependencies": { "css-tree": "^3.1.0", "ohash": "^2.0.11", "undici": "^8.0.0" } }, "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg=="], + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + "unist-util-modify-children": ["unist-util-modify-children@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "array-iterate": "^2.0.0" } }, "sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw=="], + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + "unist-util-visit-children": ["unist-util-visit-children@3.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA=="], + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], "unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], @@ -1280,6 +1520,8 @@ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="], @@ -1324,6 +1566,8 @@ "vscode-uri": ["vscode-uri@3.2.0", "", {}, "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -1394,7 +1638,9 @@ "@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], - "@types/nlcst/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/hast/@types/unist": ["@types/unist@2.0.6", "", {}, "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="], + + "@types/mdast/@types/unist": ["@types/unist@2.0.6", "", {}, "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="], "@vitest/expect/chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], @@ -1418,46 +1664,32 @@ "eslint/debug": ["debug@4.3.3", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q=="], - "hast-util-to-html/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "node-html-parser/css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - "qified/hookified": ["hookified@2.2.0", "", {}, "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA=="], - - "rolldown/@oxc-project/types": ["@oxc-project/types@0.149.0", "", {}, "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA=="], - - "satteri/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - - "strip-ansi/ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], - - "unified/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "parse-entities/@types/unist": ["@types/unist@2.0.6", "", {}, "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="], - "unist-util-is/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - "unist-util-position/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "qified/hookified": ["hookified@2.2.0", "", {}, "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA=="], - "unist-util-stringify-position/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "unist-util-visit/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "rolldown/@oxc-project/types": ["@oxc-project/types@0.149.0", "", {}, "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA=="], - "unist-util-visit-parents/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "strip-ansi/ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "vfile/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - - "vfile-message/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "vite-prerender-plugin/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "vite-prerender-plugin/source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], - "vscode-css-languageservice/vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], "vscode-languageserver-protocol/vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], diff --git a/package.json b/package.json index 5f411a9..92d0670 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,14 @@ "lint": "oxlint .", "lint:stylex": "eslint -c eslint.stylex.config.js src", "test": "vitest run --passWithNoTests", + "check-links": "node scripts/check-links.mjs", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" }, "dependencies": { + "@astrojs/markdown-remark": "^7.3.1", + "@astrojs/markdown-satteri": "^0.4.1", + "@astrojs/mdx": "^8.0.1", "@astrojs/preact": "^6.0.5", "@astrojs/prism": "^4.0.2", "@fontsource/caveat": "^5.3.0", @@ -27,6 +31,11 @@ "astro": "7.3.2", "preact": "^10.29.8", "prismjs": "^1.30.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-external-links": "^3.0.0", + "rehype-slug": "^6.0.0", + "remark-gfm": "^4.0.1", + "remark-smartypants": "^3.0.3", "sharp": "0.35.4", "three": "^0.186.0" }, diff --git a/src/content.config.ts b/src/content.config.ts new file mode 100644 index 0000000..4646406 --- /dev/null +++ b/src/content.config.ts @@ -0,0 +1,45 @@ +import { defineCollection } from "astro:content"; +import { glob } from "astro/loaders"; +import { z } from "astro/zod"; + +const colorwaySchema = z.enum(["canvas", "mint", "clay", "ink"]); + +const articleThemeSchema = z.object({ + colorway: colorwaySchema.default("canvas"), + layout: z.enum(["essay", "gallery", "docs"]).default("essay"), + density: z.enum(["compact", "comfortable", "spacious"]).default("comfortable"), +}); + +const articles = defineCollection({ + loader: glob({ base: "./src/content/articles", pattern: "**/*.{md,mdx}" }), + schema: z.object({ + title: z.string(), + description: z.string(), + pubDate: z.coerce.date(), + updatedDate: z.coerce.date().optional(), + tags: z.array(z.string()).default([]), + draft: z.boolean().default(false), + theme: articleThemeSchema.default({ + colorway: "canvas", + layout: "essay", + density: "comfortable", + }), + cover: z.url().optional(), + }), +}); + +const til = defineCollection({ + loader: glob({ base: "./src/content/til", pattern: "**/*.{md,mdx}" }), + schema: z.object({ + title: z.string(), + summary: z.string(), + pubDate: z.coerce.date(), + updatedDate: z.coerce.date().optional(), + tags: z.array(z.string()).default([]), + domains: z.array(z.string()).optional(), + visibility: z.enum(["public", "private"]).default("public"), + quizRefs: z.array(z.string()).optional(), + }), +}); + +export const collections = { articles, til }; diff --git a/src/styles/themes.ts b/src/styles/themes.ts index bd9a243..8b305b8 100644 --- a/src/styles/themes.ts +++ b/src/styles/themes.ts @@ -2,13 +2,16 @@ import * as stylex from "@stylexjs/stylex"; import { tokens } from "./tokens.stylex.ts"; -/** Phase 2에서 실값을 채운다. */ +/** Default warm editorial palette (design.md contract). */ export const canvasTheme = stylex.unstable_createThemeNested(tokens, {}); +/** Mint colorway — canvas 토큰은 ColorwayShell variant style로 오버라이드한다. */ export const mintTheme = stylex.unstable_createThemeNested(tokens, {}); +/** Clay colorway. */ export const clayTheme = stylex.unstable_createThemeNested(tokens, {}); +/** Ink colorway. */ export const inkTheme = stylex.unstable_createThemeNested(tokens, {}); export const colorwayThemes = { diff --git a/src/styles/tokens.stylex.ts b/src/styles/tokens.stylex.ts index dc4ccb6..2096995 100644 --- a/src/styles/tokens.stylex.ts +++ b/src/styles/tokens.stylex.ts @@ -17,6 +17,32 @@ export const tokens = stylex.unstable_defineVarsNested({ codeBg: "#F1EFE6", codeInk: "#393a34", codeBorder: "#DDDCD3", + mintCanvas: "#EFF8F2", + mintSurface: "#F6FCF8", + mintSurfaceMuted: "#E3F0E8", + mintAccent: "#0D9668", + mintAccentHover: "#087A54", + mintAccentSoft: "#C8EBD9", + mintAccentInk: "#065A3D", + mintCodeBg: "#E8F2EC", + clayCanvas: "#F9F2EB", + claySurface: "#FDF8F3", + claySurfaceMuted: "#F0E4D8", + clayBorder: "#E5D5C5", + clayBorderStrong: "#D4C0AD", + clayAccent: "#C45C26", + clayAccentHover: "#A84D1F", + clayAccentSoft: "#F8E4D6", + clayAccentInk: "#8A3D18", + clayCodeBg: "#F3EBE2", + inkCanvas: "#EDECE6", + inkSurface: "#F4F3EE", + inkSurfaceMuted: "#DEDCD4", + inkPrimary: "#0A0C0B", + inkSecondaryAlt: "#2E3330", + inkTertiaryAlt: "#454B47", + inkAccent: "#12805A", + inkAccentSoft: "#D5EDE2", }, space: { 1: "4px", From 2da8f8be0e1bfb15c354140938fa6a52c08c790b Mon Sep 17 00:00:00 2001 From: gitgitWi Date: Thu, 10 Sep 2026 02:54:09 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat(phase-2):=20=EC=BD=98=ED=85=90?= =?UTF-8?q?=EC=B8=A0=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=C2=B7=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=C2=B76=ED=8E=B8=20=EC=9D=B4=EC=8B=9D?= =?UTF-8?q?=EA=B3=BC=20=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=97=B0=EA=B2=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ArticleLayout/WikiLayout, tags/home 실데이터, check-links, 구경로 shim 포함. --- public/redirects.json | 11 + scripts/check-links.mjs | 130 ++++++++++ src/components/content/MdxPre.astro | 26 ++ src/components/content/SmartLink.astro | 30 +++ src/components/content/SmartLink.stories.ts | 24 ++ src/components/content/SmartLink.stylex.ts | 20 ++ src/components/content/TableCard.astro | 26 ++ src/components/content/TableCard.stories.ts | 14 + src/components/content/TableCard.stylex.ts | 35 +++ src/components/layout/ArticleLayout.astro | 157 +++++++++++ src/components/layout/ArticleLayout.stylex.ts | 76 ++++++ src/components/layout/WikiLayout.astro | 120 +++++++++ src/components/layout/WikiLayout.stylex.ts | 33 +++ src/components/patterns/ColorwayShell.astro | 10 +- .../patterns/ColorwayShell.stylex.ts | 11 + src/content/articles/design-system/01.md | 245 ++++++++++++++++++ .../articles/from-jekyll-to-nextjs/00.mdx | 94 +++++++ src/content/articles/hello.mdx | 22 ++ src/content/til/refactoring-javascript/01.mdx | 99 +++++++ .../til/tdd/fullstack-tdd-by-newbie.mdx | 104 ++++++++ .../til/ts/enum-to-template-literal.mdx | 236 +++++++++++++++++ src/lib/content-helpers.test.ts | 79 ++++++ src/lib/content-helpers.ts | 49 ++++ src/lib/content.ts | 86 ++++++ src/lib/mdx-components.ts | 9 + src/pages/articles/[...slug].astro | 37 +++ src/pages/articles/index.astro | 37 +++ src/pages/index.astro | 30 ++- src/pages/posts/[...slug].astro | 29 +++ src/pages/study/[...slug].astro | 29 +++ src/pages/tags/[tag].astro | 31 ++- src/pages/tags/index.astro | 15 +- src/pages/til/[...slug].astro | 40 +++ src/pages/til/index.astro | 37 +++ 34 files changed, 2005 insertions(+), 26 deletions(-) create mode 100644 public/redirects.json create mode 100644 scripts/check-links.mjs create mode 100644 src/components/content/MdxPre.astro create mode 100644 src/components/content/SmartLink.astro create mode 100644 src/components/content/SmartLink.stories.ts create mode 100644 src/components/content/SmartLink.stylex.ts create mode 100644 src/components/content/TableCard.astro create mode 100644 src/components/content/TableCard.stories.ts create mode 100644 src/components/content/TableCard.stylex.ts create mode 100644 src/components/layout/ArticleLayout.astro create mode 100644 src/components/layout/WikiLayout.astro create mode 100644 src/content/articles/design-system/01.md create mode 100644 src/content/articles/from-jekyll-to-nextjs/00.mdx create mode 100644 src/content/articles/hello.mdx create mode 100644 src/content/til/refactoring-javascript/01.mdx create mode 100644 src/content/til/tdd/fullstack-tdd-by-newbie.mdx create mode 100644 src/content/til/ts/enum-to-template-literal.mdx create mode 100644 src/lib/content-helpers.test.ts create mode 100644 src/lib/content-helpers.ts create mode 100644 src/lib/content.ts create mode 100644 src/lib/mdx-components.ts create mode 100644 src/pages/articles/[...slug].astro create mode 100644 src/pages/articles/index.astro create mode 100644 src/pages/posts/[...slug].astro create mode 100644 src/pages/study/[...slug].astro create mode 100644 src/pages/til/[...slug].astro create mode 100644 src/pages/til/index.astro diff --git a/public/redirects.json b/public/redirects.json new file mode 100644 index 0000000..eaa6a4f --- /dev/null +++ b/public/redirects.json @@ -0,0 +1,11 @@ +{ + "posts": { + "design-system/01": "/articles/design-system/01", + "from-jekyll-to-nextjs-mdx/00": "/articles/from-jekyll-to-nextjs/00", + "til/tdd/fullstack-tdd-by-newbie": "/til/tdd/fullstack-tdd-by-newbie", + "til/ts/enum-to-template-literal": "/til/ts/enum-to-template-literal" + }, + "study": { + "refactoring-javascript/01": "/til/refactoring-javascript/01" + } +} diff --git a/scripts/check-links.mjs b/scripts/check-links.mjs new file mode 100644 index 0000000..04d5e6d --- /dev/null +++ b/scripts/check-links.mjs @@ -0,0 +1,130 @@ +import { readFile, readdir } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const distDir = join(rootDir, "dist"); +const siteOrigin = "https://gitgitwi.github.io"; + +const walkHtml = async (dir) => { + const entries = await readdir(dir, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const fullPath = join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...(await walkHtml(fullPath))); + } else if (entry.name.endsWith(".html")) { + files.push(fullPath); + } + } + return files; +}; + +const stripBase = (href) => { + if (href.startsWith(siteOrigin)) { + return href.slice(siteOrigin.length) || "/"; + } + return href; +}; + +const hrefToDistPath = (href) => { + const pathOnly = stripBase(href.split("#")[0].split("?")[0]); + if (!pathOnly.startsWith("/")) return null; + const normalized = pathOnly.replace(/\/$/, "") || "/"; + if (normalized === "/") { + const home = join(distDir, "index.html"); + return { direct: home, asIndex: home, anchor: null, pathOnly: normalized }; + } + const direct = join(distDir, normalized.slice(1)); + const asIndex = join(direct, "index.html"); + return { direct, asIndex, anchor: href.includes("#") ? href.split("#")[1] : null, pathOnly }; +}; + +const collectFromHtml = (html) => { + const hrefs = [...html.matchAll(/\shref="([^"]+)"/g)].map((match) => match[1]); + const srcs = [...html.matchAll(/\ssrc="([^"]+)"/g)].map((match) => match[1]); + const ids = new Set([...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1])); + return { hrefs, srcs, ids }; +}; + +const isInternal = (url) => + url.startsWith("/") || + url.startsWith(siteOrigin) || + (!url.includes("://") && !url.startsWith("mailto:") && !url.startsWith("tel:")); + +const isIgnored = (url) => + url.startsWith("#") || + url.startsWith("javascript:") || + url.startsWith("data:") || + url.endsWith(".pdf"); + +const errors = []; + +const assertPathExists = async (target) => { + const { direct, asIndex } = target; + try { + await readFile(direct); + return direct; + } catch { + try { + await readFile(asIndex); + return asIndex; + } catch { + return null; + } + } +}; + +const run = async () => { + let htmlFiles; + try { + htmlFiles = await walkHtml(distDir); + } catch { + console.error("dist/ not found — run `bun run build` first."); + process.exit(1); + } + + for (const file of htmlFiles) { + const html = await readFile(file, "utf8"); + const relFile = file.slice(distDir.length + 1); + const { hrefs, srcs, ids } = collectFromHtml(html); + + for (const href of hrefs) { + if (!isInternal(href) || isIgnored(href)) continue; + const mapped = hrefToDistPath(href); + if (!mapped) continue; + const found = await assertPathExists(mapped); + if (!found) { + errors.push(`${relFile}: broken internal link ${href}`); + continue; + } + if (mapped.anchor && !ids.has(mapped.anchor)) { + const targetHtml = await readFile(found, "utf8"); + const targetIds = collectFromHtml(targetHtml).ids; + if (!targetIds.has(mapped.anchor)) { + errors.push(`${relFile}: missing anchor #${mapped.anchor} in ${mapped.pathOnly}`); + } + } + } + + for (const src of srcs) { + if (!isInternal(src) || isIgnored(src)) continue; + const mapped = hrefToDistPath(src); + if (!mapped) continue; + const found = await assertPathExists(mapped); + if (!found) { + errors.push(`${relFile}: broken image/asset ${src}`); + } + } + } + + if (errors.length > 0) { + console.error(`check-links: ${errors.length} issue(s)`); + for (const message of errors) console.error(` - ${message}`); + process.exit(1); + } + + console.log(`check-links: OK (${htmlFiles.length} html files scanned)`); +}; + +await run(); diff --git a/src/components/content/MdxPre.astro b/src/components/content/MdxPre.astro new file mode 100644 index 0000000..3650bf3 --- /dev/null +++ b/src/components/content/MdxPre.astro @@ -0,0 +1,26 @@ +--- +import CodeBlock from './CodeBlock.astro'; + +type Props = { + class?: string; + children?: unknown; +}; + +const { class: className, children } = Astro.props as Props; +const lang = className?.match(/language-(\S+)/)?.[1] ?? 'text'; + +const extractCode = (node: unknown): string => { + if (typeof node === 'string') return node; + if (!node || typeof node !== 'object') return ''; + const record = node as Record; + if (typeof record.value === 'string') return record.value; + if (Array.isArray(record.children)) { + return record.children.map(extractCode).join(''); + } + return ''; +}; + +const code = extractCode(children).replace(/\n$/, ''); +--- + + diff --git a/src/components/content/SmartLink.astro b/src/components/content/SmartLink.astro new file mode 100644 index 0000000..5062740 --- /dev/null +++ b/src/components/content/SmartLink.astro @@ -0,0 +1,30 @@ +--- +import * as stylex from '@stylexjs/stylex'; + +import { smartLinkStyles } from './SmartLink.stylex.ts'; + +type Props = { + href?: string; + rel?: string; + target?: string; + class?: string; +}; + +const { href = '#', rel, target, class: className } = Astro.props; +const isExternal = + href.startsWith('http://') || + href.startsWith('https://') || + href.startsWith('//'); +const resolvedRel = isExternal ? (rel ?? 'noopener noreferrer') : rel; +const resolvedTarget = isExternal ? (target ?? '_blank') : target; +--- + + + + diff --git a/src/components/content/SmartLink.stories.ts b/src/components/content/SmartLink.stories.ts new file mode 100644 index 0000000..db0059f --- /dev/null +++ b/src/components/content/SmartLink.stories.ts @@ -0,0 +1,24 @@ +import SmartLink from "./SmartLink.astro"; + +export default { + title: "content/SmartLink", + component: SmartLink, +}; + +export const Internal = { + args: { + href: "/articles/hello", + slots: { + default: "내부 기사 링크", + }, + }, +}; + +export const External = { + args: { + href: "https://docs.astro.build", + slots: { + default: "Astro 문서", + }, + }, +}; diff --git a/src/components/content/SmartLink.stylex.ts b/src/components/content/SmartLink.stylex.ts new file mode 100644 index 0000000..6374fe0 --- /dev/null +++ b/src/components/content/SmartLink.stylex.ts @@ -0,0 +1,20 @@ +import * as stylex from "@stylexjs/stylex"; + +import { tokens } from "../../styles/tokens.stylex.ts"; + +export const smartLinkStyles = stylex.create({ + link: { + color: tokens.color.accentInk, + textDecoration: { + default: "underline", + ":hover": "none", + }, + textUnderlineOffset: "3px", + }, + external: { + "::after": { + content: '" ↗"', + fontSize: "0.85em", + }, + }, +}); diff --git a/src/components/content/TableCard.astro b/src/components/content/TableCard.astro new file mode 100644 index 0000000..4e044a6 --- /dev/null +++ b/src/components/content/TableCard.astro @@ -0,0 +1,26 @@ +--- +import * as stylex from '@stylexjs/stylex'; + +import { tableCardStyles } from './TableCard.stylex.ts'; +--- + +
+ + +
+
+ + diff --git a/src/components/content/TableCard.stories.ts b/src/components/content/TableCard.stories.ts new file mode 100644 index 0000000..8570675 --- /dev/null +++ b/src/components/content/TableCard.stories.ts @@ -0,0 +1,14 @@ +import TableCard from "./TableCard.astro"; + +export default { + title: "content/TableCard", + component: TableCard, +}; + +export const Default = { + args: { + slots: { + default: `항목설명StyleX토큰 기반 스타일Astro정적 콘텐츠 렌더`, + }, + }, +}; diff --git a/src/components/content/TableCard.stylex.ts b/src/components/content/TableCard.stylex.ts new file mode 100644 index 0000000..41134a9 --- /dev/null +++ b/src/components/content/TableCard.stylex.ts @@ -0,0 +1,35 @@ +import * as stylex from "@stylexjs/stylex"; + +import { tokens } from "../../styles/tokens.stylex.ts"; + +export const tableCardStyles = stylex.create({ + wrapper: { + overflowX: "auto", + borderWidth: "1px", + borderStyle: "solid", + borderColor: tokens.color.border, + borderRadius: tokens.radius.md, + backgroundColor: tokens.color.surface, + marginBlock: tokens.space[6], + }, + table: { + width: "100%", + borderCollapse: "collapse", + fontSize: tokens.fontSize.sm, + lineHeight: tokens.lineHeight.body, + }, + cell: { + padding: tokens.space[3], + borderBottomWidth: "1px", + borderBottomStyle: "solid", + borderBottomColor: tokens.color.border, + textAlign: "left", + verticalAlign: "top", + }, + headCell: { + fontFamily: tokens.fontFamily.mono, + fontSize: tokens.fontSize.sm, + color: tokens.color.inkSecondary, + backgroundColor: tokens.color.surfaceMuted, + }, +}); diff --git a/src/components/layout/ArticleLayout.astro b/src/components/layout/ArticleLayout.astro new file mode 100644 index 0000000..1727068 --- /dev/null +++ b/src/components/layout/ArticleLayout.astro @@ -0,0 +1,157 @@ +--- +import ColorwayShell from '../patterns/ColorwayShell.astro'; +import Tag from '../ui/Tag.astro'; +import Heading from '../ui/Heading.astro'; +import Prose from '../ui/Prose.astro'; +import Stack from '../ui/Stack.astro'; +import Text from '../ui/Text.astro'; +import * as stylex from '@stylexjs/stylex'; + +import TableOfContents, { type TocEntry } from './TableOfContents.astro'; +import { articleLayoutStyles } from './ArticleLayout.stylex.ts'; +import { formatDisplayDate } from '../../lib/content.ts'; +import type { Colorway } from '../../styles/themes.ts'; + +type ArticleLayoutVariant = 'essay' | 'gallery' | 'docs'; +type ArticleDensity = 'compact' | 'comfortable' | 'spacious'; + +type Props = { + title: string; + description: string; + pubDate: Date; + updatedDate?: Date; + tags?: string[]; + colorway?: Colorway; + layout?: ArticleLayoutVariant; + density?: ArticleDensity; + cover?: string; + headings?: TocEntry[]; +}; + +const { + title, + description, + pubDate, + updatedDate, + tags = [], + colorway = 'canvas', + layout = 'essay', + density = 'comfortable', + cover, + headings = [], +} = Astro.props; + +const densityStyle = + density === 'compact' + ? articleLayoutStyles.densityCompact + : density === 'spacious' + ? articleLayoutStyles.densitySpacious + : null; + +const gridStyle = + layout === 'docs' + ? articleLayoutStyles.gridDocs + : layout === 'essay' || layout === 'gallery' + ? articleLayoutStyles.gridWithToc + : articleLayoutStyles.gridWithToc; + +const tocStyle = layout === 'docs' ? articleLayoutStyles.tocLeft : articleLayoutStyles.toc; +const mainStyle = + layout === 'essay' ? articleLayoutStyles.essayMain : articleLayoutStyles.main; +const showToc = headings.length > 0 && (layout === 'docs' || layout === 'essay'); + +const accentBarStyles = { + canvas: articleLayoutStyles.accentBarCanvas, + mint: articleLayoutStyles.accentBarMint, + clay: articleLayoutStyles.accentBarClay, + ink: articleLayoutStyles.accentBarInk, +} as const; +--- + + +
+ { + showToc && layout === 'docs' && ( + + ) + } + +
+ + { + layout === 'gallery' && cover && ( +
+ +
+
+ + {updatedDate && ( + <> + + updated {formatDisplayDate(updatedDate)} + + )} +
+
+ {title} +
+ {description} +
+
+ ) + } + + { + layout !== 'gallery' && ( + <> +
+ + {updatedDate && ( + <> + + updated {formatDisplayDate(updatedDate)} + + )} +
+
+ {title} +
+ {description} + + ) + } + + { + tags.length > 0 && ( +
+ {tags.map((tag) => ( + {tag} + ))} +
+ ) + } + + + + +
+
+ + { + showToc && layout !== 'docs' && ( + + ) + } +
+
diff --git a/src/components/layout/ArticleLayout.stylex.ts b/src/components/layout/ArticleLayout.stylex.ts index 9eb71d4..ba5c10a 100644 --- a/src/components/layout/ArticleLayout.stylex.ts +++ b/src/components/layout/ArticleLayout.stylex.ts @@ -39,4 +39,80 @@ export const articleLayoutStyles = stylex.create({ description: { marginBottom: tokens.space[8], }, + gridDocs: { + gridTemplateColumns: { + default: "1fr", + "@media (min-width: 1024px)": "200px minmax(0, 1fr)", + }, + }, + tocLeft: { + order: { + default: 2, + "@media (min-width: 1024px)": 0, + }, + display: { + default: "none", + "@media (min-width: 1024px)": "block", + }, + }, + essayMain: { + maxWidth: tokens.layout.readingMax, + marginInline: "auto", + width: "100%", + }, + coverGrid: { + display: "grid", + gap: tokens.space[6], + marginBottom: tokens.space[8], + gridTemplateColumns: { + default: "1fr", + "@media (min-width: 768px)": "minmax(180px, 280px) minmax(0, 1fr)", + }, + alignItems: "start", + }, + coverImage: { + width: "100%", + aspectRatio: "4 / 3", + objectFit: "cover", + borderRadius: tokens.radius.lg, + borderWidth: "1px", + borderStyle: "solid", + borderColor: tokens.color.border, + }, + tagRow: { + display: "flex", + flexWrap: "wrap", + gap: tokens.space[2], + marginTop: tokens.space[4], + }, + densityCompact: { + gap: tokens.space[6], + }, + densitySpacious: { + gap: tokens.space[12], + }, + accentBarCanvas: { + borderTopWidth: "3px", + borderTopStyle: "solid", + borderTopColor: tokens.color.accent, + paddingTop: tokens.space[4], + }, + accentBarMint: { + borderTopWidth: "3px", + borderTopStyle: "solid", + borderTopColor: tokens.color.mintAccent, + paddingTop: tokens.space[4], + }, + accentBarClay: { + borderTopWidth: "3px", + borderTopStyle: "solid", + borderTopColor: tokens.color.clayAccent, + paddingTop: tokens.space[4], + }, + accentBarInk: { + borderTopWidth: "3px", + borderTopStyle: "solid", + borderTopColor: tokens.color.inkAccent, + paddingTop: tokens.space[4], + }, }); diff --git a/src/components/layout/WikiLayout.astro b/src/components/layout/WikiLayout.astro new file mode 100644 index 0000000..2b4dbe1 --- /dev/null +++ b/src/components/layout/WikiLayout.astro @@ -0,0 +1,120 @@ +--- +import Tag from '../ui/Tag.astro'; +import Heading from '../ui/Heading.astro'; +import Prose from '../ui/Prose.astro'; +import Stack from '../ui/Stack.astro'; +import Text from '../ui/Text.astro'; +import * as stylex from '@stylexjs/stylex'; + +import TableOfContents, { type TocEntry } from './TableOfContents.astro'; +import { wikiLayoutStyles } from './WikiLayout.stylex.ts'; +import { formatDisplayDate } from '../../lib/content.ts'; + +type NavLink = { + href: string; + label: string; +}; + +type Props = { + title: string; + summary: string; + pubDate: Date; + updatedDate?: Date; + tags?: string[]; + domains?: string[]; + quizRefs?: string[]; + headings?: TocEntry[]; + prev?: NavLink; + next?: NavLink; +}; + +const { + title, + summary, + pubDate, + updatedDate, + tags = [], + domains = [], + quizRefs = [], + headings = [], + prev, + next, +} = Astro.props; +--- + +
+ { + headings.length > 0 && ( + + ) + } + +
+ + {title} + {summary} + +
+ + { + updatedDate && ( + <> + + updated {formatDisplayDate(updatedDate)} + + ) + } + { + domains.length > 0 && ( + <> + + {domains.join(', ')} + + ) + } +
+ + { + tags.length > 0 && ( +
+ {tags.map((tag) => ( + {tag} + ))} +
+ ) + } + + + + + + { + quizRefs.length > 0 && ( +
+ Quiz refs +
    + { + quizRefs.map((ref) => ( +
  • + {ref} +
  • + )) + } +
+
+ ) + } + + { + (prev || next) && ( + + ) + } +
+
+
diff --git a/src/components/layout/WikiLayout.stylex.ts b/src/components/layout/WikiLayout.stylex.ts index 18cd61f..62b88de 100644 --- a/src/components/layout/WikiLayout.stylex.ts +++ b/src/components/layout/WikiLayout.stylex.ts @@ -23,4 +23,37 @@ export const wikiLayoutStyles = stylex.create({ main: { minWidth: 0, }, + meta: { + display: "flex", + flexWrap: "wrap", + gap: tokens.space[2], + alignItems: "center", + fontFamily: tokens.fontFamily.mono, + fontSize: tokens.fontSize.sm, + color: tokens.color.inkTertiary, + }, + tagRow: { + display: "flex", + flexWrap: "wrap", + gap: tokens.space[2], + }, + quizRefs: { + marginTop: tokens.space[8], + paddingTop: tokens.space[6], + borderTopWidth: "1px", + borderTopStyle: "solid", + borderTopColor: tokens.color.border, + }, + pager: { + display: "flex", + justifyContent: "space-between", + gap: tokens.space[4], + marginTop: tokens.space[10], + paddingTop: tokens.space[6], + borderTopWidth: "1px", + borderTopStyle: "solid", + borderTopColor: tokens.color.border, + fontFamily: tokens.fontFamily.mono, + fontSize: tokens.fontSize.sm, + }, }); diff --git a/src/components/patterns/ColorwayShell.astro b/src/components/patterns/ColorwayShell.astro index 76bf03d..68899be 100644 --- a/src/components/patterns/ColorwayShell.astro +++ b/src/components/patterns/ColorwayShell.astro @@ -12,4 +12,12 @@ type Props = { const { colorway = 'canvas' } = Astro.props; --- -
+
+ +
diff --git a/src/components/patterns/ColorwayShell.stylex.ts b/src/components/patterns/ColorwayShell.stylex.ts index 913db0b..9476d51 100644 --- a/src/components/patterns/ColorwayShell.stylex.ts +++ b/src/components/patterns/ColorwayShell.stylex.ts @@ -8,4 +8,15 @@ export const colorwayShellStyles = stylex.create({ backgroundColor: tokens.color.canvas, color: tokens.color.ink, }, + canvas: {}, + mint: { + backgroundColor: tokens.color.mintCanvas, + }, + clay: { + backgroundColor: tokens.color.clayCanvas, + }, + ink: { + backgroundColor: tokens.color.inkCanvas, + color: tokens.color.inkPrimary, + }, }); diff --git a/src/content/articles/design-system/01.md b/src/content/articles/design-system/01.md new file mode 100644 index 0000000..615a839 --- /dev/null +++ b/src/content/articles/design-system/01.md @@ -0,0 +1,245 @@ +--- +title: "[FE쥬니어의 Design System] 01. Personal Design System, Storybook, Vite로 환경 설정하기" +description: "개인 디자인 시스템 GetIt-UI를 Storybook + Vite + Monorepo로 구축한 기록." +pubDate: 2022-02-03 +tags: ["design-system", "storybook", "vite", "monorepo"] +theme: + colorway: clay + layout: docs + density: spacious +--- + +> _`Storybook` + `React`로 DesignSystem/ComponentLibrary 만드는 튜토리얼은 이미 블로그, Youtube에 널려있다_ +> +> _Airbnb, GitHub 등 개발팀에서 만든 `Storybook`은 물론이고, GitHub가 공개된 경우도 있다 *(아래 references 참고)*_ + +[👉  구현 중인 `GetIt-UI` GitHub 보기](https://github.com/gitgitWi/getit-ui) + +## Motivation + +- 요즘 차트 컴포넌트 구현을 `Storybook` 기반으로 진행중([zum-stock-line-stories](http://zum-stock-line-stories.surge.sh/))인데, 생각보다 엄청난 생산성 향상을 맛보며 재미를 느낌 + + - 별도의 페이지를 구성하지 않아도 컴포넌트 구현 결과를 HMR로 볼 수 있음 (_이건 당연한 거!_) + - **_개발자 도구 들어가서 ➡️ 메뉴를 클릭하고 ➡️ 뭔가를 조정해야 하는 수고들을 덜어줌!_** + - [Measure & Outline](https://storybook.js.org/docs/react/essentials/measure-and-outline): 컴포넌트 사이즈, 아웃라인 등 확인 가능 + - [Viewport](https://storybook.js.org/docs/react/essentials/viewport): mobile viewport preset 가능 + - Docs로 한번에 여러 렌더링 상태 확인 가능 + - addon 생태계가 활성화되어있음 + - Jest, Cypress 등 다양한 툴과의 통합 + - `Storybook`을 따로 빌드, 배포 가능 + + - 현재까지 개발 진행상황 공유 가능 + - 색상, 크기, 데이터값 등 **_속성들을 [`controls`](https://storybook.js.org/docs/react/essentials/controls)로 바꿔서 바로 확인 가능_** 👉 디자이너/기획자와의 보다 빠른 소통 가능할 듯?! + + ![zum-stock-line-stori_2022-2-2](https://user-images.githubusercontent.com/57997672/156254919-f4795507-1ecf-4e88-b8ef-a037ab071169.gif) + +
+ +- 거의 1년째 방치 중인 개인 블로그는 물론이고, **_셋팅만 열심히 했던 개인 프로젝트들_** 빠르게 완성도를 높이기 위해 디자인 시스템을 만들어보자 + + - 단기적으로 1년 정도는 **컴포넌트 라이브러리** 구축 위주 방향이 될 듯, 아무래도 디자인/UX에 대한 지식이 없기 때문 + - 장기 프로젝트로 디자인 시스템으로 발전시켜 나가자 + +
+ +- 회사 업무에 적용하기 전 다양한 테스트를 위한 도구로 사용할 수 있음 + - 하고 싶은 거 다하자!! + - 얼마전 _`당근마켓 프론트엔드 개발자 라이브 QnA 세션`_ 에서 얻은 인사이트 + - _회사 코드를 포트폴리오로 사용할 수는 없으니 유사한 구현 내용을 개인 GitHub에 올려놓는 것도 방법이다!_ + +--- + +## Design Patterns & Techs I Used + +- **Atomic Design Pattern & Monorepo** + + - 기본적으로 컴포넌트 라이브러리이기 때문에, atoms, molecules, organisms 정도까지만 하면 될 듯 + - 이 단계에 따라 각각의 패키지 구성; `atoms/button`, `atoms/text` 등 + - 디자인 시스템 중 단일 Repo로 구성하는 경우도 많지만, 추후 `Svelte`, `Solid` 등 다양한 프레임워크/라이브러리 및 `rollup`, `esbuild` 등 다양한 빌드 툴 사용해볼 수 있도록 Monorepo로 구성 + - `Yarn berry` [PnP 모드](https://yarnpkg.com/features/pnp)에서 [workspace 플러그인 사용](https://github.com/yarnpkg/berry/tree/master/packages/plugin-workspace-tools) + + ```bash + getit-ui/ + ┣ atoms/ # atom packages + ┃ ┣ button/ + ┃ ┃ ┣ src/ + ┃ ┃ ┣ package.json + ┃ ┃ ┣ tsconfig.json + ┃ ┃ ┣ tsconfig.node.json + ┃ ┃ ┗ vite.config.ts + ┃ ┗ text/ + ┃ ┣ src/ + ┃ ┣ package.json + ┃ ┣ tsconfig.json + ┃ ┣ tsconfig.node.json + ┃ ┗ vite.config.ts + ┣ molecules/ # molecules packages + ┣ stories/ # storybook package + ┃ ┣ .storybook/ + ┃ ┃ ┣ main.ts + ┃ ┃ ┗ preview.ts + ┃ ┣ src/ + ┃ ┃ ┣ Introduction.stories.mdx + ┃ ┃ ┣ react-app-env.d.ts + ┃ ┃ ┗ setupTests.ts + ┃ ┣ .gitignore + ┃ ┣ package.json + ┃ ┗ tsconfig.json + ┃ # root configs + ┣ .editorconfig + ┣ .eslintignore + ┣ .eslintrc.js + ┣ .gitignore + ┣ .pnp.cjs + ┣ .pnp.loader.mjs + ┣ .prettierrc + ┣ .yarnrc.yml + ┣ Dockerfile.dev + ┣ README.md + ┣ package.json + ┣ tsconfig.components.json + ┣ tsconfig.json + ┗ yarn.lock + ``` + +
+ +- **`Storybook`** + + - 구현된 컴포넌트들 시각 테스트 + - 회귀, E2E 등 다양한 테스트, 문서화 + - 별도의 전용 패키지(`stories`)를 만들어서 실행 + +
+ +- **`Vite` + `React`** + + - 일단은 `React` 컴포넌트 위주 구성 + - `Vite` [라이브러리 모드](https://vitejs-kr.github.io/guide/build.html#library-mode)로 빌드 + +
+ +- **`CSS(SCSS) Module`** + + - 개인적으로 `CSS`를 활용할 때 가장 선호하는 방식 + - `styled-component`/`emotion` 보다 `CSS` 그 자체를 사용하면서, `JS` 방식으로 `className` 적용을 컨트롤 할 수 있음 + - [Real-world CSS vs. CSS-in-JS performance comparison](https://pustelto.com/blog/css-vs-css-in-js-perf/); 성능 면에서 `CSS-in-JS`가 좋지 않다는 이야기도.. + - 특히 이번 대선 차트 구현할 때 이점을 크게 맛보게 되었는데, IE에서 `grid(-ms-grid)`가 생각대로 컨트롤 하기 어렵다는 점 때문에, IE 전용 클래스를 따로 만들어 `userAgent`에 따라 분기처리해 적용함 + + ```scss + // 모던 브라우저 클래스 + .line { + @extend .line-default; + display: grid; + grid-template-columns: 13px minmax(36px, auto) minmax(46px, auto); + } + + // IE 전용 클래스 + .line-ie { + @extend .line-default; + display: -ms-flexbox; + + div { + margin-right: 5px; + } + + p { + width: 47px; + } + } + ``` + + ```typescript + // line 컴포넌트 (VanillaJS) + classNames: [IS_IE ? styles["line-ie"] : styles["line"], ...classNames]; + // ... + ``` + +--- + +## _Problem & Solutions_ + +### `Vite` 라이브러리 모드 빌드 -> 모노레포에서 사용시 `CSS`를 가져오지 않는 문제? + +**_Problem_** + +- HTML 파일을 생성하는 게 아닌, JS로 컴포넌트 렌더링 로직만 만드는 것 +- 일반적인 페이지를 빌드하는 경우, `Webpack` 등 번들러가 `HTML`에 `link` 태그를 자동삽입하고, 렌더링시 스타일을 반영함 +- 그런데 라이브러리 모드에서 빌드시 JS에 `CSS Module` 클래스명만 가져오고, CSS 파일(`import 'styles.css'`)을 가져오지 않음 +- **_다른 패키지에서 사용시 스타일 적용 안됨_** + + ![image](https://user-images.githubusercontent.com/57997672/156269529-6740dc85-674a-43b1-be44-badcdd9b6d4f.png) + +
+ +**_Solution_** + +- [vite-plugin-css-injected-by-js](https://www.npmjs.com/package/vite-plugin-css-injected-by-js?activeTab=readme) 사용 + + - `vite.config.ts`에서 플러그인 추가 + + ```javascript + import cssInject from "vite-plugin-css-injected-by-js"; + + export default defineConfig({ + plugins: [react(), cssInject()], + + /** 기타 설정 */ + }); + ``` + + - 빌드시 아래와 같이 `style` 태그 생성 로직을 생성, 빌드된 파일 상단에 삽입 + + ![image](https://user-images.githubusercontent.com/57997672/156269833-c3533abf-75d6-4d09-923c-b1dd37b78ec8.png) + + - 동일 컴포넌트를 여러개 호출하더라도 하나의 `style` 태그만 생성함 + + ![image](https://user-images.githubusercontent.com/57997672/156270703-4cc60b4e-36c5-4b03-9e56-86c26d0d5347.png) + + - _근데 이렇게 되면 `CSS-in-JS`를 쓰는 방식과 성능면에서 차이가 없어질 수도 있지 않을까하는 의문이.._ + +
+ +### Storybook 전용 `stories` 패키지에서 Vite 사용시 빌드 오류 + +**_Problem_** + +- 현재 각각의 컴포넌트 패키지 + `Storybook` 전용 패키지가 있는 상황 +- 컴포넌트 패키지들이 이미 `Vite`를 사용하고 있고, (왠만하면) `Webpack` 보다는 `Vite`가 빌드 타임이 짧기 때문에 [Vite 빌더](https://storybook.js.org/blog/storybook-for-vite/)를 써보려고 시도 함 +- 문제는 `Yarn berry` 환경에서 `Vite` 빌더가 의존성 제대로 못 찾고 빌드가 안됨 + - ; '22.3.2. 현재 아직 해결되지 않은 이슈😭 + +**_Solution_** + +- 아쉽지만 `stories` 패키지만 빌더를 `CRA` + `Webpack5`로 지정해 사용 중 +- 내 생각에 베스트는, `Storybook` 전용 패키지를 별도로 분리하지 않고, 각각의 컴포넌트 패키지에서 `story` 파일를 만들어서 root에서 preview를 실행하도록 하는 것 + - 다만 Monorepo 구조의 root에서는 뭔가 설정하기가 까다로움 + - `Storybook` 자체가 빈 프로젝트에서 실행하는 게 아니기 때문인듯 + > _Storybook needs to be installed into a project that is already set up with a framework. It will not work on an empty project.()_ + - _좀더 연구해봐야 할 듯..?_ + - 한편 패키지를 분리했기 때문에, 다른 패키지에서 `import` 했을 때 잘 되는지 확인 할 수 있다는 장점은 있는 듯 + +
+ +--- + +### References + +#### Design System Overview + +- [토스, 에어비앤비로 알아보는 디자인 시스템의 내일](https://dewberry9.github.io/future-of-design-system) +- [Do you think your component library is your design system? Think again](https://uxdesign.cc/do-you-think-your-component-library-is-your-design-system-think-again-7e2c902b5275) + +#### Real-World Cases + +- [awesome-storybook](https://project-awesome.org/lauthieb/awesome-storybook) +- [awesome-design-systems](https://github.com/alexpate/awesome-design-systems) +- 쿠팡: [이커머스에 최적화된 디자인 시스템, RDS](https://youtu.be/g4f7p0YcVnc) - 주로 UX 디자인에 대한 이야기 +- 토스 + - [TDS로 UI 쌓기 : 그 많던 코드는 누가 다 치웠을까?](https://youtu.be/pfKuEuufgdc) - 토스 안드로이드 + - [Do more, with less. - 디자인 시스템, 그다음은?](https://youtu.be/LmLchZ4tCXc) - 토스 FE +- 배민: [[6월 우아한테크세미나] 디자인시스템이 가져온 변화](https://youtu.be/aVHLcQzcRbA) - 배민 FE +- LINE: [개발자와 디자이너의 협업을 위한 LINE 디자인 시스템, LDS 소개](https://engineering.linecorp.com/ko/blog/line-design-system/) +- SOCAR: [[SOCAR FRAME 만들기 #1] 쏘카의 디자인 시스템 맛보기1~5](https://tech.socarcorp.kr/design/2020/06/23/socar-design-system-01.html) +- 카카오: [FE개발자의 성장 스토리 03 : 카카오 어드민 UI 컴포넌트를 모노레포로 개발하여 얻은 것들](https://tech.kakao.com/2020/12/03/frontend-growth-03/) +- AirBnB: [Building (And Re-Building) the Airbnb Design System | Maja Wichrowska & Tae Kim](https://youtu.be/fHQ1WSx41CA) - 2016년 이후 AirBnB에서 React 기반 디자인 시스템을 만들게 된 배경 diff --git a/src/content/articles/from-jekyll-to-nextjs/00.mdx b/src/content/articles/from-jekyll-to-nextjs/00.mdx new file mode 100644 index 0000000..c923f9c --- /dev/null +++ b/src/content/articles/from-jekyll-to-nextjs/00.mdx @@ -0,0 +1,94 @@ +--- +title: "00. 잘 쓰고 있던 걸 굳이 바꾸는 이유" +description: "GitHub Pages Jekyll 블로그에서 Next.js + MDX로 옮기기로 한 배경과 로드맵." +pubDate: 2022-03-01 +tags: ["jekyll", "nextjs", "mdx", "migration"] +theme: + colorway: canvas + layout: essay + density: comfortable +--- + +import Heading from "../../../components/ui/Heading.astro"; + +## GitHub Pages, `Jekyll` 기반에서 `Next.js` + `MDX`로 + +00. 잘 쓰고 있던 걸 굳이 바꾸는 이유 + +
+ +### intro. + +작년 2월, 본격 개발에 입문한 지 만 2개월 정도 되어서 `GitHub Pages`라는 걸로 블로그를 만들 수 있다는 걸 알게 되었다. + +뭣도 모르고 유튜브 따라, 블로그 따라 헤매면서 어찌어찌 `Jekyll` 기반 블로그를 만들게 되었고.. +처음 한두달은 "이런 게 개발자 블로그지!"라며 교육 받은 거, 멘토링 행사 참여한 거, 생애 첫 코딩 테스트 후기 등등 신나게 올렸다. + +
+ +### 문제는 + +- `Ruby` 기반이어서 레퍼런스를 찾기도 어렵고, +- 그래서 뭔가 커스텀을 하고 싶어도 하기 어려워지고, +- 무엇보다 포스팅이 많아질수록 **_컴파일하는 시간이 크게 늘어난다_** + - 단순히 글만 올리는게 아니라, 태그별 검색도 넣고, 페이지네이션도 들어가는데, + - 이걸 전부 `html`로 만들어버리기 때문에 불필요한 중복이 너무 많아지는 느낌 + +그래서 한동안은 그냥 티스토리, velog를 사용하거나, Notion에 정리하거나, _~~아예 안하게 되었..~~_ + +
+ +### 블로그 못 잃어.. + +최근까지 거의 강의 수강이나 독서를 하면서 Notion에 정리하고 있었다 + +아무래도 `Markdown` 문법을 지원하고 거기에 이런저런 임베딩도 편하고 공유도 편한 편이라 쓰게 되는 것 같다 + +한편으로는 구글링으로 뭔가를 검색할 때 나오는 블로그 글들의 수준이.. + +나까지 굳이 소음을 더 만들어야 하나하는 생각에 블로그를 잘 안하게 되기도 했다 + +그런데 최근 텔레그램에서 재테크 네임드들이 블로그 좀 하라고 강력 권장하는 글을 많이 보게 되기도 했고, + +무엇보다 사내 블로그 스터디를 하게 되면서 좀더 반강제적인(!) 동기가 생기게 되었다 + +
+ +### 왜 `Next.js` + `MDX` 인가 + +`Next.js`를 선택하게 된 데는 특별한 이유는 없었고, + +요즘 FE 개발한다면 십중팔구는 `React`를 하고, `React`를 하다보면 `Next.js`를 하게 되는, 대유행(?) 시대이기 때문에, 나도 언젠가 다음 개인 프로젝트는 `Next.js`로 해야겠다는 막연한 생각이 있었다 + +지금 회사에서는 `Vue.js`를 쓰기 때문에 `Nuxt.js`를 쓸 수도 있긴 하겠지만, `Vue.js` 레퍼런스도 그리 많지 않은데 `Nuxt.js` 관련해선 더 없을 것 같기도 하고, `VSCode`에서 타입추론이나 자동완성이 잘 안되는게 답답한 맛도 있기도 하고, Udemy에 결제해 논 `React`, `Next.js` 강좌도 살살 들어야해서 `Next.js`로 가기로 했다 + +그렇다면 `Jekyll`에서 했던 것처럼 `Markdown` 으로 글을 작성할 수 있는게 `Next.js`에선 뭐가 있을까 검색해보니, `Markdown` 관련 플러그인을 설치하기도 하고, MDX를 사용하기도 하는 것 같았다. + +`MDX`는 `Markdown`에 `React` 컴포넌트를 그냥 `import`로 가져와서 붙여넣기만 하면 되는 식이어서 글 쓰기도 편하고, 컴포넌트로 뭔가 커스텀하기도 좋아보였다. 다만 코드(?)가 너무 변태 같다는 것이 조금 어색하긴 하다.. + +```mdx +import Text from "../component/Text.tsx"; + +## 이건 마크다운 헤더 + + +``` + +
+ +### 마지막으로, 앞으로의 로드맵 + +할게 많구만.. + +- [ ] 아름다운 컴포넌트 구현 (⭐️⭐️) +- [ ] custom code syntax hightlight (⭐️) +- [ ] GitHub 댓글 컴포넌트 붙이기 +- [ ] 사이트 맵 -> 구글 검색 다시 등록하기 +- [ ] RSS 생성 +- [ ] 다른 사이트 임베딩; 노션 북마크, 임베딩 효과 참고.. + - [ ] Gist나 Codepen, Stackblitz 등 웹 IDE + - [ ] Youtube +- [ ] post 마다 OpenGraph 태그 생성 + - [ ] 썸네일 자동 생성기..? +- [ ] 사이트 내 검색 -> 서버리스 SQL 사용?? +- [ ] 개인 포트폴리오 연동 diff --git a/src/content/articles/hello.mdx b/src/content/articles/hello.mdx new file mode 100644 index 0000000..2aed9e0 --- /dev/null +++ b/src/content/articles/hello.mdx @@ -0,0 +1,22 @@ +--- +title: "Hello MDX!" +description: "Astro Content Collections MDX 스모크 테스트 글입니다." +pubDate: 2022-01-15 +tags: ["astro", "mdx", "migration"] +draft: true +theme: + colorway: mint + layout: gallery + density: comfortable +cover: "https://user-images.githubusercontent.com/57997672/156254919-f4795507-1ecf-4e88-b8ef-a037ab071169.gif" +--- + +import Heading from "../../components/ui/Heading.astro"; + + + Hello MDX! + + +```typescript +const hello = () => `World!`; +``` diff --git a/src/content/til/refactoring-javascript/01.mdx b/src/content/til/refactoring-javascript/01.mdx new file mode 100644 index 0000000..12db315 --- /dev/null +++ b/src/content/til/refactoring-javascript/01.mdx @@ -0,0 +1,99 @@ +--- +title: "1. 리팩토링이란?" +summary: "리팩토링의 정의와 품질 균형" +pubDate: 2023-06-01 +tags: ["refactoring", "javascript", "study"] +visibility: public +--- + +> 리팩토링은 코드를 변경하는 것이 아니다 + +- 코드 동작을 바꾸면 안 된다는 것 +- 코드 동작이 변하지 않는다는 것을 어떻게 보장? +- 코드 동작은 변하지 않으면서 코드를 변경한다는 것은 어떤 의미일까? + +## 1.1 코드 동작이 변하지 않는다는 것을 어떻게 보장할 수 있을까? + +리팩토링할 때 동작의 모든 유형을 다루는 것이 아닌, 다음 내용을 주로 다룬다 + +- 구현 세부 사항 +- 불특정하고 검증되지 않은 동작 +- 성능 + +=> 테스트, 버전 관리를 사용 + +## 1.2 구현 세부 사항에 관심을 두면 어떨까? + +예를 들어, 숫자에 2를 곱하는 간단한 함수를 구현할 때 + +- `return number * 2` +- `return number << 1` + +이런 부분이 구현 세부 사항 + +이때 `number` 값이 너무 커지면 두번째 경우는 문제가 발생 + +-> 테스트 케이스는 처음 생각했던 것보다 더 많은 경우를 포함해야 한다는 것을 의미 + +- 변경한 구현 세부 사항에서 숫자를 두 배로 한 부분이 우리가 신경 써야 할 동작, +- 세부 사항 테스트는 대부분 불필요 + +## 1.3 불특정하고 검증되지 않은 동작에 관심을 두면 어떨까? + +실행 테스트, 수동 절차, 어떻게 동작하는지 최소한의 설명이 없으면 기본적으로 해당 코드를 검증할 수 없다는 것을 의미 + +테스트가 부족한 코드 또는 실행시킬 수 있는 최소한의 문서가 없는 코드는 코드를 변경하기 어렵고, 그러면 동작 변경을 검증할 수 없어 리팩토링하기 어렵다 + +> 테스트를 거치지 않은 코드는 리팩토링할 수 없다 + +## 1.4 성능에 관심을 두면 어떨까? + +사용자는 성능보다는 동작과 기댓값에 더 중점을 둔다 + +첫번째 구현에서 결과가 충분히 좋다는 것은 적절한 시간 안에 입력으로 기댓값을 산출할 수 있다는 의미; 비기능적 검증 (nonfunctional testing) + +## 1.5 동작이 변하지 않는다면 리팩토링의 요점은? + +> 리팩토링의 요점은 행동을 유지하면서 품질을 향상하는 것 + +## 1.6 품질 균형 잡기와 일 끝마치기 + +기술부채 + +- 소규모 프로젝트에서 가시적이고 설명 가능한 기술부채는 어느 정도 허용될 수 있지만, +- 큰 프로젝트에서는 품질을 중요하게 여기며 진행해야 + +## 1.7 품질이란 무엇이며 리팩토링과 어떤 관련이 있을까? + +코드 품질을 좋게 하려는 수많은 노력들 + +- SOLID; 단일 책임, 개방 폐쇄, 리스코프 치환, 인터페이스 분리, 의존성 역전 +- DRY; 반복 X +- KISS; 정말 간단해야 +- GRASP; 일반적인 책임 할당 소프트웨어 패턴 +- YAGNI; 나중에 필요 없어질 것을 고려 + +명확한 기준은 없다, 다만 제대로 작동하고 쉽게 확장될 수 있는 코드를 만드는 것 + +코드 테스트를 작성하고 쉽게 테스트할 수 있는 코드를 작성하는 것 + +- 인터페이스 단순화하기 위해 함수와 모듈 추출 +- 테스트를 이용해 코드 동작 확인 +- 가능하면 불순한 기능 피하기 +- 변수 및 함수 이름 잘 짓기 + +자신만의 `소프트웨어 품질 원칙`을 구성해보라 + +## 1.8 탐험으로서 리팩토링 + +리팩토링은 코딩에 자신감을 갖게 하고 작업 중인 내용을 잘 이해하는 데 도움을 준다 + +## 1.9 어떤 것이 리팩토링이고 어떤 것이 아닐까? + +새로운 코드와 기능을 만드는 것은 리팩토링이 아니다 + +기존 코드에서 인터페이스(동작)에 대한 변경 사항은 테스트를 중단시키거나, 충분하지 않은 테스트를 하게 할 수 있음 + +반면 구현 세부 사항을 변경할 때는 테스트가 중단되어서는 안된다 + +## 1.10 마무리 diff --git a/src/content/til/tdd/fullstack-tdd-by-newbie.mdx b/src/content/til/tdd/fullstack-tdd-by-newbie.mdx new file mode 100644 index 0000000..e1d24d3 --- /dev/null +++ b/src/content/til/tdd/fullstack-tdd-by-newbie.mdx @@ -0,0 +1,104 @@ +--- +title: "TDD 뉴비가 느낀 러닝 커브와 재미" +summary: "주간회고 — Full-stack TDD 경험 회고" +pubDate: 2022-01-24 +tags: ["tdd", "testing", "career"] +visibility: public +--- + +- 주간회고 02. TDD 뉴비의 Full-stack(?) TDD 1편 + +#### 3줄 요약 + +> - 테스트 코드 뉴비로서 어디서부터 뭘 해야될 지 몰라 막막함을 느끼고, 파일럿 프로젝트 때의 안좋은 추억이 되살아났다 +> - 단순히 툴 사용법이 아니라, 테스트 코드를 작성하는 원칙이 뭔지 좀더 공부하고 정리해보고 싶다 +> - 많은 장점들이 있지만, 무엇보다 테스트 패스할 때의 소소한 즐거움이 제일 큰 장점인 듯 + +
+ +--- + +## 테스트 코드 앞에만 서면~ 나는 왜 작아지는가~ + +![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643041008094/t76_KRs7T.png) + +> _지금 보니, 약간 망했다는 얘기를 굉장히 구구절절 쓴 것 같다..._ + +작년 수습기간 파일럿 프로젝트(`investing.com` 클론, [👉 파일럿 프로젝트 회고](https://zuminternet.github.io/zum-front-investing-clone/)) 초반에 여러가지로 시간을 많이 날렸지만, 그중에서도 가장 허탕을 쳤던 것 중 하나는 TDD로 개발을 해본답시고 1주일을 날린 것이다 + +1년 반이라는 예상보다 길고 힘들었던 커리어 전환 기간을 거치고 취업한 상태여서 너무 신이 났었는지, **_이것도 해보겠다 저것도 해보겠다 너무 욕심이 많았던_** 것 같기도 하다 + +`Vue.js`도 처음해보는데 `TDD`를 해보겠다하고, 데이터 업데이트를 실시간으로 해보겠다고 `SSE`를 도입하고, 몇번 써보지도 않은 `MongoDB`에 잘 맞지도 않는 `TypeORM`을 억지로 끼워넣느라 많지도 않은 시간을 삽질하는데 써버렸고, 결국 중간발표까지 요구사항 절반도 못하게 되는 대참사가 벌어졌다 + +그나마 `SSE`, `TypeORM`은 어떻게든 됐고, `아 이렇게 하는거 아니구나` 하는 걸 깨닫기도 했지만, **_`Frontend TDD`는 결국 시간만 쓰고 혼란스러움만 남긴 채 접게 됐다_** + +
+ +그렇게 약 5개월의 시간이 지나고, 차트 서버를 담당하게 되면서 모놀리식 구조에서 모노레포 구조로 바꾸고, 프레임워크를 `Koa.js`에서 `Nest.js`로 변경하는 대공사를 벌이게 된다 + +내가 바꾼 코드들이 제대로 동작하는지에 대해 수작업으로 확인하는 과정들이 반복됐고, 이 **단순 반복 작업들이 너무 귀찮아지면서 자연스럽게 TDD에 대한 관심으로 흘러갔다** + +프로젝트 구조를 모노레포로 바꿀 당시에는 줌투자 2차 오픈 일정이 빡빡하게 돌아가던 중이라, 구조 변경하랴 맵차트 개발하랴 테스트 코드를 넣을 시간이 없었지만, 오픈 이후 조금 여유로워진 상황에서는 TDD 방식으로 프레임워크를 `Nest.js`로 변경할 수 있는 시간이 충분했다 + +리팩토링 관련 책들을 읽으면서 **테스트 없는 리팩토링은 반쪽짜리 리팩토링이라는 걸 깨달았기 때문에**, 이번에는 TDD를 제대로 해보면서 리팩토링을 하고싶다는 마음이 들었다 + +문제는, 다시 한번 마주한 ***테스트 코드를 어떻게 짜야하는가에 대한 막막함***이었다 + +
+ +## 지금 당장 TDD 하고 싶은데, 뭘 어떻게 해야 할까..??? + +TDD는 `요즘 개발자(?)`라면 당연히(?) 하는 거기도 하고, 워낙 많은 테스트 관련 책과 강의, 블로그, 아티클들이 있기 때문에 공부를 한다면 충분히 할 수 있는 분야인 것 같다 + +하지만 **당장 프로젝트에 적용해보려고 구글링해보면, 생각보다 볼만한 것들이 많지 않았던 것 같다** + +물론 JS 테스트 코드를 짜려고 하면 언제나 나오는 `Jest` 사용법 정도는, 정말 너무 많다 싶을 정도로 자료가 많다 + +> _이런 라이브러리나 툴 사용법은 필요할 때마다 그때그때 공식문서 위주로 찾아보고, StackOverflow 검색해보고, 그래도 뭔가 부족하다 싶으면 `GitHub`이나 [SourceGraph](https://sourcegraph.com/search), [tabnine](https://www.tabnine.com/code) 오픈 소스 코드 검색으로 예제를 찾아보면 꽤 괜찮은 내용들이 많다_ + +그러나 당장 프로젝트에 적용하기 위해 찾아보고 싶었던 주제는 이런 툴 사용법이 아닌, **_단위 테스트에선 무엇을 어떻게 테스트해야 하는가, Controller를 테스트하는데 MockService는 왜 만들어야하나 등 원리원칙적인 내용들을 찾고 싶었다_** + +파일럿 프로젝트 때 TDD에 실패했던 이유 중 하나는, 컴포넌트 단위 테스트에서 이벤트, api 요청 등 단위 테스트 범위를 벗어나는, e2e 테스트에서 해야 될 것들을 자꾸 하려고 했기 때문이다 + +다른 하나는 테스트를 할 때 테스트 대상이 아닌 나머지 로직과 데이터를 Mock으로 만들어 제공하는 의미를 알지 못했고, 모든 걸 다 Mock으로 만들어서 제공하면 당연히 성공할 수 밖에 없는거 아닌가 하는 의문 때문에, 테스트 코드를 구현하는 게 의미 없다는 생각이 들었기 때문이다 + +부족한 구글링 실력 탓도 있겠지만 원하는 결과는 찾기 어려웠고, 차라리 내가 좀더 공부하고 정리해서 공유하는게 낫겠다는 생각이 들었다 + +올해 안으로 가이드라인을 정리해서, GitBook 이나 간단한 웹으로 만들어 공유해 볼 예정이다 + +
+ +![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643045178911/CJd3G_5f3.png) + +![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643045247027/uefVjTMZ9.png) + +하지만 기왕 쓰는 김에.. 회사 팀원들, 블로그, 유튜브, 강의 등 여기저기서 주워들은 내용들, 그리고 직접 테스트 코드를 구현하며 깨닫게 된 **아주 간단한 원칙**은 이렇다 + +- 모든 테스트는 주어진 상황/컨텍스트(`given`)에 대해 성공 조건(`when`)-성공 결과(`then`), 실패 조건(`when`)-실패 결과(`then`)를 테스트한다 + +- 단위 테스트는 함수/클래스 내부 동작이 잘 동작하는지를 테스트하기 때문에, 당연히 나머지 외부에 대해서는 정상 동작한다고 가정한다 + - Mock 함수, 데이터를 잘 만들어야 하는 이유 👉 이 때문에 그다지 효율적이진 않을 수 있다 + - 새로운 기능 개발하는 경우엔, Mock 함수/데이터를 만들면서 인터페이스나 추상클래스를 만들수 있기 때문에 설계 측면에선 괜찮을 듯 + +![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643046228793/CjJWMTHL1.png) + +- e2e/통합 테스트는 내부 동작보다는 함수/클래스 등 전반적인 기능을 테스트할 수 있다 + - 말그대로 end-to-end, 사용자 input에 따른 output이 어떻게 나오는지를 테스트 + - 실제 데이터, api를 가지고 테스트할 수 있음; mock 데이터/함수도 당연히 사용 가능 + - 리팩토링 등 이미 구현된 기능에 대해 코드 변경이 있는 경우 + +
+ +## 10년 만에 느껴보는 수학 문제집 푸는 맛(?) + +![image.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1643101897718/aeW-AlUHM.png) + +> _VSCode, IntelliJ 등 에디터에서 테스트 관련 설정을 하면, 굳이 테스트 관련 명령을 하지 않아도 알아서 코드 수정을 반영해 테스트를 실행해준다_ + +테스트 코드의 장점은 누구나 알다시피, 코드 변경을 하더라도 기능이 잘 작동하는지를 빠르게 확인할 수 있다는 점이다 + +그것말고도 개인적으로 꽤 괜찮은 side-effect를 느낀 부분은 ***개발자로서 자기 효능감을 느낄 수 있다***는 점이다 + +학창 시절 수학 문제집을 풀고 채점할 때, 동그라미가 하나씩 늘어날때마다 기분이 좋았던 기억이 있는데, 깔끔하게 통과한 테스트 결과를 보면 왠지 상쾌하고 기분이 좋아지는 듯 하다 + +개발은 공부하면 할수록 스스로 부족하다고 느낄 때도 많지만, 이렇게 사소하게나마 *`내가 만든 코드가 테스트를 통과했구나`*하는 즐거움을 주는 게 가장 큰 장점이 아닐까 생각이 든다 diff --git a/src/content/til/ts/enum-to-template-literal.mdx b/src/content/til/ts/enum-to-template-literal.mdx new file mode 100644 index 0000000..1c8df5d --- /dev/null +++ b/src/content/til/ts/enum-to-template-literal.mdx @@ -0,0 +1,236 @@ +--- +title: "[TypeScript] enum -> literal 타입 갈아타기" +summary: "enum 대신 literal + ReadonlyRecord 패턴 정리" +pubDate: 2022-01-20 +tags: ["typescript", "enum", "class-validator"] +visibility: public +--- + +### 3줄 요약 + +> - `enum`은 tree-shaking, memory-leak 문제가 있고, 여러 enum을 하나의 enum으로 합칠 수 없다 +> - `class-validator`에서 `IsEnum`을 쓰려면, 타입이 아닌 객체가 필요하다 +> - `literal`과 `Readonly>` 유틸 타입을 활용해 문제를 해결했다 + +[👉 hashnode에서 보기](https://wiii.hashnode.dev/enum-to-template-literal) + +--- + +## `enum`은 비싸고 불편하다! + +다른 언어들과 마찬가지로 `TypeScript`도 열거형 타입으로 [`enum`](https://www.typescriptlang.org/ko/docs/handbook/enums.html)을 제공한다 + +함수 인자로 특정 string만 들어와야 하는 경우 등 유용하게 쓸 수 있다 + +
+ +### Tree-shaking, 메모리 문제 + +그러나 Line 기술 블로그(['TypeScript enum을 사용하지 않는 게 좋은 이유를 Tree-shaking 관점에서 소개합니다.'](https://engineering.linecorp.com/ko/blog/typescript-enum-tree-shaking/))에서 잘 설명하는 것처럼, 순진한 enum 타입은 tree-shaking이 안되고 메모리 낭비까지 이어질 수 있다. + +
+ +### 예상과는 다른 Type Union + +무엇보다 개인적으로 가장 불편하게 느껴지는 것은 다른 타입들과는 다르게, **_enum은 `union`으로 여러 enum을 하나로 합칠 수 없다는 점이다_** + +아래 예시 코드는 실무에서 구현 중인 API 타입 일부를 조금 수정해서 가져온 것이고, 전형적인 enum을 가져왔다 + +```typescript +enum PeriodA { + DAILY = "DAILY", + MONTHLY = "MONTHLY", +} + +enum PeriodCommon { + WEEKLY = "WEEKLY", + QUARTERLY = "QUARTERLY", + YEARLY = "YEARLY", +} +``` + +[Union Type](https://www.typescriptlang.org/ko/docs/handbook/2/everyday-types.html#%EC%9C%A0%EB%8B%88%EC%96%B8-%ED%83%80%EC%9E%85)을 쓰는 대부분은 아래처럼 A 타입 일수도 있고, B 타입일 수도 있을 때이다 + +```typescript +type StringOrNumber = string | number; + +const strOrNumberLogger = (what: StringOrNumber) => console.log(what); +strOrNumberLogger("asdf"); +strOrNumberLogger(123123); +``` + +
+ +**_그러나 이 넘의 enum은 그렇지 않다_** + +`PeriodA`와 `PeriodCommon`를 합친 `Periods`라는 enum을 만들고 싶지만 그 과정은 쉽지 않다 + +아래처럼 타입 Union을 하면 당장은 에러가 나진 않지만, 실질적으로는 사용할 수 없는 타입이 된다 + +```typescript +type Periods = PeriodA | PeriodCommon; + +const periodLogger = (period: Periods) => console.log(period); +periodLogger("DAILY"); // error +``` + +
+ +Intersection은 더더욱 안된다 + +둘 사이에 공통점이 없기 때문에 **`never`** 타입이 된다 + +```typescript +type Periods = PeriodA & PeriodCommon; // -> never + +const periodLogger = (period: Periods) => console.log(period); +periodLogger("DAILY"); // error +``` + +
+ +타입 assertion으로 에러를 안낼 수는 있는데..***아무 의미 없는 타입 선언***이 되어 버린다 + +![](https://images.velog.io/images/johnwi/post/669de5ad-34fb-4a23-b86e-a8cb8ea5326a/image.png) + +
+ +--- + +## `class-validator`의 `IsEnum`을 쓰려면 객체가 필요하다! + +가장 속 편한건 단순히 `literal`'만' 쓰는 거다 + +토스 기술 블로그 [Template Literal Types로 타입 안전하게 코딩하기](https://toss.tech/article/template-literal-types)에서 소개하는 것처럼, + +TypeScript에는 문자열을 가지고 다양한 타입을 만들어 낼 수 있는 재미있는(?) 기능이 있다 + +
+ +다만 위에서 예시로 가져온 `Periods`는 단순히 함수 인자의 타입만 추론하는데 쓰는 것이 아니라, + +아래와 같이 **[`class-validator`](https://github.com/typestack/class-validator)를 활용해 API 요청 유효성 검사**에도 사용될 수 있어야했다 + +```typescript +export class GetChartDto { + @IsNotEmpty() + @IsEnum(Periods) + period: PeriodNames; +} +``` + +
+ +당연한 거지만, 아래와 같이 type을 함수 인자로 넣을 수는 없다 + +![](https://images.velog.io/images/johnwi/post/515a911a-181d-4730-8f15-610f6dc508a3/image.png) + +
+ +위 라인 블로그에서 가장 추천하는 방법은 객체를 가지고 Union 타입을 만드는 것이다 + +enum으로 union 만들 방법을 구글링(`'typescript enum union'`)하면 나에게 가장 먼저 뜨는 글인 + +정규현님의 [enum type 대신 union type으로 변경하기](https://ajdkfl6445.gitbook.io/study/typescript/enum-type-union-type)에서도 그 방식을 개선하여 사용한 경험을 보여주고 있다 + +여기서 핵심은 아래와 같다 + +```typescript +const READONLY_객체 = 객체 as const; +type enumLike = keyof READONLY_객체[keyof typeof READONLY_객체]; +``` + +짧고 유용한 코드지만, 부족한 나에게는 이게 뭘 의미하는지 한번에 파악하기가 조금 어렵다고 느껴졌다 + +다만 `Readonly` 타입을 사용한다는 점에서 힌트를 얻을 수 있었다 + +_(이게 꼭 필요한 건지는 아직 테스트를 안해봤다...)_ + +
+ +## _(적어도 나에게는)_ 좀더 편한 방법! + +결론은 아래와 같다 + +```typescript +// periods.ts +type ReadonlyRecord = Readonly>; + +export type PeriodANames = "DAILY" | "MONTHLY"; +export const PeriodA: ReadonlyRecord = { + DAILY: "DAILY", + MONTHLY: "MONTHLY", +}; + +type PeriodCommonNames = "WEEKLY" | "QUARTERLY" | "YEARLY"; +const PeriodCommon: ReadonlyRecord = {/** */}; + +export type PeriodNames = PeriodANames | PeriodCommonNames; +export const Periods = { ...PeriodA, ...PeriodCommon }; +``` + +
+ +### 커스텀 유틸 타입 `ReadonlyRecord` + +이 부분은 반드시 필요한 부분은 아니고, 개인적으로 객체를 `Reaonly`로 만드는 경우가 많아 따로 선언했다 + +실무 코드에서는 좀더 편한 코딩을 위해 끼를 부려서 아래와 같이 선언했다 + +객체 `key`, `value`가 모두 `string`인 경우에는 기본값 덕분에 제네릭을 생략할 수 있고, + +`string`은 아니지만 `key`, `value`가 동일한 타입인 경우는 하나만 적어주면 된다 + +```typescript +export type ReadonlyRecord

= Readonly>; + +export const PeriodA: ReadonlyRecord = {/** */}; +``` + +
+ +### `literal` 타입; `PeriodNames`, `_RestPeriodNames` + +단순한 문자열 literal 타입이다 + +Union이라는 의미에 맞게, **_관심사에 따라 분리된 타입들을 하나로 묶는 것이 쉽다_** + +`keyof typeof 객체`를 대신하기 위해 타입을 하나하나 더 써야한다는 것이 단점이기는 하지만, + +아래 객체를 생성할 때 자동완성이 되기 때문에 크게 불편함을 느끼진 않을수 있다 + +오히려 하나의 타입에 너무 많은 속성이 있어서 타이핑 시간이 오래 걸릴 정도라면, 타입을 충분히 분리하지 못한 게 아닐지 검토해봐야 할 것 같다 + +또 토스 블로그에서 소개한 것처럼, **`template-literal` 타입들을 조합한 새로운 타입들을 선언하기 매우 편리해진다** + +```typescript +export type MarketNames = "domestic" | "overseas"; +export type CategoryNames = "index" | "stock"; + +export type DetailChartTypeNames = `${MarketNames}-${CategoryNames}`; +export const DetailChartTypes: ReadonlyRecord = { + "domestic-index": "domestic-index", + "domestic-stock": "domestic-stock", + "overseas-index": "overseas-index", + "overseas-stock": "overseas-stock", +}; +``` + +
+ +### Readonly 객체를 `class-validator IsEnum`에 활용 + +```typescript +// get-chart-dto.ts +export class GetChartDto { + @IsNotEmpty() + @IsEnum(Periods) + period: PeriodNames; +} +``` + +`literal`을 활용하여 안전한(type-safe) `Readonly` 객체를 만들고, `class-validator`에서 활용한다 + +`class-validator`가 아니더라도, 기존 `enum` 사용하듯 `Periods.DAILY`와 같이 사용할 수도 있다 + +JS로 Transpile 되었을때, 단순 객체 리터럴이기 때문에 라인 블로그대로라면 tree-shaking도 가능하다 diff --git a/src/lib/content-helpers.test.ts b/src/lib/content-helpers.test.ts new file mode 100644 index 0000000..8de79e0 --- /dev/null +++ b/src/lib/content-helpers.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest"; + +import { + buildTagIndex, + filterByTag, + formatDisplayDate, + sortByPubDateDesc, + type ContentListItem, +} from "./content-helpers.ts"; + +describe("sortByPubDateDesc", () => { + it("최신 pubDate가 앞에 온다", () => { + const older = { data: { pubDate: new Date("2022-01-01") } }; + const newer = { data: { pubDate: new Date("2023-01-01") } }; + expect(sortByPubDateDesc(older, newer)).toBeGreaterThan(0); + expect(sortByPubDateDesc(newer, older)).toBeLessThan(0); + }); +}); + +describe("buildTagIndex", () => { + it("태그 slug별 count를 집계한다", () => { + const items: ContentListItem[] = [ + { + id: "a", + title: "A", + summary: "s", + pubDate: new Date("2022-01-01"), + tags: ["astro", "mdx"], + href: "/articles/a", + kind: "article", + }, + { + id: "b", + title: "B", + summary: "s", + pubDate: new Date("2022-02-01"), + tags: ["astro"], + href: "/articles/b", + kind: "article", + }, + ]; + const index = buildTagIndex(items); + expect(index.find((tag) => tag.slug === "astro")?.count).toBe(2); + expect(index.find((tag) => tag.slug === "mdx")?.count).toBe(1); + }); +}); + +describe("filterByTag", () => { + it("지정 slug를 포함한 글만 남긴다", () => { + const items: ContentListItem[] = [ + { + id: "a", + title: "A", + summary: "s", + pubDate: new Date("2022-01-01"), + tags: ["astro"], + href: "/articles/a", + kind: "article", + }, + { + id: "b", + title: "B", + summary: "s", + pubDate: new Date("2022-02-01"), + tags: ["til"], + href: "/til/b", + kind: "til", + }, + ]; + expect(filterByTag(items, "astro")).toHaveLength(1); + expect(filterByTag(items, "missing")).toHaveLength(0); + }); +}); + +describe("formatDisplayDate", () => { + it("YYYY.MM.DD 형식으로 포맷한다", () => { + expect(formatDisplayDate(new Date("2022-03-01T12:00:00Z"))).toMatch(/^2022\.03\.01$/); + }); +}); diff --git a/src/lib/content-helpers.ts b/src/lib/content-helpers.ts new file mode 100644 index 0000000..45442d9 --- /dev/null +++ b/src/lib/content-helpers.ts @@ -0,0 +1,49 @@ +export type ContentListItem = { + id: string; + title: string; + summary: string; + pubDate: Date; + tags: string[]; + href: string; + kind: "article" | "til"; +}; + +export type TagIndexEntry = { + slug: string; + label: string; + count: number; +}; + +export const sortByPubDateDesc = (a: T, b: T): number => + b.data.pubDate.getTime() - a.data.pubDate.getTime(); + +export const sortListItemsByPubDateDesc = (a: ContentListItem, b: ContentListItem): number => + b.pubDate.getTime() - a.pubDate.getTime(); + +const formatTagLabel = (slug: string): string => + slug + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); + +export const buildTagIndex = (items: ContentListItem[]): TagIndexEntry[] => { + const counts = new Map(); + for (const item of items) { + for (const tag of item.tags) { + counts.set(tag, (counts.get(tag) ?? 0) + 1); + } + } + return [...counts.entries()] + .map(([slug, count]) => ({ slug, label: formatTagLabel(slug), count })) + .sort((a, b) => a.slug.localeCompare(b.slug)); +}; + +export const filterByTag = (items: ContentListItem[], tagSlug: string): ContentListItem[] => + items.filter((item) => item.tags.includes(tagSlug)); + +export const formatDisplayDate = (date: Date): string => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}.${month}.${day}`; +}; diff --git a/src/lib/content.ts b/src/lib/content.ts new file mode 100644 index 0000000..5134f4d --- /dev/null +++ b/src/lib/content.ts @@ -0,0 +1,86 @@ +import { getCollection, type CollectionEntry } from "astro:content"; + +import { + type ContentListItem, + sortByPubDateDesc, + sortListItemsByPubDateDesc, +} from "./content-helpers.ts"; + +export type { ContentListItem, TagIndexEntry } from "./content-helpers.ts"; +export { + buildTagIndex, + filterByTag, + formatDisplayDate, + sortByPubDateDesc, + sortListItemsByPubDateDesc, +} from "./content-helpers.ts"; + +export type ArticleEntry = CollectionEntry<"articles">; +export type TilEntry = CollectionEntry<"til">; + +export const isPublished = ( + entry: CollectionEntry<"articles"> | CollectionEntry<"til">, +): boolean => { + if (import.meta.env.PROD) { + if ("draft" in entry.data && entry.data.draft) return false; + if ("visibility" in entry.data && entry.data.visibility !== "public") return false; + } + return true; +}; + +export const getPublishedArticles = async (): Promise => { + const entries = await getCollection("articles"); + return entries.filter(isPublished).sort(sortByPubDateDesc); +}; + +export const getPublishedTil = async (): Promise => { + const entries = await getCollection("til"); + return entries.filter(isPublished).sort(sortByPubDateDesc); +}; + +export const toArticleListItem = (entry: ArticleEntry): ContentListItem => ({ + id: entry.id, + title: entry.data.title, + summary: entry.data.description, + pubDate: entry.data.pubDate, + tags: entry.data.tags, + href: `/articles/${entry.id}`, + kind: "article", +}); + +export const toTilListItem = (entry: TilEntry): ContentListItem => ({ + id: entry.id, + title: entry.data.title, + summary: entry.data.summary, + pubDate: entry.data.pubDate, + tags: entry.data.tags, + href: `/til/${entry.id}`, + kind: "til", +}); + +export const getRecentPosts = async (limit = 6): Promise => { + const [articles, tilEntries] = await Promise.all([getPublishedArticles(), getPublishedTil()]); + return [...articles.map(toArticleListItem), ...tilEntries.map(toTilListItem)] + .sort(sortListItemsByPubDateDesc) + .slice(0, limit); +}; + +export const getAllTaggedItems = async (): Promise => { + const [articles, tilEntries] = await Promise.all([getPublishedArticles(), getPublishedTil()]); + return [...articles.map(toArticleListItem), ...tilEntries.map(toTilListItem)].sort( + sortListItemsByPubDateDesc, + ); +}; + +export const findAdjacentTil = ( + entries: TilEntry[], + currentId: string, +): { prev?: TilEntry; next?: TilEntry } => { + const sorted = [...entries].sort(sortByPubDateDesc); + const index = sorted.findIndex((entry) => entry.id === currentId); + if (index === -1) return {}; + return { + prev: sorted[index - 1], + next: sorted[index + 1], + }; +}; diff --git a/src/lib/mdx-components.ts b/src/lib/mdx-components.ts new file mode 100644 index 0000000..10ae732 --- /dev/null +++ b/src/lib/mdx-components.ts @@ -0,0 +1,9 @@ +import MdxPre from "../components/content/MdxPre.astro"; +import SmartLink from "../components/content/SmartLink.astro"; +import TableCard from "../components/content/TableCard.astro"; + +export const mdxComponents = { + a: SmartLink, + pre: MdxPre, + table: TableCard, +} as const; diff --git a/src/pages/articles/[...slug].astro b/src/pages/articles/[...slug].astro new file mode 100644 index 0000000..e21ba28 --- /dev/null +++ b/src/pages/articles/[...slug].astro @@ -0,0 +1,37 @@ +--- +import ArticleLayout from '../../components/layout/ArticleLayout.astro'; +import { mdxComponents } from '../../lib/mdx-components.ts'; +import Page from '../../layouts/Page.astro'; +import { getCollection, render } from 'astro:content'; + +export async function getStaticPaths() { + const articles = await getCollection('articles', ({ data }) => + !import.meta.env.PROD || !data.draft, + ); + return articles.map((article) => ({ + params: { slug: article.id }, + props: { article }, + })); +} + +const { article } = Astro.props; +const { Content, headings } = await render(article); +const { theme, title, description, pubDate, updatedDate, tags, cover } = article.data; +--- + + + + + + diff --git a/src/pages/articles/index.astro b/src/pages/articles/index.astro new file mode 100644 index 0000000..c46f1e4 --- /dev/null +++ b/src/pages/articles/index.astro @@ -0,0 +1,37 @@ +--- +import PostCard from '../../components/content/PostCard.astro'; +import Heading from '../../components/ui/Heading.astro'; +import Stack from '../../components/ui/Stack.astro'; +import Text from '../../components/ui/Text.astro'; +import { formatDisplayDate, getPublishedArticles, toArticleListItem } from '../../lib/content.ts'; +import Page from '../../layouts/Page.astro'; + +const articles = await getPublishedArticles(); +const items = articles.map(toArticleListItem); +--- + + + + Articles + 에세이·튜토리얼·회고 글 모음입니다. + { + items.length === 0 ? ( + 아직 공개된 글이 없습니다. + ) : ( +

+ { + items.map((post) => ( + + )) + } +
+ ) + } + + diff --git a/src/pages/index.astro b/src/pages/index.astro index 15aa036..c161a78 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -8,10 +8,10 @@ import Stack from '../components/ui/Stack.astro'; import Text from '../components/ui/Text.astro'; import HeroCanvas from '../components/visual/HeroCanvas.tsx'; import HeroVisual from '../components/visual/HeroVisual.astro'; -import { mockPosts } from '../data/mock.ts'; +import { formatDisplayDate, getRecentPosts } from '../lib/content.ts'; import Page from '../layouts/Page.astro'; -const recentPosts = mockPosts.slice(0, 3); +const recentPosts = await getRecentPosts(6); --- @@ -24,8 +24,8 @@ const recentPosts = mockPosts.slice(0, 3); 캔버스 위에서 긴 글을 읽기 좋게, 코드는 정확하게.
- - + + @@ -38,15 +38,19 @@ const recentPosts = mockPosts.slice(0, 3); 최근 글
{ - recentPosts.map((post) => ( - - )) + recentPosts.length === 0 ? ( + 아직 공개된 글이 없습니다. + ) : ( + recentPosts.map((post) => ( + + )) + ) }
diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro new file mode 100644 index 0000000..594d814 --- /dev/null +++ b/src/pages/posts/[...slug].astro @@ -0,0 +1,29 @@ +--- +import redirects from '../../../public/redirects.json'; + +export function getStaticPaths() { + return Object.keys(redirects.posts).map((slug) => ({ + params: { slug }, + })); +} + +const slug = Astro.params.slug ?? ''; +const target = redirects.posts[slug as keyof typeof redirects.posts]; +const canonical = new URL(target, Astro.site); +--- + + + + + + Redirecting… + + + + + +

+ 이 페이지는 {target}(으)로 이동했습니다. +

+ + diff --git a/src/pages/study/[...slug].astro b/src/pages/study/[...slug].astro new file mode 100644 index 0000000..f525489 --- /dev/null +++ b/src/pages/study/[...slug].astro @@ -0,0 +1,29 @@ +--- +import redirects from '../../../public/redirects.json'; + +export function getStaticPaths() { + return Object.keys(redirects.study).map((slug) => ({ + params: { slug }, + })); +} + +const slug = Astro.params.slug ?? ''; +const target = redirects.study[slug as keyof typeof redirects.study]; +const canonical = new URL(target, Astro.site); +--- + + + + + + Redirecting… + + + + + +

+ 이 페이지는 {target}(으)로 이동했습니다. +

+ + diff --git a/src/pages/tags/[tag].astro b/src/pages/tags/[tag].astro index e239151..abf4cf8 100644 --- a/src/pages/tags/[tag].astro +++ b/src/pages/tags/[tag].astro @@ -3,21 +3,30 @@ import Card from '../../components/ui/Card.astro'; import Heading from '../../components/ui/Heading.astro'; import Stack from '../../components/ui/Stack.astro'; import Text from '../../components/ui/Text.astro'; -import { filterPostsByTag, mockTags, type MockPost, type MockTag } from '../../data/mock.ts'; +import { + buildTagIndex, + filterByTag, + formatDisplayDate, + getAllTaggedItems, + type ContentListItem, +} from '../../lib/content.ts'; import Page from '../../layouts/Page.astro'; -export const getStaticPaths = () => - mockTags.map((tag) => ({ +export async function getStaticPaths() { + const items = await getAllTaggedItems(); + const tags = buildTagIndex(items); + return tags.map((tag) => ({ params: { tag: tag.slug }, props: { tag, - posts: filterPostsByTag({ tagSlug: tag.slug }), + posts: filterByTag(items, tag.slug), }, })); +} type Props = { - tag: MockTag; - posts: ReadonlyArray; + tag: { slug: string; label: string; count: number }; + posts: ContentListItem[]; }; const { tag, posts } = Astro.props; @@ -26,7 +35,9 @@ const { tag, posts } = Astro.props; {tag.label} - 태그 "{tag.label}"에 해당하는 mock 글 목록입니다. + + 태그 "{tag.label}"에 해당하는 글 {posts.length}개입니다. + { posts.length === 0 ? ( @@ -37,8 +48,10 @@ const { tag, posts } = Astro.props; posts.map((post) => ( - {post.title} - {post.date} + + {post.title} + + {formatDisplayDate(post.pubDate)} {post.summary} diff --git a/src/pages/tags/index.astro b/src/pages/tags/index.astro index 2c5810b..188c72b 100644 --- a/src/pages/tags/index.astro +++ b/src/pages/tags/index.astro @@ -3,14 +3,23 @@ import TagList from '../../components/patterns/TagList.astro'; import Heading from '../../components/ui/Heading.astro'; import Stack from '../../components/ui/Stack.astro'; import Text from '../../components/ui/Text.astro'; -import { mockTags } from '../../data/mock.ts'; +import { buildTagIndex, getAllTaggedItems } from '../../lib/content.ts'; import Page from '../../layouts/Page.astro'; + +const items = await getAllTaggedItems(); +const tags = buildTagIndex(items).map(({ slug, label }) => ({ slug, label })); --- Tags - Phase 1 mock 태그 목록입니다. - + articles와 til 콜렉션에서 수집한 태그입니다. + { + tags.length === 0 ? ( + 아직 태그가 없습니다. + ) : ( + + ) + } diff --git a/src/pages/til/[...slug].astro b/src/pages/til/[...slug].astro new file mode 100644 index 0000000..b80b0d3 --- /dev/null +++ b/src/pages/til/[...slug].astro @@ -0,0 +1,40 @@ +--- +import WikiLayout from '../../components/layout/WikiLayout.astro'; +import { findAdjacentTil, getPublishedTil } from '../../lib/content.ts'; +import { mdxComponents } from '../../lib/mdx-components.ts'; +import Page from '../../layouts/Page.astro'; +import { getCollection, render } from 'astro:content'; + +export async function getStaticPaths() { + const entries = await getCollection('til', ({ data }) => + !import.meta.env.PROD || data.visibility === 'public', + ); + return entries.map((entry) => ({ + params: { slug: entry.id }, + props: { entry }, + })); +} + +const { entry } = Astro.props; +const allTil = await getPublishedTil(); +const { prev, next } = findAdjacentTil(allTil, entry.id); +const { Content, headings } = await render(entry); +const { title, summary, pubDate, updatedDate, tags, domains, quizRefs } = entry.data; +--- + + + + + + diff --git a/src/pages/til/index.astro b/src/pages/til/index.astro new file mode 100644 index 0000000..7ef5746 --- /dev/null +++ b/src/pages/til/index.astro @@ -0,0 +1,37 @@ +--- +import PostCard from '../../components/content/PostCard.astro'; +import Heading from '../../components/ui/Heading.astro'; +import Stack from '../../components/ui/Stack.astro'; +import Text from '../../components/ui/Text.astro'; +import { formatDisplayDate, getPublishedTil, toTilListItem } from '../../lib/content.ts'; +import Page from '../../layouts/Page.astro'; + +const entries = await getPublishedTil(); +const items = entries.map(toTilListItem); +--- + + + + TIL / Wiki + Today I Learned 노트와 위키형 정리 글입니다. + { + items.length === 0 ? ( + 아직 공개된 글이 없습니다. + ) : ( +
+ { + items.map((post) => ( + + )) + } +
+ ) + } +
+
From 14fafb86354c777e1164b6b4f94c53805cd7e6ee Mon Sep 17 00:00:00 2001 From: gitgitWi Date: Thu, 10 Sep 2026 02:54:19 +0900 Subject: [PATCH 4/9] =?UTF-8?q?docs(phase-2):=20Phase=202=20LOG=EC=97=90?= =?UTF-8?q?=20=EA=B2=8C=EC=9D=B4=ED=8A=B8=20=EA=B2=B0=EA=B3=BC=EB=A5=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .tasks/phase-2-content-routes/LOG.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.tasks/phase-2-content-routes/LOG.md b/.tasks/phase-2-content-routes/LOG.md index 418e7f5..216d318 100644 --- a/.tasks/phase-2-content-routes/LOG.md +++ b/.tasks/phase-2-content-routes/LOG.md @@ -2,10 +2,24 @@ - Harness: cursor - Started: 2026-09-10 -- Status: PLAN-READY (스택 진행 지시로 shim·공개안전 선별 채택). base `feat/phase-1.5-visual` (#58 ready, 미머지). +- Status: PR-DRAFT (see below) +- Base: `feat/phase-1.5-visual` (#58) ## Notes - 콘텐츠 원본: `legacy-next/pages/posts/**`, `legacy-next/pages/study/**`. -- `llm-wiki-template`는 이 레포에 없음. 필요 시 GitHub에서 읽기 전용 clone, 통과분만 `src/content/til/`에 복사. clone 자체는 커밋하지 않는다. -- 스택: `feat/phase-1.5-visual` ← `feat/phase-2-content`. 머지는 사람. Phase 1(#56) → 1.5(#58) 먼저. +- `llm-wiki-template` clone(`/tmp/llm-wiki-template`) 확인 — wiki 페이지 21개 존재하나 공개안전 선별·frontmatter 매핑 작업량 대비 Phase 2 게이트(6편) 충족으로 **이식 생략**. 후속 PR 가능. +- StyleX `@stylexjs/babel-plugin` 0.19: `createThemeNested`에 비어 있지 않은 오버라이드 객체를 넣으면 빌드 실패. **대안**: `tokens.color.mintCanvas` 등 팔레트 토큰 + `ColorwayShell`/`ArticleLayout` variant style로 colorway 시각 차등 (mint vs canvas 확인 가능). +- `hello.mdx`는 `draft: true` — PROD 빌드 `dist/articles/hello` 없음 확인. +- articles colorway: clay(design-system), mint(hello draft/dev), canvas(from-jekyll). layout: docs, gallery, essay 각 1편 이상. + +## Gates (HEAD 84f7576) + +- `bun run check` — 0 errors +- `bunx oxfmt --check .` — OK +- `bunx oxlint .` — OK +- `bunx vitest run` — 8 passed +- `bun run lint:stylex` — OK +- `bun run build` — 33 pages +- `bun run build-storybook` — OK +- `bun run check-links` — 33 html OK From 52190f875eac6f74e7470cbf43a66aafd7f39d70 Mon Sep 17 00:00:00 2001 From: gitgitWi Date: Thu, 10 Sep 2026 02:54:45 +0900 Subject: [PATCH 5/9] =?UTF-8?q?docs(phase-2):=20LOG=EC=97=90=20draft=20PR?= =?UTF-8?q?=20=EB=B2=88=ED=98=B8=EB=A5=BC=20=EA=B8=B0=EB=A1=9D=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .tasks/phase-2-content-routes/LOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.tasks/phase-2-content-routes/LOG.md b/.tasks/phase-2-content-routes/LOG.md index 216d318..62872ed 100644 --- a/.tasks/phase-2-content-routes/LOG.md +++ b/.tasks/phase-2-content-routes/LOG.md @@ -2,7 +2,7 @@ - Harness: cursor - Started: 2026-09-10 -- Status: PR-DRAFT (see below) +- Status: PR-DRAFT #59 - Base: `feat/phase-1.5-visual` (#58) ## Notes From 9b356b0f3bccd326eb0cce3640eef298a871e0e9 Mon Sep 17 00:00:00 2001 From: gitgitWi Date: Thu, 10 Sep 2026 02:57:36 +0900 Subject: [PATCH 6/9] =?UTF-8?q?docs(phase-2):=20StyleX=20createTheme=20?= =?UTF-8?q?=ED=81=AC=EB=9E=98=EC=8B=9C=20=EB=8C=80=EC=95=88=EC=9D=84=20PLA?= =?UTF-8?q?N=EC=97=90=20=EA=B3=A0=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit colorway는 ColorwayShell variant로 시각 차등을 낸다. Co-authored-by: Cursor --- .tasks/phase-2-content-routes/PLAN.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.tasks/phase-2-content-routes/PLAN.md b/.tasks/phase-2-content-routes/PLAN.md index 970adef..e5854d9 100644 --- a/.tasks/phase-2-content-routes/PLAN.md +++ b/.tasks/phase-2-content-routes/PLAN.md @@ -30,3 +30,9 @@ - [x] 구경로 shim 방식 동의? (vs 404 안내) — 2026-09-10 스택 진행 지시. PLAN 기본안(shim + canonical) 채택. - [x] llm-wiki 선별 기준(공개안전) 동의? — 동. 회사정보·개인정보·저작권 불통과분은 이식하지 않는다. `raw/notes` 금지. + +## 승인된 편차 (2026-09-10) + +- StyleX 0.19 `unstable_createThemeNested`에 비어 있지 않은 오버라이드를 넣으면 babel-plugin이 크래시한다 (`undefined.slice`). colorway 시각 차등은 `ColorwayShell`/`ArticleLayout` variant + 팔레트 토큰으로 낸다. `themes.ts`의 createTheme 껍데기는 `{}`로 둔다. +- `llm-wiki-template` 이식은 6편 게이트와 분리. wiki 21개 확인했으나 공개안전 큐레이션은 후속. + From 3f2e0460f6f8793c73d17b38e8e2edc382f3b432 Mon Sep 17 00:00:00 2001 From: gitgitWi Date: Thu, 10 Sep 2026 02:59:24 +0900 Subject: [PATCH 7/9] =?UTF-8?q?fix(phase-2):=20MDX=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=B8=94=EB=A1=9D=20=EC=8A=AC=EB=A1=AF=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EC=99=80=20=EC=95=B5=EC=BB=A4=20=EA=B2=80=EC=82=AC=EB=A5=BC=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MdxPre가 default slot을 CodeBlock에 전달하고 check-code-blocks로 빌드 산출물을 검증한다. --- package.json | 1 + scripts/check-code-blocks.mjs | 23 ++++++++ scripts/check-links.mjs | 20 ++++--- src/components/content/CodeBlock.astro | 16 ++++-- src/components/content/MdxPre.astro | 20 ++----- src/components/content/MdxPre.stories.ts | 24 +++++++++ .../layout/ArticleLayout.stories.ts | 53 +++++++++++++++++++ src/components/layout/WikiLayout.stories.ts | 37 +++++++++++++ 8 files changed, 169 insertions(+), 25 deletions(-) create mode 100644 scripts/check-code-blocks.mjs create mode 100644 src/components/content/MdxPre.stories.ts create mode 100644 src/components/layout/ArticleLayout.stories.ts create mode 100644 src/components/layout/WikiLayout.stories.ts diff --git a/package.json b/package.json index 92d0670..bd20379 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "lint:stylex": "eslint -c eslint.stylex.config.js src", "test": "vitest run --passWithNoTests", "check-links": "node scripts/check-links.mjs", + "check-code-blocks": "node scripts/check-code-blocks.mjs", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build" }, diff --git a/scripts/check-code-blocks.mjs b/scripts/check-code-blocks.mjs new file mode 100644 index 0000000..5d21a83 --- /dev/null +++ b/scripts/check-code-blocks.mjs @@ -0,0 +1,23 @@ +import { readFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const articlePath = join(rootDir, "dist/articles/from-jekyll-to-nextjs/00/index.html"); + +const html = await readFile(articlePath, "utf8"); + +if (!/language-\w+/.test(html)) { + console.error("check-code-blocks: missing language-* class in migrated article HTML"); + process.exit(1); +} + +const codeTextMarkers = ["Markdown", "React"]; +const hasCodeText = codeTextMarkers.some((marker) => html.includes(marker)); + +if (!hasCodeText) { + console.error("check-code-blocks: fenced code body appears empty in from-jekyll article"); + process.exit(1); +} + +console.log("check-code-blocks: OK (from-jekyll article has language class and code text)"); diff --git a/scripts/check-links.mjs b/scripts/check-links.mjs index 04d5e6d..b9556ed 100644 --- a/scripts/check-links.mjs +++ b/scripts/check-links.mjs @@ -53,10 +53,7 @@ const isInternal = (url) => (!url.includes("://") && !url.startsWith("mailto:") && !url.startsWith("tel:")); const isIgnored = (url) => - url.startsWith("#") || - url.startsWith("javascript:") || - url.startsWith("data:") || - url.endsWith(".pdf"); + url.startsWith("javascript:") || url.startsWith("data:") || url.endsWith(".pdf"); const errors = []; @@ -90,7 +87,18 @@ const run = async () => { const { hrefs, srcs, ids } = collectFromHtml(html); for (const href of hrefs) { - if (!isInternal(href) || isIgnored(href)) continue; + if (isIgnored(href)) continue; + + if (href.startsWith("#")) { + const anchor = href.slice(1); + if (anchor && !ids.has(anchor)) { + errors.push(`${relFile}: missing same-page anchor #${anchor}`); + } + continue; + } + + if (!isInternal(href)) continue; + const mapped = hrefToDistPath(href); if (!mapped) continue; const found = await assertPathExists(mapped); @@ -98,7 +106,7 @@ const run = async () => { errors.push(`${relFile}: broken internal link ${href}`); continue; } - if (mapped.anchor && !ids.has(mapped.anchor)) { + if (mapped.anchor) { const targetHtml = await readFile(found, "utf8"); const targetIds = collectFromHtml(targetHtml).ids; if (!targetIds.has(mapped.anchor)) { diff --git a/src/components/content/CodeBlock.astro b/src/components/content/CodeBlock.astro index 65e038f..ebdce18 100644 --- a/src/components/content/CodeBlock.astro +++ b/src/components/content/CodeBlock.astro @@ -6,12 +6,14 @@ import { codeBlockStyles } from './CodeBlock.stylex.ts'; type Props = { lang: string; - code: string; + code?: string; + preClass?: string; showCopy?: boolean; }; -const { lang, code, showCopy = true } = Astro.props; +const { lang, code, preClass, showCopy = true } = Astro.props; const blockId = `code-${Math.random().toString(36).slice(2, 9)}`; +const useSlot = Astro.slots.has('default'); ---
@@ -26,7 +28,15 @@ const blockId = `code-${Math.random().toString(36).slice(2, 9)}`; }
- + { + useSlot ? ( +
+          
+        
+ ) : ( + + ) + }
diff --git a/src/components/content/MdxPre.astro b/src/components/content/MdxPre.astro index 3650bf3..6affcec 100644 --- a/src/components/content/MdxPre.astro +++ b/src/components/content/MdxPre.astro @@ -3,24 +3,12 @@ import CodeBlock from './CodeBlock.astro'; type Props = { class?: string; - children?: unknown; }; -const { class: className, children } = Astro.props as Props; +const { class: className } = Astro.props as Props; const lang = className?.match(/language-(\S+)/)?.[1] ?? 'text'; - -const extractCode = (node: unknown): string => { - if (typeof node === 'string') return node; - if (!node || typeof node !== 'object') return ''; - const record = node as Record; - if (typeof record.value === 'string') return record.value; - if (Array.isArray(record.children)) { - return record.children.map(extractCode).join(''); - } - return ''; -}; - -const code = extractCode(children).replace(/\n$/, ''); --- - + + + diff --git a/src/components/content/MdxPre.stories.ts b/src/components/content/MdxPre.stories.ts new file mode 100644 index 0000000..27d8089 --- /dev/null +++ b/src/components/content/MdxPre.stories.ts @@ -0,0 +1,24 @@ +import MdxPre from "./MdxPre.astro"; + +export default { + title: "content/MdxPre", + component: MdxPre, +}; + +export const TypeScript = { + args: { + class: "language-typescript", + slots: { + default: 'const hello = () => `World!`;', + }, + }, +}; + +export const Bash = { + args: { + class: "language-bash", + slots: { + default: 'bun run dev\nbun run build', + }, + }, +}; diff --git a/src/components/layout/ArticleLayout.stories.ts b/src/components/layout/ArticleLayout.stories.ts new file mode 100644 index 0000000..51ee669 --- /dev/null +++ b/src/components/layout/ArticleLayout.stories.ts @@ -0,0 +1,53 @@ +import ArticleLayout from "./ArticleLayout.astro"; + +export default { + title: "layout/ArticleLayout", + component: ArticleLayout, +}; + +export const Essay = { + args: { + title: "에세이 레이아웃 샘플", + description: "Prose 중앙 정렬과 우측 TOC를 확인하는 짧은 기사입니다.", + pubDate: new Date("2022-03-01"), + tags: ["astro", "migration"], + colorway: "canvas", + layout: "essay", + headings: [ + { depth: 2, slug: "intro", text: "소개" }, + { depth: 2, slug: "next", text: "다음" }, + ], + slots: { + default: '

소개

본문 슬롯입니다.

', + }, + }, +}; + +export const DocsClay = { + args: { + title: "Docs + clay colorway", + description: "좌측 TOC와 clay accent bar 조합.", + pubDate: new Date("2022-02-03"), + colorway: "clay", + layout: "docs", + headings: [{ depth: 2, slug: "setup", text: "설정" }], + slots: { + default: '

설정

docs 레이아웃 본문.

', + }, + }, +}; + +export const GalleryMint = { + args: { + title: "Gallery + mint colorway", + description: "커버 그리드 상단 배치.", + pubDate: new Date("2022-01-15"), + colorway: "mint", + layout: "gallery", + cover: + "https://user-images.githubusercontent.com/57997672/156254919-f4795507-1ecf-4e88-b8ef-a037ab071169.gif", + slots: { + default: "

gallery 본문 슬롯.

", + }, + }, +}; diff --git a/src/components/layout/WikiLayout.stories.ts b/src/components/layout/WikiLayout.stories.ts new file mode 100644 index 0000000..226daf2 --- /dev/null +++ b/src/components/layout/WikiLayout.stories.ts @@ -0,0 +1,37 @@ +import WikiLayout from "./WikiLayout.astro"; + +export default { + title: "layout/WikiLayout", + component: WikiLayout, +}; + +export const Default = { + args: { + title: "Getting Started", + summary: "위키 고정 레이아웃 — 제목·요약·메타·TOC·본문·prev/next.", + pubDate: new Date("2023-06-01"), + tags: ["wiki", "astro"], + domains: ["dev"], + headings: [ + { depth: 2, slug: "intro", text: "Intro" }, + { depth: 2, slug: "next", text: "Next" }, + ], + prev: { href: "/til/mock", label: "이전 글" }, + next: { href: "/til/mock", label: "다음 글" }, + slots: { + default: '

Intro

WikiLayout 본문 슬롯.

', + }, + }, +}; + +export const WithQuizRefs = { + args: { + title: "Quiz refs 샘플", + summary: "quizRefs 링크 목록이 하단에 표시됩니다.", + pubDate: new Date("2023-06-01"), + quizRefs: ["/quiz/sample-1", "/quiz/sample-2"], + slots: { + default: "

Phase 3 quiz UI 연결 전 placeholder.

", + }, + }, +}; From 3122531b445b6f55ceee5c1d4a75e20436a8f890 Mon Sep 17 00:00:00 2001 From: gitgitWi Date: Thu, 10 Sep 2026 03:00:07 +0900 Subject: [PATCH 8/9] =?UTF-8?q?docs(phase-2):=20review=20fix=20=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EA=B2=B0=EA=B3=BC=EB=A5=BC=20LOG=EC=97=90?= =?UTF-8?q?=20=EB=B0=98=EC=98=81=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .tasks/phase-2-content-routes/LOG.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.tasks/phase-2-content-routes/LOG.md b/.tasks/phase-2-content-routes/LOG.md index 62872ed..3a63fd2 100644 --- a/.tasks/phase-2-content-routes/LOG.md +++ b/.tasks/phase-2-content-routes/LOG.md @@ -2,24 +2,26 @@ - Harness: cursor - Started: 2026-09-10 -- Status: PR-DRAFT #59 +- Status: PR-DRAFT #59 (review fixes pushed) - Base: `feat/phase-1.5-visual` (#58) ## Notes - 콘텐츠 원본: `legacy-next/pages/posts/**`, `legacy-next/pages/study/**`. - `llm-wiki-template` clone(`/tmp/llm-wiki-template`) 확인 — wiki 페이지 21개 존재하나 공개안전 선별·frontmatter 매핑 작업량 대비 Phase 2 게이트(6편) 충족으로 **이식 생략**. 후속 PR 가능. -- StyleX `@stylexjs/babel-plugin` 0.19: `createThemeNested`에 비어 있지 않은 오버라이드 객체를 넣으면 빌드 실패. **대안**: `tokens.color.mintCanvas` 등 팔레트 토큰 + `ColorwayShell`/`ArticleLayout` variant style로 colorway 시각 차등 (mint vs canvas 확인 가능). +- PLAN-UPDATED: colorway는 `createThemeNested {}` + `ColorwayShell`/`ArticleLayout` variant style (StyleX babel 0.19는 non-empty nested override 불가). - `hello.mdx`는 `draft: true` — PROD 빌드 `dist/articles/hello` 없음 확인. -- articles colorway: clay(design-system), mint(hello draft/dev), canvas(from-jekyll). layout: docs, gallery, essay 각 1편 이상. +- Review fix: `MdxPre` → default slot → `CodeBlock` slot mode; `check-code-blocks.mjs`로 from-jekyll fence 검증; same-page `#anchor` check-links 검사. -## Gates (HEAD 84f7576) +## Gates (HEAD d4430d7) -- `bun run check` — 0 errors +- `bun run check` — 0 errors (105 files) - `bunx oxfmt --check .` — OK - `bunx oxlint .` — OK - `bunx vitest run` — 8 passed - `bun run lint:stylex` — OK - `bun run build` — 33 pages - `bun run build-storybook` — OK -- `bun run check-links` — 33 html OK +- `bun run check-links` — 33 html OK (same-page anchors 포함) +- `bun run check-code-blocks` — OK (language-mdx + code text) +- draft exclusion — `dist/articles/hello` absent From ebac76db2bdf3a48bc138bccf4a6c3aacc5730d2 Mon Sep 17 00:00:00 2001 From: gitgitWi Date: Thu, 10 Sep 2026 03:03:15 +0900 Subject: [PATCH 9/9] =?UTF-8?q?docs(phase-2):=20Sol=20APPROVE=EC=99=80=20p?= =?UTF-8?q?r=20ready=EB=A5=BC=20=EA=B8=B0=EB=A1=9D=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .tasks/phase-2-content-routes/LOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.tasks/phase-2-content-routes/LOG.md b/.tasks/phase-2-content-routes/LOG.md index 3a63fd2..c17f24e 100644 --- a/.tasks/phase-2-content-routes/LOG.md +++ b/.tasks/phase-2-content-routes/LOG.md @@ -2,7 +2,7 @@ - Harness: cursor - Started: 2026-09-10 -- Status: PR-DRAFT #59 (review fixes pushed) +- Status: PR-READY #59 - Base: `feat/phase-1.5-visual` (#58) ## Notes @@ -13,7 +13,7 @@ - `hello.mdx`는 `draft: true` — PROD 빌드 `dist/articles/hello` 없음 확인. - Review fix: `MdxPre` → default slot → `CodeBlock` slot mode; `check-code-blocks.mjs`로 from-jekyll fence 검증; same-page `#anchor` check-links 검사. -## Gates (HEAD d4430d7) +## Gates (HEAD 88bd9d5) - `bun run check` — 0 errors (105 files) - `bunx oxfmt --check .` — OK @@ -25,3 +25,9 @@ - `bun run check-links` — 33 html OK (same-page anchors 포함) - `bun run check-code-blocks` — OK (language-mdx + code text) - draft exclusion — `dist/articles/hello` absent + +## 2026-09-10 review + +- Sol `gpt-5.6-sol-medium` 1차: `REVIEW CHANGES phase-2 59`. usage: not visible. +- Sol 재리뷰: `REVIEW APPROVE phase-2 59`. usage: not visible. +- `gh pr ready 59`. 머지는 사람. 스택: #56 → #58 → #59.