From ff2c33cfff51318421b033d06c01c85a13787b54 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 10 Nov 2025 15:06:08 +0100 Subject: [PATCH 01/20] chore: use mdc nightly --- package.json | 2 +- pnpm-lock.yaml | 899 +++++++++---------------------------------------- 2 files changed, 167 insertions(+), 734 deletions(-) diff --git a/package.json b/package.json index 955f38a2..c25545d0 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "@iconify-json/lucide": "^1.2.72", - "@nuxtjs/mdc": "^0.18.2", + "@nuxtjs/mdc": "https://pkg.pr.new/@nuxtjs/mdc@cb6a227", "@vueuse/core": "^13.9.0", "defu": "^6.1.4", "destr": "^2.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fe1084e..6293bebe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,8 +15,8 @@ importers: specifier: ^1.2.72 version: 1.2.72 '@nuxtjs/mdc': - specifier: ^0.18.2 - version: 0.18.2(magicast@0.3.5) + specifier: https://pkg.pr.new/@nuxtjs/mdc@cb6a227 + version: https://pkg.pr.new/@nuxtjs/mdc@cb6a227(magicast@0.3.5) '@vueuse/core': specifier: ^13.9.0 version: 13.9.0(vue@3.5.22(typescript@5.9.3)) @@ -44,7 +44,7 @@ importers: version: 4.2.0(magicast@0.3.5) '@nuxt/module-builder': specifier: ^1.0.2 - version: 1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) + version: 1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) '@nuxt/ui': specifier: ^4.1.0 version: 4.1.0(@babel/parser@7.28.5)(change-case@5.4.4)(db0@0.3.4(better-sqlite3@12.4.1))(embla-carousel@8.6.0)(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.3.5)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12) @@ -122,19 +122,19 @@ importers: dependencies: '@nuxt/content': specifier: latest - version: 3.8.0(better-sqlite3@12.4.1)(magicast@0.5.0) + version: 3.8.0(better-sqlite3@12.4.1)(magicast@0.3.5) '@nuxt/ui': specifier: 4.1.0 - version: 4.1.0(@babel/parser@7.28.5)(change-case@5.4.4)(db0@0.3.4(better-sqlite3@12.4.1))(embla-carousel@8.6.0)(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12) + version: 4.1.0(@babel/parser@7.28.5)(change-case@5.4.4)(db0@0.3.4(better-sqlite3@12.4.1))(embla-carousel@8.6.0)(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.3.5)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12) better-sqlite3: specifier: ^12.4.1 version: 12.4.1 docus: specifier: ^5.2.1 - version: 5.2.1(98b70214e17197e9487d2d5efeebcd8f) + version: 5.2.1(3ff5337f0b09ee5607893a498d9b0666) nuxt: specifier: latest - version: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) + version: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) nuxt-studio: specifier: workspace:* version: link:../.. @@ -1025,6 +1025,10 @@ packages: resolution: {integrity: sha512-1yN3LL6RDN5GjkNLPUYCbNRkaYnat6hqejPyfIBBVzrWOrpiQeNMGxQM/IcVdaSuBJXAnu0sUvTKXpXkmPhljg==} engines: {node: '>=18.12.0'} + '@nuxt/kit@4.2.1': + resolution: {integrity: sha512-lLt8KLHyl7IClc3RqRpRikz15eCfTRlAWL9leVzPyg5N87FfKE/7EWgWvpiL/z4Tf3dQCIqQb88TmHE0JTIDvA==} + engines: {node: '>=18.12.0'} + '@nuxt/module-builder@1.0.2': resolution: {integrity: sha512-9M+0oZimbwom1J+HrfDuR5NDPED6C+DlM+2xfXju9wqB6VpVfYkS6WNEmS0URw8kpJcKBuogAc7ADO7vRS4s4A==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1097,8 +1101,9 @@ packages: '@nuxtjs/mdc@0.18.0': resolution: {integrity: sha512-/rWEOiLpD6oNx2FC/UsYxLn1pP31pvRmaX5y8GurBOogATKDWd3jlfKCGgshLnsWM6dCKgNkF0mCZQCMZMfpIQ==} - '@nuxtjs/mdc@0.18.2': - resolution: {integrity: sha512-pdeWd2/oOPriPVa1F6QNoK4fZCp/b4sxEVoouXJJCETCBIFSNS4OOtdRTY1ATLM1Gr+6ZvNyNPABvaaUEGC4Lw==} + '@nuxtjs/mdc@https://pkg.pr.new/@nuxtjs/mdc@cb6a227': + resolution: {tarball: https://pkg.pr.new/@nuxtjs/mdc@cb6a227} + version: 0.18.2 '@nuxtjs/robots@5.5.6': resolution: {integrity: sha512-PFp0sSaQs2ceEubvkiUPrWQ0GYTTu5bDH0lGVmJlm0h/Dqmt/e9TziXNKahL8HUV3VG22YzRyuyjd7p8+BaNgw==} @@ -1862,24 +1867,45 @@ packages: '@shikijs/core@3.14.0': resolution: {integrity: sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==} + '@shikijs/core@3.15.0': + resolution: {integrity: sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==} + '@shikijs/engine-javascript@3.14.0': resolution: {integrity: sha512-3v1kAXI2TsWQuwv86cREH/+FK9Pjw3dorVEykzQDhwrZj0lwsHYlfyARaKmn6vr5Gasf8aeVpb8JkzeWspxOLQ==} + '@shikijs/engine-javascript@3.15.0': + resolution: {integrity: sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==} + '@shikijs/engine-oniguruma@3.14.0': resolution: {integrity: sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==} + '@shikijs/engine-oniguruma@3.15.0': + resolution: {integrity: sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==} + '@shikijs/langs@3.14.0': resolution: {integrity: sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==} + '@shikijs/langs@3.15.0': + resolution: {integrity: sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==} + '@shikijs/themes@3.14.0': resolution: {integrity: sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==} + '@shikijs/themes@3.15.0': + resolution: {integrity: sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==} + '@shikijs/transformers@3.14.0': resolution: {integrity: sha512-i67zQnY9wLMMnKasonVW1L9fKneSLZDj1ePsA4o0AZWU4uUobmJY9baRDa36z+a9/g0aG76/2tybQvm4hrwxIQ==} + '@shikijs/transformers@3.15.0': + resolution: {integrity: sha512-Hmwip5ovvSkg+Kc41JTvSHHVfCYF+C8Cp1omb5AJj4Xvd+y9IXz2rKJwmFRGsuN0vpHxywcXJ1+Y4B9S7EG1/A==} + '@shikijs/types@3.14.0': resolution: {integrity: sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==} + '@shikijs/types@3.15.0': + resolution: {integrity: sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==} + '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -2386,6 +2412,9 @@ packages: '@vue/compiler-core@3.5.22': resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} + '@vue/compiler-core@3.5.24': + resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==} + '@vue/compiler-dom@3.5.22': resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==} @@ -2453,6 +2482,9 @@ packages: '@vue/shared@3.5.22': resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + '@vue/shared@3.5.24': + resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==} + '@vueuse/core@10.11.1': resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} @@ -5706,6 +5738,9 @@ packages: shiki@3.14.0: resolution: {integrity: sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g==} + shiki@3.15.0: + resolution: {integrity: sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -7024,16 +7059,6 @@ snapshots: transitivePeerDependencies: - magicast - '@dxup/nuxt@0.2.0(magicast@0.5.0)': - dependencies: - '@dxup/unimport': 0.1.0 - '@nuxt/kit': 4.2.0(magicast@0.5.0) - chokidar: 4.0.3 - pathe: 2.0.3 - tinyglobby: 0.2.15 - transitivePeerDependencies: - - magicast - '@dxup/unimport@0.1.0': {} '@emnapi/core@1.6.0': @@ -7662,38 +7687,6 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/cli@3.29.3(magicast@0.5.0)': - dependencies: - c12: 3.3.1(magicast@0.5.0) - citty: 0.1.6 - clipboardy: 5.0.0 - confbox: 0.2.2 - consola: 3.4.2 - defu: 6.1.4 - exsolve: 1.0.7 - fuse.js: 7.1.0 - get-port-please: 3.2.0 - giget: 2.0.0 - h3: 1.15.4 - jiti: 2.6.1 - listhen: 1.9.0 - nypm: 0.6.2 - ofetch: 1.5.1 - ohash: 2.0.11 - pathe: 2.0.3 - perfect-debounce: 2.0.0 - pkg-types: 2.3.0 - scule: 1.3.0 - semver: 7.7.3 - srvx: 0.8.16 - std-env: 3.10.0 - tinyexec: 1.0.1 - ufo: 1.6.1 - undici: 7.16.0 - youch: 4.1.0-beta.11 - transitivePeerDependencies: - - magicast - '@nuxt/content@3.8.0(better-sqlite3@12.4.1)(magicast@0.3.5)': dependencies: '@nuxt/kit': 4.2.0(magicast@0.3.5) @@ -7754,66 +7747,6 @@ snapshots: - supports-color - utf-8-validate - '@nuxt/content@3.8.0(better-sqlite3@12.4.1)(magicast@0.5.0)': - dependencies: - '@nuxt/kit': 4.2.0(magicast@0.5.0) - '@nuxtjs/mdc': 0.18.0(magicast@0.5.0) - '@shikijs/langs': 3.14.0 - '@sqlite.org/sqlite-wasm': 3.50.4-build1 - '@standard-schema/spec': 1.0.0 - '@webcontainer/env': 1.1.1 - c12: 3.3.1(magicast@0.5.0) - chokidar: 4.0.3 - consola: 3.4.2 - db0: 0.3.4(better-sqlite3@12.4.1) - defu: 6.1.4 - destr: 2.0.5 - git-url-parse: 16.1.0 - hookable: 5.5.3 - jiti: 2.6.1 - json-schema-to-typescript: 15.0.4 - knitwork: 1.2.0 - mdast-util-to-hast: 13.2.0 - mdast-util-to-string: 4.0.0 - micromark: 4.0.2 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromatch: 4.0.8 - minimark: 0.2.0 - minimatch: 10.0.3 - modern-tar: 0.6.1 - nuxt-component-meta: https://pkg.pr.new/nuxt-component-meta@e3eb2c4(magicast@0.5.0) - nypm: 0.6.2 - ohash: 2.0.11 - pathe: 2.0.3 - pkg-types: 2.3.0 - remark-mdc: 3.8.1 - scule: 1.3.0 - shiki: 3.14.0 - slugify: 1.6.6 - socket.io-client: 4.8.1 - std-env: 3.10.0 - tinyglobby: 0.2.15 - ufo: 1.6.1 - unctx: 2.4.1 - unified: 11.0.5 - unist-util-stringify-position: 4.0.0 - unist-util-visit: 5.0.0 - unplugin: 2.3.10 - zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) - optionalDependencies: - better-sqlite3: 12.4.1 - transitivePeerDependencies: - - bufferutil - - drizzle-orm - - magicast - - mysql2 - - supports-color - - utf-8-validate - '@nuxt/devalue@2.0.2': {} '@nuxt/devtools-kit@2.7.0(magicast@0.3.5)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': @@ -7824,14 +7757,6 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/devtools-kit@2.7.0(magicast@0.5.0)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': - dependencies: - '@nuxt/kit': 3.20.0(magicast@0.5.0) - execa: 8.0.1 - vite: 7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) - transitivePeerDependencies: - - magicast - '@nuxt/devtools-wizard@2.7.0': dependencies: consola: 3.4.2 @@ -7970,52 +7895,6 @@ snapshots: - uploadthing - vite - '@nuxt/fonts@0.11.4(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.0)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': - dependencies: - '@nuxt/devtools-kit': 2.7.0(magicast@0.5.0)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) - '@nuxt/kit': 3.20.0(magicast@0.5.0) - consola: 3.4.2 - css-tree: 3.1.0 - defu: 6.1.4 - esbuild: 0.25.11 - fontaine: 0.6.0 - h3: 1.15.4 - jiti: 2.6.1 - magic-regexp: 0.10.0 - magic-string: 0.30.21 - node-fetch-native: 1.6.7 - ohash: 2.0.11 - pathe: 2.0.3 - sirv: 3.0.2 - tinyglobby: 0.2.15 - ufo: 1.6.1 - unifont: 0.4.1 - unplugin: 2.3.10 - unstorage: 1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - db0 - - encoding - - idb-keyval - - ioredis - - magicast - - uploadthing - - vite - '@nuxt/icon@2.1.0(magicast@0.3.5)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': dependencies: '@iconify/collections': 1.0.612 @@ -8038,31 +7917,9 @@ snapshots: - vite - vue - '@nuxt/icon@2.1.0(magicast@0.5.0)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))': - dependencies: - '@iconify/collections': 1.0.612 - '@iconify/types': 2.0.0 - '@iconify/utils': 3.0.2 - '@iconify/vue': 5.0.0(vue@3.5.22(typescript@5.9.3)) - '@nuxt/devtools-kit': 2.7.0(magicast@0.5.0)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) - '@nuxt/kit': 4.2.0(magicast@0.5.0) - consola: 3.4.2 - local-pkg: 1.1.2 - mlly: 1.8.0 - ohash: 2.0.11 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinyglobby: 0.2.15 - transitivePeerDependencies: - - magicast - - supports-color - - vite - - vue - - '@nuxt/image@1.11.0(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.0)': + '@nuxt/image@1.11.0(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.3.5)': dependencies: - '@nuxt/kit': 3.20.0(magicast@0.5.0) + '@nuxt/kit': 3.20.0(magicast@0.3.5) consola: 3.4.2 defu: 6.1.4 h3: 1.15.4 @@ -8125,32 +7982,6 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/kit@3.20.0(magicast@0.5.0)': - dependencies: - c12: 3.3.1(magicast@0.5.0) - consola: 3.4.2 - defu: 6.1.4 - destr: 2.0.5 - errx: 0.1.0 - exsolve: 1.0.7 - ignore: 7.0.5 - jiti: 2.6.1 - klona: 2.0.6 - knitwork: 1.2.0 - mlly: 1.8.0 - ohash: 2.0.11 - pathe: 2.0.3 - pkg-types: 2.3.0 - rc9: 2.1.2 - scule: 1.3.0 - semver: 7.7.3 - tinyglobby: 0.2.15 - ufo: 1.6.1 - unctx: 2.4.1 - untyped: 2.0.0 - transitivePeerDependencies: - - magicast - '@nuxt/kit@4.2.0(magicast@0.3.5)': dependencies: c12: 3.3.1(magicast@0.3.5) @@ -8176,9 +8007,9 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/kit@4.2.0(magicast@0.5.0)': + '@nuxt/kit@4.2.1(magicast@0.3.5)': dependencies: - c12: 3.3.1(magicast@0.5.0) + c12: 3.3.1(magicast@0.3.5) consola: 3.4.2 defu: 6.1.4 destr: 2.0.5 @@ -8201,7 +8032,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/module-builder@1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3))': + '@nuxt/module-builder@1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3))': dependencies: '@nuxt/cli': 3.29.3(magicast@0.3.5) citty: 0.1.6 @@ -8209,14 +8040,14 @@ snapshots: defu: 6.1.4 jiti: 2.6.1 magic-regexp: 0.10.0 - mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) + mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 tsconfck: 3.1.6(typescript@5.9.3) typescript: 5.9.3 - unbuild: 3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) - vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)) + unbuild: 3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) + vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)) transitivePeerDependencies: - '@vue/compiler-core' - esbuild @@ -8288,108 +8119,27 @@ snapshots: - uploadthing - xml2js - '@nuxt/nitro-server@4.2.0(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1))(typescript@5.9.3)': + '@nuxt/schema@4.2.0': dependencies: - '@nuxt/devalue': 2.0.2 - '@nuxt/kit': 4.2.0(magicast@0.5.0) - '@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.9.3)) '@vue/shared': 3.5.22 - consola: 3.4.2 defu: 6.1.4 - destr: 2.0.5 - devalue: 5.4.2 - errx: 0.1.0 - escape-string-regexp: 5.0.0 - exsolve: 1.0.7 - h3: 1.15.4 - impound: 1.0.0 - klona: 2.0.6 - mocked-exports: 0.1.1 - nitropack: 2.12.9(better-sqlite3@12.4.1)(idb-keyval@6.2.2) - nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) pathe: 2.0.3 pkg-types: 2.3.0 - radix3: 1.1.2 - std-env: 3.10.0 - ufo: 1.6.1 - unctx: 2.4.1 - unstorage: 1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2) - vue: 3.5.22(typescript@5.9.3) - vue-bundle-renderer: 2.2.0 - vue-devtools-stub: 0.1.0 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@libsql/client' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bare-abort-controller - - better-sqlite3 - - db0 - - drizzle-orm - - encoding - - idb-keyval - - ioredis - - magicast - - mysql2 - - react-native-b4a - - rolldown - - sqlite3 - - supports-color - - typescript - - uploadthing - - xml2js - - '@nuxt/schema@4.2.0': - dependencies: - '@vue/shared': 3.5.22 - defu: 6.1.4 - pathe: 2.0.3 - pkg-types: 2.3.0 - std-env: 3.10.0 - - '@nuxt/telemetry@2.6.6(magicast@0.3.5)': - dependencies: - '@nuxt/kit': 3.20.0(magicast@0.3.5) - citty: 0.1.6 - consola: 3.4.2 - destr: 2.0.5 - dotenv: 16.6.1 - git-url-parse: 16.1.0 - is-docker: 3.0.0 - ofetch: 1.5.1 - package-manager-detector: 1.5.0 - pathe: 2.0.3 - rc9: 2.1.2 - std-env: 3.10.0 - transitivePeerDependencies: - - magicast - - '@nuxt/telemetry@2.6.6(magicast@0.5.0)': - dependencies: - '@nuxt/kit': 3.20.0(magicast@0.5.0) - citty: 0.1.6 - consola: 3.4.2 - destr: 2.0.5 - dotenv: 16.6.1 - git-url-parse: 16.1.0 - is-docker: 3.0.0 - ofetch: 1.5.1 - package-manager-detector: 1.5.0 - pathe: 2.0.3 - rc9: 2.1.2 + std-env: 3.10.0 + + '@nuxt/telemetry@2.6.6(magicast@0.3.5)': + dependencies: + '@nuxt/kit': 3.20.0(magicast@0.3.5) + citty: 0.1.6 + consola: 3.4.2 + destr: 2.0.5 + dotenv: 16.6.1 + git-url-parse: 16.1.0 + is-docker: 3.0.0 + ofetch: 1.5.1 + package-manager-detector: 1.5.0 + pathe: 2.0.3 + rc9: 2.1.2 std-env: 3.10.0 transitivePeerDependencies: - magicast @@ -8488,100 +8238,6 @@ snapshots: - vite - vue - '@nuxt/ui@4.1.0(@babel/parser@7.28.5)(change-case@5.4.4)(db0@0.3.4(better-sqlite3@12.4.1))(embla-carousel@8.6.0)(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12)': - dependencies: - '@ai-sdk/vue': 2.0.81(vue@3.5.22(typescript@5.9.3))(zod@4.1.12) - '@iconify/vue': 5.0.0(vue@3.5.22(typescript@5.9.3)) - '@internationalized/date': 3.10.0 - '@internationalized/number': 3.6.5 - '@nuxt/fonts': 0.11.4(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.0)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) - '@nuxt/icon': 2.1.0(magicast@0.5.0)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) - '@nuxt/kit': 4.2.0(magicast@0.5.0) - '@nuxt/schema': 4.2.0 - '@nuxtjs/color-mode': 3.5.2(magicast@0.5.0) - '@standard-schema/spec': 1.0.0 - '@tailwindcss/postcss': 4.1.16 - '@tailwindcss/vite': 4.1.16(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) - '@tanstack/vue-table': 8.21.3(vue@3.5.22(typescript@5.9.3)) - '@tanstack/vue-virtual': 3.13.12(vue@3.5.22(typescript@5.9.3)) - '@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.9.3)) - '@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3)) - '@vueuse/integrations': 13.9.0(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(vue@3.5.22(typescript@5.9.3)) - colortranslator: 5.0.0 - consola: 3.4.2 - defu: 6.1.4 - embla-carousel-auto-height: 8.6.0(embla-carousel@8.6.0) - embla-carousel-auto-scroll: 8.6.0(embla-carousel@8.6.0) - embla-carousel-autoplay: 8.6.0(embla-carousel@8.6.0) - embla-carousel-class-names: 8.6.0(embla-carousel@8.6.0) - embla-carousel-fade: 8.6.0(embla-carousel@8.6.0) - embla-carousel-vue: 8.6.0(vue@3.5.22(typescript@5.9.3)) - embla-carousel-wheel-gestures: 8.1.0(embla-carousel@8.6.0) - fuse.js: 7.1.0 - hookable: 5.5.3 - knitwork: 1.2.0 - magic-string: 0.30.21 - mlly: 1.8.0 - motion-v: 1.7.4(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) - ohash: 2.0.11 - pathe: 2.0.3 - reka-ui: 2.6.0(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) - scule: 1.3.0 - tailwind-merge: 3.3.1 - tailwind-variants: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.16) - tailwindcss: 4.1.16 - tinyglobby: 0.2.15 - typescript: 5.9.3 - unplugin: 2.3.10 - unplugin-auto-import: 20.2.0(@nuxt/kit@4.2.0(magicast@0.5.0))(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.3))) - unplugin-vue-components: 30.0.0(@babel/parser@7.28.5)(@nuxt/kit@4.2.0(magicast@0.5.0))(vue@3.5.22(typescript@5.9.3)) - vaul-vue: 0.4.1(reka-ui@2.6.0(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) - vue-component-type-helpers: 3.1.2 - optionalDependencies: - vue-router: 4.6.3(vue@3.5.22(typescript@5.9.3)) - zod: 4.1.12 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@babel/parser' - - '@capacitor/preferences' - - '@deno/kv' - - '@emotion/is-prop-valid' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - '@vue/composition-api' - - async-validator - - aws4fetch - - axios - - change-case - - db0 - - drauu - - embla-carousel - - encoding - - focus-trap - - idb-keyval - - ioredis - - jwt-decode - - magicast - - nprogress - - qrcode - - react - - react-dom - - sortablejs - - supports-color - - universal-cookie - - uploadthing - - vite - - vue - '@nuxt/vite-builder@4.2.0(@types/node@24.9.1)(eslint@9.39.1(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.3.5)(meow@13.2.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)': dependencies: '@nuxt/kit': 4.2.0(magicast@0.3.5) @@ -8641,65 +8297,6 @@ snapshots: - vue-tsc - yaml - '@nuxt/vite-builder@4.2.0(@types/node@24.9.1)(eslint@9.39.1(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.0)(meow@13.2.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)': - dependencies: - '@nuxt/kit': 4.2.0(magicast@0.5.0) - '@rollup/plugin-replace': 6.0.2(rollup@4.52.5) - '@vitejs/plugin-vue': 6.0.1(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) - '@vitejs/plugin-vue-jsx': 5.1.1(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) - autoprefixer: 10.4.21(postcss@8.5.6) - consola: 3.4.2 - cssnano: 7.1.1(postcss@8.5.6) - defu: 6.1.4 - esbuild: 0.25.11 - escape-string-regexp: 5.0.0 - exsolve: 1.0.7 - get-port-please: 3.2.0 - h3: 1.15.4 - jiti: 2.6.1 - knitwork: 1.2.0 - magic-string: 0.30.21 - mlly: 1.8.0 - mocked-exports: 0.1.1 - nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) - pathe: 2.0.3 - pkg-types: 2.3.0 - postcss: 8.5.6 - rollup-plugin-visualizer: 6.0.5(rollup@4.52.5) - seroval: 1.3.2 - std-env: 3.10.0 - ufo: 1.6.1 - unenv: 2.0.0-rc.23 - vite: 7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) - vite-plugin-checker: 0.11.0(eslint@9.39.1(jiti@2.6.1))(meow@13.2.0)(optionator@0.9.4)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3)) - vue: 3.5.22(typescript@5.9.3) - vue-bundle-renderer: 2.2.0 - transitivePeerDependencies: - - '@biomejs/biome' - - '@types/node' - - eslint - - less - - lightningcss - - magicast - - meow - - optionator - - oxlint - - rollup - - sass - - sass-embedded - - stylelint - - stylus - - sugarss - - supports-color - - terser - - tsx - - typescript - - vls - - vti - - vue-tsc - - yaml - '@nuxtjs/color-mode@3.5.2(magicast@0.3.5)': dependencies: '@nuxt/kit': 3.20.0(magicast@0.3.5) @@ -8709,16 +8306,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxtjs/color-mode@3.5.2(magicast@0.5.0)': - dependencies: - '@nuxt/kit': 3.20.0(magicast@0.5.0) - pathe: 1.1.2 - pkg-types: 1.3.1 - semver: 7.7.3 - transitivePeerDependencies: - - magicast - - '@nuxtjs/i18n@10.1.2(@vue/compiler-dom@3.5.22)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.0)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3))': + '@nuxtjs/i18n@10.1.2(@vue/compiler-dom@3.5.22)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.3.5)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3))': dependencies: '@intlify/core': 11.1.12 '@intlify/h3': 0.7.1 @@ -8726,7 +8314,7 @@ snapshots: '@intlify/unplugin-vue-i18n': 11.0.1(@vue/compiler-dom@3.5.22)(eslint@9.39.1(jiti@2.6.1))(rollup@4.52.5)(typescript@5.9.3)(vue-i18n@11.1.12(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) '@intlify/utils': 0.13.0 '@miyaneee/rollup-plugin-json5': 1.2.0(rollup@4.52.5) - '@nuxt/kit': 4.2.0(magicast@0.5.0) + '@nuxt/kit': 4.2.0(magicast@0.3.5) '@rollup/plugin-yaml': 4.1.2(rollup@4.52.5) '@vue/compiler-sfc': 3.5.22 defu: 6.1.4 @@ -8825,65 +8413,16 @@ snapshots: - magicast - supports-color - '@nuxtjs/mdc@0.18.0(magicast@0.5.0)': - dependencies: - '@nuxt/kit': 4.2.0(magicast@0.5.0) - '@shikijs/core': 3.14.0 - '@shikijs/langs': 3.14.0 - '@shikijs/themes': 3.14.0 - '@shikijs/transformers': 3.14.0 - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@vue/compiler-core': 3.5.22 - consola: 3.4.2 - debug: 4.4.3 - defu: 6.1.4 - destr: 2.0.5 - detab: 3.0.2 - github-slugger: 2.0.0 - hast-util-format: 1.1.0 - hast-util-to-mdast: 10.1.2 - hast-util-to-string: 3.0.1 - mdast-util-to-hast: 13.2.0 - micromark-util-sanitize-uri: 2.0.1 - parse5: 8.0.0 - pathe: 2.0.3 - property-information: 7.1.0 - rehype-external-links: 3.0.0 - rehype-minify-whitespace: 6.0.2 - rehype-raw: 7.0.0 - rehype-remark: 10.0.1 - rehype-slug: 6.0.0 - rehype-sort-attribute-values: 5.0.1 - rehype-sort-attributes: 5.0.1 - remark-emoji: 5.0.2 - remark-gfm: 4.0.1 - remark-mdc: 3.8.1 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - remark-stringify: 11.0.0 - scule: 1.3.0 - shiki: 3.14.0 - ufo: 1.6.1 - unified: 11.0.5 - unist-builder: 4.0.0 - unist-util-visit: 5.0.0 - unwasm: 0.3.11 - vfile: 6.0.3 - transitivePeerDependencies: - - magicast - - supports-color - - '@nuxtjs/mdc@0.18.2(magicast@0.3.5)': + '@nuxtjs/mdc@https://pkg.pr.new/@nuxtjs/mdc@cb6a227(magicast@0.3.5)': dependencies: - '@nuxt/kit': 4.2.0(magicast@0.3.5) - '@shikijs/core': 3.14.0 - '@shikijs/langs': 3.14.0 - '@shikijs/themes': 3.14.0 - '@shikijs/transformers': 3.14.0 + '@nuxt/kit': 4.2.1(magicast@0.3.5) + '@shikijs/core': 3.15.0 + '@shikijs/langs': 3.15.0 + '@shikijs/themes': 3.15.0 + '@shikijs/transformers': 3.15.0 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@vue/compiler-core': 3.5.22 + '@vue/compiler-core': 3.5.24 consola: 3.4.2 debug: 4.4.3 defu: 6.1.4 @@ -8912,7 +8451,7 @@ snapshots: remark-rehype: 11.1.2 remark-stringify: 11.0.0 scule: 1.3.0 - shiki: 3.14.0 + shiki: 3.15.0 ufo: 1.6.1 unified: 11.0.5 unist-builder: 4.0.0 @@ -8923,13 +8462,13 @@ snapshots: - magicast - supports-color - '@nuxtjs/robots@5.5.6(h3@1.15.4)(magicast@0.5.0)(vue@3.5.22(typescript@5.9.3))': + '@nuxtjs/robots@5.5.6(h3@1.15.4)(magicast@0.3.5)(vue@3.5.22(typescript@5.9.3))': dependencies: '@fingerprintjs/botd': 1.9.1 - '@nuxt/kit': 4.2.0(magicast@0.5.0) + '@nuxt/kit': 4.2.0(magicast@0.3.5) consola: 3.4.2 defu: 6.1.4 - nuxt-site-config: 3.2.11(h3@1.15.4)(magicast@0.5.0)(vue@3.5.22(typescript@5.9.3)) + nuxt-site-config: 3.2.11(h3@1.15.4)(magicast@0.3.5)(vue@3.5.22(typescript@5.9.3)) pathe: 2.0.3 pkg-types: 2.3.0 sirv: 3.0.2 @@ -9491,35 +9030,71 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 + '@shikijs/core@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + '@shikijs/engine-javascript@3.14.0': dependencies: '@shikijs/types': 3.14.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.3 + '@shikijs/engine-javascript@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 + '@shikijs/engine-oniguruma@3.14.0': dependencies: '@shikijs/types': 3.14.0 '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/engine-oniguruma@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/langs@3.14.0': dependencies: '@shikijs/types': 3.14.0 + '@shikijs/langs@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/themes@3.14.0': dependencies: '@shikijs/types': 3.14.0 + '@shikijs/themes@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/transformers@3.14.0': dependencies: '@shikijs/core': 3.14.0 '@shikijs/types': 3.14.0 + '@shikijs/transformers@3.15.0': + dependencies: + '@shikijs/core': 3.15.0 + '@shikijs/types': 3.15.0 + '@shikijs/types@3.14.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + '@shikijs/types@3.15.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/vscode-textmate@10.0.2': {} '@shuding/opentype.js@1.4.0-beta.0': @@ -10049,6 +9624,14 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.5.24': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.24 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-dom@3.5.22': dependencies: '@vue/compiler-core': 3.5.22 @@ -10165,6 +9748,8 @@ snapshots: '@vue/shared@3.5.22': {} + '@vue/shared@3.5.24': {} + '@vueuse/core@10.11.1(vue@3.5.22(typescript@5.9.3))': dependencies: '@types/web-bluetooth': 0.0.20 @@ -10996,18 +10581,18 @@ snapshots: diff@8.0.2: {} - docus@5.2.1(98b70214e17197e9487d2d5efeebcd8f): + docus@5.2.1(3ff5337f0b09ee5607893a498d9b0666): dependencies: '@iconify-json/lucide': 1.2.71 '@iconify-json/simple-icons': 1.2.56 '@iconify-json/vscode-icons': 1.2.33 - '@nuxt/content': 3.8.0(better-sqlite3@12.4.1)(magicast@0.5.0) - '@nuxt/image': 1.11.0(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.0) - '@nuxt/kit': 4.2.0(magicast@0.5.0) - '@nuxt/ui': 4.1.0(@babel/parser@7.28.5)(change-case@5.4.4)(db0@0.3.4(better-sqlite3@12.4.1))(embla-carousel@8.6.0)(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12) - '@nuxtjs/i18n': 10.1.2(@vue/compiler-dom@3.5.22)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.0)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3)) - '@nuxtjs/mdc': 0.18.0(magicast@0.5.0) - '@nuxtjs/robots': 5.5.6(h3@1.15.4)(magicast@0.5.0)(vue@3.5.22(typescript@5.9.3)) + '@nuxt/content': 3.8.0(better-sqlite3@12.4.1)(magicast@0.3.5) + '@nuxt/image': 1.11.0(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.3.5) + '@nuxt/kit': 4.2.0(magicast@0.3.5) + '@nuxt/ui': 4.1.0(@babel/parser@7.28.5)(change-case@5.4.4)(db0@0.3.4(better-sqlite3@12.4.1))(embla-carousel@8.6.0)(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.3.5)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12) + '@nuxtjs/i18n': 10.1.2(@vue/compiler-dom@3.5.22)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.3.5)(rollup@4.52.5)(vue@3.5.22(typescript@5.9.3)) + '@nuxtjs/mdc': 0.18.0(magicast@0.3.5) + '@nuxtjs/robots': 5.5.6(h3@1.15.4)(magicast@0.3.5)(vue@3.5.22(typescript@5.9.3)) '@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3)) better-sqlite3: 12.4.1 defu: 6.1.4 @@ -11015,9 +10600,9 @@ snapshots: git-url-parse: 16.1.0 minimark: 0.2.0 motion-v: 1.7.4(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) - nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) - nuxt-llms: 0.1.3(magicast@0.5.0) - nuxt-og-image: 5.1.12(@unhead/vue@2.0.19(vue@3.5.22(typescript@5.9.3)))(h3@1.15.4)(magicast@0.5.0)(unstorage@1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.3.5)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) + nuxt-llms: 0.1.3(magicast@0.3.5) + nuxt-og-image: 5.1.12(@unhead/vue@2.0.19(vue@3.5.22(typescript@5.9.3)))(h3@1.15.4)(magicast@0.3.5)(unstorage@1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) pkg-types: 2.3.0 scule: 1.3.0 tailwindcss: 4.1.16 @@ -12808,7 +12393,7 @@ snapshots: mkdirp-classic@0.5.3: {} - mkdist@2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)): + mkdist@2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)): dependencies: autoprefixer: 10.4.21(postcss@8.5.6) citty: 0.1.6 @@ -12826,7 +12411,7 @@ snapshots: optionalDependencies: typescript: 5.9.3 vue: 3.5.22(typescript@5.9.3) - vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)) + vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)) vue-tsc: 3.1.3(typescript@5.9.3) mlly@1.8.0: @@ -13063,32 +12648,18 @@ snapshots: transitivePeerDependencies: - magicast - nuxt-component-meta@https://pkg.pr.new/nuxt-component-meta@e3eb2c4(magicast@0.5.0): - dependencies: - '@nuxt/kit': 4.2.0(magicast@0.5.0) - citty: 0.1.6 - json-schema-to-zod: 2.6.1 - mlly: 1.8.0 - ohash: 2.0.11 - scule: 1.3.0 - typescript: 5.9.3 - ufo: 1.6.1 - vue-component-meta: 3.1.2(typescript@5.9.3) - transitivePeerDependencies: - - magicast - nuxt-define@1.0.0: {} - nuxt-llms@0.1.3(magicast@0.5.0): + nuxt-llms@0.1.3(magicast@0.3.5): dependencies: - '@nuxt/kit': 3.20.0(magicast@0.5.0) + '@nuxt/kit': 3.20.0(magicast@0.3.5) transitivePeerDependencies: - magicast - nuxt-og-image@5.1.12(@unhead/vue@2.0.19(vue@3.5.22(typescript@5.9.3)))(h3@1.15.4)(magicast@0.5.0)(unstorage@1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)): + nuxt-og-image@5.1.12(@unhead/vue@2.0.19(vue@3.5.22(typescript@5.9.3)))(h3@1.15.4)(magicast@0.3.5)(unstorage@1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)): dependencies: - '@nuxt/devtools-kit': 2.7.0(magicast@0.5.0)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) - '@nuxt/kit': 4.2.0(magicast@0.5.0) + '@nuxt/devtools-kit': 2.7.0(magicast@0.3.5)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + '@nuxt/kit': 4.2.0(magicast@0.3.5) '@resvg/resvg-js': 2.6.2 '@resvg/resvg-wasm': 2.6.2 '@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.9.3)) @@ -13101,7 +12672,7 @@ snapshots: image-size: 2.0.2 magic-string: 0.30.21 mocked-exports: 0.1.1 - nuxt-site-config: 3.2.11(h3@1.15.4)(magicast@0.5.0)(vue@3.5.22(typescript@5.9.3)) + nuxt-site-config: 3.2.11(h3@1.15.4)(magicast@0.3.5)(vue@3.5.22(typescript@5.9.3)) nypm: 0.6.2 ofetch: 1.5.1 ohash: 2.0.11 @@ -13126,9 +12697,9 @@ snapshots: - vite - vue - nuxt-site-config-kit@3.2.11(magicast@0.5.0)(vue@3.5.22(typescript@5.9.3)): + nuxt-site-config-kit@3.2.11(magicast@0.3.5)(vue@3.5.22(typescript@5.9.3)): dependencies: - '@nuxt/kit': 4.2.0(magicast@0.5.0) + '@nuxt/kit': 4.2.0(magicast@0.3.5) pkg-types: 2.3.0 site-config-stack: 3.2.11(vue@3.5.22(typescript@5.9.3)) std-env: 3.10.0 @@ -13137,11 +12708,11 @@ snapshots: - magicast - vue - nuxt-site-config@3.2.11(h3@1.15.4)(magicast@0.5.0)(vue@3.5.22(typescript@5.9.3)): + nuxt-site-config@3.2.11(h3@1.15.4)(magicast@0.3.5)(vue@3.5.22(typescript@5.9.3)): dependencies: - '@nuxt/kit': 4.2.0(magicast@0.5.0) + '@nuxt/kit': 4.2.0(magicast@0.3.5) h3: 1.15.4 - nuxt-site-config-kit: 3.2.11(magicast@0.5.0)(vue@3.5.22(typescript@5.9.3)) + nuxt-site-config-kit: 3.2.11(magicast@0.3.5)(vue@3.5.22(typescript@5.9.3)) pathe: 2.0.3 pkg-types: 2.3.0 sirv: 3.0.2 @@ -13271,126 +12842,6 @@ snapshots: - xml2js - yaml - nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1): - dependencies: - '@dxup/nuxt': 0.2.0(magicast@0.5.0) - '@nuxt/cli': 3.29.3(magicast@0.5.0) - '@nuxt/devtools': 2.7.0(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) - '@nuxt/kit': 4.2.0(magicast@0.5.0) - '@nuxt/nitro-server': 4.2.0(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1))(typescript@5.9.3) - '@nuxt/schema': 4.2.0 - '@nuxt/telemetry': 2.6.6(magicast@0.5.0) - '@nuxt/vite-builder': 4.2.0(@types/node@24.9.1)(eslint@9.39.1(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.0)(meow@13.2.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.1)(@vue/compiler-sfc@3.5.22)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(meow@13.2.0)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1) - '@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.9.3)) - '@vue/shared': 3.5.22 - c12: 3.3.1(magicast@0.5.0) - chokidar: 4.0.3 - compatx: 0.2.0 - consola: 3.4.2 - cookie-es: 2.0.0 - defu: 6.1.4 - destr: 2.0.5 - devalue: 5.4.2 - errx: 0.1.0 - escape-string-regexp: 5.0.0 - exsolve: 1.0.7 - h3: 1.15.4 - hookable: 5.5.3 - ignore: 7.0.5 - impound: 1.0.0 - jiti: 2.6.1 - klona: 2.0.6 - knitwork: 1.2.0 - magic-string: 0.30.21 - mlly: 1.8.0 - nanotar: 0.2.0 - nypm: 0.6.2 - ofetch: 1.5.0 - ohash: 2.0.11 - on-change: 6.0.1 - oxc-minify: 0.95.0 - oxc-parser: 0.95.0 - oxc-transform: 0.95.0 - oxc-walker: 0.5.2(oxc-parser@0.95.0) - pathe: 2.0.3 - perfect-debounce: 2.0.0 - pkg-types: 2.3.0 - radix3: 1.1.2 - scule: 1.3.0 - semver: 7.7.3 - std-env: 3.10.0 - tinyglobby: 0.2.15 - ufo: 1.6.1 - ultrahtml: 1.6.0 - uncrypto: 0.1.3 - unctx: 2.4.1 - unimport: 5.5.0 - unplugin: 2.3.10 - unplugin-vue-router: 0.16.0(@vue/compiler-sfc@3.5.22)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)) - untyped: 2.0.0 - vue: 3.5.22(typescript@5.9.3) - vue-router: 4.6.3(vue@3.5.22(typescript@5.9.3)) - optionalDependencies: - '@parcel/watcher': 2.5.1 - '@types/node': 24.9.1 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@biomejs/biome' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@libsql/client' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - '@vue/compiler-sfc' - - aws4fetch - - bare-abort-controller - - better-sqlite3 - - bufferutil - - db0 - - drizzle-orm - - encoding - - eslint - - idb-keyval - - ioredis - - less - - lightningcss - - magicast - - meow - - mysql2 - - optionator - - oxlint - - react-native-b4a - - rolldown - - rollup - - sass - - sass-embedded - - sqlite3 - - stylelint - - stylus - - sugarss - - supports-color - - terser - - tsx - - typescript - - uploadthing - - utf-8-validate - - vite - - vls - - vti - - vue-tsc - - xml2js - - yaml - nypm@0.6.2: dependencies: citty: 0.1.6 @@ -14375,6 +13826,17 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + shiki@3.15.0: + dependencies: + '@shikijs/core': 3.15.0 + '@shikijs/engine-javascript': 3.15.0 + '@shikijs/engine-oniguruma': 3.15.0 + '@shikijs/langs': 3.15.0 + '@shikijs/themes': 3.15.0 + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -14781,7 +14243,7 @@ snapshots: ultrahtml@1.6.0: {} - unbuild@3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)): + unbuild@3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)): dependencies: '@rollup/plugin-alias': 5.1.1(rollup@4.52.5) '@rollup/plugin-commonjs': 28.0.9(rollup@4.52.5) @@ -14797,7 +14259,7 @@ snapshots: hookable: 5.5.3 jiti: 2.6.1 magic-string: 0.30.21 - mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) + mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 @@ -14936,18 +14398,6 @@ snapshots: '@nuxt/kit': 4.2.0(magicast@0.3.5) '@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3)) - unplugin-auto-import@20.2.0(@nuxt/kit@4.2.0(magicast@0.5.0))(@vueuse/core@13.9.0(vue@3.5.22(typescript@5.9.3))): - dependencies: - local-pkg: 1.1.2 - magic-string: 0.30.21 - picomatch: 4.0.3 - unimport: 5.5.0 - unplugin: 2.3.10 - unplugin-utils: 0.3.1 - optionalDependencies: - '@nuxt/kit': 4.2.0(magicast@0.5.0) - '@vueuse/core': 13.9.0(vue@3.5.22(typescript@5.9.3)) - unplugin-utils@0.2.5: dependencies: pathe: 2.0.3 @@ -14975,23 +14425,6 @@ snapshots: transitivePeerDependencies: - supports-color - unplugin-vue-components@30.0.0(@babel/parser@7.28.5)(@nuxt/kit@4.2.0(magicast@0.5.0))(vue@3.5.22(typescript@5.9.3)): - dependencies: - chokidar: 4.0.3 - debug: 4.4.3 - local-pkg: 1.1.2 - magic-string: 0.30.21 - mlly: 1.8.0 - tinyglobby: 0.2.15 - unplugin: 2.3.10 - unplugin-utils: 0.3.1 - vue: 3.5.22(typescript@5.9.3) - optionalDependencies: - '@babel/parser': 7.28.5 - '@nuxt/kit': 4.2.0(magicast@0.5.0) - transitivePeerDependencies: - - supports-color - unplugin-vue-router@0.16.0(@vue/compiler-sfc@3.5.22)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3)): dependencies: '@babel/generator': 7.28.5 @@ -15344,10 +14777,10 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.22(typescript@5.9.3) - vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)): + vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)): dependencies: '@babel/parser': 7.28.5 - '@vue/compiler-core': 3.5.22 + '@vue/compiler-core': 3.5.24 esbuild: 0.25.11 vue: 3.5.22(typescript@5.9.3) From 0e83b9784069bd85ef23d0d92efbd19cd33122b1 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Mon, 10 Nov 2025 17:12:50 +0100 Subject: [PATCH 02/20] chore: use mdc nightly --- package.json | 3 +- pnpm-lock.yaml | 193 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 170 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 955f38a2..9cd4fd8d 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,8 @@ "zod": "^4.1.12" }, "resolutions": { - "remark-mdc": "3.8.1" + "remark-mdc": "3.8.1", + "@nuxtjs/mdc": "https://pkg.pr.new/@nuxtjs/mdc@cb6a227" }, "packageManager": "pnpm@10.20.0", "keywords": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fe1084e..aeab5cf7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: remark-mdc: 3.8.1 + '@nuxtjs/mdc': https://pkg.pr.new/@nuxtjs/mdc@cb6a227 importers: @@ -15,8 +16,8 @@ importers: specifier: ^1.2.72 version: 1.2.72 '@nuxtjs/mdc': - specifier: ^0.18.2 - version: 0.18.2(magicast@0.3.5) + specifier: https://pkg.pr.new/@nuxtjs/mdc@cb6a227 + version: https://pkg.pr.new/@nuxtjs/mdc@cb6a227(magicast@0.3.5) '@vueuse/core': specifier: ^13.9.0 version: 13.9.0(vue@3.5.22(typescript@5.9.3)) @@ -44,7 +45,7 @@ importers: version: 4.2.0(magicast@0.3.5) '@nuxt/module-builder': specifier: ^1.0.2 - version: 1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) + version: 1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) '@nuxt/ui': specifier: ^4.1.0 version: 4.1.0(@babel/parser@7.28.5)(change-case@5.4.4)(db0@0.3.4(better-sqlite3@12.4.1))(embla-carousel@8.6.0)(idb-keyval@6.2.2)(ioredis@5.8.2)(magicast@0.3.5)(typescript@5.9.3)(vite@7.2.0(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.6.3(vue@3.5.22(typescript@5.9.3)))(vue@3.5.22(typescript@5.9.3))(zod@4.1.12) @@ -1025,6 +1026,10 @@ packages: resolution: {integrity: sha512-1yN3LL6RDN5GjkNLPUYCbNRkaYnat6hqejPyfIBBVzrWOrpiQeNMGxQM/IcVdaSuBJXAnu0sUvTKXpXkmPhljg==} engines: {node: '>=18.12.0'} + '@nuxt/kit@4.2.1': + resolution: {integrity: sha512-lLt8KLHyl7IClc3RqRpRikz15eCfTRlAWL9leVzPyg5N87FfKE/7EWgWvpiL/z4Tf3dQCIqQb88TmHE0JTIDvA==} + engines: {node: '>=18.12.0'} + '@nuxt/module-builder@1.0.2': resolution: {integrity: sha512-9M+0oZimbwom1J+HrfDuR5NDPED6C+DlM+2xfXju9wqB6VpVfYkS6WNEmS0URw8kpJcKBuogAc7ADO7vRS4s4A==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1097,8 +1102,9 @@ packages: '@nuxtjs/mdc@0.18.0': resolution: {integrity: sha512-/rWEOiLpD6oNx2FC/UsYxLn1pP31pvRmaX5y8GurBOogATKDWd3jlfKCGgshLnsWM6dCKgNkF0mCZQCMZMfpIQ==} - '@nuxtjs/mdc@0.18.2': - resolution: {integrity: sha512-pdeWd2/oOPriPVa1F6QNoK4fZCp/b4sxEVoouXJJCETCBIFSNS4OOtdRTY1ATLM1Gr+6ZvNyNPABvaaUEGC4Lw==} + '@nuxtjs/mdc@https://pkg.pr.new/@nuxtjs/mdc@cb6a227': + resolution: {tarball: https://pkg.pr.new/@nuxtjs/mdc@cb6a227} + version: 0.18.2 '@nuxtjs/robots@5.5.6': resolution: {integrity: sha512-PFp0sSaQs2ceEubvkiUPrWQ0GYTTu5bDH0lGVmJlm0h/Dqmt/e9TziXNKahL8HUV3VG22YzRyuyjd7p8+BaNgw==} @@ -1862,24 +1868,45 @@ packages: '@shikijs/core@3.14.0': resolution: {integrity: sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw==} + '@shikijs/core@3.15.0': + resolution: {integrity: sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==} + '@shikijs/engine-javascript@3.14.0': resolution: {integrity: sha512-3v1kAXI2TsWQuwv86cREH/+FK9Pjw3dorVEykzQDhwrZj0lwsHYlfyARaKmn6vr5Gasf8aeVpb8JkzeWspxOLQ==} + '@shikijs/engine-javascript@3.15.0': + resolution: {integrity: sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==} + '@shikijs/engine-oniguruma@3.14.0': resolution: {integrity: sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==} + '@shikijs/engine-oniguruma@3.15.0': + resolution: {integrity: sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==} + '@shikijs/langs@3.14.0': resolution: {integrity: sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==} + '@shikijs/langs@3.15.0': + resolution: {integrity: sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==} + '@shikijs/themes@3.14.0': resolution: {integrity: sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==} + '@shikijs/themes@3.15.0': + resolution: {integrity: sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==} + '@shikijs/transformers@3.14.0': resolution: {integrity: sha512-i67zQnY9wLMMnKasonVW1L9fKneSLZDj1ePsA4o0AZWU4uUobmJY9baRDa36z+a9/g0aG76/2tybQvm4hrwxIQ==} + '@shikijs/transformers@3.15.0': + resolution: {integrity: sha512-Hmwip5ovvSkg+Kc41JTvSHHVfCYF+C8Cp1omb5AJj4Xvd+y9IXz2rKJwmFRGsuN0vpHxywcXJ1+Y4B9S7EG1/A==} + '@shikijs/types@3.14.0': resolution: {integrity: sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ==} + '@shikijs/types@3.15.0': + resolution: {integrity: sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==} + '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -2386,6 +2413,9 @@ packages: '@vue/compiler-core@3.5.22': resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} + '@vue/compiler-core@3.5.24': + resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==} + '@vue/compiler-dom@3.5.22': resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==} @@ -2453,6 +2483,9 @@ packages: '@vue/shared@3.5.22': resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + '@vue/shared@3.5.24': + resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==} + '@vueuse/core@10.11.1': resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} @@ -5706,6 +5739,9 @@ packages: shiki@3.14.0: resolution: {integrity: sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g==} + shiki@3.15.0: + resolution: {integrity: sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -8201,7 +8237,57 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/module-builder@1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3))': + '@nuxt/kit@4.2.1(magicast@0.3.5)': + dependencies: + c12: 3.3.1(magicast@0.3.5) + consola: 3.4.2 + defu: 6.1.4 + destr: 2.0.5 + errx: 0.1.0 + exsolve: 1.0.7 + ignore: 7.0.5 + jiti: 2.6.1 + klona: 2.0.6 + mlly: 1.8.0 + ohash: 2.0.11 + pathe: 2.0.3 + pkg-types: 2.3.0 + rc9: 2.1.2 + scule: 1.3.0 + semver: 7.7.3 + tinyglobby: 0.2.15 + ufo: 1.6.1 + unctx: 2.4.1 + untyped: 2.0.0 + transitivePeerDependencies: + - magicast + + '@nuxt/kit@4.2.1(magicast@0.5.0)': + dependencies: + c12: 3.3.1(magicast@0.5.0) + consola: 3.4.2 + defu: 6.1.4 + destr: 2.0.5 + errx: 0.1.0 + exsolve: 1.0.7 + ignore: 7.0.5 + jiti: 2.6.1 + klona: 2.0.6 + mlly: 1.8.0 + ohash: 2.0.11 + pathe: 2.0.3 + pkg-types: 2.3.0 + rc9: 2.1.2 + scule: 1.3.0 + semver: 7.7.3 + tinyglobby: 0.2.15 + ufo: 1.6.1 + unctx: 2.4.1 + untyped: 2.0.0 + transitivePeerDependencies: + - magicast + + '@nuxt/module-builder@1.0.2(@nuxt/cli@3.29.3(magicast@0.3.5))(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3))': dependencies: '@nuxt/cli': 3.29.3(magicast@0.3.5) citty: 0.1.6 @@ -8209,14 +8295,14 @@ snapshots: defu: 6.1.4 jiti: 2.6.1 magic-regexp: 0.10.0 - mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) + mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 tsconfck: 3.1.6(typescript@5.9.3) typescript: 5.9.3 - unbuild: 3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) - vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)) + unbuild: 3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) + vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)) transitivePeerDependencies: - '@vue/compiler-core' - esbuild @@ -8874,16 +8960,16 @@ snapshots: - magicast - supports-color - '@nuxtjs/mdc@0.18.2(magicast@0.3.5)': + '@nuxtjs/mdc@https://pkg.pr.new/@nuxtjs/mdc@cb6a227(magicast@0.3.5)': dependencies: - '@nuxt/kit': 4.2.0(magicast@0.3.5) - '@shikijs/core': 3.14.0 - '@shikijs/langs': 3.14.0 - '@shikijs/themes': 3.14.0 - '@shikijs/transformers': 3.14.0 + '@nuxt/kit': 4.2.1(magicast@0.3.5) + '@shikijs/core': 3.15.0 + '@shikijs/langs': 3.15.0 + '@shikijs/themes': 3.15.0 + '@shikijs/transformers': 3.15.0 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@vue/compiler-core': 3.5.22 + '@vue/compiler-core': 3.5.24 consola: 3.4.2 debug: 4.4.3 defu: 6.1.4 @@ -8912,7 +8998,7 @@ snapshots: remark-rehype: 11.1.2 remark-stringify: 11.0.0 scule: 1.3.0 - shiki: 3.14.0 + shiki: 3.15.0 ufo: 1.6.1 unified: 11.0.5 unist-builder: 4.0.0 @@ -9491,35 +9577,71 @@ snapshots: '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 + '@shikijs/core@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + '@shikijs/engine-javascript@3.14.0': dependencies: '@shikijs/types': 3.14.0 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.3 + '@shikijs/engine-javascript@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.3 + '@shikijs/engine-oniguruma@3.14.0': dependencies: '@shikijs/types': 3.14.0 '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/engine-oniguruma@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + '@shikijs/langs@3.14.0': dependencies: '@shikijs/types': 3.14.0 + '@shikijs/langs@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/themes@3.14.0': dependencies: '@shikijs/types': 3.14.0 + '@shikijs/themes@3.15.0': + dependencies: + '@shikijs/types': 3.15.0 + '@shikijs/transformers@3.14.0': dependencies: '@shikijs/core': 3.14.0 '@shikijs/types': 3.14.0 + '@shikijs/transformers@3.15.0': + dependencies: + '@shikijs/core': 3.15.0 + '@shikijs/types': 3.15.0 + '@shikijs/types@3.14.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + '@shikijs/types@3.15.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + '@shikijs/vscode-textmate@10.0.2': {} '@shuding/opentype.js@1.4.0-beta.0': @@ -10049,6 +10171,14 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.5.24': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.24 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-dom@3.5.22': dependencies: '@vue/compiler-core': 3.5.22 @@ -10165,6 +10295,8 @@ snapshots: '@vue/shared@3.5.22': {} + '@vue/shared@3.5.24': {} + '@vueuse/core@10.11.1(vue@3.5.22(typescript@5.9.3))': dependencies: '@types/web-bluetooth': 0.0.20 @@ -12808,7 +12940,7 @@ snapshots: mkdirp-classic@0.5.3: {} - mkdist@2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)): + mkdist@2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)): dependencies: autoprefixer: 10.4.21(postcss@8.5.6) citty: 0.1.6 @@ -12826,7 +12958,7 @@ snapshots: optionalDependencies: typescript: 5.9.3 vue: 3.5.22(typescript@5.9.3) - vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)) + vue-sfc-transformer: 0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)) vue-tsc: 3.1.3(typescript@5.9.3) mlly@1.8.0: @@ -13051,7 +13183,7 @@ snapshots: nuxt-component-meta@https://pkg.pr.new/nuxt-component-meta@e3eb2c4(magicast@0.3.5): dependencies: - '@nuxt/kit': 4.2.0(magicast@0.3.5) + '@nuxt/kit': 4.2.1(magicast@0.3.5) citty: 0.1.6 json-schema-to-zod: 2.6.1 mlly: 1.8.0 @@ -13065,7 +13197,7 @@ snapshots: nuxt-component-meta@https://pkg.pr.new/nuxt-component-meta@e3eb2c4(magicast@0.5.0): dependencies: - '@nuxt/kit': 4.2.0(magicast@0.5.0) + '@nuxt/kit': 4.2.1(magicast@0.5.0) citty: 0.1.6 json-schema-to-zod: 2.6.1 mlly: 1.8.0 @@ -14375,6 +14507,17 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + shiki@3.15.0: + dependencies: + '@shikijs/core': 3.15.0 + '@shikijs/engine-javascript': 3.15.0 + '@shikijs/engine-oniguruma': 3.15.0 + '@shikijs/langs': 3.15.0 + '@shikijs/themes': 3.15.0 + '@shikijs/types': 3.15.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -14781,7 +14924,7 @@ snapshots: ultrahtml@1.6.0: {} - unbuild@3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)): + unbuild@3.6.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)): dependencies: '@rollup/plugin-alias': 5.1.1(rollup@4.52.5) '@rollup/plugin-commonjs': 28.0.9(rollup@4.52.5) @@ -14797,7 +14940,7 @@ snapshots: hookable: 5.5.3 jiti: 2.6.1 magic-string: 0.30.21 - mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) + mkdist: 2.4.1(typescript@5.9.3)(vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)))(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.22(typescript@5.9.3)) mlly: 1.8.0 pathe: 2.0.3 pkg-types: 2.3.0 @@ -15344,10 +15487,10 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.22(typescript@5.9.3) - vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.22)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)): + vue-sfc-transformer@0.1.17(@vue/compiler-core@3.5.24)(esbuild@0.25.11)(vue@3.5.22(typescript@5.9.3)): dependencies: '@babel/parser': 7.28.5 - '@vue/compiler-core': 3.5.22 + '@vue/compiler-core': 3.5.24 esbuild: 0.25.11 vue: 3.5.22(typescript@5.9.3) From c8cce123cdedaf285d8b0518d894090b028fdf1c Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 11 Nov 2025 12:13:46 +0100 Subject: [PATCH 03/20] fix: drop auto generated title and description from documents --- src/module/src/runtime/utils/collection.ts | 37 +++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/module/src/runtime/utils/collection.ts b/src/module/src/runtime/utils/collection.ts index 3f4b6ec9..0e8d70cc 100644 --- a/src/module/src/runtime/utils/collection.ts +++ b/src/module/src/runtime/utils/collection.ts @@ -1,10 +1,11 @@ -import type { CollectionInfo, CollectionSource, Draft07, CollectionItemBase, PageCollectionItemBase, ResolvedCollectionSource, Draft07DefinitionProperty } from '@nuxt/content' +import type { CollectionInfo, CollectionSource, Draft07, CollectionItemBase, PageCollectionItemBase, ResolvedCollectionSource, Draft07DefinitionProperty, MinimarkTree } from '@nuxt/content' import { hash } from 'ohash' import { pathMetaTransform } from './path-meta' import { minimatch } from 'minimatch' import { join, dirname, parse } from 'pathe' import type { DatabaseItem } from 'nuxt-studio/app' import { withoutLeadingSlash } from 'ufo' +import { textContent } from 'minimark' export const getCollectionByFilePath = (path: string, collections: Record): CollectionInfo | undefined => { let matchedSource: ResolvedCollectionSource | undefined @@ -160,6 +161,19 @@ export function normalizeDocument(document: DatabaseItem) { } } + const meta = pathMetaTransform(document as unknown as PageCollectionItemBase) + if (meta.title === document.title) { + Reflect.deleteProperty(document, 'title') + } + + const extractedContentHeading = contentHeading(document.body as MinimarkTree) + if (extractedContentHeading.title === document.title) { + Reflect.deleteProperty(document, 'title') + } + if (extractedContentHeading.description === document.description) { + Reflect.deleteProperty(document, 'description') + } + return document } @@ -214,3 +228,24 @@ export function generateRecordInsert(collection: CollectionInfo, data: Record node[0] !== "hr"); + if (children.length && children[0]?.[0] === "h1") { + const node = children.shift()! + title = textContent(node); + } + if (children.length && children[0]?.[0] === "p") { + const node = children.shift()!; + description = textContent(node); + } + return { + title, + description + }; +} \ No newline at end of file From 42f4557d90633eeac65f9e4c4646ce39eb515582 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 11 Nov 2025 13:26:46 +0100 Subject: [PATCH 04/20] fix(editor): prevent keeping invalid fields in front-matter --- src/app/src/components/content/ContentEditor.vue | 5 +---- src/app/src/utils/content.ts | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/app/src/components/content/ContentEditor.vue b/src/app/src/components/content/ContentEditor.vue index 69f73b3b..8a4b67b2 100644 --- a/src/app/src/components/content/ContentEditor.vue +++ b/src/app/src/components/content/ContentEditor.vue @@ -50,10 +50,7 @@ const document = computed({ return } - context.activeTree.value.draft.update(props.draftItem.id, { - ...toRaw(document.value as DatabasePageItem), - ...toRaw(value), - }) + context.activeTree.value.draft.update(props.draftItem.id, value) }, }) diff --git a/src/app/src/utils/content.ts b/src/app/src/utils/content.ts index e37d6500..820b5bcb 100644 --- a/src/app/src/utils/content.ts +++ b/src/app/src/utils/content.ts @@ -88,7 +88,7 @@ export async function generateDocumentFromContent(id: string, content: string): return null } -async function generateDocumentFromYAMLContent(id: string, content: string): Promise { +export async function generateDocumentFromYAMLContent(id: string, content: string): Promise { const { data } = parseFrontMatter(`---\n${content}\n---`) // Keep array contents under `body` key @@ -108,7 +108,7 @@ async function generateDocumentFromYAMLContent(id: string, content: string): Pro } as DatabaseItem } -async function generateDocumentFromJSONContent(id: string, content: string): Promise { +export async function generateDocumentFromJSONContent(id: string, content: string): Promise { let parsed: Record = destr(content) // Keep array contents under `body` key @@ -129,7 +129,7 @@ async function generateDocumentFromJSONContent(id: string, content: string): Pro } as DatabaseItem } -async function generateDocumentFromMarkdownContent(id: string, content: string): Promise { +export async function generateDocumentFromMarkdownContent(id: string, content: string): Promise { const document = await parseMarkdown(content, { remark: { plugins: { From 4d96e738e4cd2814d3312c84a10c9ae7526ec977 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 11 Nov 2025 13:27:41 +0100 Subject: [PATCH 05/20] Update src/module/src/runtime/utils/collection.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- src/module/src/runtime/utils/collection.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/module/src/runtime/utils/collection.ts b/src/module/src/runtime/utils/collection.ts index 0e8d70cc..274a6638 100644 --- a/src/module/src/runtime/utils/collection.ts +++ b/src/module/src/runtime/utils/collection.ts @@ -166,7 +166,9 @@ export function normalizeDocument(document: DatabaseItem) { Reflect.deleteProperty(document, 'title') } - const extractedContentHeading = contentHeading(document.body as MinimarkTree) + const extractedContentHeading = document.body + ? contentHeading(document.body as MinimarkTree) + : { title: '', description: '' } if (extractedContentHeading.title === document.title) { Reflect.deleteProperty(document, 'title') } From 8ab69b1fc08d025282a0b98d9337083f1703e9b2 Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Tue, 11 Nov 2025 13:29:46 +0100 Subject: [PATCH 06/20] lint: fix --- .../src/components/content/ContentEditor.vue | 2 +- src/module/src/runtime/utils/collection.ts | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/app/src/components/content/ContentEditor.vue b/src/app/src/components/content/ContentEditor.vue index 8a4b67b2..d2e5002a 100644 --- a/src/app/src/components/content/ContentEditor.vue +++ b/src/app/src/components/content/ContentEditor.vue @@ -1,5 +1,5 @@ diff --git a/src/app/src/components/content/ContentEditorText.vue b/src/app/src/components/content/ContentEditorText.vue index ba50a9a4..e5092504 100644 --- a/src/app/src/components/content/ContentEditorText.vue +++ b/src/app/src/components/content/ContentEditorText.vue @@ -5,8 +5,9 @@ import { parseMarkdown, stringifyMarkdown } from '@nuxtjs/mdc/runtime' import { decompressTree, compressTree } from '@nuxt/content/runtime' import type { MDCRoot } from '@nuxtjs/mdc' import type { MarkdownRoot } from '@nuxt/content' -import { removeReservedKeysFromDocument } from '../../utils/content' +import { useStudio } from '../../composables/useStudio' +const { host } = useStudio() const document = defineModel() const content = ref('') @@ -14,7 +15,7 @@ watch(() => document.value?.id, async () => { if (document.value?.body) { // @ts-expect-error todo fix MarkdownRoot/MDCRoot conversion in MDC module const tree = document.value.body.type === 'minimark' ? decompressTree(document.value.body) : (document.value.body as MDCRoot) - const data = removeReservedKeysFromDocument(document.value) + const data = host.document.utils.removeReservedKeys(document.value) stringifyMarkdown(tree, data).then((md) => { content.value = md || '' }) diff --git a/src/app/src/composables/useContext.ts b/src/app/src/composables/useContext.ts index f81571dd..9f22dc94 100644 --- a/src/app/src/composables/useContext.ts +++ b/src/app/src/composables/useContext.ts @@ -106,8 +106,8 @@ export const useContext = createSharedComposable(( const rootDocumentFsPath = joinURL(fsPath, 'index.md') const navigationDocumentFsPath = joinURL(fsPath, '.navigation.yml') - const navigationDocument = await host.document.create(navigationDocumentFsPath, `title: ${folderName}`) - const rootDocument = await host.document.create(rootDocumentFsPath, `# ${upperFirst(folderName)} root file`) + const navigationDocument = await host.document.db.create(navigationDocumentFsPath, `title: ${folderName}`) + const rootDocument = await host.document.db.create(rootDocumentFsPath, `# ${upperFirst(folderName)} root file`) await activeTree.value.draft.create(navigationDocumentFsPath, navigationDocument) const rootDocumentDraftItem = await activeTree.value.draft.create(rootDocumentFsPath, rootDocument) @@ -136,7 +136,7 @@ export const useContext = createSharedComposable(( }, [StudioItemActionId.CreateDocument]: async (params: CreateFileParams) => { const { fsPath, content } = params - const document = await host.document.create(fsPath, content) + const document = await host.document.db.create(fsPath, content) const draftItem = await activeTree.value.draft.create(fsPath, document as DatabaseItem) await activeTree.value.selectItemByFsPath(draftItem.fsPath) }, diff --git a/src/app/src/composables/useDraftBase.ts b/src/app/src/composables/useDraftBase.ts index 6b39b3d9..74ac0f1f 100644 --- a/src/app/src/composables/useDraftBase.ts +++ b/src/app/src/composables/useDraftBase.ts @@ -18,7 +18,7 @@ export function useDraftBase( const current = ref | null>(null) const ghPathPrefix = type === 'media' ? 'public' : 'content' - const hostDb = type === 'media' ? host.media : host.document + const hostDb = type === 'media' ? host.media : host.document.db const hookName = `studio:draft:${type}:updated` as const const hooks = useHooks() @@ -38,7 +38,7 @@ export function useDraftBase( const draftItem: DraftItem = { fsPath, githubFile, - status: getDraftStatus(item, original), + status: getDraftStatus(item, original!, host.document.utils.areEqual), modified: item, } @@ -128,14 +128,14 @@ export function useDraftBase( // Renamed draft if (existingItem.original) { - await revert(existingItem.original.fsPath, { rerender: false }) + await revert(existingItem.original.fsPath!, { rerender: false }) } } else { // @ts-expect-error upsert type is wrong, second param should be DatabaseItem | MediaItem await hostDb.upsert(draftItem.fsPath, existingItem.original) existingItem.modified = existingItem.original - existingItem.status = getDraftStatus(existingItem.modified, existingItem.original) + existingItem.status = getDraftStatus(existingItem.modified as DatabaseItem, existingItem.original as DatabaseItem, host.document.utils.areEqual) await storage.setItem(draftItem.fsPath, existingItem) } } diff --git a/src/app/src/composables/useDraftDocuments.ts b/src/app/src/composables/useDraftDocuments.ts index 23736ec1..8080f666 100644 --- a/src/app/src/composables/useDraftDocuments.ts +++ b/src/app/src/composables/useDraftDocuments.ts @@ -1,7 +1,6 @@ import type { DatabaseItem, DraftItem, StudioHost, RawFile } from '../types' import { DraftStatus } from '../types/draft' import type { useGit } from './useGit' -import { generateContentFromDocument } from '../utils/content' import { getDraftStatus } from '../utils/draft' import { createSharedComposable } from '@vueuse/core' import { useHooks } from './useHooks' @@ -26,6 +25,8 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: } = useDraftBase('document', host, git, storage) const hooks = useHooks() + const hostDb = host.document.db + const generateContentFromDocument = host.document.generate.contentFromDocument async function update(fsPath: string, document: DatabaseItem): Promise> { const existingItem = list.value.find(item => item.fsPath === fsPath) as DraftItem @@ -34,7 +35,7 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: } const oldStatus = existingItem.status - existingItem.status = getDraftStatus(document, existingItem.original) + existingItem.status = getDraftStatus(document, existingItem.original as DatabaseItem, host.document.utils.areEqual) existingItem.modified = document await storage.setItem(fsPath, existingItem) @@ -42,7 +43,7 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: list.value = list.value.map(item => item.fsPath === fsPath ? existingItem : item) // Upsert document in database - await host.document.upsert(fsPath, existingItem.modified) + await hostDb.upsert(fsPath, existingItem.modified) // Trigger hook to warn that draft list has changed if (existingItem.status !== oldStatus) { @@ -61,7 +62,7 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: const { fsPath, newFsPath } = item const existingDraftToRename = list.value.find(draftItem => draftItem.fsPath === fsPath) as DraftItem - const dbItemToRename = await host.document.get(fsPath) + const dbItemToRename = await hostDb.get(fsPath) if (!dbItemToRename) { throw new Error(`Database item not found for document fsPath: ${fsPath}`) } @@ -76,7 +77,7 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: await remove([fsPath], { rerender: false }) - const newDbItem = await host.document.create(newFsPath, content!) + const newDbItem = await hostDb.create(newFsPath, content!) await create(newFsPath, newDbItem, originalDbItem, { rerender: false }) } @@ -85,7 +86,7 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: } async function duplicate(fsPath: string): Promise> { - let currentDbItem = await host.document.get(fsPath) + let currentDbItem = await hostDb.get(fsPath) if (!currentDbItem) { throw new Error(`Database item not found for document fsPath: ${fsPath}`) } @@ -103,7 +104,7 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: const newFsPath = `${currentFsPath.split('/').slice(0, -1).join('/')}/${currentNameWithoutExtension}-copy.${currentExtension}` - const newDbItem = await host.document.create(newFsPath, currentContent) + const newDbItem = await hostDb.create(newFsPath, currentContent) return await create(newFsPath, newDbItem) } diff --git a/src/app/src/composables/useStudio.ts b/src/app/src/composables/useStudio.ts index 4b7cc535..93a58c6e 100644 --- a/src/app/src/composables/useStudio.ts +++ b/src/app/src/composables/useStudio.ts @@ -7,7 +7,7 @@ import { useDraftMedias } from './useDraftMedias' import { ref } from 'vue' import { useTree } from './useTree' import type { RouteLocationNormalized } from 'vue-router' -import type { StudioHost, GitOptions } from '../types' +import type { StudioHost, GitOptions, DatabaseItem } from '../types' import { StudioFeature } from '../types' import { documentStorage, mediaStorage, nullStorageDriver } from '../utils/storage' import { useHooks } from './useHooks' @@ -83,6 +83,7 @@ export const useStudio = createSharedComposable(() => { function initDevelopmentMode(host: StudioHost, draftDocuments: ReturnType, draftMedias: ReturnType, documentTree: ReturnType, mediaTree: ReturnType) { const hooks = useHooks() + const areDocumentsEqual = host.document.utils.areEqual // Disable browser storages documentStorage.mount('/', nullStorageDriver) @@ -99,10 +100,10 @@ function initDevelopmentMode(host: StudioHost, draftDocuments: ReturnType Promise - list: () => Promise - upsert: (fsPath: string, document: DatabaseItem) => Promise - create: (fsPath: string, content: string) => Promise - delete: (fsPath: string) => Promise - detectActives: () => Array<{ fsPath: string, title: string }> - isEqual: (content: string, document: DatabaseItem) => Promise - generateDocumentFromContent: (id: string, content: string) => Promise + db: { + get: (fsPath: string) => Promise + list: () => Promise + upsert: (fsPath: string, document: DatabaseItem) => Promise + create: (fsPath: string, content: string) => Promise + delete: (fsPath: string) => Promise + } + utils: { + areEqual: (document1: DatabaseItem, document2: DatabaseItem) => boolean + isMatchingContent: (content: string, document: DatabaseItem) => Promise + pickReservedKeys: (document: DatabaseItem) => DatabaseItem + removeReservedKeys: (document: DatabaseItem) => DatabaseItem + detectActives: () => Array<{ fsPath: string, title: string }> + } + generate: { + documentFromContent: (id: string, content: string) => Promise + contentFromDocument: (document: DatabaseItem) => Promise + } } media: { get: (fsPath: string) => Promise diff --git a/src/app/src/types/item.ts b/src/app/src/types/item.ts index b804abea..f7165edf 100644 --- a/src/app/src/types/item.ts +++ b/src/app/src/types/item.ts @@ -1,6 +1,6 @@ export interface BaseItem { id: string - fsPath: string + fsPath?: string extension: string stem: string path?: string diff --git a/src/app/src/utils/content.ts b/src/app/src/utils/content.ts index 0da734e1..802281dc 100644 --- a/src/app/src/utils/content.ts +++ b/src/app/src/utils/content.ts @@ -1,124 +1,7 @@ -// import type { ParsedContentFile } from '@nuxt/content' -import { stringifyMarkdown } from '@nuxtjs/mdc/runtime' -import { stringifyFrontMatter } from 'remark-mdc' -import type { MDCElement, MDCRoot } from '@nuxtjs/mdc' -import { type DatabasePageItem, type DatabaseItem, ContentFileExtension } from '../types' -import { omit, pick } from './object' -import { decompressTree } from '@nuxt/content/runtime' -import { visit } from 'unist-util-visit' -import type { Node } from 'unist' -import { getFileExtension } from './file' - -const reservedKeys = ['id', 'fsPath', 'stem', 'extension', '__hash__', 'path', 'body', 'meta', 'rawbody'] - -export function pickReservedKeysFromDocument(document: DatabaseItem) { - return pick(document, reservedKeys) -} - -export function removeReservedKeysFromDocument(document: DatabaseItem) { - const result = omit(document, reservedKeys) - // Default value of navigation is true, so we can safely remove it - if (result.navigation === true) { - Reflect.deleteProperty(result, 'navigation') - } - - if (document.seo) { - const seo = document.seo as Record - if ( - (!seo.title || seo.title === document.title) - && (!seo.description || seo.description === document.description) - ) { - Reflect.deleteProperty(result, 'seo') - } - } - - if (!document.title) { - Reflect.deleteProperty(result, 'title') - } - if (!document.description) { - Reflect.deleteProperty(result, 'description') - } - - // expand meta to the root - for (const key in (document.meta || {})) { - if (key !== '__hash__') { - result[key] = (document.meta as Record)[key] - } - } - - for (const key in (result || {})) { - if (result[key] === null) { - Reflect.deleteProperty(result, key) - } - } - - return result -} - -export function isEqual(content1: string | null, content2: string | null): boolean { +export function areContentEqual(content1: string | null, content2: string | null): boolean { if (content1 && content2) { return content1.trim() === content2.trim() } return false } - -export async function generateContentFromDocument(document: DatabaseItem): Promise { - const [id, _hash] = document.id.split('#') - const extension = getFileExtension(id) - - if (extension === ContentFileExtension.Markdown) { - return await generateContentFromMarkdownDocument(document as DatabasePageItem) - } - - if (extension === ContentFileExtension.YAML || extension === ContentFileExtension.YML) { - return await generateContentFromYAMLDocument(document) - } - - if (extension === ContentFileExtension.JSON) { - return await generateContentFromJSONDocument(document) - } - - return null -} - -export async function generateContentFromYAMLDocument(document: DatabaseItem): Promise { - return await stringifyFrontMatter(removeReservedKeysFromDocument(document), '', { - prefix: '', - suffix: '', - lineWidth: 0, - }) -} - -export async function generateContentFromJSONDocument(document: DatabaseItem): Promise { - return JSON.stringify(removeReservedKeysFromDocument(document), null, 2) -} - -export async function generateContentFromMarkdownDocument(document: DatabasePageItem): Promise { - // @ts-expect-error todo fix MarkdownRoot/MDCRoot conversion in MDC module - const body = document.body.type === 'minimark' ? decompressTree(document.body) : (document.body as MDCRoot) - - // Remove nofollow from links - visit(body, (node: Node) => (node as MDCElement).type === 'element' && (node as MDCElement).tag === 'a', (node: Node) => { - if ((node as MDCElement).props?.rel?.join(' ') === 'nofollow') { - Reflect.deleteProperty((node as MDCElement).props!, 'rel') - } - }) - - const markdown = await stringifyMarkdown(body, removeReservedKeysFromDocument(document), { - frontMatter: { - options: { - lineWidth: 0, - }, - }, - plugins: { - remarkMDC: { - options: { - autoUnwrap: true, - }, - }, - }, - }) - - return typeof markdown === 'string' ? markdown.replace(/*/g, '*') : markdown -} diff --git a/src/app/src/utils/database.ts b/src/app/src/utils/database.ts deleted file mode 100644 index 7cf73fcc..00000000 --- a/src/app/src/utils/database.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { compressTree } from '@nuxt/content/runtime' -import { type DatabasePageItem, ContentFileExtension } from '../types' -import { stringify } from 'minimark/stringify' -import type { MDCRoot } from '@nuxtjs/mdc' -import type { MarkdownRoot } from '@nuxt/content' -import { isDeepEqual } from './object' - -export function isEqual(document1: Record, document2: Record) { - function withoutLastStyles(body: MarkdownRoot) { - if (body.value[body.value.length - 1]?.[0] === 'style') { - return { ...body, value: body.value.slice(0, -1) } - } - return body - } - - const { body: body1, meta: meta1, ...documentData1 } = document1 - const { body: body2, meta: meta2, ...documentData2 } = document2 - - // Compare body first - if (document1.extension === ContentFileExtension.Markdown) { - const minifiedBody1 = withoutLastStyles( - (document1 as DatabasePageItem).body.type === 'minimark' ? document1.body as MarkdownRoot : compressTree(document1.body as unknown as MDCRoot), - ) - const minifiedBody2 = withoutLastStyles( - (document2 as DatabasePageItem).body.type === 'minimark' ? document2.body as MarkdownRoot : compressTree(document2.body as unknown as MDCRoot), - ) - - if (stringify(minifiedBody1) !== stringify(minifiedBody2)) { - return false - } - } - else if (typeof body1 === 'object' && typeof body2 === 'object') { - if (!isDeepEqual(body1 as Record, body2 as Record)) { - return false - } - } - else { - // For other file types, we compare the JSON stringified bodies - if (JSON.stringify(body1) !== JSON.stringify(body2)) { - return false - } - } - - const data1 = refineDocumentData({ ...documentData1, ...(meta1 || {}) }) - const data2 = refineDocumentData({ ...documentData2, ...(meta2 || {}) }) - if (!isDeepEqual(data1, data2)) { - return false - } - - return true -} - -function refineDocumentData(doc: Record) { - if (doc.seo) { - const seo = doc.seo as Record - doc.seo = { - ...seo, - title: seo.title || doc.title, - description: seo.description || doc.description, - } - } - // documents with same id are being compared, so it is safe to remove `path` and `__hash__` - Reflect.deleteProperty(doc, '__hash__') - Reflect.deleteProperty(doc, 'path') - - // default value of navigation is true - if (typeof doc.navigation === 'undefined') { - doc.navigation = true - } - - return doc -} diff --git a/src/app/src/utils/draft.ts b/src/app/src/utils/draft.ts index 30cc7a4b..e3313e1a 100644 --- a/src/app/src/utils/draft.ts +++ b/src/app/src/utils/draft.ts @@ -1,12 +1,12 @@ -import type { DatabaseItem, MediaItem, DatabasePageItem, DraftItem, BaseItem, ContentConflict, StudioHost } from '../types' +import type { DatabaseItem, MediaItem, DraftItem, BaseItem, ContentConflict, StudioHost } from '../types' import { DraftStatus, ContentFileExtension } from '../types' -import { isEqual } from './database' import { studioFlags } from '../composables/useStudio' -import { generateContentFromDocument } from './content' import { fromBase64ToUTF8 } from '../utils/string' import { isMediaFile } from './file' export async function checkConflict(host: StudioHost, draftItem: DraftItem): Promise { + const generateContentFromDocument = host.document.generate.contentFromDocument + if (isMediaFile(draftItem.fsPath) || draftItem.fsPath.endsWith('.gitkeep')) { return } @@ -29,7 +29,7 @@ export async function checkConflict(host: StudioHost, draftItem: DraftItem boolean): DraftStatus { if (studioFlags.dev) { return DraftStatus.Pristine } @@ -62,12 +62,12 @@ export function getDraftStatus(modified?: BaseItem, original?: BaseItem): DraftS } if (original.extension === ContentFileExtension.Markdown) { - if (!isEqual(original as DatabasePageItem, modified as DatabasePageItem)) { + if (!comparisonMethod(original as DatabaseItem, modified as DatabaseItem)) { return DraftStatus.Updated } } else if (typeof original === 'object' && typeof modified === 'object') { - if (!isEqual(original as unknown as Record, modified as unknown as Record)) { + if (!comparisonMethod(original as DatabaseItem, modified as DatabaseItem)) { return DraftStatus.Updated } } diff --git a/src/app/src/utils/object.ts b/src/app/src/utils/object.ts index 8232da4c..fa132231 100644 --- a/src/app/src/utils/object.ts +++ b/src/app/src/utils/object.ts @@ -1,15 +1,5 @@ const dateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}\.\d{3}Z)?$/ -export const omit = (obj: Record, keys: string | string[]) => { - return Object.fromEntries(Object.entries(obj) - .filter(([key]) => !keys.includes(key))) -} - -export const pick = (obj: Record, keys: string | string[]) => { - return Object.fromEntries(Object.entries(obj) - .filter(([key]) => keys.includes(key))) -} - export function isDeepEqual(obj1: Record, obj2: Record) { if (typeof obj1 === 'string' && typeof obj2 === 'string') { if (String(obj1).match(dateRegex) && String(obj2).match(dateRegex)) { diff --git a/src/app/src/utils/tree.ts b/src/app/src/utils/tree.ts index dbf1c89b..2e5cd960 100644 --- a/src/app/src/utils/tree.ts +++ b/src/app/src/utils/tree.ts @@ -8,7 +8,6 @@ import { } from '../types' import type { RouteLocationNormalized } from 'vue-router' import type { BaseItem } from '../types/item' -import { isEqual } from './database' import { studioFlags } from '../composables/useStudio' import { getFileExtension, parseName } from './file' @@ -28,7 +27,7 @@ export const COLOR_UI_STATUS_MAP: { [key in TreeStatus]?: string } = { [TreeStatus.Opened]: 'neutral', } as const -export function buildTree(dbItems: BaseItem[], draftList: DraftItem[] | null): +export function buildTree(dbItems: BaseItem[], draftList: DraftItem[] | null, comparisonMethod: (document1: DatabasePageItem, document2: DatabasePageItem) => boolean): TreeItem[] { const tree: TreeItem[] = [] const directoryMap = new Map() @@ -65,7 +64,7 @@ TreeItem[] { for (const dbItem of virtualDbItems) { const itemHasPathField = 'path' in dbItem && dbItem.path - const fsPathSegments = dbItem.fsPath.split('/').filter(Boolean) + const fsPathSegments = dbItem.fsPath!.split('/').filter(Boolean) const directorySegments = fsPathSegments.slice(0, -1) let fileName = fsPathSegments[fsPathSegments.length - 1].replace(/\.[^/.]+$/, '') @@ -77,12 +76,12 @@ TreeItem[] { fileName = name === 'index' ? 'home' : name const fileItem: TreeItem = { name: fileName, - fsPath: dbItem.fsPath, + fsPath: dbItem.fsPath!, type: 'file', prefix, } - if (dbItem.fsPath.endsWith('.gitkeep')) { + if (dbItem.fsPath!.endsWith('.gitkeep')) { fileItem.hide = true } @@ -92,7 +91,7 @@ TreeItem[] { const draftFileItem = draftList?.find(draft => draft.fsPath === dbItem.fsPath) if (draftFileItem) { - fileItem.status = getTreeStatus(draftFileItem.modified!, draftFileItem.original!) + fileItem.status = getTreeStatus(draftFileItem.modified!, draftFileItem.original!, comparisonMethod) } tree.push(fileItem) @@ -138,18 +137,18 @@ TreeItem[] { const { name, prefix } = parseName(fileName) const fileItem: TreeItem = { name, - fsPath: dbItem.fsPath, + fsPath: dbItem.fsPath!, type: 'file', prefix, } - if (dbItem.fsPath.endsWith('.gitkeep')) { + if (dbItem.fsPath!.endsWith('.gitkeep')) { fileItem.hide = true } const draftFileItem = draftList?.find(draft => draft.fsPath === dbItem.fsPath) if (draftFileItem) { - fileItem.status = getTreeStatus(draftFileItem.modified!, draftFileItem.original!) + fileItem.status = getTreeStatus(draftFileItem.modified!, draftFileItem.original!, comparisonMethod) } if (dbItem.path) { @@ -164,7 +163,7 @@ TreeItem[] { return tree } -export function getTreeStatus(modified?: BaseItem, original?: BaseItem): TreeStatus { +export function getTreeStatus(modified: BaseItem, original: BaseItem, comparisonMethod: (document1: DatabasePageItem, document2: DatabasePageItem) => boolean): TreeStatus { if (studioFlags.dev) { return TreeStatus.Opened } @@ -186,7 +185,7 @@ export function getTreeStatus(modified?: BaseItem, original?: BaseItem): TreeSta } if (original.extension === ContentFileExtension.Markdown) { - if (!isEqual(original as DatabasePageItem, modified as DatabasePageItem)) { + if (!comparisonMethod(original as DatabasePageItem, modified as DatabasePageItem)) { return TreeStatus.Updated } } diff --git a/src/app/test/mocks/host.ts b/src/app/test/mocks/host.ts index 552dbf24..4e34a04d 100644 --- a/src/app/test/mocks/host.ts +++ b/src/app/test/mocks/host.ts @@ -5,7 +5,7 @@ import { createMockDocument } from './document' import { createMockMedia } from './media' import { joinURL } from 'ufo' import type { MediaItem } from '../../src/types/media' -import { isDocumentMatchContent } from '../../../module/dist/runtime/utils/document' +import { isDocumentMatchingContent } from '../../../module/dist/runtime/utils/document' // Helper to convert fsPath to id (simulates module's internal mapping) export const fsPathToId = (fsPath: string, type: 'document' | 'media') => { @@ -53,9 +53,9 @@ export const createMockHost = (): StudioHost => ({ return Array.from(documentDb.values()) }), isEqual: vi.fn().mockImplementation(async (content: string, document: DatabaseItem) => { - return isDocumentMatchContent(content, document) + return isDocumentMatchingContent(content, document) }), - generateDocumentFromContent: vi.fn().mockImplementation(async (_id: string, _content: string) => { + generateFromContent: vi.fn().mockImplementation(async (_id: string, _content: string) => { throw new Error('Not implemented') }), }, diff --git a/src/app/test/unit/utils/database.test.ts b/src/app/test/unit/utils/database.test.ts deleted file mode 100644 index a77aabc6..00000000 --- a/src/app/test/unit/utils/database.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { isEqual } from '../../../src/utils/database' -import type { DatabasePageItem } from '../../../src/types' -import { ContentFileExtension } from '../../../src/types' - -describe('isEqual', () => { - it('should return true for two identical markdown documents with diffrent hash', () => { - const document1: DatabasePageItem = { - id: 'content:index.md', - path: '/index', - title: 'Test Document', - description: 'A test document', - extension: ContentFileExtension.Markdown, - stem: 'index', - seo: {}, - body: { - type: 'minimark', - value: ['Hello World'], - }, - meta: { - __hash__: 'hash123', - }, - } - - const document2: DatabasePageItem = { - id: 'content:index.md', - path: '/index', - title: 'Test Document', - description: 'A test document', - extension: ContentFileExtension.Markdown, - stem: 'index', - seo: {}, - body: { - type: 'minimark', - value: ['Hello World'], - }, - meta: { - __hash__: 'hash456', - }, - } - - expect(isEqual(document1, document2)).toBe(true) - }) - - it('should return false for two different markdown documents', () => { - const document1: DatabasePageItem = { - id: 'content:index.md', - path: '/index', - title: 'Test Document', - description: 'A test document', - extension: ContentFileExtension.Markdown, - stem: 'index', - seo: {}, - body: { - type: 'minimark', - value: ['Hello World'], - }, - meta: { - title: 'Test Document', - }, - } - - const document2: DatabasePageItem = { - id: 'content:index.md', - path: '/index', - title: 'Test Document', - description: 'A test document', - extension: ContentFileExtension.Markdown, - stem: 'index', - seo: {}, - body: { - type: 'minimark', - value: ['Different content'], - }, - meta: { - title: 'Test Document', - }, - } - - expect(isEqual(document1, document2)).toBe(false) - }) - - it('should return true for two identical yaml document with different order of keys', () => { - const document1: DatabasePageItem = { - extension: ContentFileExtension.YAML, - description: 'A test document', - title: 'Test Document', - path: '/index', - id: 'content:index.yml', - tags: ['tag1', 'tag2'], - } - - const document2: DatabasePageItem = { - id: 'content:index.yml', - path: '/index', - title: 'Test Document', - description: 'A test document', - extension: ContentFileExtension.YAML, - tags: ['tag1', 'tag2'], - } - - expect(isEqual(document1, document2)).toBe(true) - }) - - it('should return true if one document has extra key with null/undefined value', () => { - const document1: DatabasePageItem = { - id: 'content:index.yml', - path: '/index', - title: 'Test Document', - description: 'A test document', - } - const document2: DatabasePageItem = { - id: 'content:index.yml', - path: '/index', - title: 'Test Document', - description: 'A test document', - extra: null, - } - expect(isEqual(document1, document2)).toBe(true) - }) - - it('should ignore null/undefiend values', () => { - const document1: DatabasePageItem = { - id: 'content:index.yml', - path: '/index', - title: 'Test Document', - description: null, - } - - const document2: DatabasePageItem = { - id: 'content:index.yml', - path: '/index', - title: 'Test Document', - description: undefined, - } - - expect(isEqual(document1, document2)).toBe(true) - }) - - it('should return false if one of documents missing a key', () => { - const document1: DatabasePageItem = { - id: 'content:index.yml', - path: '/index', - title: 'Test Document', - description: 'A test document', - } - const document2: DatabasePageItem = { - id: 'content:index.yml', - path: '/index', - title: 'Test Document', - } - - expect(isEqual(document1, document2)).toBe(false) - }) - - it('should return false if array values are different', () => { - const document1: DatabasePageItem = { - id: 'content:index.yml', - path: '/index', - title: 'Test Document', - tags: ['tag1', 'tag2'], - } - const document2: DatabasePageItem = { - id: 'content:index.yml', - path: '/index', - tags: ['tag1', 'tag3'], - title: 'Test Document', - } - - expect(isEqual(document1, document2)).toBe(false) - }) - - it('should return true if date values are same but different format', () => { - const document1: DatabasePageItem = { - id: 'content:index.yml', - path: '/index', - title: 'Test Document', - date: '2025-11-04', - } - const document2: DatabasePageItem = { - id: 'content:index.yml', - path: '/index', - title: 'Test Document', - date: '2025-11-04T00:00:00.000Z', - } - - expect(isEqual(document1, document2)).toBe(true) - }) -}) diff --git a/src/module/src/runtime/host.dev.ts b/src/module/src/runtime/host.dev.ts index c50ff3e6..63c2fec2 100644 --- a/src/module/src/runtime/host.dev.ts +++ b/src/module/src/runtime/host.dev.ts @@ -1,8 +1,7 @@ import { useStudioHost as useStudioHostBase } from './host' import type { StudioUser, DatabaseItem, Repository } from 'nuxt-studio/app' -import { generateContentFromDocument } from 'nuxt-studio/app/utils' import { getCollectionByFilePath, generateIdFromFsPath, generateFsPathFromId, getCollectionById } from './utils/collection' -import { createCollectionDocument } from './utils/document' +import { populateDocumentbasedOnCollectionInfo } from './utils/document' import { createStorage } from 'unstorage' import httpDriver from 'unstorage/drivers/http' import { useRuntimeConfig } from '#imports' @@ -29,16 +28,16 @@ export function useStudioHost(user: StudioUser, repository: Repository) { } // TODO @farnabaz to check - host.document.upsert = debounce(async (fsPath: string, upsertedDocument: DatabaseItem) => { + host.document.db.upsert = debounce(async (fsPath: string, upsertedDocument: DatabaseItem) => { const collectionInfo = getCollectionByFilePath(fsPath, collections) if (!collectionInfo) { throw new Error(`Collection not found for fsPath: ${fsPath}`) } const id = generateIdFromFsPath(fsPath, collectionInfo) - const doc = createCollectionDocument(id, collectionInfo, upsertedDocument) + const doc = populateDocumentbasedOnCollectionInfo(id, collectionInfo, upsertedDocument) - const content = await generateContentFromDocument(doc) + const content = await host.document.generate.contentFromDocument(doc) await devStorage.setItem(fsPath, content, { headers: { @@ -48,7 +47,7 @@ export function useStudioHost(user: StudioUser, repository: Repository) { }, 100) // TODO @farnabaz to check - host.document.delete = async (fsPath: string) => { + host.document.db.delete = async (fsPath: string) => { await devStorage.removeItem(fsPath) } diff --git a/src/module/src/runtime/host.ts b/src/module/src/runtime/host.ts index 5db19f3a..2a9705af 100644 --- a/src/module/src/runtime/host.ts +++ b/src/module/src/runtime/host.ts @@ -3,7 +3,7 @@ import { ensure } from './utils/ensure' import type { CollectionInfo, CollectionItemBase, CollectionSource, DatabaseAdapter } from '@nuxt/content' import type { ContentDatabaseAdapter } from '../types/content' import { getCollectionByFilePath, generateIdFromFsPath, generateRecordDeletion, generateRecordInsert, generateFsPathFromId, getCollectionById } from './utils/collection' -import { createCollectionDocument, isDocumentMatchContent, normalizeDocument, generateDocumentFromContent } from './utils/document' +import { populateDocumentbasedOnCollectionInfo, isDocumentMatchingContent, normalizeDocument, generateDocumentFromContent, generateContentFromDocument, areDocumentsEqual, pickReservedKeysFromDocument, removeReservedKeysFromDocument } from './utils/document' import { kebabCase } from 'scule' import type { StudioHost, StudioUser, DatabaseItem, MediaItem, Repository } from 'nuxt-studio/app' import type { RouteLocationNormalized, Router } from 'vue-router' @@ -184,96 +184,106 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH }, document: { - get: async (fsPath: string): Promise => { - const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections()) - if (!collectionInfo) { - throw new Error(`Collection not found for fsPath: ${fsPath}`) - } - - const id = generateIdFromFsPath(fsPath, collectionInfo) - const item = await useContentCollectionQuery(collectionInfo.name).where('id', '=', id).first() - - return item ? normalizeDocument(fsPath, item as DatabaseItem) : undefined - }, - list: async (): Promise => { - const collections = Object.values(useContentCollections()).filter(collection => collection.name !== 'info') - const documentsByCollection = await Promise.all(collections.map(async (collection) => { - const documents = await useContentCollectionQuery(collection.name).all() as DatabaseItem[] + db: { + get: async (fsPath: string): Promise => { + const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections()) + if (!collectionInfo) { + throw new Error(`Collection not found for fsPath: ${fsPath}`) + } - return documents.map((document) => { - const source = getCollectionSourceById(document.id, collection.source) - const fsPath = generateFsPathFromId(document.id, source!) + const id = generateIdFromFsPath(fsPath, collectionInfo) + const item = await useContentCollectionQuery(collectionInfo.name).where('id', '=', id).first() + + return item ? normalizeDocument(fsPath, item as DatabaseItem) : undefined + }, + list: async (): Promise => { + const collections = Object.values(useContentCollections()).filter(collection => collection.name !== 'info') + const documentsByCollection = await Promise.all(collections.map(async (collection) => { + const documents = await useContentCollectionQuery(collection.name).all() as DatabaseItem[] + + return documents.map((document) => { + const source = getCollectionSourceById(document.id, collection.source) + const fsPath = generateFsPathFromId(document.id, source!) + + return normalizeDocument(fsPath, document) + }) + })) + + return documentsByCollection.flat() + }, + create: async (fsPath: string, content: string) => { + const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections()) + if (!collectionInfo) { + throw new Error(`Collection not found for fsPath: ${fsPath}`) + } - return normalizeDocument(fsPath, document) - }) - })) + const id = generateIdFromFsPath(fsPath, collectionInfo!) - return documentsByCollection.flat() - }, - create: async (fsPath: string, content: string) => { - const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections()) - if (!collectionInfo) { - throw new Error(`Collection not found for fsPath: ${fsPath}`) - } + const existingDocument = await host.document.db.get(id) + if (existingDocument) { + throw new Error(`Cannot create document with id "${id}": document already exists.`) + } - const id = generateIdFromFsPath(fsPath, collectionInfo!) + const document = await generateDocumentFromContent(id, content) + const collectionDocument = populateDocumentbasedOnCollectionInfo(id, collectionInfo, document!) - const existingDocument = await host.document.get(id) - if (existingDocument) { - throw new Error(`Cannot create document with id "${id}": document already exists.`) - } + await host.document.db.upsert(fsPath, collectionDocument) - const document = await generateDocumentFromContent(id, content) - const collectionDocument = createCollectionDocument(id, collectionInfo, document!) + return normalizeDocument(fsPath, collectionDocument!) + }, + upsert: async (fsPath: string, document: CollectionItemBase) => { + const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections()) + if (!collectionInfo) { + throw new Error(`Collection not found for fsPath: ${fsPath}`) + } - await host.document.upsert(fsPath, collectionDocument) + const id = generateIdFromFsPath(fsPath, collectionInfo) - return normalizeDocument(fsPath, collectionDocument!) - }, - upsert: async (fsPath: string, document: CollectionItemBase) => { - const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections()) - if (!collectionInfo) { - throw new Error(`Collection not found for fsPath: ${fsPath}`) - } + const doc = populateDocumentbasedOnCollectionInfo(id, collectionInfo, document) - const id = generateIdFromFsPath(fsPath, collectionInfo) + await useContentDatabaseAdapter(collectionInfo.name).exec(generateRecordDeletion(collectionInfo, id)) + await useContentDatabaseAdapter(collectionInfo.name).exec(generateRecordInsert(collectionInfo, doc)) + }, + delete: async (fsPath: string) => { + const collection = getCollectionByFilePath(fsPath, useContentCollections()) + if (!collection) { + throw new Error(`Collection not found for fsPath: ${fsPath}`) + } - const doc = createCollectionDocument(id, collectionInfo, document) + const id = generateIdFromFsPath(fsPath, collection) - await useContentDatabaseAdapter(collectionInfo.name).exec(generateRecordDeletion(collectionInfo, id)) - await useContentDatabaseAdapter(collectionInfo.name).exec(generateRecordInsert(collectionInfo, doc)) + await useContentDatabaseAdapter(collection.name).exec(generateRecordDeletion(collection, id)) + }, }, - delete: async (fsPath: string) => { - const collection = getCollectionByFilePath(fsPath, useContentCollections()) - if (!collection) { - throw new Error(`Collection not found for fsPath: ${fsPath}`) - } - - const id = generateIdFromFsPath(fsPath, collection) - - await useContentDatabaseAdapter(collection.name).exec(generateRecordDeletion(collection, id)) + utils: { + areEqual: (document1: DatabaseItem, document2: DatabaseItem) => areDocumentsEqual(document1, document2), + isMatchingContent: async (content: string, document: DatabaseItem) => isDocumentMatchingContent(content, document), + pickReservedKeys: (document: DatabaseItem) => pickReservedKeysFromDocument(document), + removeReservedKeys: (document: DatabaseItem) => removeReservedKeysFromDocument(document), + detectActives: () => { + // TODO: introduce a new convention to detect data contents [data-content-id!] + const wrappers = document.querySelectorAll('[data-content-id]') + return Array.from(wrappers).map((wrapper) => { + const id = wrapper.getAttribute('data-content-id')! + const title = id.split(/[/:]/).pop() || id + + const collection = getCollectionById(id, useContentCollections()) + + const source = getCollectionSourceById(id, collection.source) + + const fsPath = generateFsPathFromId(id, source!) + + return { + fsPath, + title, + } + }) + }, }, - detectActives: () => { - // TODO: introduce a new convention to detect data contents [data-content-id!] - const wrappers = document.querySelectorAll('[data-content-id]') - return Array.from(wrappers).map((wrapper) => { - const id = wrapper.getAttribute('data-content-id')! - const title = id.split(/[/:]/).pop() || id - - const collection = getCollectionById(id, useContentCollections()) - - const source = getCollectionSourceById(id, collection.source) - - const fsPath = generateFsPathFromId(id, source!) - - return { - fsPath, - title, - } - }) + generate: { + documentFromContent: async (id: string, content: string) => generateDocumentFromContent(id, content), + contentFromDocument: async (document: DatabaseItem) => generateContentFromDocument(document), }, - isEqual: async (content: string, document: DatabaseItem) => isDocumentMatchContent(content, document), - generateDocumentFromContent: async (id: string, content: string) => generateDocumentFromContent(id, content) as Promise, }, media: { diff --git a/src/module/src/runtime/utils/document.ts b/src/module/src/runtime/utils/document.ts index e34c3c13..fbb3b0e1 100644 --- a/src/module/src/runtime/utils/document.ts +++ b/src/module/src/runtime/utils/document.ts @@ -1,17 +1,154 @@ import type { CollectionInfo, CollectionItemBase, MarkdownRoot, PageCollectionItemBase } from '@nuxt/content' import { getOrderedSchemaKeys } from './collection' import { pathMetaTransform } from './path-meta' -import type { DatabaseItem } from 'nuxt-studio/app' -import { isObjectMatch } from './object' +import type { DatabaseItem, DatabasePageItem } from 'nuxt-studio/app' +import { areObjectsEqual, omit, pick } from './object' import { ContentFileExtension } from '../types/content' import { parseMarkdown } from '@nuxtjs/mdc/runtime/parser/index' -import type { MDCElement } from '@nuxtjs/mdc' +import type { MDCElement, MDCRoot } from '@nuxtjs/mdc' import { visit } from 'unist-util-visit' -import { compressTree } from '@nuxt/content/runtime' +import { compressTree, decompressTree } from '@nuxt/content/runtime' import destr from 'destr' -import { parseFrontMatter } from 'remark-mdc' +import { parseFrontMatter, stringifyFrontMatter } from 'remark-mdc' +import { stringify } from 'minimark/stringify' +// import type { ParsedContentFile } from '@nuxt/content' +import { stringifyMarkdown } from '@nuxtjs/mdc/runtime' +import type { Node } from 'unist' -export function createCollectionDocument(id: string, collectionInfo: CollectionInfo, document: CollectionItemBase) { +const reservedKeys = ['id', 'fsPath', 'stem', 'extension', '__hash__', 'path', 'body', 'meta', 'rawbody'] + +/* +** Normalization utils +*/ +export function normalizeDocument(fsPath: string, document: DatabaseItem): DatabaseItem { + return { + ...document, + fsPath, + } +} + +export function pickReservedKeysFromDocument(document: DatabaseItem) { + return pick(document, reservedKeys) as DatabaseItem +} + +export function removeReservedKeysFromDocument(document: DatabaseItem) { + const result = omit(document, reservedKeys) + // Default value of navigation is true, so we can safely remove it + if (result.navigation === true) { + Reflect.deleteProperty(result, 'navigation') + } + + if (document.seo) { + const seo = document.seo as Record + if ( + (!seo.title || seo.title === document.title) + && (!seo.description || seo.description === document.description) + ) { + Reflect.deleteProperty(result, 'seo') + } + } + + if (!document.title) { + Reflect.deleteProperty(result, 'title') + } + if (!document.description) { + Reflect.deleteProperty(result, 'description') + } + + // expand meta to the root + for (const key in (document.meta || {})) { + if (key !== '__hash__') { + result[key] = (document.meta as Record)[key] + } + } + + for (const key in (result || {})) { + if (result[key] === null) { + Reflect.deleteProperty(result, key) + } + } + + return result as DatabaseItem +} + +/* +** Comparison utils +*/ +export async function isDocumentMatchingContent(content: string, document: DatabaseItem): Promise { + const generatedDocument = await generateDocumentFromContent(document.id, content) as DatabaseItem + return areObjectsEqual(generatedDocument, document) +} + +export function areDocumentsEqual(document1: Record, document2: Record) { + function withoutLastStyles(body: MarkdownRoot) { + if (body.value[body.value.length - 1]?.[0] === 'style') { + return { ...body, value: body.value.slice(0, -1) } + } + return body + } + + const { body: body1, meta: meta1, ...documentData1 } = document1 + const { body: body2, meta: meta2, ...documentData2 } = document2 + + // Compare body first + if (document1.extension === ContentFileExtension.Markdown) { + const minifiedBody1 = withoutLastStyles( + (document1 as DatabasePageItem).body.type === 'minimark' ? document1.body as MarkdownRoot : compressTree(document1.body as unknown as MDCRoot), + ) + const minifiedBody2 = withoutLastStyles( + (document2 as DatabasePageItem).body.type === 'minimark' ? document2.body as MarkdownRoot : compressTree(document2.body as unknown as MDCRoot), + ) + + if (stringify(minifiedBody1) !== stringify(minifiedBody2)) { + return false + } + } + else if (typeof body1 === 'object' && typeof body2 === 'object') { + if (!areObjectsEqual(body1 as Record, body2 as Record)) { + return false + } + } + else { + // For other file types, we compare the JSON stringified bodies + if (JSON.stringify(body1) !== JSON.stringify(body2)) { + return false + } + } + + function refineDocumentData(doc: Record) { + if (doc.seo) { + const seo = doc.seo as Record + doc.seo = { + ...seo, + title: seo.title || doc.title, + description: seo.description || doc.description, + } + } + // documents with same id are being compared, so it is safe to remove `path` and `__hash__` + Reflect.deleteProperty(doc, '__hash__') + Reflect.deleteProperty(doc, 'path') + + // default value of navigation is true + if (typeof doc.navigation === 'undefined') { + doc.navigation = true + } + + return doc + } + + const data1 = refineDocumentData({ ...documentData1, ...(meta1 || {}) }) + const data2 = refineDocumentData({ ...documentData2, ...(meta2 || {}) }) + if (!areObjectsEqual(data1, data2)) { + return false + } + + return true +} + +/* +** Generation utils +*/ +export function populateDocumentbasedOnCollectionInfo(id: string, collectionInfo: CollectionInfo, document: CollectionItemBase) { const parsedContent = [ pathMetaTransform, ].reduce((acc, fn) => collectionInfo.type === 'page' ? fn(acc as PageCollectionItemBase) : acc, { ...document, id } as PageCollectionItemBase) @@ -45,18 +182,6 @@ export function createCollectionDocument(id: string, collectionInfo: CollectionI return result } -export async function isDocumentMatchContent(content: string, document: DatabaseItem): Promise { - const generatedDocument = await generateDocumentFromContent(document.id, content) as DatabaseItem - return isObjectMatch(generatedDocument, document) -} - -export function normalizeDocument(fsPath: string, document: DatabaseItem): DatabaseItem { - return { - ...document, - fsPath, - } -} - export async function generateDocumentFromContent(id: string, content: string): Promise { const [_id, _hash] = id.split('#') const extension = getFileExtension(id) @@ -93,7 +218,7 @@ export async function generateDocumentFromYAMLContent(id: string, content: strin meta: {}, ...parsed, body: parsed.body || parsed, - } as never as DatabaseItem + } as DatabaseItem } export async function generateDocumentFromJSONContent(id: string, content: string): Promise { @@ -115,7 +240,7 @@ export async function generateDocumentFromJSONContent(id: string, content: strin meta: {}, ...parsed, body: parsed.body || parsed, - } as never as DatabaseItem + } as DatabaseItem } export async function generateDocumentFromMarkdownContent(id: string, content: string): Promise { @@ -152,13 +277,73 @@ export async function generateDocumentFromMarkdownContent(id: string, content: s ...document.data, } - return pathMetaTransform(result as PageCollectionItemBase) as unknown as DatabaseItem + return pathMetaTransform(result as PageCollectionItemBase) as DatabaseItem +} + +export async function generateContentFromDocument(document: DatabaseItem): Promise { + const [id, _hash] = document.id.split('#') + const extension = getFileExtension(id!) + + if (extension === ContentFileExtension.Markdown) { + return await generateContentFromMarkdownDocument(document as DatabasePageItem) + } + + if (extension === ContentFileExtension.YAML || extension === ContentFileExtension.YML) { + return await generateContentFromYAMLDocument(document) + } + + if (extension === ContentFileExtension.JSON) { + return await generateContentFromJSONDocument(document) + } + + return null +} + +export async function generateContentFromYAMLDocument(document: DatabaseItem): Promise { + return await stringifyFrontMatter(removeReservedKeysFromDocument(document), '', { + prefix: '', + suffix: '', + lineWidth: 0, + }) +} + +export async function generateContentFromJSONDocument(document: DatabaseItem): Promise { + return JSON.stringify(removeReservedKeysFromDocument(document), null, 2) +} + +export async function generateContentFromMarkdownDocument(document: DatabasePageItem): Promise { + // @ts-expect-error todo fix MarkdownRoot/MDCRoot conversion in MDC module + const body = document.body.type === 'minimark' ? decompressTree(document.body) : (document.body as MDCRoot) + + // Remove nofollow from links + visit(body, (node: Node) => (node as MDCElement).type === 'element' && (node as MDCElement).tag === 'a', (node: Node) => { + if ((node as MDCElement).props?.rel?.join(' ') === 'nofollow') { + Reflect.deleteProperty((node as MDCElement).props!, 'rel') + } + }) + + const markdown = await stringifyMarkdown(body, removeReservedKeysFromDocument(document), { + frontMatter: { + options: { + lineWidth: 0, + }, + }, + plugins: { + remarkMDC: { + options: { + autoUnwrap: true, + }, + }, + }, + }) + + return typeof markdown === 'string' ? markdown.replace(/*/g, '*') : markdown } function generateStemFromId(id: string) { return id.split('/').slice(1).join('/').split('.').slice(0, -1).join('.') } -export function getFileExtension(fsPath: string) { - return fsPath.split('#')[0]?.split('.').pop()!.toLowerCase() +function getFileExtension(id: string) { + return id.split('#')[0]?.split('.').pop()!.toLowerCase() } diff --git a/src/module/src/runtime/utils/object.ts b/src/module/src/runtime/utils/object.ts index 4e8b0902..ee27f8b4 100644 --- a/src/module/src/runtime/utils/object.ts +++ b/src/module/src/runtime/utils/object.ts @@ -3,7 +3,12 @@ export const omit = (obj: Record, keys: string | string[]) => { .filter(([key]) => !keys.includes(key))) } -export function isObjectMatch(base: Record, target: Record) { +export const pick = (obj: Record, keys: string | string[]) => { + return Object.fromEntries(Object.entries(obj) + .filter(([key]) => keys.includes(key))) +} + +export function areObjectsEqual(base: Record, target: Record) { if (typeof base !== 'object' || typeof target !== 'object') { return base === target } @@ -14,7 +19,7 @@ export function isObjectMatch(base: Record, target: Record, target: Record, target[key] as Record)) { + if (!areObjectsEqual(base[key] as Record, target[key] as Record)) { return false } } diff --git a/src/module/test/utils/contente+document.test.ts b/src/module/test/utils/contente+document.test.ts deleted file mode 100644 index b53771be..00000000 --- a/src/module/test/utils/contente+document.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { isDocumentMatchContent } from '../../src/runtime/utils/document' - -describe('Content + Document', () => { - it('should be true', async () => { - const markdownContent = `Hello` - - const document = { - id: 'docs/1.getting-started/test.md', - title: 'Test', - body: { - type: 'minimark', - value: [ - [ - 'p', - {}, - 'Hello', - ], - ], - toc: { - title: '', - searchDepth: 2, - depth: 2, - links: [], - }, - }, - description: 'Hello', - extension: 'md', - layout: null, - links: null, - meta: {}, - navigation: true, - path: '/getting-started/test', - seo: { - title: 'Test', - description: 'Hello', - }, - stem: '1.getting-started/test', - __hash__: 'FORE7RbeCNfOhf3pzpQ6iehsHwCfTab-N64UgQZUOIg', - fsPath: '1.getting-started/test.md', - } - const _isDocumentMatchContent = await isDocumentMatchContent(markdownContent, document) - - expect(_isDocumentMatchContent).toBe(true) - }) - - it('should be true for fallback title and description', async () => { - const markdownContent = ` -# Hello - -Description` - - const document = { - id: 'docs/1.getting-started/test.md', - title: 'Hello', - body: { - type: 'minimark', - value: [ - [ - 'h1', - { - id: 'hello', - }, - 'Hello', - ], - [ - 'p', - {}, - 'Description', - ], - ], - toc: { - title: '', - searchDepth: 2, - depth: 2, - links: [], - }, - }, - description: 'Description', - extension: 'md', - layout: null, - links: null, - meta: {}, - navigation: true, - path: '/getting-started/test', - seo: { - title: 'Hello', - description: 'Description', - }, - stem: '1.getting-started/test', - __hash__: 'gaOnNORwr1k3a615yjZjMBMBc_KE4FlOETknzMqD884', - fsPath: '1.getting-started/test.md', - } - - const _isDocumentMatchContent = await isDocumentMatchContent(markdownContent, document) - expect(_isDocumentMatchContent).toBe(true) - }) - - it('should be true for Seo', async () => { - const markdownContent = `--- -seo: - title: 'Seo Hello' - description: 'Seo Description' ---- -# Hello - -Description` - - const document = { - id: 'docs/1.getting-started/test.md', - title: 'Hello', - body: { - type: 'minimark', - value: [ - [ - 'h1', - { - id: 'hello', - }, - 'Hello', - ], - [ - 'p', - {}, - 'Description', - ], - ], - toc: { - title: '', - searchDepth: 2, - depth: 2, - links: [], - }, - }, - description: 'Description', - extension: 'md', - layout: null, - links: null, - meta: {}, - navigation: true, - path: '/getting-started/test', - seo: { - title: 'Seo Hello', - description: 'Seo Description', - }, - stem: '1.getting-started/test', - __hash__: 'gaOnNORwr1k3a615yjZjMBMBc_KE4FlOETknzMqD884', - fsPath: '1.getting-started/test.md', - } - - const _isDocumentMatchContent = await isDocumentMatchContent(markdownContent, document) - expect(_isDocumentMatchContent).toBe(true) - }) -}) diff --git a/src/module/test/utils/document.test.ts b/src/module/test/utils/document.test.ts new file mode 100644 index 00000000..56dd66ad --- /dev/null +++ b/src/module/test/utils/document.test.ts @@ -0,0 +1,464 @@ +import { describe, it, expect } from 'vitest' +import { areDocumentsEqual, isDocumentMatchingContent } from '../../src/runtime/utils/document' +import { ContentFileExtension } from '../../src/types/content' +import type { DatabasePageItem } from 'nuxt-studio/app' + +describe('areDocumentsEqual', () => { + it('should return true for two identical markdown documents with diffrent hash', () => { + const document1: DatabasePageItem = { + id: 'content:index.md', + path: '/index', + title: 'Test Document', + description: 'A test document', + extension: ContentFileExtension.Markdown, + stem: 'index', + seo: {}, + body: { + type: 'minimark', + value: ['Hello World'], + }, + meta: { + __hash__: 'hash123', + }, + } + + const document2: DatabasePageItem = { + id: 'content:index.md', + path: '/index', + title: 'Test Document', + description: 'A test document', + extension: ContentFileExtension.Markdown, + stem: 'index', + seo: {}, + body: { + type: 'minimark', + value: ['Hello World'], + }, + meta: { + __hash__: 'hash456', + }, + } + + expect(areDocumentsEqual(document1, document2)).toBe(true) + }) + + it('should return false for two different markdown documents', () => { + const document1: DatabasePageItem = { + id: 'content:index.md', + path: '/index', + title: 'Test Document', + description: 'A test document', + extension: ContentFileExtension.Markdown, + stem: 'index', + seo: {}, + body: { + type: 'minimark', + value: ['Hello World'], + }, + meta: { + title: 'Test Document', + }, + } + + const document2: DatabasePageItem = { + id: 'content:index.md', + path: '/index', + title: 'Test Document', + description: 'A test document', + extension: ContentFileExtension.Markdown, + stem: 'index', + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + + expect(areDocumentsEqual(document1, document2)).toBe(false) + }) + + it('should return true for two identical yaml document with different order of keys', () => { + const document1: DatabasePageItem = { + extension: ContentFileExtension.YAML, + description: 'A test document', + title: 'Test Document', + path: '/index', + stem: 'index.yml', + id: 'content:index.yml', + tags: ['tag1', 'tag2'], + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + + const document2: DatabasePageItem = { + id: 'content:index.yml', + stem: 'index.yml', + path: '/index', + title: 'Test Document', + description: 'A test document', + extension: ContentFileExtension.YAML, + tags: ['tag1', 'tag2'], + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + + expect(areDocumentsEqual(document1, document2)).toBe(true) + }) + + it('should return true if one document has extra key with null/undefined value', () => { + const document1: DatabasePageItem = { + id: 'content:index.yml', + path: '/index', + title: 'Test Document', + description: 'A test document', + stem: 'index.yml', + extension: 'yml', + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + const document2: DatabasePageItem = { + id: 'content:index.yml', + path: '/index', + title: 'Test Document', + description: 'A test document', + extra: null, + stem: 'index.yml', + extension: 'yml', + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + expect(areDocumentsEqual(document1, document2)).toBe(true) + }) + + it('should ignore null/undefiend values', () => { + const document1: DatabasePageItem = { + id: 'content:index.yml', + path: '/index', + title: 'Test Document', + description: null as never, + stem: 'index.yml', + extension: 'yml', + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + + const document2: DatabasePageItem = { + id: 'content:index.yml', + path: '/index', + title: 'Test Document', + description: undefined as never, + stem: 'index.yml', + extension: 'yml', + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + + expect(areDocumentsEqual(document1, document2)).toBe(true) + }) + + it('should return false if one of documents missing a key', () => { + const document1: DatabasePageItem = { + id: 'content:index.yml', + path: '/index', + title: 'Test Document', + description: 'A test document', + stem: 'index.yml', + extension: 'yml', + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + const document2 = { + id: 'content:index.yml', + path: '/index', + title: 'Test Document', + extension: 'yml', + stem: 'index.yml', + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } as never as DatabasePageItem + + expect(areDocumentsEqual(document1, document2)).toBe(false) + }) + + it('should return false if array values are different', () => { + const document1: DatabasePageItem = { + id: 'content:index.yml', + path: '/index', + title: 'Test Document', + description: 'A test document', + tags: ['tag1', 'tag2'], + stem: 'index.yml', + extension: 'yml', + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + + const document2: DatabasePageItem = { + id: 'content:index.yml', + path: '/index', + tags: ['tag1', 'tag3'], + title: 'Test Document', + description: 'A test document', + stem: 'index.yml', + extension: 'yml', + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + + expect(areDocumentsEqual(document1, document2)).toBe(false) + }) + + it('should return true if date values are same but different format', () => { + const document1: DatabasePageItem = { + id: 'content:index.yml', + path: '/index', + title: 'Test Document', + date: '2025-11-04', + description: 'A test document', + stem: 'index.yml', + extension: 'yml', + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + const document2: DatabasePageItem = { + id: 'content:index.yml', + path: '/index', + title: 'Test Document', + date: '2025-11-04T00:00:00.000Z', + description: 'A test document', + stem: 'index.yml', + extension: 'yml', + seo: {}, + body: { + type: 'minimark', + value: ['Different content'], + }, + meta: { + title: 'Test Document', + }, + } + + expect(areDocumentsEqual(document1, document2)).toBe(true) + }) +}) + +describe('isDocumentMatchingContent', () => { + it('should be true', async () => { + const markdownContent = `Hello` + + const document = { + id: 'docs/1.getting-started/test.md', + title: 'Test', + body: { + type: 'minimark', + value: [ + [ + 'p', + {}, + 'Hello', + ], + ], + toc: { + title: '', + searchDepth: 2, + depth: 2, + links: [], + }, + }, + description: 'Hello', + extension: 'md', + layout: null, + links: null, + meta: {}, + navigation: true, + path: '/getting-started/test', + seo: { + title: 'Test', + description: 'Hello', + }, + stem: '1.getting-started/test', + __hash__: 'FORE7RbeCNfOhf3pzpQ6iehsHwCfTab-N64UgQZUOIg', + fsPath: '1.getting-started/test.md', + } + const _isDocumentMatchingContent = await isDocumentMatchingContent(markdownContent, document) + + expect(_isDocumentMatchingContent).toBe(true) + }) + + it('should be true for fallback title and description', async () => { + const markdownContent = ` +# Hello + +Description` + + const document = { + id: 'docs/1.getting-started/test.md', + title: 'Hello', + body: { + type: 'minimark', + value: [ + [ + 'h1', + { + id: 'hello', + }, + 'Hello', + ], + [ + 'p', + {}, + 'Description', + ], + ], + toc: { + title: '', + searchDepth: 2, + depth: 2, + links: [], + }, + }, + description: 'Description', + extension: 'md', + layout: null, + links: null, + meta: {}, + navigation: true, + path: '/getting-started/test', + seo: { + title: 'Hello', + description: 'Description', + }, + stem: '1.getting-started/test', + __hash__: 'gaOnNORwr1k3a615yjZjMBMBc_KE4FlOETknzMqD884', + fsPath: '1.getting-started/test.md', + } + + const _isDocumentMatchingContent = await isDocumentMatchingContent(markdownContent, document) + expect(_isDocumentMatchingContent).toBe(true) + }) + + it('should be true for Seo', async () => { + const markdownContent = `--- +seo: + title: 'Seo Hello' + description: 'Seo Description' +--- +# Hello + +Description` + + const document = { + id: 'docs/1.getting-started/test.md', + title: 'Hello', + body: { + type: 'minimark', + value: [ + [ + 'h1', + { + id: 'hello', + }, + 'Hello', + ], + [ + 'p', + {}, + 'Description', + ], + ], + toc: { + title: '', + searchDepth: 2, + depth: 2, + links: [], + }, + }, + description: 'Description', + extension: 'md', + layout: null, + links: null, + meta: {}, + navigation: true, + path: '/getting-started/test', + seo: { + title: 'Seo Hello', + description: 'Seo Description', + }, + stem: '1.getting-started/test', + __hash__: 'gaOnNORwr1k3a615yjZjMBMBc_KE4FlOETknzMqD884', + fsPath: '1.getting-started/test.md', + } + + const _isDocumentMatchingContent = await isDocumentMatchingContent(markdownContent, document) + expect(_isDocumentMatchingContent).toBe(true) + }) +}) From f53c6bea4d172e5acf215fa7d8fb51732027893f Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Wed, 12 Nov 2025 18:22:34 +0100 Subject: [PATCH 11/20] fix tests --- src/app/test/integration/actions.test.ts | 10 +- src/app/test/mocks/host.ts | 116 ++++++++++++++++------- src/app/test/unit/utils/draft.test.ts | 13 ++- src/app/test/unit/utils/tree.test.ts | 78 +++++++-------- src/module/src/runtime/utils/document.ts | 41 +++++++- src/module/test/utils/document.test.ts | 6 +- 6 files changed, 177 insertions(+), 87 deletions(-) diff --git a/src/app/test/integration/actions.test.ts b/src/app/test/integration/actions.test.ts index 4d10aa4c..9cd5942f 100644 --- a/src/app/test/integration/actions.test.ts +++ b/src/app/test/integration/actions.test.ts @@ -242,7 +242,7 @@ describe('Document - Action Chains Integration Tests', () => { const consoleInfoSpy = vi.spyOn(console, 'info') // Create document in db and load tree - await mockHost.document.create(documentFsPath, 'Test content') + await mockHost.document.db.create(documentFsPath, 'Test content') await context.activeTree.value.draft.load() /* STEP 1: SELECT */ @@ -345,7 +345,7 @@ describe('Document - Action Chains Integration Tests', () => { const consoleInfoSpy = vi.spyOn(console, 'info') // Create document in db and load tree - await mockHost.document.create(documentFsPath, 'Test content') + await mockHost.document.db.create(documentFsPath, 'Test content') await context.activeTree.value.draft.load() /* STEP 1: SELECT */ @@ -428,7 +428,7 @@ describe('Document - Action Chains Integration Tests', () => { const consoleInfoSpy = vi.spyOn(console, 'info') // Create document in db and load tree - await mockHost.document.create(documentFsPath, 'Test content') + await mockHost.document.db.create(documentFsPath, 'Test content') await context.activeTree.value.draft.load() /* STEP 1: SELECT */ @@ -556,7 +556,7 @@ describe('Document - Action Chains Integration Tests', () => { const consoleInfoSpy = vi.spyOn(console, 'info') // Create document in db and load tree - await mockHost.document.create(documentFsPath, 'Test content') + await mockHost.document.db.create(documentFsPath, 'Test content') await context.activeTree.value.draft.load() /* STEP 1: SELECT */ @@ -616,7 +616,7 @@ describe('Document - Action Chains Integration Tests', () => { const consoleInfoSpy = vi.spyOn(console, 'info') // Create document in db and load tree - await mockHost.document.create(documentFsPath, 'Test content') + await mockHost.document.db.create(documentFsPath, 'Test content') await context.activeTree.value.draft.load() /* STEP 1: SELECT */ diff --git a/src/app/test/mocks/host.ts b/src/app/test/mocks/host.ts index 4e34a04d..f6950ea1 100644 --- a/src/app/test/mocks/host.ts +++ b/src/app/test/mocks/host.ts @@ -5,7 +5,7 @@ import { createMockDocument } from './document' import { createMockMedia } from './media' import { joinURL } from 'ufo' import type { MediaItem } from '../../src/types/media' -import { isDocumentMatchingContent } from '../../../module/dist/runtime/utils/document' +import { isDocumentMatchingContent, areDocumentsEqual, generateDocumentFromContent, generateContentFromDocument, pickReservedKeysFromDocument, removeReservedKeysFromDocument } from '../../../module/dist/runtime/utils/document' // Helper to convert fsPath to id (simulates module's internal mapping) export const fsPathToId = (fsPath: string, type: 'document' | 'media') => { @@ -26,38 +26,57 @@ const mediaDb = new Map() export const createMockHost = (): StudioHost => ({ document: { - get: vi.fn().mockImplementation(async (fsPath: string) => { - const id = fsPathToId(fsPath, 'document') - if (documentDb.has(id)) { - return documentDb.get(id) - } - const document = createMockDocument(id) - documentDb.set(id, document) - return document - }), - create: vi.fn().mockImplementation(async (fsPath: string, content: string) => { - const id = fsPathToId(fsPath, 'document') - const document = createMockDocument(id, { body: { type: 'minimark', value: [content?.trim() || 'Test content'] }, fsPath }) - documentDb.set(id, document) - return document - }), - upsert: vi.fn().mockImplementation(async (fsPath: string, document: DatabaseItem) => { - const id = fsPathToId(fsPath, 'document') - documentDb.set(id, document) - }), - delete: vi.fn().mockImplementation(async (fsPath: string) => { - const id = fsPathToId(fsPath, 'document') - documentDb.delete(id) - }), - list: vi.fn().mockImplementation(async () => { - return Array.from(documentDb.values()) - }), - isEqual: vi.fn().mockImplementation(async (content: string, document: DatabaseItem) => { - return isDocumentMatchingContent(content, document) - }), - generateFromContent: vi.fn().mockImplementation(async (_id: string, _content: string) => { - throw new Error('Not implemented') - }), + db: { + get: vi.fn().mockImplementation(async (fsPath: string) => { + const id = fsPathToId(fsPath, 'document') + if (documentDb.has(id)) { + return documentDb.get(id) + } + const document = createMockDocument(id) + documentDb.set(id, document) + return document + }), + create: vi.fn().mockImplementation(async (fsPath: string, content: string) => { + const id = fsPathToId(fsPath, 'document') + const document = createMockDocument(id, { body: { type: 'minimark', value: [content?.trim() || 'Test content'] }, fsPath }) + documentDb.set(id, document) + return document + }), + upsert: vi.fn().mockImplementation(async (fsPath: string, document: DatabaseItem) => { + const id = fsPathToId(fsPath, 'document') + documentDb.set(id, document) + }), + delete: vi.fn().mockImplementation(async (fsPath: string) => { + const id = fsPathToId(fsPath, 'document') + documentDb.delete(id) + }), + list: vi.fn().mockImplementation(async () => { + return Array.from(documentDb.values()) + }), + }, + utils: { + areEqual: vi.fn().mockImplementation((document1: DatabaseItem, document2: DatabaseItem) => { + return areDocumentsEqual(document1, document2) + }), + isMatchingContent: vi.fn().mockImplementation(async (content: string, document: DatabaseItem) => { + return isDocumentMatchingContent(content, document) + }), + pickReservedKeys: vi.fn().mockImplementation((document: DatabaseItem) => { + return pickReservedKeysFromDocument(document) as DatabaseItem + }), + removeReservedKeys: vi.fn().mockImplementation((document: DatabaseItem) => { + return removeReservedKeysFromDocument(document) as DatabaseItem + }), + detectActives: vi.fn().mockReturnValue([]), + }, + generate: { + documentFromContent: vi.fn().mockImplementation(async (id: string, content: string) => { + return generateDocumentFromContent(id, content) + }), + contentFromDocument: vi.fn().mockImplementation(async (document: DatabaseItem) => { + return generateContentFromDocument(document) + }), + }, }, media: { get: vi.fn().mockImplementation(async (fsPath: string) => { @@ -90,6 +109,37 @@ export const createMockHost = (): StudioHost => ({ app: { requestRerender: vi.fn(), navigateTo: vi.fn(), + getManifestId: vi.fn().mockResolvedValue('test-manifest-id'), + }, + meta: { + dev: false, + components: vi.fn().mockReturnValue([]), + }, + on: { + routeChange: vi.fn(), + mounted: vi.fn(), + beforeUnload: vi.fn(), + colorModeChange: vi.fn(), + manifestUpdate: vi.fn(), + documentUpdate: vi.fn(), + mediaUpdate: vi.fn(), + }, + ui: { + colorMode: 'light', + activateStudio: vi.fn(), + deactivateStudio: vi.fn(), + expandSidebar: vi.fn(), + collapseSidebar: vi.fn(), + updateStyles: vi.fn(), + }, + user: { + get: vi.fn().mockReturnValue({ name: 'Test User', email: 'test@example.com' }), + }, + repository: { + provider: 'github', + owner: 'test-owner', + name: 'test-repo', + branch: 'main', }, } as never) diff --git a/src/app/test/unit/utils/draft.test.ts b/src/app/test/unit/utils/draft.test.ts index 8975ffd8..786a9c87 100644 --- a/src/app/test/unit/utils/draft.test.ts +++ b/src/app/test/unit/utils/draft.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest' import { findDescendantsFromFsPath, getDraftStatus } from '../../../src/utils/draft' import { draftItemsList } from '../../../test/mocks/draft' import { dbItemsList } from '../../../test/mocks/database' +import type { DatabaseItem } from '../../../src/types' import { DraftStatus } from '../../../src/types' describe('findDescendantsFromFsPath', () => { @@ -52,23 +53,25 @@ describe('findDescendantsFromFsPath', () => { }) describe('getDraftStatus', () => { + const areDocumentsEqual = (document1: DatabaseItem, document2: DatabaseItem) => JSON.stringify(document1) === JSON.stringify(document2) + it('returns Deleted status when modified item is undefined', () => { const original = dbItemsList[0] // landing/index.md - expect(getDraftStatus(undefined, original)).toBe(DraftStatus.Deleted) + expect(getDraftStatus(undefined as never, original, areDocumentsEqual)).toBe(DraftStatus.Deleted) }) it('returns Created status when original is undefined', () => { const modified = dbItemsList[1] // docs/1.getting-started/2.introduction.md - expect(getDraftStatus(modified, undefined)).toBe(DraftStatus.Created) + expect(getDraftStatus(modified, undefined as never, areDocumentsEqual)).toBe(DraftStatus.Created) }) it('returns Created status when original has different id', () => { const original = dbItemsList[0] // landing/index.md const modified = dbItemsList[1] // docs/1.getting-started/2.introduction.md - expect(getDraftStatus(modified, original)).toBe(DraftStatus.Created) + expect(getDraftStatus(modified, original, areDocumentsEqual)).toBe(DraftStatus.Created) }) it('returns Updated status when markdown content is different', () => { @@ -81,12 +84,12 @@ describe('getDraftStatus', () => { }, } - expect(getDraftStatus(modified, original)).toBe(DraftStatus.Updated) + expect(getDraftStatus(modified, original, () => false)).toBe(DraftStatus.Updated) }) it('returns Pristine status when markdown content is identical', () => { const original = dbItemsList[1] // docs/1.getting-started/2.introduction.md - expect(getDraftStatus(original, original)).toBe(DraftStatus.Pristine) + expect(getDraftStatus(original, original, areDocumentsEqual)).toBe(DraftStatus.Pristine) }) }) diff --git a/src/app/test/unit/utils/tree.test.ts b/src/app/test/unit/utils/tree.test.ts index dcd313f3..9fb28773 100644 --- a/src/app/test/unit/utils/tree.test.ts +++ b/src/app/test/unit/utils/tree.test.ts @@ -11,6 +11,8 @@ import type { DatabaseItem } from '../../../src/types/database' import { joinURL, withLeadingSlash } from 'ufo' import { VirtualMediaCollectionName } from '../../../src/utils/media' +const areDocumentsEqual = (document1: DatabaseItem, document2: DatabaseItem) => JSON.stringify(document1) === JSON.stringify(document2) + describe('buildTree of documents with one level of depth', () => { // Result based on dbItemsList mock const result: TreeItem[] = [ @@ -46,7 +48,7 @@ describe('buildTree of documents with one level of depth', () => { ] it('Without draft', () => { - const tree = buildTree(dbItemsList, null) + const tree = buildTree(dbItemsList, null, areDocumentsEqual) expect(tree).toStrictEqual(result as TreeItem[]) }) @@ -54,13 +56,13 @@ describe('buildTree of documents with one level of depth', () => { const createdDbItem: DatabaseItem = dbItemsList[0] const draftList: DraftItem[] = [{ - fsPath: createdDbItem.fsPath, + fsPath: createdDbItem.fsPath!, status: DraftStatus.Created, original: undefined, modified: createdDbItem, }] - const tree = buildTree(dbItemsList, draftList) + const tree = buildTree(dbItemsList, draftList, areDocumentsEqual) expect(tree).toStrictEqual([ { @@ -74,7 +76,7 @@ describe('buildTree of documents with one level of depth', () => { const deletedDbItem: DatabaseItem = dbItemsList[1] // 2.introduction.md const draftList: DraftItem[] = [{ - fsPath: deletedDbItem.fsPath, + fsPath: deletedDbItem.fsPath!, status: DraftStatus.Deleted, modified: undefined, original: deletedDbItem, @@ -82,7 +84,7 @@ describe('buildTree of documents with one level of depth', () => { const dbItemsListWithoutDeletedDbItem = dbItemsList.filter(item => item.id !== deletedDbItem.id) - const tree = buildTree(dbItemsListWithoutDeletedDbItem, draftList) + const tree = buildTree(dbItemsListWithoutDeletedDbItem, draftList, areDocumentsEqual) expect(tree).toStrictEqual([ { ...result[0] }, @@ -93,7 +95,7 @@ describe('buildTree of documents with one level of depth', () => { result[1].children![1], { name: 'introduction', - fsPath: deletedDbItem.fsPath, + fsPath: deletedDbItem.fsPath!, type: 'file', routePath: deletedDbItem.path, status: TreeStatus.Deleted, @@ -108,7 +110,7 @@ describe('buildTree of documents with one level of depth', () => { const deletedDbItem: DatabaseItem = dbItemsList[2] // 3.installation.md const draftList: DraftItem[] = [{ - fsPath: deletedDbItem.fsPath, + fsPath: deletedDbItem.fsPath!, status: DraftStatus.Deleted, modified: undefined, original: deletedDbItem, @@ -116,7 +118,7 @@ describe('buildTree of documents with one level of depth', () => { const dbItemsListWithoutDeletedDbItem = dbItemsList.filter(item => item.id !== deletedDbItem.id) - const tree = buildTree(dbItemsListWithoutDeletedDbItem, draftList) + const tree = buildTree(dbItemsListWithoutDeletedDbItem, draftList, areDocumentsEqual) expect(tree).toStrictEqual([ result[0], @@ -127,7 +129,7 @@ describe('buildTree of documents with one level of depth', () => { result[1].children![0], { name: 'installation', - fsPath: deletedDbItem.fsPath, + fsPath: deletedDbItem.fsPath!, type: 'file', routePath: deletedDbItem.path, status: TreeStatus.Deleted, @@ -142,7 +144,7 @@ describe('buildTree of documents with one level of depth', () => { const updatedDbItem: DatabaseItem = dbItemsList[1] // 2.introduction.md const draftList: DraftItem[] = [{ - fsPath: updatedDbItem.fsPath, + fsPath: updatedDbItem.fsPath!, status: DraftStatus.Updated, original: updatedDbItem, modified: { @@ -154,7 +156,7 @@ describe('buildTree of documents with one level of depth', () => { }, }] - const tree = buildTree(dbItemsList, draftList) + const tree = buildTree(dbItemsList, draftList, areDocumentsEqual) const expectedTree = [ result[0], @@ -179,18 +181,18 @@ describe('buildTree of documents with one level of depth', () => { const openedDbItem: DatabaseItem = dbItemsList[2] // 3.installation.md const draftList: DraftItem[] = [{ - fsPath: createdDbItem.fsPath, + fsPath: createdDbItem.fsPath!, status: DraftStatus.Created, original: undefined, modified: createdDbItem, }, { - fsPath: openedDbItem.fsPath, + fsPath: openedDbItem.fsPath!, status: DraftStatus.Pristine, original: openedDbItem, modified: openedDbItem, }] - const tree = buildTree(dbItemsList, draftList) + const tree = buildTree(dbItemsList, draftList, areDocumentsEqual) const expectedTree = [ result[0], @@ -212,18 +214,18 @@ describe('buildTree of documents with one level of depth', () => { const openedDbItem2: DatabaseItem = dbItemsList[2] // 3.installation.md const draftList: DraftItem[] = [{ - fsPath: openedDbItem1.fsPath, + fsPath: openedDbItem1.fsPath!, status: DraftStatus.Pristine, original: openedDbItem1, modified: openedDbItem1, }, { - fsPath: openedDbItem2.fsPath, + fsPath: openedDbItem2.fsPath!, status: DraftStatus.Pristine, original: openedDbItem2, modified: openedDbItem2, }] - const tree = buildTree(dbItemsList, draftList) + const tree = buildTree(dbItemsList, draftList, areDocumentsEqual) const expectedTree = [ result[0], @@ -252,12 +254,12 @@ describe('buildTree of documents with one level of depth', () => { } const draftList: DraftItem[] = [{ - fsPath: deletedDbItem.fsPath, + fsPath: deletedDbItem.fsPath!, status: DraftStatus.Deleted, modified: undefined, original: deletedDbItem, }, { - fsPath: createdDbItem.fsPath, + fsPath: createdDbItem.fsPath!, status: DraftStatus.Created, modified: createdDbItem, original: deletedDbItem, @@ -267,7 +269,7 @@ describe('buildTree of documents with one level of depth', () => { const dbItemsWithoutDeletedWithCreated = dbItemsList.filter(item => item.id !== deletedDbItem.id) dbItemsWithoutDeletedWithCreated.push(createdDbItem) - const tree = buildTree(dbItemsWithoutDeletedWithCreated, draftList) + const tree = buildTree(dbItemsWithoutDeletedWithCreated, draftList, areDocumentsEqual) expect(tree).toStrictEqual([ result[0], @@ -277,7 +279,7 @@ describe('buildTree of documents with one level of depth', () => { children: [ ...result[1].children!.slice(1), { - fsPath: createdDbItem.fsPath, + fsPath: createdDbItem.fsPath!, routePath: createdDbItem.path, name: createdDbItem.path!.split('/').pop()!, type: 'file', @@ -325,7 +327,7 @@ describe('buildTree of documents with two levels of depth', () => { ] it('Without draft', () => { - const tree = buildTree(nestedDbItemsList, null) + const tree = buildTree(nestedDbItemsList, null, areDocumentsEqual) expect(tree).toStrictEqual(result) }) @@ -333,7 +335,7 @@ describe('buildTree of documents with two levels of depth', () => { const updatedDbItem: DatabaseItem = nestedDbItemsList[0] // 1.essentials/2.configuration.md const draftList: DraftItem[] = [{ - fsPath: updatedDbItem.fsPath, + fsPath: updatedDbItem.fsPath!, status: DraftStatus.Updated, original: updatedDbItem, modified: { @@ -345,7 +347,7 @@ describe('buildTree of documents with two levels of depth', () => { }, }] - const tree = buildTree(nestedDbItemsList, draftList) + const tree = buildTree(nestedDbItemsList, draftList, areDocumentsEqual) expect(tree).toStrictEqual([{ ...result[0], @@ -361,7 +363,7 @@ describe('buildTree of documents with two levels of depth', () => { const updatedDbItem: DatabaseItem = nestedDbItemsList[1] // 1.essentials/1.nested/2.advanced.md const draftList: DraftItem[] = [{ - fsPath: updatedDbItem.fsPath, + fsPath: updatedDbItem.fsPath!, status: DraftStatus.Updated, original: updatedDbItem, modified: { @@ -373,7 +375,7 @@ describe('buildTree of documents with two levels of depth', () => { }, }] - const tree = buildTree(nestedDbItemsList, draftList) + const tree = buildTree(nestedDbItemsList, draftList, areDocumentsEqual) expect(tree).toStrictEqual([{ ...result[0], @@ -398,7 +400,7 @@ describe('buildTree of documents with two levels of depth', () => { const deletedDbItem: DatabaseItem = nestedDbItemsList[1] // 1.essentials/1.nested/2.advanced.md const draftList: DraftItem[] = [{ - fsPath: deletedDbItem.fsPath, + fsPath: deletedDbItem.fsPath!, status: DraftStatus.Deleted, modified: undefined, original: deletedDbItem, @@ -407,7 +409,7 @@ describe('buildTree of documents with two levels of depth', () => { // Remove the deleted item from the nestedDbItemsList const nestedDbItemsListWithoutDeletedDbItem = nestedDbItemsList.filter(item => item.id !== deletedDbItem.id) - const tree = buildTree(nestedDbItemsListWithoutDeletedDbItem, draftList) + const tree = buildTree(nestedDbItemsListWithoutDeletedDbItem, draftList, areDocumentsEqual) expect(tree).toStrictEqual([{ ...result[0], @@ -420,7 +422,7 @@ describe('buildTree of documents with two levels of depth', () => { children: [ { name: 'advanced', - fsPath: deletedDbItem.fsPath, + fsPath: deletedDbItem.fsPath!, routePath: deletedDbItem.path, type: 'file', status: TreeStatus.Deleted, @@ -475,7 +477,7 @@ describe('buildTree of documents with language prefixed', () => { ] it('Without draft', () => { - const tree = buildTree(languagePrefixedDbItemsList, null) + const tree = buildTree(languagePrefixedDbItemsList, null, areDocumentsEqual) expect(tree).toStrictEqual(result) }) }) @@ -505,13 +507,13 @@ describe('buildTree of medias', () => { } const draftList: DraftItem[] = [{ - fsPath: gitkeepDbItem.fsPath, + fsPath: gitkeepDbItem.fsPath!, status: DraftStatus.Created, original: undefined, modified: gitkeepDbItem, }] - const tree = buildTree([gitkeepDbItem, mediaDbItem], draftList) + const tree = buildTree([gitkeepDbItem, mediaDbItem], draftList, areDocumentsEqual) expect(tree).toHaveLength(1) expect(tree[0]).toHaveProperty('fsPath', mediaFolderName) @@ -530,14 +532,14 @@ describe('getTreeStatus', () => { it('draft is CREATED if originalDatabaseItem is not defined', () => { const modified: DatabaseItem = dbItemsList[0] // index.md - const status = getTreeStatus(modified, undefined) + const status = getTreeStatus(modified, undefined as never, areDocumentsEqual) expect(status).toBe(TreeStatus.Created) }) it('draft is OPENED if originalDatabaseItem is defined and is the same as draftedDocument', () => { const original: DatabaseItem = dbItemsList[0] // index.md - const status = getTreeStatus(original, original) + const status = getTreeStatus(original, original, areDocumentsEqual) expect(status).toBe(TreeStatus.Opened) }) @@ -548,7 +550,7 @@ describe('getTreeStatus', () => { title: 'New title', } - const status = getTreeStatus(modified, original) + const status = getTreeStatus(modified, original, areDocumentsEqual) expect(status).toBe(TreeStatus.Updated) }) @@ -559,7 +561,7 @@ describe('getTreeStatus', () => { body: { type: 'minimark', value: ['New body'] }, } - const status = getTreeStatus(modified, original) + const status = getTreeStatus(modified, original, areDocumentsEqual) expect(status).toBe(TreeStatus.Updated) }) @@ -570,7 +572,7 @@ describe('getTreeStatus', () => { id: 'renamed.md', } - const status = getTreeStatus(modified, original) + const status = getTreeStatus(modified, original, areDocumentsEqual) expect(status).toBe(TreeStatus.Renamed) }) @@ -578,7 +580,7 @@ describe('getTreeStatus', () => { const original: DatabaseItem = dbItemsList[0] // index.md const modified: DatabaseItem = undefined as never - const status = getTreeStatus(modified, original) + const status = getTreeStatus(modified, original, areDocumentsEqual) expect(status).toBe(TreeStatus.Deleted) }) }) diff --git a/src/module/src/runtime/utils/document.ts b/src/module/src/runtime/utils/document.ts index fbb3b0e1..4352d1d9 100644 --- a/src/module/src/runtime/utils/document.ts +++ b/src/module/src/runtime/utils/document.ts @@ -27,11 +27,11 @@ export function normalizeDocument(fsPath: string, document: DatabaseItem): Datab } } -export function pickReservedKeysFromDocument(document: DatabaseItem) { +export function pickReservedKeysFromDocument(document: DatabaseItem): DatabaseItem { return pick(document, reservedKeys) as DatabaseItem } -export function removeReservedKeysFromDocument(document: DatabaseItem) { +export function removeReservedKeysFromDocument(document: DatabaseItem): DatabaseItem { const result = omit(document, reservedKeys) // Default value of navigation is true, so we can safely remove it if (result.navigation === true) { @@ -133,7 +133,42 @@ export function areDocumentsEqual(document1: Record, document2: doc.navigation = true } - return doc + // Normalize date values to ISO string format for comparison + for (const key in doc) { + const value = doc[key] + if (typeof value === 'string' && !Number.isNaN(Date.parse(value))) { + // Check if it looks like a date string (YYYY-MM-DD or ISO format) + if (/^\d{4}-\d{2}-\d{2}/.test(value)) { + doc[key] = new Date(value).toISOString().split('T')[0] + } + } + } + + // Remove null and undefined values recursively + function removeNullAndUndefined(obj: Record): Record { + const result: Record = {} + + for (const key in obj) { + const value = obj[key] + + // Skip null and undefined values + if (value === null || value === undefined) { + continue + } + + // Recursively clean nested objects (but not arrays) + if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) { + result[key] = removeNullAndUndefined(value as Record) + } + else { + result[key] = value + } + } + + return result + } + + return removeNullAndUndefined(doc) } const data1 = refineDocumentData({ ...documentData1, ...(meta1 || {}) }) diff --git a/src/module/test/utils/document.test.ts b/src/module/test/utils/document.test.ts index 56dd66ad..61a1e571 100644 --- a/src/module/test/utils/document.test.ts +++ b/src/module/test/utils/document.test.ts @@ -157,7 +157,7 @@ describe('areDocumentsEqual', () => { expect(areDocumentsEqual(document1, document2)).toBe(true) }) - it('should ignore null/undefiend values', () => { + it('should ignore null/undefined values', () => { const document1: DatabasePageItem = { id: 'content:index.yml', path: '/index', @@ -168,7 +168,7 @@ describe('areDocumentsEqual', () => { seo: {}, body: { type: 'minimark', - value: ['Different content'], + value: ['Same content'], }, meta: { title: 'Test Document', @@ -185,7 +185,7 @@ describe('areDocumentsEqual', () => { seo: {}, body: { type: 'minimark', - value: ['Different content'], + value: ['Same content'], }, meta: { title: 'Test Document', From b69f2bae69fb193dfad32dec2e5f1fd74ca2977f Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Thu, 13 Nov 2025 11:03:15 +0100 Subject: [PATCH 12/20] feat: show diff when auto parsing applies --- .../components/content/ContentEditorCode.vue | 37 +++++++++++++++++- .../components/shared/AlertMDCFormatting.vue | 38 ++++++++++++++++++- src/app/src/utils/draft.ts | 3 +- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/app/src/components/content/ContentEditorCode.vue b/src/app/src/components/content/ContentEditorCode.vue index 0218b5b0..51b67b62 100644 --- a/src/app/src/components/content/ContentEditorCode.vue +++ b/src/app/src/components/content/ContentEditorCode.vue @@ -5,6 +5,7 @@ import type { PropType } from 'vue' import { setupSuggestion } from '../../utils/monaco' import { useStudio } from '../../composables/useStudio' import { useMonaco } from '../../composables/useMonaco' +import { useMonacoDiff } from '../../composables/useMonacoDiff' import { fromBase64ToUTF8 } from '../../utils/string' import { areContentEqual } from '../../utils/content' @@ -24,10 +25,13 @@ const document = defineModel() const { mediaTree, host, ui } = useStudio() const editorRef = ref() +const diffEditorRef = ref() + const content = ref('') const currentDocumentId = ref(null) const localStatus = ref(props.draftItem.status) const isAutomaticFormattingDetected = ref(false) +const showAutomaticFormattingDiff = ref(false) const language = computed(() => { switch (document.value?.extension) { @@ -102,6 +106,19 @@ watch(() => props.readOnly, (newReadOnly) => { } }) +const originalContent = ref('') +const formattedContent = ref('') +watch(showAutomaticFormattingDiff, async (show) => { + if (show && diffEditorRef.value) { + useMonacoDiff(diffEditorRef, { + original: originalContent.value, + modified: formattedContent.value, + language: language.value, + colorMode: ui.colorMode, + }) + } +}) + // Trigger on document changes watch(() => document.value?.id + '-' + props.draftItem.version, async () => { if (document.value?.body) { @@ -118,23 +135,41 @@ async function setContent(document: DatabasePageItem) { isAutomaticFormattingDetected.value = false if (props.draftItem.original && props.draftItem.githubFile?.content) { - const localOriginal = await contentFromDocument(props.draftItem.original as DatabaseItem) + const localOriginal = await contentFromDocument(props.draftItem.original as DatabaseItem) as string const gitHubOriginal = fromBase64ToUTF8(props.draftItem.githubFile.content) isAutomaticFormattingDetected.value = !areContentEqual(localOriginal, gitHubOriginal) + if (isAutomaticFormattingDetected.value) { + originalContent.value = gitHubOriginal + formattedContent.value = localOriginal + } } } + +function toggleDiffView() { + showAutomaticFormattingDiff.value = !showAutomaticFormattingDiff.value +} diff --git a/src/app/src/components/shared/AlertMDCFormatting.vue b/src/app/src/components/shared/AlertMDCFormatting.vue index 3a82f44a..249304b2 100644 --- a/src/app/src/components/shared/AlertMDCFormatting.vue +++ b/src/app/src/components/shared/AlertMDCFormatting.vue @@ -1,8 +1,44 @@ + + diff --git a/src/app/src/utils/draft.ts b/src/app/src/utils/draft.ts index e3313e1a..f0f8f0cb 100644 --- a/src/app/src/utils/draft.ts +++ b/src/app/src/utils/draft.ts @@ -6,6 +6,7 @@ import { isMediaFile } from './file' export async function checkConflict(host: StudioHost, draftItem: DraftItem): Promise { const generateContentFromDocument = host.document.generate.contentFromDocument + const isDocumentMatchingContent = host.document.utils.isMatchingContent if (isMediaFile(draftItem.fsPath) || draftItem.fsPath.endsWith('.gitkeep')) { return @@ -29,7 +30,7 @@ export async function checkConflict(host: StudioHost, draftItem: DraftItem Date: Thu, 13 Nov 2025 11:28:55 +0100 Subject: [PATCH 13/20] refactor getDraftStatus --- src/app/src/composables/useDraftBase.ts | 48 +++++++++++++++++-- src/app/src/composables/useDraftDocuments.ts | 5 +- src/app/src/composables/useDraftMedias.ts | 2 + src/app/src/composables/useStudio.ts | 18 ++++--- src/app/src/utils/draft.ts | 41 +--------------- .../test/unit/compsables/draft-base.test.ts | 47 ++++++++++++++++++ src/app/test/unit/utils/draft.test.ts | 47 +----------------- 7 files changed, 107 insertions(+), 101 deletions(-) create mode 100644 src/app/test/unit/compsables/draft-base.test.ts diff --git a/src/app/src/composables/useDraftBase.ts b/src/app/src/composables/useDraftBase.ts index 74ac0f1f..0d35cec3 100644 --- a/src/app/src/composables/useDraftBase.ts +++ b/src/app/src/composables/useDraftBase.ts @@ -1,8 +1,10 @@ import type { Storage } from 'unstorage' import { joinURL } from 'ufo' -import type { DraftItem, StudioHost, GithubFile, DatabaseItem, MediaItem } from '../types' +import type { DraftItem, StudioHost, GithubFile, DatabaseItem, MediaItem, BaseItem } from '../types' +import { ContentFileExtension } from '../types' +import { studioFlags } from './useStudio' import { DraftStatus } from '../types/draft' -import { checkConflict, findDescendantsFromFsPath, getDraftStatus } from '../utils/draft' +import { checkConflict, findDescendantsFromFsPath } from '../utils/draft' import type { useGit } from './useGit' import { useHooks } from './useHooks' import { ref } from 'vue' @@ -20,6 +22,7 @@ export function useDraftBase( const ghPathPrefix = type === 'media' ? 'public' : 'content' const hostDb = type === 'media' ? host.media : host.document.db const hookName = `studio:draft:${type}:updated` as const + const areDocumentsEqual = host.document.utils.areEqual const hooks = useHooks() @@ -38,7 +41,7 @@ export function useDraftBase( const draftItem: DraftItem = { fsPath, githubFile, - status: getDraftStatus(item, original!, host.document.utils.areEqual), + status: getStatus(item, original!), modified: item, } @@ -135,7 +138,7 @@ export function useDraftBase( // @ts-expect-error upsert type is wrong, second param should be DatabaseItem | MediaItem await hostDb.upsert(draftItem.fsPath, existingItem.original) existingItem.modified = existingItem.original - existingItem.status = getDraftStatus(existingItem.modified as DatabaseItem, existingItem.original as DatabaseItem, host.document.utils.areEqual) + existingItem.status = getStatus(existingItem.modified as DatabaseItem, existingItem.original as DatabaseItem) await storage.setItem(draftItem.fsPath, existingItem) } } @@ -211,6 +214,42 @@ export function useDraftBase( await hooks.callHook(hookName, { caller: 'useDraftBase.load', selectItem: false }) } + function getStatus(modified: BaseItem, original: BaseItem): DraftStatus { + if (studioFlags.dev) { + return DraftStatus.Pristine + } + + if (!modified && !original) { + throw new Error('Unconsistent state: both modified and original are undefined') + } + + if (!modified) { + return DraftStatus.Deleted + } + + if (!original || original.id !== modified.id) { + return DraftStatus.Created + } + + if (original.extension === ContentFileExtension.Markdown) { + if (!areDocumentsEqual(original as DatabaseItem, modified as DatabaseItem)) { + return DraftStatus.Updated + } + } + else if (typeof original === 'object' && typeof modified === 'object') { + if (!areDocumentsEqual(original as DatabaseItem, modified as DatabaseItem)) { + return DraftStatus.Updated + } + } + else { + if (JSON.stringify(original) !== JSON.stringify(modified)) { + return DraftStatus.Updated + } + } + + return DraftStatus.Pristine + } + return { isLoading, list, @@ -224,5 +263,6 @@ export function useDraftBase( unselect, load, checkConflict, + getStatus, } } diff --git a/src/app/src/composables/useDraftDocuments.ts b/src/app/src/composables/useDraftDocuments.ts index 8080f666..8794dfa1 100644 --- a/src/app/src/composables/useDraftDocuments.ts +++ b/src/app/src/composables/useDraftDocuments.ts @@ -1,7 +1,6 @@ import type { DatabaseItem, DraftItem, StudioHost, RawFile } from '../types' import { DraftStatus } from '../types/draft' import type { useGit } from './useGit' -import { getDraftStatus } from '../utils/draft' import { createSharedComposable } from '@vueuse/core' import { useHooks } from './useHooks' import { joinURL } from 'ufo' @@ -22,6 +21,7 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: selectByFsPath, unselect, load, + getStatus, } = useDraftBase('document', host, git, storage) const hooks = useHooks() @@ -35,7 +35,7 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: } const oldStatus = existingItem.status - existingItem.status = getDraftStatus(document, existingItem.original as DatabaseItem, host.document.utils.areEqual) + existingItem.status = getStatus(document, existingItem.original as DatabaseItem) existingItem.modified = document await storage.setItem(fsPath, existingItem) @@ -145,5 +145,6 @@ export const useDraftDocuments = createSharedComposable((host: StudioHost, git: load, selectByFsPath, unselect, + getStatus, } }) diff --git a/src/app/src/composables/useDraftMedias.ts b/src/app/src/composables/useDraftMedias.ts index 91559573..1f862ab0 100644 --- a/src/app/src/composables/useDraftMedias.ts +++ b/src/app/src/composables/useDraftMedias.ts @@ -24,6 +24,7 @@ export const useDraftMedias = createSharedComposable((host: StudioHost, git: Ret selectByFsPath, unselect, load, + getStatus, } = useDraftBase('media', host, git, storage) async function upload(parentFsPath: string, file: File) { @@ -131,5 +132,6 @@ export const useDraftMedias = createSharedComposable((host: StudioHost, git: Ret unselect, upload, listAsRawFiles, + getStatus, } }) diff --git a/src/app/src/composables/useStudio.ts b/src/app/src/composables/useStudio.ts index 93a58c6e..5e6742e6 100644 --- a/src/app/src/composables/useStudio.ts +++ b/src/app/src/composables/useStudio.ts @@ -11,7 +11,6 @@ import type { StudioHost, GitOptions, DatabaseItem } from '../types' import { StudioFeature } from '../types' import { documentStorage, mediaStorage, nullStorageDriver } from '../utils/storage' import { useHooks } from './useHooks' -import { getDraftStatus } from '../utils/draft' import { useStudioState } from './useStudioState' export const studioFlags = { @@ -44,7 +43,7 @@ export const useStudio = createSharedComposable(() => { host.on.mounted(async () => { if (studioFlags.dev) { - initDevelopmentMode(host, draftDocuments, draftMedias, documentTree, mediaTree) + initDevelopmentMode(host, documentTree, mediaTree) } await draftDocuments.load() @@ -81,20 +80,19 @@ export const useStudio = createSharedComposable(() => { } }) -function initDevelopmentMode(host: StudioHost, draftDocuments: ReturnType, draftMedias: ReturnType, documentTree: ReturnType, mediaTree: ReturnType) { +function initDevelopmentMode(host: StudioHost, documentTree: ReturnType, mediaTree: ReturnType) { const hooks = useHooks() - const areDocumentsEqual = host.document.utils.areEqual // Disable browser storages documentStorage.mount('/', nullStorageDriver) mediaStorage.mount('/', nullStorageDriver) host.on.documentUpdate(async (fsPath: string, type: 'remove' | 'update') => { - const item = draftDocuments.list.value.find(item => item.fsPath === fsPath) + const item = documentTree.draft.list.value.find(item => item.fsPath === fsPath) if (type === 'remove') { if (item) { - await draftDocuments.remove([fsPath]) + await documentTree.draft.remove([fsPath]) } } else if (item) { @@ -103,7 +101,7 @@ function initDevelopmentMode(host: StudioHost, draftDocuments: ReturnType { - const item = draftMedias.list.value.find(item => item.fsPath === fsPath) + const item = mediaTree.draft.list.value.find(item => item.fsPath === fsPath) if (type === 'remove') { if (item) { - await draftMedias.remove([fsPath]) + await mediaTree.draft.remove([fsPath]) } } else if (item) { @@ -124,7 +122,7 @@ function initDevelopmentMode(host: StudioHost, draftDocuments: ReturnType boolean): DraftStatus { - if (studioFlags.dev) { - return DraftStatus.Pristine - } - - if (!modified && !original) { - throw new Error('Unconsistent state: both modified and original are undefined') - } - - if (!modified) { - return DraftStatus.Deleted - } - - if (!original || original.id !== modified.id) { - return DraftStatus.Created - } - - if (original.extension === ContentFileExtension.Markdown) { - if (!comparisonMethod(original as DatabaseItem, modified as DatabaseItem)) { - return DraftStatus.Updated - } - } - else if (typeof original === 'object' && typeof modified === 'object') { - if (!comparisonMethod(original as DatabaseItem, modified as DatabaseItem)) { - return DraftStatus.Updated - } - } - else { - if (JSON.stringify(original) !== JSON.stringify(modified)) { - return DraftStatus.Updated - } - } - - return DraftStatus.Pristine -} - export function findDescendantsFromFsPath(list: DraftItem[], fsPath: string): DraftItem[] { if (fsPath === '/') { return list diff --git a/src/app/test/unit/compsables/draft-base.test.ts b/src/app/test/unit/compsables/draft-base.test.ts new file mode 100644 index 00000000..a14f2bb4 --- /dev/null +++ b/src/app/test/unit/compsables/draft-base.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest' +import { useDraftBase } from '../../../src/composables/useDraftBase' +import { dbItemsList } from '../../mocks/database' +import { DraftStatus } from '../../../src/types' +import { createMockHost } from '../../mocks/host' + +const { getStatus } = useDraftBase('document', createMockHost(), null as never, null as never) + +describe('getStatus', () => { + it('returns Deleted status when modified item is undefined', () => { + const original = dbItemsList[0] // landing/index.md + + expect(getStatus(undefined as never, original)).toBe(DraftStatus.Deleted) + }) + + it('returns Created status when original is undefined', () => { + const modified = dbItemsList[1] // docs/1.getting-started/2.introduction.md + + expect(getStatus(modified, undefined as never)).toBe(DraftStatus.Created) + }) + + it('returns Created status when original has different id', () => { + const original = dbItemsList[0] // landing/index.md + const modified = dbItemsList[1] // docs/1.getting-started/2.introduction.md + + expect(getStatus(modified, original)).toBe(DraftStatus.Created) + }) + + it('returns Updated status when markdown content is different', () => { + const original = dbItemsList[1] // docs/1.getting-started/2.introduction.md + const modified = { + ...original, + body: { + type: 'minimark', + value: ['text', 'Modified'], + }, + } + + expect(getStatus(modified, original)).toBe(DraftStatus.Updated) + }) + + it('returns Pristine status when markdown content is identical', () => { + const original = dbItemsList[1] // docs/1.getting-started/2.introduction.md + + expect(getStatus(original, original)).toBe(DraftStatus.Pristine) + }) +}) diff --git a/src/app/test/unit/utils/draft.test.ts b/src/app/test/unit/utils/draft.test.ts index 786a9c87..6a639c92 100644 --- a/src/app/test/unit/utils/draft.test.ts +++ b/src/app/test/unit/utils/draft.test.ts @@ -1,9 +1,6 @@ import { describe, it, expect } from 'vitest' -import { findDescendantsFromFsPath, getDraftStatus } from '../../../src/utils/draft' +import { findDescendantsFromFsPath } from '../../../src/utils/draft' import { draftItemsList } from '../../../test/mocks/draft' -import { dbItemsList } from '../../../test/mocks/database' -import type { DatabaseItem } from '../../../src/types' -import { DraftStatus } from '../../../src/types' describe('findDescendantsFromFsPath', () => { it('returns exact match for a root level file', () => { @@ -51,45 +48,3 @@ describe('findDescendantsFromFsPath', () => { expect(descendants[0].fsPath).toBe('1.getting-started/1.advanced/1.studio.md') }) }) - -describe('getDraftStatus', () => { - const areDocumentsEqual = (document1: DatabaseItem, document2: DatabaseItem) => JSON.stringify(document1) === JSON.stringify(document2) - - it('returns Deleted status when modified item is undefined', () => { - const original = dbItemsList[0] // landing/index.md - - expect(getDraftStatus(undefined as never, original, areDocumentsEqual)).toBe(DraftStatus.Deleted) - }) - - it('returns Created status when original is undefined', () => { - const modified = dbItemsList[1] // docs/1.getting-started/2.introduction.md - - expect(getDraftStatus(modified, undefined as never, areDocumentsEqual)).toBe(DraftStatus.Created) - }) - - it('returns Created status when original has different id', () => { - const original = dbItemsList[0] // landing/index.md - const modified = dbItemsList[1] // docs/1.getting-started/2.introduction.md - - expect(getDraftStatus(modified, original, areDocumentsEqual)).toBe(DraftStatus.Created) - }) - - it('returns Updated status when markdown content is different', () => { - const original = dbItemsList[1] // docs/1.getting-started/2.introduction.md - const modified = { - ...original, - body: { - type: 'minimark', - value: ['text', 'Modified'], - }, - } - - expect(getDraftStatus(modified, original, () => false)).toBe(DraftStatus.Updated) - }) - - it('returns Pristine status when markdown content is identical', () => { - const original = dbItemsList[1] // docs/1.getting-started/2.introduction.md - - expect(getDraftStatus(original, original, areDocumentsEqual)).toBe(DraftStatus.Pristine) - }) -}) From 59d087b2c96418030ae0f4d983c93b27cc361e12 Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Thu, 13 Nov 2025 11:55:03 +0100 Subject: [PATCH 14/20] refactor getDraftStatus --- src/app/src/utils/tree.ts | 39 +++------ src/app/test/unit/utils/tree.test.ts | 114 +++++++++++++++++---------- 2 files changed, 85 insertions(+), 68 deletions(-) diff --git a/src/app/src/utils/tree.ts b/src/app/src/utils/tree.ts index 2e5cd960..61aa7b91 100644 --- a/src/app/src/utils/tree.ts +++ b/src/app/src/utils/tree.ts @@ -1,8 +1,6 @@ import { - ContentFileExtension, DraftStatus, TreeStatus, - type DatabasePageItem, type DraftItem, type TreeItem, } from '../types' @@ -27,7 +25,7 @@ export const COLOR_UI_STATUS_MAP: { [key in TreeStatus]?: string } = { [TreeStatus.Opened]: 'neutral', } as const -export function buildTree(dbItems: BaseItem[], draftList: DraftItem[] | null, comparisonMethod: (document1: DatabasePageItem, document2: DatabasePageItem) => boolean): +export function buildTree(dbItems: BaseItem[], draftList: DraftItem[] | null): TreeItem[] { const tree: TreeItem[] = [] const directoryMap = new Map() @@ -91,7 +89,7 @@ TreeItem[] { const draftFileItem = draftList?.find(draft => draft.fsPath === dbItem.fsPath) if (draftFileItem) { - fileItem.status = getTreeStatus(draftFileItem.modified!, draftFileItem.original!, comparisonMethod) + fileItem.status = getTreeStatus(draftFileItem) } tree.push(fileItem) @@ -148,7 +146,7 @@ TreeItem[] { const draftFileItem = draftList?.find(draft => draft.fsPath === dbItem.fsPath) if (draftFileItem) { - fileItem.status = getTreeStatus(draftFileItem.modified!, draftFileItem.original!, comparisonMethod) + fileItem.status = getTreeStatus(draftFileItem) } if (dbItem.path) { @@ -163,36 +161,25 @@ TreeItem[] { return tree } -export function getTreeStatus(modified: BaseItem, original: BaseItem, comparisonMethod: (document1: DatabasePageItem, document2: DatabasePageItem) => boolean): TreeStatus { - if (studioFlags.dev) { +export function getTreeStatus(draftItem: DraftItem): TreeStatus { + if (draftItem.status === DraftStatus.Pristine) { return TreeStatus.Opened } - if (!original && !modified) { - throw new Error('Unconsistent state: both modified and original are undefined') - } - - if (!original) { - return TreeStatus.Created - } - - if (!modified) { + if (draftItem.status === DraftStatus.Deleted) { return TreeStatus.Deleted } - if (modified.id !== original.id) { - return TreeStatus.Renamed + if (draftItem.status === DraftStatus.Updated) { + return TreeStatus.Updated } - if (original.extension === ContentFileExtension.Markdown) { - if (!comparisonMethod(original as DatabasePageItem, modified as DatabasePageItem)) { - return TreeStatus.Updated - } - } - else { - if (JSON.stringify(original) !== JSON.stringify(modified)) { - return TreeStatus.Updated + if (draftItem.status === DraftStatus.Created) { + const { original, modified } = draftItem + if (original && modified && original.id !== modified.id) { + return TreeStatus.Renamed } + return TreeStatus.Created } return TreeStatus.Opened diff --git a/src/app/test/unit/utils/tree.test.ts b/src/app/test/unit/utils/tree.test.ts index 9fb28773..dbfe9fce 100644 --- a/src/app/test/unit/utils/tree.test.ts +++ b/src/app/test/unit/utils/tree.test.ts @@ -11,8 +11,6 @@ import type { DatabaseItem } from '../../../src/types/database' import { joinURL, withLeadingSlash } from 'ufo' import { VirtualMediaCollectionName } from '../../../src/utils/media' -const areDocumentsEqual = (document1: DatabaseItem, document2: DatabaseItem) => JSON.stringify(document1) === JSON.stringify(document2) - describe('buildTree of documents with one level of depth', () => { // Result based on dbItemsList mock const result: TreeItem[] = [ @@ -48,7 +46,7 @@ describe('buildTree of documents with one level of depth', () => { ] it('Without draft', () => { - const tree = buildTree(dbItemsList, null, areDocumentsEqual) + const tree = buildTree(dbItemsList, null) expect(tree).toStrictEqual(result as TreeItem[]) }) @@ -62,7 +60,7 @@ describe('buildTree of documents with one level of depth', () => { modified: createdDbItem, }] - const tree = buildTree(dbItemsList, draftList, areDocumentsEqual) + const tree = buildTree(dbItemsList, draftList) expect(tree).toStrictEqual([ { @@ -84,7 +82,7 @@ describe('buildTree of documents with one level of depth', () => { const dbItemsListWithoutDeletedDbItem = dbItemsList.filter(item => item.id !== deletedDbItem.id) - const tree = buildTree(dbItemsListWithoutDeletedDbItem, draftList, areDocumentsEqual) + const tree = buildTree(dbItemsListWithoutDeletedDbItem, draftList) expect(tree).toStrictEqual([ { ...result[0] }, @@ -118,7 +116,7 @@ describe('buildTree of documents with one level of depth', () => { const dbItemsListWithoutDeletedDbItem = dbItemsList.filter(item => item.id !== deletedDbItem.id) - const tree = buildTree(dbItemsListWithoutDeletedDbItem, draftList, areDocumentsEqual) + const tree = buildTree(dbItemsListWithoutDeletedDbItem, draftList) expect(tree).toStrictEqual([ result[0], @@ -156,7 +154,7 @@ describe('buildTree of documents with one level of depth', () => { }, }] - const tree = buildTree(dbItemsList, draftList, areDocumentsEqual) + const tree = buildTree(dbItemsList, draftList) const expectedTree = [ result[0], @@ -192,7 +190,7 @@ describe('buildTree of documents with one level of depth', () => { modified: openedDbItem, }] - const tree = buildTree(dbItemsList, draftList, areDocumentsEqual) + const tree = buildTree(dbItemsList, draftList) const expectedTree = [ result[0], @@ -225,7 +223,7 @@ describe('buildTree of documents with one level of depth', () => { modified: openedDbItem2, }] - const tree = buildTree(dbItemsList, draftList, areDocumentsEqual) + const tree = buildTree(dbItemsList, draftList) const expectedTree = [ result[0], @@ -269,7 +267,7 @@ describe('buildTree of documents with one level of depth', () => { const dbItemsWithoutDeletedWithCreated = dbItemsList.filter(item => item.id !== deletedDbItem.id) dbItemsWithoutDeletedWithCreated.push(createdDbItem) - const tree = buildTree(dbItemsWithoutDeletedWithCreated, draftList, areDocumentsEqual) + const tree = buildTree(dbItemsWithoutDeletedWithCreated, draftList) expect(tree).toStrictEqual([ result[0], @@ -327,7 +325,7 @@ describe('buildTree of documents with two levels of depth', () => { ] it('Without draft', () => { - const tree = buildTree(nestedDbItemsList, null, areDocumentsEqual) + const tree = buildTree(nestedDbItemsList, null) expect(tree).toStrictEqual(result) }) @@ -347,7 +345,7 @@ describe('buildTree of documents with two levels of depth', () => { }, }] - const tree = buildTree(nestedDbItemsList, draftList, areDocumentsEqual) + const tree = buildTree(nestedDbItemsList, draftList) expect(tree).toStrictEqual([{ ...result[0], @@ -375,7 +373,7 @@ describe('buildTree of documents with two levels of depth', () => { }, }] - const tree = buildTree(nestedDbItemsList, draftList, areDocumentsEqual) + const tree = buildTree(nestedDbItemsList, draftList) expect(tree).toStrictEqual([{ ...result[0], @@ -409,7 +407,7 @@ describe('buildTree of documents with two levels of depth', () => { // Remove the deleted item from the nestedDbItemsList const nestedDbItemsListWithoutDeletedDbItem = nestedDbItemsList.filter(item => item.id !== deletedDbItem.id) - const tree = buildTree(nestedDbItemsListWithoutDeletedDbItem, draftList, areDocumentsEqual) + const tree = buildTree(nestedDbItemsListWithoutDeletedDbItem, draftList) expect(tree).toStrictEqual([{ ...result[0], @@ -477,7 +475,7 @@ describe('buildTree of documents with language prefixed', () => { ] it('Without draft', () => { - const tree = buildTree(languagePrefixedDbItemsList, null, areDocumentsEqual) + const tree = buildTree(languagePrefixedDbItemsList, null) expect(tree).toStrictEqual(result) }) }) @@ -513,7 +511,7 @@ describe('buildTree of medias', () => { modified: gitkeepDbItem, }] - const tree = buildTree([gitkeepDbItem, mediaDbItem], draftList, areDocumentsEqual) + const tree = buildTree([gitkeepDbItem, mediaDbItem], draftList) expect(tree).toHaveLength(1) expect(tree[0]).toHaveProperty('fsPath', mediaFolderName) @@ -529,59 +527,91 @@ describe('buildTree of medias', () => { }) describe('getTreeStatus', () => { - it('draft is CREATED if originalDatabaseItem is not defined', () => { - const modified: DatabaseItem = dbItemsList[0] // index.md + it('should return OPENED when draft status is Pristine', () => { + const draftItem: DraftItem = { + fsPath: 'index.md', + status: DraftStatus.Pristine, + original: dbItemsList[0], + modified: dbItemsList[0], + } - const status = getTreeStatus(modified, undefined as never, areDocumentsEqual) - expect(status).toBe(TreeStatus.Created) + const status = getTreeStatus(draftItem) + expect(status).toBe(TreeStatus.Opened) }) - it('draft is OPENED if originalDatabaseItem is defined and is the same as draftedDocument', () => { - const original: DatabaseItem = dbItemsList[0] // index.md + it('should return DELETED when draft status is Deleted', () => { + const draftItem: DraftItem = { + fsPath: 'index.md', + status: DraftStatus.Deleted, + original: dbItemsList[0], + modified: undefined, + } - const status = getTreeStatus(original, original, areDocumentsEqual) - expect(status).toBe(TreeStatus.Opened) + const status = getTreeStatus(draftItem) + expect(status).toBe(TreeStatus.Deleted) }) - it('draft is UPDATED if originalDatabaseItem is defined and one of its data field is different from draftedDocument', () => { + it('should return UPDATED when draft status is Updated', () => { const original: DatabaseItem = dbItemsList[0] const modified: DatabaseItem = { ...original, title: 'New title', } - const status = getTreeStatus(modified, original, areDocumentsEqual) - expect(status).toBe(TreeStatus.Updated) - }) - - it('draft is UPDATED if originalDatabaseItem is defined and its body is different from draftedDocument', () => { - const original: DatabaseItem = dbItemsList[0] - const modified: DatabaseItem = { - ...original, - body: { type: 'minimark', value: ['New body'] }, + const draftItem: DraftItem = { + fsPath: 'index.md', + status: DraftStatus.Updated, + original, + modified, } - const status = getTreeStatus(modified, original, areDocumentsEqual) + const status = getTreeStatus(draftItem) expect(status).toBe(TreeStatus.Updated) }) - it('draft is RENAMED if originalDatabaseItem is defined and id is different from draftedDocument', () => { + it('should return RENAMED when draft status is Created and original.id differs from modified.id', () => { const original: DatabaseItem = dbItemsList[0] // index.md const modified: DatabaseItem = { ...original, id: 'renamed.md', } - const status = getTreeStatus(modified, original, areDocumentsEqual) + const draftItem: DraftItem = { + fsPath: 'renamed.md', + status: DraftStatus.Created, + original, + modified, + } + + const status = getTreeStatus(draftItem) expect(status).toBe(TreeStatus.Renamed) }) - it('draft is DELETED if modifiedDatabaseItem is not defined', () => { - const original: DatabaseItem = dbItemsList[0] // index.md - const modified: DatabaseItem = undefined as never + it('should return CREATED when draft status is Created without original', () => { + const draftItem: DraftItem = { + fsPath: 'index.md', + status: DraftStatus.Created, + original: undefined, + modified: dbItemsList[0], + } - const status = getTreeStatus(modified, original, areDocumentsEqual) - expect(status).toBe(TreeStatus.Deleted) + const status = getTreeStatus(draftItem) + expect(status).toBe(TreeStatus.Created) + }) + + it('should return CREATED when draft status is Created with original that has same id', () => { + const original: DatabaseItem = dbItemsList[0] + const modified: DatabaseItem = dbItemsList[0] + + const draftItem: DraftItem = { + fsPath: 'index.md', + status: DraftStatus.Created, + original, + modified, + } + + const status = getTreeStatus(draftItem) + expect(status).toBe(TreeStatus.Created) }) }) From 7a520cf9334d34aa923a2303bb431188ca6a0a4c Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Thu, 13 Nov 2025 12:10:07 +0100 Subject: [PATCH 15/20] remove fsPath before storing file --- src/module/src/runtime/host.dev.ts | 6 +- src/module/src/runtime/host.ts | 29 +++++++--- src/module/src/runtime/utils/document.ts | 74 ++++++++++++------------ 3 files changed, 60 insertions(+), 49 deletions(-) diff --git a/src/module/src/runtime/host.dev.ts b/src/module/src/runtime/host.dev.ts index 63c2fec2..6e5a41fc 100644 --- a/src/module/src/runtime/host.dev.ts +++ b/src/module/src/runtime/host.dev.ts @@ -1,7 +1,7 @@ import { useStudioHost as useStudioHostBase } from './host' import type { StudioUser, DatabaseItem, Repository } from 'nuxt-studio/app' import { getCollectionByFilePath, generateIdFromFsPath, generateFsPathFromId, getCollectionById } from './utils/collection' -import { populateDocumentbasedOnCollectionInfo } from './utils/document' +import { normalizeDocument } from './utils/document' import { createStorage } from 'unstorage' import httpDriver from 'unstorage/drivers/http' import { useRuntimeConfig } from '#imports' @@ -35,9 +35,9 @@ export function useStudioHost(user: StudioUser, repository: Repository) { } const id = generateIdFromFsPath(fsPath, collectionInfo) - const doc = populateDocumentbasedOnCollectionInfo(id, collectionInfo, upsertedDocument) + const document = normalizeDocument(id, collectionInfo, upsertedDocument) - const content = await host.document.generate.contentFromDocument(doc) + const content = await host.document.generate.contentFromDocument(document) await devStorage.setItem(fsPath, content, { headers: { diff --git a/src/module/src/runtime/host.ts b/src/module/src/runtime/host.ts index 2a9705af..d044a142 100644 --- a/src/module/src/runtime/host.ts +++ b/src/module/src/runtime/host.ts @@ -3,7 +3,7 @@ import { ensure } from './utils/ensure' import type { CollectionInfo, CollectionItemBase, CollectionSource, DatabaseAdapter } from '@nuxt/content' import type { ContentDatabaseAdapter } from '../types/content' import { getCollectionByFilePath, generateIdFromFsPath, generateRecordDeletion, generateRecordInsert, generateFsPathFromId, getCollectionById } from './utils/collection' -import { populateDocumentbasedOnCollectionInfo, isDocumentMatchingContent, normalizeDocument, generateDocumentFromContent, generateContentFromDocument, areDocumentsEqual, pickReservedKeysFromDocument, removeReservedKeysFromDocument } from './utils/document' +import { normalizeDocument, isDocumentMatchingContent, generateDocumentFromContent, generateContentFromDocument, areDocumentsEqual, pickReservedKeysFromDocument, removeReservedKeysFromDocument } from './utils/document' import { kebabCase } from 'scule' import type { StudioHost, StudioUser, DatabaseItem, MediaItem, Repository } from 'nuxt-studio/app' import type { RouteLocationNormalized, Router } from 'vue-router' @@ -194,7 +194,14 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH const id = generateIdFromFsPath(fsPath, collectionInfo) const item = await useContentCollectionQuery(collectionInfo.name).where('id', '=', id).first() - return item ? normalizeDocument(fsPath, item as DatabaseItem) : undefined + if (!item) { + return undefined + } + + return { + ...item, + fsPath, + } }, list: async (): Promise => { const collections = Object.values(useContentCollections()).filter(collection => collection.name !== 'info') @@ -205,7 +212,10 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH const source = getCollectionSourceById(document.id, collection.source) const fsPath = generateFsPathFromId(document.id, source!) - return normalizeDocument(fsPath, document) + return { + ...document, + fsPath, + } }) })) @@ -225,11 +235,14 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH } const document = await generateDocumentFromContent(id, content) - const collectionDocument = populateDocumentbasedOnCollectionInfo(id, collectionInfo, document!) + const normalizedDocument = normalizeDocument(id, collectionInfo, document!) - await host.document.db.upsert(fsPath, collectionDocument) + await host.document.db.upsert(fsPath, normalizedDocument) - return normalizeDocument(fsPath, collectionDocument!) + return { + ...normalizedDocument, + fsPath, + } }, upsert: async (fsPath: string, document: CollectionItemBase) => { const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections()) @@ -239,10 +252,10 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH const id = generateIdFromFsPath(fsPath, collectionInfo) - const doc = populateDocumentbasedOnCollectionInfo(id, collectionInfo, document) + const normalizedDocument = normalizeDocument(id, collectionInfo, document) await useContentDatabaseAdapter(collectionInfo.name).exec(generateRecordDeletion(collectionInfo, id)) - await useContentDatabaseAdapter(collectionInfo.name).exec(generateRecordInsert(collectionInfo, doc)) + await useContentDatabaseAdapter(collectionInfo.name).exec(generateRecordInsert(collectionInfo, normalizedDocument)) }, delete: async (fsPath: string) => { const collection = getCollectionByFilePath(fsPath, useContentCollections()) diff --git a/src/module/src/runtime/utils/document.ts b/src/module/src/runtime/utils/document.ts index 4352d1d9..3f9bc24b 100644 --- a/src/module/src/runtime/utils/document.ts +++ b/src/module/src/runtime/utils/document.ts @@ -20,11 +20,43 @@ const reservedKeys = ['id', 'fsPath', 'stem', 'extension', '__hash__', 'path', ' /* ** Normalization utils */ -export function normalizeDocument(fsPath: string, document: DatabaseItem): DatabaseItem { - return { - ...document, - fsPath, +export function normalizeDocument(id: string, collectionInfo: CollectionInfo, document: CollectionItemBase) { + const parsedContent = [ + pathMetaTransform, + ].reduce((acc, fn) => collectionInfo.type === 'page' ? fn(acc as PageCollectionItemBase) : acc, { ...document, id } as PageCollectionItemBase) + const result = { id } as DatabaseItem + const meta = parsedContent.meta as Record + + const collectionKeys = getOrderedSchemaKeys(collectionInfo.schema) + for (const key of Object.keys(parsedContent)) { + if (collectionKeys.includes(key)) { + result[key] = parsedContent[key as keyof PageCollectionItemBase] + } + else { + meta[key] = parsedContent[key as keyof PageCollectionItemBase] + } + } + + // Clean fsPath from meta to avoid storing it in the database + if (meta.fsPath) { + Reflect.deleteProperty(meta, 'fsPath') } + + result.meta = meta + + // Storing `content` into `rawbody` field + // TODO: handle rawbody + // if (collectionKeys.includes('rawbody')) { + // result.rawbody = result.rawbody ?? file.body + // } + + if (collectionKeys.includes('seo')) { + const seo = result.seo = (result.seo || {}) as PageCollectionItemBase['seo'] + seo.title = seo.title || result.title as string + seo.description = seo.description || result.description as string + } + + return result } export function pickReservedKeysFromDocument(document: DatabaseItem): DatabaseItem { @@ -183,40 +215,6 @@ export function areDocumentsEqual(document1: Record, document2: /* ** Generation utils */ -export function populateDocumentbasedOnCollectionInfo(id: string, collectionInfo: CollectionInfo, document: CollectionItemBase) { - const parsedContent = [ - pathMetaTransform, - ].reduce((acc, fn) => collectionInfo.type === 'page' ? fn(acc as PageCollectionItemBase) : acc, { ...document, id } as PageCollectionItemBase) - const result = { id } as DatabaseItem - const meta = parsedContent.meta as Record - - const collectionKeys = getOrderedSchemaKeys(collectionInfo.schema) - for (const key of Object.keys(parsedContent)) { - if (collectionKeys.includes(key)) { - result[key] = parsedContent[key as keyof PageCollectionItemBase] - } - else { - meta[key] = parsedContent[key as keyof PageCollectionItemBase] - } - } - - result.meta = meta - - // Storing `content` into `rawbody` field - // TODO: handle rawbody - // if (collectionKeys.includes('rawbody')) { - // result.rawbody = result.rawbody ?? file.body - // } - - if (collectionKeys.includes('seo')) { - const seo = result.seo = (result.seo || {}) as PageCollectionItemBase['seo'] - seo.title = seo.title || result.title as string - seo.description = seo.description || result.description as string - } - - return result -} - export async function generateDocumentFromContent(id: string, content: string): Promise { const [_id, _hash] = id.split('#') const extension = getFileExtension(id) From 02d91d4e9700802a1a144e453e4d0809b2126711 Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Thu, 13 Nov 2025 12:13:53 +0100 Subject: [PATCH 16/20] up --- src/app/src/composables/useTree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/src/composables/useTree.ts b/src/app/src/composables/useTree.ts index d68ae5e5..99071a75 100644 --- a/src/app/src/composables/useTree.ts +++ b/src/app/src/composables/useTree.ts @@ -101,7 +101,7 @@ export const useTree = (type: StudioFeature, host: StudioHost, draft: ReturnType const hostDb = type === StudioFeature.Content ? host.document.db : host.media const list = await hostDb.list() as DatabaseItem[] - tree.value = buildTree(list, draft.list.value, host.document.utils.areEqual) + tree.value = buildTree(list, draft.list.value) // Reselect current item to update status if (selectItem) { From 6728f78ad836280337975e14964c29edf524a09e Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Thu, 13 Nov 2025 12:22:14 +0100 Subject: [PATCH 17/20] update mdc --- package.json | 5 ++-- pnpm-lock.yaml | 79 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 941274dc..cf761516 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "@iconify-json/lucide": "^1.2.72", - "@nuxtjs/mdc": "https://pkg.pr.new/@nuxtjs/mdc@cb6a227", + "@nuxtjs/mdc": "^0.18.3", "@vueuse/core": "^13.9.0", "defu": "^6.1.4", "destr": "^2.0.5", @@ -83,8 +83,7 @@ "zod": "^4.1.12" }, "resolutions": { - "remark-mdc": "3.8.1", - "@nuxtjs/mdc": "https://pkg.pr.new/@nuxtjs/mdc@cb6a227" + "remark-mdc": "3.8.1" }, "packageManager": "pnpm@10.21.0", "keywords": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8be33766..3ae1d610 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,6 @@ settings: overrides: remark-mdc: 3.8.1 - '@nuxtjs/mdc': https://pkg.pr.new/@nuxtjs/mdc@cb6a227 importers: @@ -16,8 +15,8 @@ importers: specifier: ^1.2.72 version: 1.2.73 '@nuxtjs/mdc': - specifier: https://pkg.pr.new/@nuxtjs/mdc@cb6a227 - version: https://pkg.pr.new/@nuxtjs/mdc@cb6a227(magicast@0.5.1) + specifier: ^0.18.3 + version: 0.18.3 '@vueuse/core': specifier: ^13.9.0 version: 13.9.0(vue@3.5.24(typescript@5.9.3)) @@ -132,7 +131,7 @@ importers: version: 12.4.1 docus: specifier: ^5.2.1 - version: 5.2.1(555a7c5d52dcff5715f86c255e099713) + version: 5.2.1(9b58943e116e8343a25ebf63a38b8a7a) nuxt: specifier: ^4.2.1 version: 4.2.1(@parcel/watcher@2.5.1)(@types/node@24.10.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(meow@13.2.0)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) @@ -1086,6 +1085,9 @@ packages: resolution: {integrity: sha512-noQTJICmiWLgi6QgF26n//IwBT0p4ntEV+CALhj+/4EFGEoBxl5/AEm1Ved6WSLBWHodrayMk3dRGVfQdRP2nQ==} engines: {node: '>=20.11.1'} + '@nuxtjs/mdc@0.18.3': + resolution: {integrity: sha512-Fl64a9OZBH3J7ZpqzSWkrS64oFmLLvledZMcnYH3UzVtgFPo/GaICdJN3Ml83NSs/J9At6HHaP0k3+nrxu2qJw==} + '@nuxtjs/mdc@https://pkg.pr.new/@nuxtjs/mdc@cb6a227': resolution: {tarball: https://pkg.pr.new/@nuxtjs/mdc@cb6a227} version: 0.18.2 @@ -6577,6 +6579,9 @@ packages: unwasm@0.4.2: resolution: {integrity: sha512-/DWXXXn63zAbdoQ6jtdbhr1WP2Cz6ax2nwIu4/yqj4617VWmva4UAzNH16q2fD4I6fym4tSAXFT8P89weXn2AA==} + unwasm@0.5.0: + resolution: {integrity: sha512-6pGyUVJuqGRyyWc0JHbbjXwalBlc/9lEqfuE5j56L4V/MXhdBLRtbX7ciazT+EnzOVV+j++qnxJRTkjna+LgcQ==} + update-browserslist-db@1.1.4: resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true @@ -8528,6 +8533,55 @@ snapshots: - uploadthing - vue + '@nuxtjs/mdc@0.18.3': + dependencies: + '@nuxt/kit': 4.2.1(magicast@0.5.1) + '@shikijs/core': 3.15.0 + '@shikijs/langs': 3.15.0 + '@shikijs/themes': 3.15.0 + '@shikijs/transformers': 3.15.0 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@vue/compiler-core': 3.5.24 + consola: 3.4.2 + debug: 4.4.3 + defu: 6.1.4 + destr: 2.0.5 + detab: 3.0.2 + github-slugger: 2.0.0 + hast-util-format: 1.1.0 + hast-util-to-mdast: 10.1.2 + hast-util-to-string: 3.0.1 + mdast-util-to-hast: 13.2.0 + micromark-util-sanitize-uri: 2.0.1 + parse5: 8.0.0 + pathe: 2.0.3 + property-information: 7.1.0 + rehype-external-links: 3.0.0 + rehype-minify-whitespace: 6.0.2 + rehype-raw: 7.0.0 + rehype-remark: 10.0.1 + rehype-slug: 6.0.0 + rehype-sort-attribute-values: 5.0.1 + rehype-sort-attributes: 5.0.1 + remark-emoji: 5.0.2 + remark-gfm: 4.0.1 + remark-mdc: 3.8.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-stringify: 11.0.0 + scule: 1.3.0 + shiki: 3.15.0 + ufo: 1.6.1 + unified: 11.0.5 + unist-builder: 4.0.0 + unist-util-visit: 5.0.0 + unwasm: 0.5.0 + vfile: 6.0.3 + transitivePeerDependencies: + - magicast + - supports-color + '@nuxtjs/mdc@https://pkg.pr.new/@nuxtjs/mdc@cb6a227(magicast@0.5.1)': dependencies: '@nuxt/kit': 4.2.1(magicast@0.5.1) @@ -10727,7 +10781,7 @@ snapshots: diff@8.0.2: {} - docus@5.2.1(555a7c5d52dcff5715f86c255e099713): + docus@5.2.1(9b58943e116e8343a25ebf63a38b8a7a): dependencies: '@iconify-json/lucide': 1.2.73 '@iconify-json/simple-icons': 1.2.58 @@ -10748,7 +10802,7 @@ snapshots: motion-v: 1.7.4(@vueuse/core@13.9.0(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3)) nuxt: 4.2.1(@parcel/watcher@2.5.1)(@types/node@24.10.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(meow@13.2.0)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) nuxt-llms: 0.1.3(magicast@0.5.1) - nuxt-og-image: 5.1.12(@unhead/vue@2.0.19(vue@3.5.24(typescript@5.9.3)))(h3@1.15.4)(magicast@0.5.1)(unstorage@1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3)) + nuxt-og-image: 5.1.12(@unhead/vue@2.0.19(vue@3.5.24(typescript@5.9.3)))(h3@1.15.4)(magicast@0.5.1)(unstorage@1.17.2(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3)) pkg-types: 2.3.0 scule: 1.3.0 tailwindcss: 4.1.17 @@ -12804,7 +12858,7 @@ snapshots: transitivePeerDependencies: - magicast - nuxt-og-image@5.1.12(@unhead/vue@2.0.19(vue@3.5.24(typescript@5.9.3)))(h3@1.15.4)(magicast@0.5.1)(unstorage@1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3)): + nuxt-og-image@5.1.12(@unhead/vue@2.0.19(vue@3.5.24(typescript@5.9.3)))(h3@1.15.4)(magicast@0.5.1)(unstorage@1.17.2(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2))(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3)): dependencies: '@nuxt/devtools-kit': 2.7.0(magicast@0.5.1)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1)) '@nuxt/kit': 4.2.1(magicast@0.5.1) @@ -12835,7 +12889,7 @@ snapshots: strip-literal: 3.1.0 ufo: 1.6.1 unplugin: 2.3.10 - unstorage: 1.17.1(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2) + unstorage: 1.17.2(db0@0.3.4(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.2) unwasm: 0.3.11 yoga-wasm-web: 0.3.3 transitivePeerDependencies: @@ -14715,6 +14769,15 @@ snapshots: pathe: 2.0.3 pkg-types: 2.3.0 + unwasm@0.5.0: + dependencies: + exsolve: 1.0.8 + knitwork: 1.2.0 + magic-string: 0.30.21 + mlly: 1.8.0 + pathe: 2.0.3 + pkg-types: 2.3.0 + update-browserslist-db@1.1.4(browserslist@4.28.0): dependencies: browserslist: 4.28.0 From 90b5d419d604193c374b83514f1fae65373a90dd Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 13 Nov 2025 12:47:44 +0100 Subject: [PATCH 18/20] fix: better match for markdown files --- src/module/src/runtime/utils/document.ts | 32 ++++++++++++++++-------- src/module/src/runtime/utils/object.ts | 6 ++--- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/module/src/runtime/utils/document.ts b/src/module/src/runtime/utils/document.ts index 3f9bc24b..c958992f 100644 --- a/src/module/src/runtime/utils/document.ts +++ b/src/module/src/runtime/utils/document.ts @@ -2,7 +2,7 @@ import type { CollectionInfo, CollectionItemBase, MarkdownRoot, PageCollectionIt import { getOrderedSchemaKeys } from './collection' import { pathMetaTransform } from './path-meta' import type { DatabaseItem, DatabasePageItem } from 'nuxt-studio/app' -import { areObjectsEqual, omit, pick } from './object' +import { doObjectsMatch, omit, pick } from './object' import { ContentFileExtension } from '../types/content' import { parseMarkdown } from '@nuxtjs/mdc/runtime/parser/index' import type { MDCElement, MDCRoot } from '@nuxtjs/mdc' @@ -108,17 +108,22 @@ export function removeReservedKeysFromDocument(document: DatabaseItem): Database */ export async function isDocumentMatchingContent(content: string, document: DatabaseItem): Promise { const generatedDocument = await generateDocumentFromContent(document.id, content) as DatabaseItem - return areObjectsEqual(generatedDocument, document) -} -export function areDocumentsEqual(document1: Record, document2: Record) { - function withoutLastStyles(body: MarkdownRoot) { - if (body.value[body.value.length - 1]?.[0] === 'style') { - return { ...body, value: body.value.slice(0, -1) } + if (generatedDocument.extension === ContentFileExtension.Markdown) { + const { body: generatedBody, ...generatedDocumentData } = generatedDocument + const { body: documentBody, ...documentData } = document + + if (stringify(withoutLastStyles(generatedBody)) !== stringify(withoutLastStyles(documentBody))) { + return false } - return body + + return doObjectsMatch(generatedDocumentData, documentData) } + return doObjectsMatch(generatedDocument, document) +} + +export function areDocumentsEqual(document1: Record, document2: Record) { const { body: body1, meta: meta1, ...documentData1 } = document1 const { body: body2, meta: meta2, ...documentData2 } = document2 @@ -136,7 +141,7 @@ export function areDocumentsEqual(document1: Record, document2: } } else if (typeof body1 === 'object' && typeof body2 === 'object') { - if (!areObjectsEqual(body1 as Record, body2 as Record)) { + if (!doObjectsMatch(body1 as Record, body2 as Record)) { return false } } @@ -205,7 +210,7 @@ export function areDocumentsEqual(document1: Record, document2: const data1 = refineDocumentData({ ...documentData1, ...(meta1 || {}) }) const data2 = refineDocumentData({ ...documentData2, ...(meta2 || {}) }) - if (!areObjectsEqual(data1, data2)) { + if (!doObjectsMatch(data1, data2)) { return false } @@ -380,3 +385,10 @@ function generateStemFromId(id: string) { function getFileExtension(id: string) { return id.split('#')[0]?.split('.').pop()!.toLowerCase() } + +function withoutLastStyles(body: MarkdownRoot) { + if (body.value[body.value.length - 1]?.[0] === 'style') { + return { ...body, value: body.value.slice(0, -1) } + } + return body +} diff --git a/src/module/src/runtime/utils/object.ts b/src/module/src/runtime/utils/object.ts index ee27f8b4..f264ed1f 100644 --- a/src/module/src/runtime/utils/object.ts +++ b/src/module/src/runtime/utils/object.ts @@ -8,7 +8,7 @@ export const pick = (obj: Record, keys: string | string[]) => { .filter(([key]) => keys.includes(key))) } -export function areObjectsEqual(base: Record, target: Record) { +export function doObjectsMatch(base: Record, target: Record) { if (typeof base !== 'object' || typeof target !== 'object') { return base === target } @@ -19,7 +19,7 @@ export function areObjectsEqual(base: Record, target: Record, target: Record, target[key] as Record)) { + if (!doObjectsMatch(base[key] as Record, target[key] as Record)) { return false } } From 92979d6b227d0282e7926a0cab70a0ecc851b4ee Mon Sep 17 00:00:00 2001 From: Farnabaz Date: Thu, 13 Nov 2025 12:53:40 +0100 Subject: [PATCH 19/20] types: fix --- src/module/src/runtime/utils/document.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/module/src/runtime/utils/document.ts b/src/module/src/runtime/utils/document.ts index c958992f..bbe45312 100644 --- a/src/module/src/runtime/utils/document.ts +++ b/src/module/src/runtime/utils/document.ts @@ -113,7 +113,7 @@ export async function isDocumentMatchingContent(content: string, document: Datab const { body: generatedBody, ...generatedDocumentData } = generatedDocument const { body: documentBody, ...documentData } = document - if (stringify(withoutLastStyles(generatedBody)) !== stringify(withoutLastStyles(documentBody))) { + if (stringify(withoutLastStyles(generatedBody as MarkdownRoot)) !== stringify(withoutLastStyles(documentBody as MarkdownRoot))) { return false } From cd2f0ec1d2bb4ec6d86acf0f825626c7e998b4a6 Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Thu, 13 Nov 2025 12:58:16 +0100 Subject: [PATCH 20/20] fix regression --- src/module/src/runtime/host.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/module/src/runtime/host.ts b/src/module/src/runtime/host.ts index d044a142..0a64d85e 100644 --- a/src/module/src/runtime/host.ts +++ b/src/module/src/runtime/host.ts @@ -222,18 +222,17 @@ export function useStudioHost(user: StudioUser, repository: Repository): StudioH return documentsByCollection.flat() }, create: async (fsPath: string, content: string) => { + const existingDocument = await host.document.db.get(fsPath) + if (existingDocument) { + throw new Error(`Cannot create document with fsPath "${fsPath}": document already exists.`) + } + const collectionInfo = getCollectionByFilePath(fsPath, useContentCollections()) if (!collectionInfo) { throw new Error(`Collection not found for fsPath: ${fsPath}`) } const id = generateIdFromFsPath(fsPath, collectionInfo!) - - const existingDocument = await host.document.db.get(id) - if (existingDocument) { - throw new Error(`Cannot create document with id "${id}": document already exists.`) - } - const document = await generateDocumentFromContent(id, content) const normalizedDocument = normalizeDocument(id, collectionInfo, document!)