diff --git a/docs/content/docs/1.getting-started/3.configuration.md b/docs/content/docs/1.getting-started/3.configuration.md index 46bb1de8a..e0e15f029 100644 --- a/docs/content/docs/1.getting-started/3.configuration.md +++ b/docs/content/docs/1.getting-started/3.configuration.md @@ -303,6 +303,24 @@ export default defineTransformer({ Read more about transformers in the [Transformers](/docs/advanced/transformers) documentation. +### `assets` + +Nuxt Content resolves relative paths to co-located images, video and documents in markdown, MDC components and frontmatter. The files are copied next to the built site and the references are rewritten. Enabled by default. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + assets: { + imageSize: 'style', // inject image dimensions to avoid layout shift + }, + }, + }, +}) +``` + +Read more in the [Assets](/docs/files/assets) documentation. + ## `database` By default Nuxt Content uses a local SQLite database to store and query content. If you like to use another database or you plan to deploy on Cloudflare Workers, you can modify this option. diff --git a/docs/content/docs/3.files/1.markdown.md b/docs/content/docs/3.files/1.markdown.md index 7bbc07441..8d0f9d43a 100644 --- a/docs/content/docs/3.files/1.markdown.md +++ b/docs/content/docs/3.files/1.markdown.md @@ -506,23 +506,32 @@ Each line of a code block gets its line number in the `line` attribute so lines ## Images -You can add images to your `public` directory: +Reference an image **relative to your content file**. Nuxt Content copies it +next to the built site and resolves the path to a served URL automatically: ```bash [Directory structure] content/ index.md -public/ - image.png -nuxt.config.ts -package.json + img/ + cover.png ``` -And then use them in your markdown files in the `content` directory as such: +```md [content/index.md] +![my image](./img/cover.png) +``` + +For an image shared across many pages, place it in the `public/` directory and +reference it with an absolute path: ```md [content/index.md] -![my image](/image.png) +![logo](/logo.png) ``` +::tip +Relative resolution also works in MDC components, raw HTML and frontmatter, and +covers video and documents too. See [Assets](/docs/files/assets). +:: + ## Excerpt Content excerpt or summary can be extracted from the content using `` as a divider. diff --git a/docs/content/docs/3.files/5.assets.md b/docs/content/docs/3.files/5.assets.md new file mode 100644 index 000000000..f2d5da632 --- /dev/null +++ b/docs/content/docs/3.files/5.assets.md @@ -0,0 +1,206 @@ +--- +title: Assets +description: How Nuxt Content resolves relative paths to images, video and documents. +--- + +Nuxt Content resolves **relative paths** to images, video and documents +co-located with your content. Matching files are copied next to the built site +and every reference (in markdown, MDC components, raw HTML and frontmatter) is +rewritten to an absolute URL. + +```bash [Directory structure] +content/ + blog/ + launch/ + index.md + media/ + cover.png + demo.mp4 + files/ + press-kit.pdf +``` + +```md [content/blog/launch/index.md] +--- +title: Launch +cover: media/cover.png +--- + +![Cover](media/cover.png) + +:video{src="media/demo.mp4"} + +[Press kit](files/press-kit.pdf) +``` + +::note +Absolute references (`/image.png`) and remote URLs are left untouched. Use the +`public/` directory and absolute paths for assets shared across many pages. +:: + +## Usage + +A relative path is resolved wherever a value points to a co-located file. + +::code-group +```md [Markdown image] +![mountains](media/mountains.png) +``` + +```md [Markdown link] +[Download](files/report.pdf) +``` + +```md [MDC component] +:video{src="media/clip.mp4"} +``` + +```md [HTML] + +``` +:: + +Relative paths also work in **frontmatter**, including inside arrays: + +```md +--- +title: Portfolio +cover: media/cover.jpg +gallery: + - media/image-1.jpg + - media/image-2.jpg + - media/image-3.jpg +--- +``` + +```md [Pass the resolved paths to a component] +:image-gallery{:images="gallery"} +``` + +Ordering prefixes are stripped from the served URL, exactly like for content +routes: an asset stored under `1.launch/media/01.cover.png` is served at +`/launch/media/cover.png`. + +Links pointing to an asset open in a new tab (`target="_blank"`). + +## Image sizing + +To prevent layout shift, Nuxt Content reads each image's dimensions at build time +and injects them. By default it adds an `aspect-ratio` style. Use `imageSize` to +choose where the dimensions go, or to turn it off: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + assets: { + imageSize: 'style', // the default + }, + }, + }, +}) +``` + +You can combine one or more hints: + +| Hint | Effect | +| --------- | ----------------------------------------------------------------- | +| `'style'` | Adds `style="aspect-ratio:W/H"` to the `` (default) | +| `'attrs'` | Adds `width` and `height` attributes to the `` | +| `'src'` | Adds `?width=W&height=H` to the resolved path (frontmatter values)| +| `'url'` | Alias of `'src'` | +| `''` | Inject nothing | + +::warning +If you use **only** `'attrs'`, add the following CSS so images stay responsive: + +```css +img { + max-width: 100%; + height: auto; +} +``` +:: + +When you pass a frontmatter image to a custom component with the `'src'` hint, the +component receives the dimensions as a query string you can extract and apply: + +```html + +``` + +## Live reload + +In development, adding, editing, moving or deleting an asset updates the browser +without a full reload: + +- editing an image refreshes it in place; +- deleting an asset greys it out until you restore it or change the reference; +- resizing an image re-injects its dimensions so the layout stays correct. + +## Nuxt Image + +When [`@nuxt/image`](https://image.nuxt.com) is installed, `` optimises +co-located assets too: Nuxt Content registers its asset directory with the IPX +provider. List `@nuxt/content` **before** `@nuxt/image` in `modules`, so the +directory is registered before IPX reads it. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + modules: ['@nuxt/content', '@nuxt/image'], +}) +``` + +```vue [components/content/ProseImg.vue] + +``` + +::warning +Do not use `imageSize: 'src'` together with Nuxt Image: the query string prevents +IPX from resolving the image and breaks static generation. The default `'style'` +is safe. +:: + +## Configuration + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + assets: { + // resolve relative asset paths (set to `false` to opt out) + enabled: true, + + // where to inject image dimensions: 'style' | 'attrs' | 'src' | '' (none) + imageSize: 'style', + + // open links pointing to an asset in a new tab + blankLinks: true, + + // file extensions treated as assets (and excluded from content collections) + extensions: ['png', 'jpg', 'svg', 'mp4', 'pdf' /* … */], + + // URL prefix prepended to served assets (namespacing / collision avoidance) + prefix: '', + + // log discovered and copied assets + debug: false, + }, + }, + }, +}) +``` + +::note +You rarely need to touch `extensions`. It only matters when a custom transformer +treats a normally-binary extension as content, or vice versa. +:: + +## Notes + +- Relative references are resolved at build time, so the file must exist when the + site is built. +- On a path collision, files in your `public/` directory take precedence. Use + `prefix` to namespace assets if needed. diff --git a/examples/basic/content/cover.svg b/examples/basic/content/cover.svg new file mode 100644 index 000000000..001c1bdb2 --- /dev/null +++ b/examples/basic/content/cover.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/examples/basic/content/index.md b/examples/basic/content/index.md index 13bbdecdb..d00f7d4bd 100644 --- a/examples/basic/content/index.md +++ b/examples/basic/content/index.md @@ -1,9 +1,14 @@ # Welcome to Nuxt Content starter +![Nuxt Content](cover.svg) + ## Manage your contents Create new pages or modify the existing ones in `content/` directory. +The image above sits next to this file (`content/cover.svg`) and is referenced +with a relative path. + ## Query & Render pages You can find an example of querying contents and rendering them in `app/pages/[...slug].vue` diff --git a/examples/blog/app/pages/index.vue b/examples/blog/app/pages/index.vue index d0f572bc5..eb9c89021 100644 --- a/examples/blog/app/pages/index.vue +++ b/examples/blog/app/pages/index.vue @@ -1,7 +1,7 @@ + + diff --git a/playground/content.config.ts b/playground/content.config.ts index 3fd30edb2..8455c1c3a 100644 --- a/playground/content.config.ts +++ b/playground/content.config.ts @@ -41,6 +41,8 @@ const content = defineCollection({ icon: z.string(), to: z.string(), })), + featured: z.string().optional(), + gallery: z.array(z.string()).optional(), }), }) diff --git a/playground/content/1.real-content/assets-demo.md b/playground/content/1.real-content/assets-demo.md new file mode 100644 index 000000000..488b08764 --- /dev/null +++ b/playground/content/1.real-content/assets-demo.md @@ -0,0 +1,113 @@ +--- +title: Co-located assets +featured: media/turkey-casserole.jpg +gallery: + - italian-bean-stew.jpg + - media/pesto-salmon-lentils.jpg + - media/turkey-casserole.jpg +--- + +# Co-located assets + +Reference assets sitting next to this file with **relative paths**. At build time +they are copied next to the site and every reference (markdown, MDC components, +raw HTML and frontmatter) is rewritten to an absolute URL. + +## Path resolution + +Same folder: + +![Same folder](italian-bean-stew.jpg) + +Sub folder: + +![Sub folder](media/pesto-salmon-lentils.jpg) + +Parent folder: + +![Parent folder](../shared/sicilian-fish-stew.jpg) + +Absolute path (the asset is served at its de-ordered location): + +![Absolute path](/real-content/media/pesto-salmon-lentils.jpg) + +## Media & files + +Links to an asset open in a new tab: + +- [Download the PDF](files/sample.pdf) + +Video, with a co-located poster image: + + + +Iframe to a local HTML file: + + + +Embed a PDF: + + + +## Frontmatter + +A single value bound to a component (`:src="featured"`): + +:img{:src="featured" alt="From frontmatter"} + +An array bound to a custom component (a gallery built from `gallery`): + +:content-gallery{:items="gallery"} + +## Image sizing + +By default an `aspect-ratio` is injected so images reserve their space before +loading — no layout shift, no configuration: + +![Sized image](media/turkey-casserole.jpg) + +Switch to `width`/`height` attributes with `imageSize: 'attrs'`, encode the size +in a `?width=&height=` query with `'src'`, or turn it off with `imageSize: ''`. + +## AST traversal + +Existing attributes are preserved and merged (`width`, `style`): + +:img{src="media/turkey-casserole.jpg" width="240" style="transform: rotate(-3deg);" alt="Rotated casserole"} + +Paths inside fenced code are never rewritten: + +```ts +const src = 'media/turkey-casserole.jpg' +``` + +Components inside tables are processed too: + +| Label | Image | +| ------ | ------------------------------------------------------------------ | +| Recipe | :img{src="italian-bean-stew.jpg" style="width: 80px;" alt="Stew"} | + +## Ordering + +This page lives in `1.real-content/`, yet its assets are served without the +numbering prefix: `media/pesto-salmon-lentils.jpg` is served at +`/real-content/media/pesto-salmon-lentils.jpg`. + +## Nuxt Image + +Assets are served from the site root, so with `@nuxt/image` installed +`:nuxt-img{src="media/turkey-casserole.jpg"}` resolves them through the default +IPX provider. diff --git a/playground/content/1.real-content/files/sample.pdf b/playground/content/1.real-content/files/sample.pdf new file mode 100644 index 000000000..dbf091df9 Binary files /dev/null and b/playground/content/1.real-content/files/sample.pdf differ diff --git a/playground/content/1.real-content/italian-bean-stew.jpg b/playground/content/1.real-content/italian-bean-stew.jpg new file mode 100644 index 000000000..c14abf39b Binary files /dev/null and b/playground/content/1.real-content/italian-bean-stew.jpg differ diff --git a/playground/content/1.real-content/media/pesto-salmon-lentils.jpg b/playground/content/1.real-content/media/pesto-salmon-lentils.jpg new file mode 100644 index 000000000..656979e69 Binary files /dev/null and b/playground/content/1.real-content/media/pesto-salmon-lentils.jpg differ diff --git a/playground/content/1.real-content/media/sample.html b/playground/content/1.real-content/media/sample.html new file mode 100644 index 000000000..19e5e0458 --- /dev/null +++ b/playground/content/1.real-content/media/sample.html @@ -0,0 +1,17 @@ + + + + + Sample + + + + +

Yay, Assets!

+

This page is displaying in an iframe

+ + diff --git a/playground/content/1.real-content/media/sample.mp4 b/playground/content/1.real-content/media/sample.mp4 new file mode 100644 index 000000000..b11552f9c Binary files /dev/null and b/playground/content/1.real-content/media/sample.mp4 differ diff --git a/playground/content/1.real-content/media/turkey-casserole.jpg b/playground/content/1.real-content/media/turkey-casserole.jpg new file mode 100644 index 000000000..237bfe653 Binary files /dev/null and b/playground/content/1.real-content/media/turkey-casserole.jpg differ diff --git a/playground/content/shared/sicilian-fish-stew.jpg b/playground/content/shared/sicilian-fish-stew.jpg new file mode 100644 index 000000000..2f10579b2 Binary files /dev/null and b/playground/content/shared/sicilian-fish-stew.jpg differ diff --git a/playground/nuxt.config.ts b/playground/nuxt.config.ts index 5ada5df5c..f9b9396b8 100644 --- a/playground/nuxt.config.ts +++ b/playground/nuxt.config.ts @@ -37,6 +37,14 @@ export default defineNuxtConfig({ }, }, compatibilityDate: '2025-10-15', + vite: { + optimizeDeps: { + include: [ + '@vue/devtools-core', + '@vue/devtools-kit', + ], + }, + }, llms: { domain: 'http://localhost:3000', title: 'Content Playground', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c61a1a21..08c39683c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,6 +65,9 @@ importers: hookable: specifier: ^5.5.3 version: 5.5.3 + image-size: + specifier: ^2.0.2 + version: 2.0.2 isomorphic-git: specifier: ^1.38.4 version: 1.38.5 @@ -290,19 +293,19 @@ importers: version: 3.0.2(magicast@0.5.2) '@vercel/analytics': specifier: ^2.0.1 - version: 2.0.1(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vue@3.5.34(typescript@5.9.3)) + version: 2.0.1(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vue@3.5.34(typescript@5.9.3)) '@vercel/speed-insights': specifier: ^2.0.0 - version: 2.0.0(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vue@3.5.34(typescript@5.9.3)) + version: 2.0.0(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vue@3.5.34(typescript@5.9.3)) '@vueuse/nuxt': specifier: ^14.3.0 - version: 14.3.0(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vue@3.5.34(typescript@5.9.3)) + version: 14.3.0(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vue@3.5.34(typescript@5.9.3)) better-sqlite3: specifier: ^12.10.0 version: 12.10.0 docus: specifier: ^5.11.0 - version: 5.11.0(7be736386eac822290d67dfa1d56f35d) + version: 5.11.0(ce604dd20a7a75c44e066ad05d4cf9aa) drizzle-kit: specifier: ^0.31.10 version: 0.31.10 @@ -314,7 +317,7 @@ importers: version: 7.2.0 nuxt: specifier: ^4.4.5 - version: 4.4.5(9fcd7314e3eecf945123399c7742c3cc) + version: 4.4.5(62a2f48613f90195bda1847309522735) nuxt-studio: specifier: ^1.7.0 version: 1.7.0(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1)(magicast@0.5.2)(vue@3.5.34(typescript@5.9.3)) @@ -388,6 +391,8 @@ importers: specifier: ^1.1.0 version: 1.1.0 + test/fixtures/assets: {} + test/fixtures/basic: {} test/fixtures/csv: {} @@ -713,9 +718,6 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workers-types@4.20260518.1': - resolution: {integrity: sha512-xXzGrbRi8RHRBNQFgXYkzrB4DgF0RXvmp8E1vCxoBmINpeitM/ZjVDd1CNC+N3uXjgcNjacoz4OgTa0rxgig1A==} - '@cloudflare/workers-types@4.20260617.1': resolution: {integrity: sha512-HdbP3CNcdMZBwegitFDjWvzv+6wPkFXvV9gBXMnf6RjV2Cy3W8TJL3IhSEGul0S6F1DHjnucP7lrpIsvkzNEjA==} @@ -5861,9 +5863,6 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - citty@0.2.1: - resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==} - citty@0.2.2: resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} @@ -7320,6 +7319,11 @@ packages: image-meta@0.2.2: resolution: {integrity: sha512-3MOLanc3sb3LNGWQl1RlQlNWURE5g32aUphrDyFeCsxBTk08iE3VNe4CwsUZ0Qs1X+EfX0+r29Sxdpza4B+yRA==} + image-size@2.0.2: + resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} + engines: {node: '>=16.x'} + hasBin: true + import-without-cache@0.2.5: resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==} engines: {node: '>=20.19.0'} @@ -8697,9 +8701,6 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pkg-types@2.3.0: - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} @@ -10871,21 +10872,6 @@ packages: pinia: optional: true - vue-router@5.0.7: - resolution: {integrity: sha512-dqfk8kvRbCutmCOCj/XLDqDEYxc1wBdAOGLuVy5M93ifYMsBd5fIjfaPN4tQAbxr5IprdBDIox1gr4wYyOx/SA==} - peerDependencies: - '@pinia/colada': '>=0.21.2' - '@vue/compiler-sfc': ^3.5.34 - pinia: ^3.0.4 - vue: ^3.5.34 - peerDependenciesMeta: - '@pinia/colada': - optional: true - '@vue/compiler-sfc': - optional: true - pinia: - optional: true - vue-router@5.1.0: resolution: {integrity: sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==} peerDependencies: @@ -11264,7 +11250,7 @@ snapshots: '@babel/code-frame@7.29.0': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 @@ -11277,10 +11263,10 @@ snapshots: '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.3 + '@babel/parser': 7.29.7 '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -11292,8 +11278,8 @@ snapshots: '@babel/generator@7.29.1': dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 @@ -11309,7 +11295,7 @@ snapshots: '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@babel/helper-compilation-targets@7.28.6': dependencies: @@ -11337,14 +11323,14 @@ snapshots: '@babel/helper-member-expression-to-functions@7.28.5': dependencies: '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.28.6': dependencies: '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -11352,14 +11338,14 @@ snapshots: dependencies: '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@babel/helper-plugin-utils@7.28.6': {} @@ -11375,7 +11361,7 @@ snapshots: '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -11396,7 +11382,7 @@ snapshots: '@babel/helpers@7.28.6': dependencies: '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@babel/parser@7.29.2': dependencies: @@ -11438,17 +11424,17 @@ snapshots: '@babel/template@7.28.6': dependencies: '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@babel/traverse@7.29.0': dependencies: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.3 + '@babel/parser': 7.29.7 '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11547,8 +11533,6 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260616.1': optional: true - '@cloudflare/workers-types@4.20260518.1': {} - '@cloudflare/workers-types@4.20260617.1': {} '@colordx/core@5.4.3': {} @@ -11557,7 +11541,7 @@ snapshots: dependencies: '@simple-libs/child-process-utils': 1.0.1 '@simple-libs/stream-utils': 1.2.0 - semver: 7.8.0 + semver: 7.8.4 optionalDependencies: conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.3.0 @@ -11571,10 +11555,10 @@ snapshots: '@dxup/nuxt@0.4.1(magicast@0.5.2)(typescript@5.9.3)': dependencies: '@dxup/unimport': 0.1.2 - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) chokidar: 5.0.0 pathe: 2.0.3 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -12457,7 +12441,7 @@ snapshots: https-proxy-agent: 7.0.6 node-fetch: 2.7.0(encoding@0.1.13) nopt: 8.1.0 - semver: 7.8.0 + semver: 7.8.4 tar: 7.5.2 transitivePeerDependencies: - encoding @@ -12529,7 +12513,7 @@ snapshots: '@npmcli/fs@1.1.1': dependencies: '@gar/promisify': 1.1.3 - semver: 7.8.0 + semver: 7.8.4 optional: true '@npmcli/move-file@1.1.2': @@ -12541,7 +12525,7 @@ snapshots: '@nuxt/cli@3.35.2(@nuxt/schema@4.4.5)(cac@6.7.14)(magicast@0.5.2)': dependencies: '@bomb.sh/tab': 0.0.15(cac@6.7.14)(citty@0.2.2) - '@clack/prompts': 1.4.0 + '@clack/prompts': 1.5.1 c12: 3.3.4(magicast@0.5.2) citty: 0.2.2 confbox: 0.2.4 @@ -12554,18 +12538,18 @@ snapshots: giget: 3.2.0 jiti: 2.7.0 listhen: 1.10.0 - nypm: 0.6.6 + nypm: 0.6.7 ofetch: 1.5.1 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 2.1.0 pkg-types: 2.3.1 scule: 1.3.0 - semver: 7.8.0 + semver: 7.8.4 srvx: 0.11.15 std-env: 4.1.0 tinyclip: 0.1.12 - tinyexec: 1.1.2 + tinyexec: 1.2.4 ufo: 1.6.4 youch: 4.1.1 optionalDependencies: @@ -12579,7 +12563,7 @@ snapshots: '@nuxt/cli@3.35.2(@nuxt/schema@4.4.8)(cac@6.7.14)(magicast@0.5.2)': dependencies: '@bomb.sh/tab': 0.0.15(cac@6.7.14)(citty@0.2.2) - '@clack/prompts': 1.4.0 + '@clack/prompts': 1.5.1 c12: 3.3.4(magicast@0.5.2) citty: 0.2.2 confbox: 0.2.4 @@ -12592,18 +12576,18 @@ snapshots: giget: 3.2.0 jiti: 2.7.0 listhen: 1.10.0 - nypm: 0.6.6 + nypm: 0.6.7 ofetch: 1.5.1 ohash: 2.0.11 pathe: 2.0.3 perfect-debounce: 2.1.0 pkg-types: 2.3.1 scule: 1.3.0 - semver: 7.8.0 + semver: 7.8.4 srvx: 0.11.15 std-env: 4.1.0 tinyclip: 0.1.12 - tinyexec: 1.1.2 + tinyexec: 1.2.4 ufo: 1.6.4 youch: 4.1.1 optionalDependencies: @@ -12626,7 +12610,7 @@ snapshots: '@nuxt/devtools-kit@3.2.4(magicast@0.5.2)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) execa: 8.0.1 vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: @@ -12634,8 +12618,8 @@ snapshots: '@nuxt/devtools-kit@4.0.0-alpha.3(magicast@0.5.2)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@nuxt/kit': 4.4.5(magicast@0.5.2) - tinyexec: 1.1.2 + '@nuxt/kit': 4.4.8(magicast@0.5.2) + tinyexec: 1.2.4 vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - magicast @@ -12649,7 +12633,7 @@ snapshots: magicast: 0.5.2 pathe: 2.0.3 pkg-types: 2.3.1 - semver: 7.8.0 + semver: 7.8.4 '@nuxt/devtools@3.2.4(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))': dependencies: @@ -12776,7 +12760,7 @@ snapshots: '@nuxt/fonts@0.14.0(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1)(magicast@0.5.2)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0)) - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) consola: 3.4.2 defu: 6.1.7 fontless: 0.2.1(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0)) @@ -12785,11 +12769,11 @@ snapshots: ofetch: 1.5.1 pathe: 2.0.3 sirv: 3.0.2 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 ufo: 1.6.4 unifont: 0.7.4 unplugin: 3.0.0 - unstorage: 1.17.4(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1) + unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12820,15 +12804,15 @@ snapshots: '@iconify/utils': 3.1.0 '@iconify/vue': 5.0.0(vue@3.5.34(typescript@5.9.3)) '@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0)) - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) consola: 3.4.2 - local-pkg: 1.1.2 + local-pkg: 1.2.1 mlly: 1.8.2 ohash: 2.0.11 pathe: 2.0.3 picomatch: 4.0.4 std-env: 3.10.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 transitivePeerDependencies: - magicast - vite @@ -12836,7 +12820,7 @@ snapshots: '@nuxt/image@2.0.0(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1)(magicast@0.5.2)': dependencies: - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) consola: 3.4.2 defu: 6.1.7 h3: 1.15.11 @@ -12888,8 +12872,8 @@ snapshots: pkg-types: 2.3.1 rc9: 3.0.1 scule: 1.3.0 - semver: 7.8.0 - tinyglobby: 0.2.16 + semver: 7.8.4 + tinyglobby: 0.2.17 ufo: 1.6.4 unctx: 2.5.0 untyped: 2.0.0 @@ -12913,8 +12897,8 @@ snapshots: pkg-types: 2.3.1 rc9: 3.0.1 scule: 1.3.0 - semver: 7.8.0 - tinyglobby: 0.2.16 + semver: 7.8.4 + tinyglobby: 0.2.17 ufo: 1.6.4 unctx: 2.5.0 untyped: 2.0.0 @@ -12969,13 +12953,13 @@ snapshots: - vue - vue-tsc - '@nuxt/nitro-server@4.4.5(@babel/core@7.29.0)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(encoding@0.1.13)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57)(sqlite3@5.1.7)(typescript@5.9.3)': + '@nuxt/nitro-server@4.4.5(@babel/core@7.29.0)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(encoding@0.1.13)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(sqlite3@5.1.7)(typescript@5.9.3)': dependencies: '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.4.5(magicast@0.5.2) - '@unhead/vue': 2.1.15(vue@3.5.34(typescript@5.9.3)) - '@vue/shared': 3.5.34 + '@unhead/vue': 2.1.15(vue@3.5.38(typescript@5.9.3)) + '@vue/shared': 3.5.38 consola: 3.4.2 defu: 6.1.7 destr: 2.0.5 @@ -12987,9 +12971,9 @@ snapshots: impound: 1.1.5 klona: 2.0.6 mocked-exports: 0.1.1 - nitropack: 2.13.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(encoding@0.1.13)(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57)(sqlite3@5.1.7) - nuxt: 4.4.5(9fcd7314e3eecf945123399c7742c3cc) - nypm: 0.6.6 + nitropack: 2.13.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(encoding@0.1.13)(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(sqlite3@5.1.7) + nuxt: 4.4.5(62a2f48613f90195bda1847309522735) + nypm: 0.6.7 ohash: 2.0.11 pathe: 2.0.3 rou3: 0.8.1 @@ -12997,7 +12981,7 @@ snapshots: ufo: 1.6.4 unctx: 2.5.0 unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1) - vue: 3.5.34(typescript@5.9.3) + vue: 3.5.38(typescript@5.9.3) vue-bundle-renderer: 2.2.0 vue-devtools-stub: 0.1.0 transitivePeerDependencies: @@ -13042,8 +13026,8 @@ snapshots: '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.4.5(magicast@0.5.2) - '@unhead/vue': 2.1.15(vue@3.5.34(typescript@5.9.3)) - '@vue/shared': 3.5.34 + '@unhead/vue': 2.1.15(vue@3.5.38(typescript@5.9.3)) + '@vue/shared': 3.5.38 consola: 3.4.2 defu: 6.1.7 destr: 2.0.5 @@ -13057,7 +13041,7 @@ snapshots: mocked-exports: 0.1.1 nitropack: 2.13.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(encoding@0.1.13)(oxc-parser@0.128.0)(rolldown@1.0.3)(sqlite3@5.1.7) nuxt: 4.4.5(baa66a6a16e24a1b54c16423ff94cee6) - nypm: 0.6.6 + nypm: 0.6.7 ohash: 2.0.11 pathe: 2.0.3 rou3: 0.8.1 @@ -13065,7 +13049,7 @@ snapshots: ufo: 1.6.4 unctx: 2.5.0 unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1) - vue: 3.5.34(typescript@5.9.3) + vue: 3.5.38(typescript@5.9.3) vue-bundle-renderer: 2.2.0 vue-devtools-stub: 0.1.0 transitivePeerDependencies: @@ -13110,8 +13094,8 @@ snapshots: '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.4.5(magicast@0.5.2) - '@unhead/vue': 2.1.15(vue@3.5.34(typescript@5.9.3)) - '@vue/shared': 3.5.34 + '@unhead/vue': 2.1.15(vue@3.5.38(typescript@5.9.3)) + '@vue/shared': 3.5.38 consola: 3.4.2 defu: 6.1.7 destr: 2.0.5 @@ -13125,7 +13109,7 @@ snapshots: mocked-exports: 0.1.1 nitropack: 2.13.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(encoding@0.1.13)(oxc-parser@0.128.0)(rolldown@1.0.3)(sqlite3@5.1.7) nuxt: 4.4.5(c678397a5d359c68ec65d805733eddf4) - nypm: 0.6.6 + nypm: 0.6.7 ohash: 2.0.11 pathe: 2.0.3 rou3: 0.8.1 @@ -13133,7 +13117,7 @@ snapshots: ufo: 1.6.4 unctx: 2.5.0 unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1) - vue: 3.5.34(typescript@5.9.3) + vue: 3.5.38(typescript@5.9.3) vue-bundle-renderer: 2.2.0 vue-devtools-stub: 0.1.0 transitivePeerDependencies: @@ -13192,7 +13176,7 @@ snapshots: mocked-exports: 0.1.1 nitropack: 2.13.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(encoding@0.1.13)(oxc-parser@0.133.0)(rolldown@1.0.3)(sqlite3@5.1.7) nuxt: 4.4.8(15f32171ed0ebfb0b2ff2e97d246f626) - nypm: 0.6.6 + nypm: 0.6.7 ohash: 2.0.11 pathe: 2.0.3 rou3: 0.8.1 @@ -13243,7 +13227,7 @@ snapshots: '@nuxt/schema@4.4.5': dependencies: - '@vue/shared': 3.5.34 + '@vue/shared': 3.5.38 defu: 6.1.7 pathe: 2.0.3 pkg-types: 2.3.1 @@ -13660,7 +13644,7 @@ snapshots: - vue - yjs - '@nuxt/vite-builder@4.4.5(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@25.8.0)(eslint@10.5.0(jiti@2.7.0))(lightningcss@1.32.0)(magicast@0.5.2)(meow@13.2.0)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(optionator@0.9.4)(rolldown@1.0.0-beta.57)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-beta.57)(rollup@4.60.4))(rollup@4.60.4)(terser@5.45.0)(tsx@4.21.0)(typescript@5.9.3)(vue-tsc@3.2.9(typescript@5.9.3))(vue@3.5.34(typescript@5.9.3))(yaml@2.9.0)': + '@nuxt/vite-builder@4.4.5(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@25.8.0)(eslint@10.5.0(jiti@2.7.0))(lightningcss@1.32.0)(magicast@0.5.2)(meow@13.2.0)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(optionator@0.9.4)(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup@4.60.4))(rollup@4.60.4)(terser@5.45.0)(tsx@4.21.0)(typescript@5.9.3)(vue-tsc@3.2.9(typescript@5.9.3))(vue@3.5.34(typescript@5.9.3))(yaml@2.9.0)': dependencies: '@nuxt/kit': 4.4.5(magicast@0.5.2) '@rollup/plugin-replace': 6.0.3(rollup@4.60.4) @@ -13678,8 +13662,8 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.2 mocked-exports: 0.1.1 - nuxt: 4.4.5(9fcd7314e3eecf945123399c7742c3cc) - nypm: 0.6.6 + nuxt: 4.4.5(62a2f48613f90195bda1847309522735) + nypm: 0.6.7 pathe: 2.0.3 pkg-types: 2.3.1 postcss: 8.5.14 @@ -13694,8 +13678,8 @@ snapshots: vue-bundle-renderer: 2.2.0 optionalDependencies: '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - rolldown: 1.0.0-beta.57 - rollup-plugin-visualizer: 7.0.1(rolldown@1.0.0-beta.57)(rollup@4.60.4) + rolldown: 1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + rollup-plugin-visualizer: 7.0.1(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup@4.60.4) transitivePeerDependencies: - '@biomejs/biome' - '@types/node' @@ -13740,7 +13724,7 @@ snapshots: mlly: 1.8.2 mocked-exports: 0.1.1 nuxt: 4.4.5(baa66a6a16e24a1b54c16423ff94cee6) - nypm: 0.6.6 + nypm: 0.6.7 pathe: 2.0.3 pkg-types: 2.3.1 postcss: 8.5.14 @@ -13801,7 +13785,7 @@ snapshots: mlly: 1.8.2 mocked-exports: 0.1.1 nuxt: 4.4.5(c678397a5d359c68ec65d805733eddf4) - nypm: 0.6.6 + nypm: 0.6.7 pathe: 2.0.3 pkg-types: 2.3.1 postcss: 8.5.14 @@ -13862,7 +13846,7 @@ snapshots: mlly: 1.8.2 mocked-exports: 0.1.1 nuxt: 4.4.8(15f32171ed0ebfb0b2ff2e97d246f626) - nypm: 0.6.6 + nypm: 0.6.7 pathe: 2.0.3 pkg-types: 2.3.1 postcss: 8.5.15 @@ -13904,12 +13888,12 @@ snapshots: '@nuxthub/core@0.10.7(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1)(magicast@0.5.2)(typescript@5.9.3)(vue-tsc@3.2.9(typescript@5.9.3))': dependencies: - '@cloudflare/workers-types': 4.20260518.1 + '@cloudflare/workers-types': 4.20260617.1 '@nuxt/kit': 4.4.5(magicast@0.5.2) '@uploadthing/mime-types': 0.3.6 - c12: 3.3.3(magicast@0.5.2) + c12: 3.3.4(magicast@0.5.2) chokidar: 5.0.0 - citty: 0.2.1 + citty: 0.2.2 consola: 3.4.2 defu: 6.1.7 execa: 9.6.1 @@ -13920,14 +13904,14 @@ snapshots: nypm: 0.6.6 ofetch: 1.5.1 pathe: 2.0.3 - pkg-types: 2.3.0 + pkg-types: 2.3.1 scule: 1.3.0 std-env: 3.10.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 tsdown: 0.18.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(typescript@5.9.3)(vue-tsc@3.2.9(typescript@5.9.3)) - ufo: 1.6.3 + ufo: 1.6.4 uncrypto: 0.1.3 - unstorage: 1.17.4(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1) + unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1) zod: 4.3.6 transitivePeerDependencies: - '@arethetypeswrong/core' @@ -13969,7 +13953,7 @@ snapshots: '@nuxt/kit': 3.21.2(magicast@0.5.2) pathe: 1.1.2 pkg-types: 1.3.1 - semver: 7.8.0 + semver: 7.8.4 transitivePeerDependencies: - magicast @@ -14036,9 +14020,9 @@ snapshots: '@nuxtjs/mcp-toolkit@0.13.4(h3@1.15.11)(magicast@0.5.2)(zod@4.3.6)': dependencies: '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) h3: 1.15.11 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 zod: 4.3.6 transitivePeerDependencies: - '@cfworker/json-schema' @@ -14047,7 +14031,7 @@ snapshots: '@nuxtjs/mdc@0.21.1(magicast@0.5.2)': dependencies: - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) '@shikijs/core': 4.0.2 '@shikijs/engine-javascript': 4.0.2 '@shikijs/langs': 4.0.2 @@ -14154,15 +14138,15 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxtjs/robots@6.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6)': + '@nuxtjs/robots@6.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6)': dependencies: '@fingerprintjs/botd': 2.0.0 - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) consola: 3.4.2 defu: 6.1.7 h3: 1.15.11 - nuxt-site-config: 4.0.8(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) - nuxtseo-shared: 5.1.3(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@3.25.76))(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) + nuxt-site-config: 4.0.8(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) + nuxtseo-shared: 5.1.3(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@3.25.76))(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) pathe: 2.0.3 pkg-types: 2.3.1 ufo: 1.6.4 @@ -15034,9 +15018,12 @@ snapshots: '@rolldown/binding-openharmony-arm64@1.0.3': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.57': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true '@rolldown/binding-wasm32-wasi@1.0.0-beta.60(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': @@ -16019,8 +16006,8 @@ snapshots: '@typescript-eslint/visitor-keys': 8.57.2 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.0 - tinyglobby: 0.2.16 + semver: 7.8.4 + tinyglobby: 0.2.17 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -16034,8 +16021,8 @@ snapshots: '@typescript-eslint/visitor-keys': 8.61.1 debug: 4.4.3 minimatch: 10.2.5 - semver: 7.8.0 - tinyglobby: 0.2.16 + semver: 7.8.4 + tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -16141,9 +16128,9 @@ snapshots: dependencies: valibot: 1.4.1(typescript@5.9.3) - '@vercel/analytics@2.0.1(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vue@3.5.34(typescript@5.9.3))': + '@vercel/analytics@2.0.1(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vue@3.5.34(typescript@5.9.3))': optionalDependencies: - nuxt: 4.4.5(9fcd7314e3eecf945123399c7742c3cc) + nuxt: 4.4.5(62a2f48613f90195bda1847309522735) vue: 3.5.34(typescript@5.9.3) '@vercel/nft@1.5.0(encoding@0.1.13)(rollup@4.60.4)': @@ -16167,9 +16154,9 @@ snapshots: '@vercel/oidc@3.2.0': {} - '@vercel/speed-insights@2.0.0(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vue@3.5.34(typescript@5.9.3))': + '@vercel/speed-insights@2.0.0(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vue@3.5.34(typescript@5.9.3))': optionalDependencies: - nuxt: 4.4.5(9fcd7314e3eecf945123399c7742c3cc) + nuxt: 4.4.5(62a2f48613f90195bda1847309522735) vue: 3.5.34(typescript@5.9.3) '@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))': @@ -16263,9 +16250,9 @@ snapshots: '@vue-macros/common@3.1.2(vue@3.5.34(typescript@5.9.3))': dependencies: - '@vue/compiler-sfc': 3.5.34 + '@vue/compiler-sfc': 3.5.38 ast-kit: 2.2.0 - local-pkg: 1.1.2 + local-pkg: 1.2.1 magic-string-ast: 1.0.3 unplugin-utils: 0.3.1 optionalDependencies: @@ -16273,9 +16260,9 @@ snapshots: '@vue-macros/common@3.1.2(vue@3.5.38(typescript@5.9.3))': dependencies: - '@vue/compiler-sfc': 3.5.34 + '@vue/compiler-sfc': 3.5.38 ast-kit: 2.2.0 - local-pkg: 1.1.2 + local-pkg: 1.2.1 magic-string-ast: 1.0.3 unplugin-utils: 0.3.1 optionalDependencies: @@ -16290,10 +16277,10 @@ snapshots: '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@vue/babel-helper-vue-transform-on': 2.0.1 '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.29.0) - '@vue/shared': 3.5.34 + '@vue/shared': 3.5.38 optionalDependencies: '@babel/core': 7.29.0 transitivePeerDependencies: @@ -16305,8 +16292,8 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/parser': 7.29.3 - '@vue/compiler-sfc': 3.5.34 + '@babel/parser': 7.29.7 + '@vue/compiler-sfc': 3.5.38 transitivePeerDependencies: - supports-color @@ -16363,14 +16350,14 @@ snapshots: '@vue/compiler-sfc@3.5.34': dependencies: - '@babel/parser': 7.29.3 + '@babel/parser': 7.29.7 '@vue/compiler-core': 3.5.34 '@vue/compiler-dom': 3.5.34 '@vue/compiler-ssr': 3.5.34 '@vue/shared': 3.5.34 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.14 + postcss: 8.5.15 source-map-js: 1.2.1 '@vue/compiler-sfc@3.5.38': @@ -16443,8 +16430,8 @@ snapshots: '@vue/language-core@3.2.9': dependencies: '@volar/language-core': 2.4.28 - '@vue/compiler-dom': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/compiler-dom': 3.5.38 + '@vue/shared': 3.5.38 alien-signals: 3.2.1 muggle-string: 0.4.1 path-browserify: 1.0.1 @@ -16455,7 +16442,7 @@ snapshots: dependencies: '@volar/language-core': 2.4.28 '@vue/compiler-dom': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/shared': 3.5.38 alien-signals: 3.2.1 muggle-string: 0.4.1 path-browserify: 1.0.1 @@ -16465,7 +16452,7 @@ snapshots: dependencies: '@volar/language-core': 2.4.28 '@vue/compiler-dom': 3.5.34 - '@vue/shared': 3.5.34 + '@vue/shared': 3.5.38 alien-signals: 3.2.1 muggle-string: 0.4.1 path-browserify: 1.0.1 @@ -16569,13 +16556,13 @@ snapshots: '@vueuse/metadata@14.3.0': {} - '@vueuse/nuxt@14.3.0(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vue@3.5.34(typescript@5.9.3))': + '@vueuse/nuxt@14.3.0(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vue@3.5.34(typescript@5.9.3))': dependencies: '@nuxt/kit': 4.4.5(magicast@0.5.2) '@vueuse/core': 14.3.0(vue@3.5.34(typescript@5.9.3)) '@vueuse/metadata': 14.3.0 local-pkg: 1.1.2 - nuxt: 4.4.5(9fcd7314e3eecf945123399c7742c3cc) + nuxt: 4.4.5(62a2f48613f90195bda1847309522735) vue: 3.5.34(typescript@5.9.3) transitivePeerDependencies: - magicast @@ -16744,7 +16731,7 @@ snapshots: ast-kit@2.2.0: dependencies: - '@babel/parser': 7.29.3 + '@babel/parser': 7.29.7 pathe: 2.0.3 ast-types@0.13.4: @@ -16758,8 +16745,8 @@ snapshots: ast-walker-scope@0.9.0: dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 ast-kit: 2.2.0 async-lock@1.4.1: {} @@ -16881,7 +16868,7 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.14 - caniuse-lite: 1.0.30001781 + caniuse-lite: 1.0.30001793 electron-to-chromium: 1.5.267 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -16916,7 +16903,7 @@ snapshots: bun-types@1.3.14: dependencies: - '@types/node': 25.8.0 + '@types/node': 25.9.3 bundle-name@4.1.0: dependencies: @@ -17066,8 +17053,6 @@ snapshots: dependencies: consola: 3.4.2 - citty@0.2.1: {} - citty@0.2.2: {} clean-git-ref@2.0.1: {} @@ -17186,7 +17171,7 @@ snapshots: conventional-commits-filter: 5.0.0 handlebars: 4.7.8 meow: 13.2.0 - semver: 7.8.0 + semver: 7.8.4 conventional-changelog@7.2.0(conventional-commits-filter@5.0.0): dependencies: @@ -17296,7 +17281,7 @@ snapshots: cssnano-preset-default@7.0.11(postcss@8.5.8): dependencies: - browserslist: 4.28.1 + browserslist: 4.28.2 css-declaration-sorter: 7.3.1(postcss@8.5.8) cssnano-utils: 5.0.1(postcss@8.5.8) postcss: 8.5.8 @@ -17524,7 +17509,7 @@ snapshots: diff@8.0.3: {} - docus@5.11.0(7be736386eac822290d67dfa1d56f35d): + docus@5.11.0(ce604dd20a7a75c44e066ad05d4cf9aa): dependencies: '@ai-sdk/gateway': 3.0.115(zod@4.3.6) '@ai-sdk/mcp': 1.0.42(zod@4.3.6) @@ -17539,7 +17524,7 @@ snapshots: '@nuxtjs/i18n': 10.3.0(@vue/compiler-dom@3.5.38)(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(eslint@10.5.0(jiti@2.7.0))(ioredis@5.10.1)(magicast@0.5.2)(rollup@4.60.4)(typescript@5.9.3)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3)) '@nuxtjs/mcp-toolkit': 0.13.4(h3@1.15.11)(magicast@0.5.2)(zod@4.3.6) '@nuxtjs/mdc': 0.21.1(magicast@0.5.2) - '@nuxtjs/robots': 6.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) + '@nuxtjs/robots': 6.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) '@shikijs/core': 4.0.2 '@shikijs/engine-javascript': 4.0.2 '@shikijs/langs': 4.0.2 @@ -17552,9 +17537,9 @@ snapshots: exsolve: 1.0.8 git-url-parse: 16.1.0 motion-v: 2.2.1(@vueuse/core@14.2.1(vue@3.5.34(typescript@5.9.3)))(vue@3.5.34(typescript@5.9.3)) - nuxt: 4.4.5(9fcd7314e3eecf945123399c7742c3cc) + nuxt: 4.4.5(62a2f48613f90195bda1847309522735) nuxt-llms: 0.2.0(magicast@0.5.2) - nuxt-og-image: 6.5.0(40230ecdb76470dff0ccde617fd5a564) + nuxt-og-image: 6.5.0(86a9d2a5375bab160bd82d7aedcce194) pathe: 2.0.3 pkg-types: 2.3.1 scule: 1.3.0 @@ -17702,7 +17687,7 @@ snapshots: '@one-ini/wasm': 0.1.1 commander: 10.0.1 minimatch: 9.0.1 - semver: 7.8.0 + semver: 7.8.4 ee-first@1.1.1: {} @@ -17980,7 +17965,7 @@ snapshots: eslint-import-context: 0.1.9(unrs-resolver@1.11.1) is-glob: 4.0.3 minimatch: 10.2.5 - semver: 7.8.0 + semver: 7.8.4 stable-hash-x: 0.2.0 unrs-resolver: 1.11.1 optionalDependencies: @@ -18035,7 +18020,7 @@ snapshots: jsesc: 3.1.0 pluralize: 8.0.0 regjsparser: 0.13.0 - semver: 7.8.0 + semver: 7.8.4 strip-indent: 4.1.1 eslint-plugin-vue@10.9.2(@stylistic/eslint-plugin@5.10.0(eslint@10.5.0(jiti@2.7.0)))(@typescript-eslint/parser@8.61.1(eslint@10.5.0(jiti@2.7.0))(typescript@5.9.3))(eslint@10.5.0(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@10.5.0(jiti@2.7.0))): @@ -18045,7 +18030,7 @@ snapshots: natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 7.1.1 - semver: 7.8.0 + semver: 7.8.4 vue-eslint-parser: 10.4.1(eslint@10.5.0(jiti@2.7.0)) xml-name-validator: 4.0.0 optionalDependencies: @@ -18497,7 +18482,7 @@ snapshots: consola: 3.4.2 defu: 6.1.7 node-fetch-native: 1.6.7 - nypm: 0.6.6 + nypm: 0.6.7 pathe: 2.0.3 giget@3.2.0: {} @@ -18872,6 +18857,8 @@ snapshots: image-meta@0.2.2: {} + image-size@2.0.2: {} + import-without-cache@0.2.5: {} impound@1.1.5: @@ -19158,7 +19145,7 @@ snapshots: acorn: 8.16.0 eslint-visitor-keys: 3.4.3 espree: 9.6.1 - semver: 7.8.0 + semver: 7.8.4 keyv@4.5.4: dependencies: @@ -19397,8 +19384,8 @@ snapshots: magicast@0.5.2: dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 source-map-js: 1.2.1 make-fetch-happen@9.1.0: @@ -19898,8 +19885,8 @@ snapshots: pkg-types: 2.3.1 postcss: 8.5.8 postcss-nested: 7.0.2(postcss@8.5.8) - semver: 7.8.0 - tinyglobby: 0.2.16 + semver: 7.8.4 + tinyglobby: 0.2.17 optionalDependencies: typescript: 5.9.3 vue: 3.5.38(typescript@5.9.3) @@ -19967,7 +19954,7 @@ snapshots: dependencies: type-fest: 2.19.0 - nitropack@2.13.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(encoding@0.1.13)(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57)(sqlite3@5.1.7): + nitropack@2.13.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(encoding@0.1.13)(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(sqlite3@5.1.7): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.60.4) @@ -20020,9 +20007,9 @@ snapshots: pretty-bytes: 7.1.0 radix3: 1.1.2 rollup: 4.60.4 - rollup-plugin-visualizer: 7.0.1(rolldown@1.0.0-beta.57)(rollup@4.60.4) + rollup-plugin-visualizer: 7.0.1(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup@4.60.4) scule: 1.3.0 - semver: 7.8.0 + semver: 7.8.4 serve-placeholder: 2.0.2 serve-static: 2.2.1 source-map: 0.7.6 @@ -20032,7 +20019,7 @@ snapshots: uncrypto: 0.1.3 unctx: 2.5.0 unenv: 2.0.0-rc.24 - unimport: 6.3.0(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57) + unimport: 6.3.0(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)) unplugin-utils: 0.3.1 unstorage: 1.17.5(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(ioredis@5.10.1) untyped: 2.0.0 @@ -20125,7 +20112,7 @@ snapshots: rollup: 4.60.4 rollup-plugin-visualizer: 7.0.1(rolldown@1.0.3)(rollup@4.60.4) scule: 1.3.0 - semver: 7.8.0 + semver: 7.8.4 serve-placeholder: 2.0.2 serve-static: 2.2.1 source-map: 0.7.6 @@ -20228,7 +20215,7 @@ snapshots: rollup: 4.60.4 rollup-plugin-visualizer: 7.0.1(rolldown@1.0.3)(rollup@4.60.4) scule: 1.3.0 - semver: 7.8.0 + semver: 7.8.4 serve-placeholder: 2.0.2 serve-static: 2.2.1 source-map: 0.7.6 @@ -20278,7 +20265,7 @@ snapshots: node-abi@3.85.0: dependencies: - semver: 7.8.0 + semver: 7.8.4 node-addon-api@7.1.1: {} @@ -20313,7 +20300,7 @@ snapshots: nopt: 5.0.0 npmlog: 6.0.2 rimraf: 3.0.2 - semver: 7.8.0 + semver: 7.8.4 tar: 6.2.1 which: 2.0.2 transitivePeerDependencies: @@ -20343,7 +20330,7 @@ snapshots: normalize-package-data@7.0.1: dependencies: hosted-git-info: 8.1.0 - semver: 7.8.0 + semver: 7.8.4 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -20390,10 +20377,10 @@ snapshots: transitivePeerDependencies: - magicast - nuxt-og-image@6.5.0(40230ecdb76470dff0ccde617fd5a564): + nuxt-og-image@6.5.0(86a9d2a5375bab160bd82d7aedcce194): dependencies: '@clack/prompts': 1.4.0 - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) '@unhead/vue': 2.1.15(vue@3.5.34(typescript@5.9.3)) '@vue/compiler-sfc': 3.5.34 chrome-launcher: 1.2.1 @@ -20406,9 +20393,9 @@ snapshots: magic-string: 0.30.21 magicast: 0.5.2 mocked-exports: 0.1.1 - nuxt-site-config: 4.0.8(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) - nuxtseo-shared: 5.1.3(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@3.25.76))(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) - nypm: 0.6.6 + nuxt-site-config: 4.0.8(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) + nuxtseo-shared: 5.1.3(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@3.25.76))(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) + nypm: 0.6.7 ofetch: 1.5.1 ohash: 2.0.11 oxc-parser: 0.128.0 @@ -20419,7 +20406,7 @@ snapshots: std-env: 4.1.0 strip-literal: 3.1.0 tinyexec: 1.1.2 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 ufo: 1.6.4 ultrahtml: 1.6.0 unplugin: 3.0.0 @@ -20440,7 +20427,7 @@ snapshots: nuxt-site-config-kit@4.0.8(magicast@0.5.2)(vue@3.5.34(typescript@5.9.3)): dependencies: - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) site-config-stack: 4.0.8(vue@3.5.34(typescript@5.9.3)) std-env: 4.1.0 ufo: 1.6.4 @@ -20448,12 +20435,12 @@ snapshots: - magicast - vue - nuxt-site-config@4.0.8(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6): + nuxt-site-config@4.0.8(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6): dependencies: - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) h3: 1.15.11 nuxt-site-config-kit: 4.0.8(magicast@0.5.2)(vue@3.5.34(typescript@5.9.3)) - nuxtseo-shared: 5.1.3(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@3.25.76))(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) + nuxtseo-shared: 5.1.3(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@3.25.76))(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) pathe: 2.0.3 pkg-types: 2.3.1 site-config-stack: 4.0.8(vue@3.5.34(typescript@5.9.3)) @@ -20510,16 +20497,16 @@ snapshots: - uploadthing - vue - nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc): + nuxt@4.4.5(62a2f48613f90195bda1847309522735): dependencies: '@dxup/nuxt': 0.4.1(magicast@0.5.2)(typescript@5.9.3) '@nuxt/cli': 3.35.2(@nuxt/schema@4.4.5)(cac@6.7.14)(magicast@0.5.2) '@nuxt/devtools': 3.2.4(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3)) '@nuxt/kit': 4.4.5(magicast@0.5.2) - '@nuxt/nitro-server': 4.4.5(@babel/core@7.29.0)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(encoding@0.1.13)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57)(sqlite3@5.1.7)(typescript@5.9.3) + '@nuxt/nitro-server': 4.4.5(@babel/core@7.29.0)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(db0@0.3.4(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(better-sqlite3@12.10.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(sqlite3@5.1.7))(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260617.1)(@electric-sql/pglite@0.4.2)(@libsql/client@0.17.3)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(bun-types@1.3.14)(sqlite3@5.1.7))(encoding@0.1.13)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(sqlite3@5.1.7)(typescript@5.9.3) '@nuxt/schema': 4.4.5 '@nuxt/telemetry': 2.8.0(@nuxt/kit@4.4.5(magicast@0.5.2)) - '@nuxt/vite-builder': 4.4.5(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@25.8.0)(eslint@10.5.0(jiti@2.7.0))(lightningcss@1.32.0)(magicast@0.5.2)(meow@13.2.0)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(optionator@0.9.4)(rolldown@1.0.0-beta.57)(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-beta.57)(rollup@4.60.4))(rollup@4.60.4)(terser@5.45.0)(tsx@4.21.0)(typescript@5.9.3)(vue-tsc@3.2.9(typescript@5.9.3))(vue@3.5.34(typescript@5.9.3))(yaml@2.9.0) + '@nuxt/vite-builder': 4.4.5(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@types/node@25.8.0)(eslint@10.5.0(jiti@2.7.0))(lightningcss@1.32.0)(magicast@0.5.2)(meow@13.2.0)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(optionator@0.9.4)(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup@4.60.4))(rollup@4.60.4)(terser@5.45.0)(tsx@4.21.0)(typescript@5.9.3)(vue-tsc@3.2.9(typescript@5.9.3))(vue@3.5.34(typescript@5.9.3))(yaml@2.9.0) '@unhead/vue': 2.1.15(vue@3.5.34(typescript@5.9.3)) '@vue/shared': 3.5.34 chokidar: 5.0.0 @@ -20561,12 +20548,12 @@ snapshots: ultrahtml: 1.6.0 uncrypto: 0.1.3 unctx: 2.5.0 - unimport: 6.3.0(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57) + unimport: 6.3.0(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)) unplugin: 3.0.0 unrouting: 0.1.7 untyped: 2.0.0 vue: 3.5.34(typescript@5.9.3) - vue-router: 5.0.7(@vue/compiler-sfc@3.5.38)(vue@3.5.34(typescript@5.9.3)) + vue-router: 5.1.0(@vue/compiler-sfc@3.5.38)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3)) optionalDependencies: '@parcel/watcher': 2.5.6 '@types/node': 25.8.0 @@ -20694,7 +20681,7 @@ snapshots: unrouting: 0.1.7 untyped: 2.0.0 vue: 3.5.34(typescript@5.9.3) - vue-router: 5.0.7(@vue/compiler-sfc@3.5.31)(vue@3.5.34(typescript@5.9.3)) + vue-router: 5.1.0(@vue/compiler-sfc@3.5.31)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3)) optionalDependencies: '@parcel/watcher': 2.5.6 '@types/node': 25.8.0 @@ -20822,7 +20809,7 @@ snapshots: unrouting: 0.1.7 untyped: 2.0.0 vue: 3.5.34(typescript@5.9.3) - vue-router: 5.0.7(@vue/compiler-sfc@3.5.38)(vue@3.5.34(typescript@5.9.3)) + vue-router: 5.1.0(@vue/compiler-sfc@3.5.38)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3)) optionalDependencies: '@parcel/watcher': 2.5.6 '@types/node': 25.8.0 @@ -20924,7 +20911,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.2 nanotar: 0.3.0 - nypm: 0.6.6 + nypm: 0.6.7 ofetch: 1.5.1 ohash: 2.0.11 on-change: 6.0.2 @@ -21021,16 +21008,16 @@ snapshots: - xml2js - yaml - nuxtseo-shared@5.1.3(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@3.25.76))(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6): + nuxtseo-shared@5.1.3(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@3.25.76))(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6): dependencies: '@clack/prompts': 1.4.0 '@nuxt/devtools-kit': 4.0.0-alpha.3(magicast@0.5.2)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0)) - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) '@nuxt/schema': 4.4.5 birpc: 4.0.0 consola: 3.4.2 defu: 6.1.7 - nuxt: 4.4.5(9fcd7314e3eecf945123399c7742c3cc) + nuxt: 4.4.5(62a2f48613f90195bda1847309522735) ofetch: 1.5.1 pathe: 2.0.3 pkg-types: 2.3.1 @@ -21040,22 +21027,22 @@ snapshots: ufo: 1.6.4 vue: 3.5.34(typescript@5.9.3) optionalDependencies: - nuxt-site-config: 4.0.8(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) + nuxt-site-config: 4.0.8(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) zod: 4.3.6 transitivePeerDependencies: - magicast - vite - nuxtseo-shared@5.1.3(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@3.25.76))(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6): + nuxtseo-shared@5.1.3(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt-site-config@4.0.8(@nuxt/schema@4.4.5)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@3.25.76))(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6): dependencies: '@clack/prompts': 1.4.0 '@nuxt/devtools-kit': 4.0.0-alpha.3(magicast@0.5.2)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0)) - '@nuxt/kit': 4.4.5(magicast@0.5.2) + '@nuxt/kit': 4.4.8(magicast@0.5.2) '@nuxt/schema': 4.4.8 birpc: 4.0.0 consola: 3.4.2 defu: 6.1.7 - nuxt: 4.4.5(9fcd7314e3eecf945123399c7742c3cc) + nuxt: 4.4.5(62a2f48613f90195bda1847309522735) ofetch: 1.5.1 pathe: 2.0.3 pkg-types: 2.3.1 @@ -21065,7 +21052,7 @@ snapshots: ufo: 1.6.4 vue: 3.5.34(typescript@5.9.3) optionalDependencies: - nuxt-site-config: 4.0.8(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt@4.4.5(9fcd7314e3eecf945123399c7742c3cc))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) + nuxt-site-config: 4.0.8(@nuxt/schema@4.4.8)(magicast@0.5.2)(nuxt@4.4.5(62a2f48613f90195bda1847309522735))(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3))(zod@4.3.6) zod: 4.3.6 transitivePeerDependencies: - magicast @@ -21527,12 +21514,6 @@ snapshots: mlly: 1.8.2 pathe: 2.0.3 - pkg-types@2.3.0: - dependencies: - confbox: 0.2.4 - exsolve: 1.0.8 - pathe: 2.0.3 - pkg-types@2.3.1: dependencies: confbox: 0.2.4 @@ -22486,24 +22467,24 @@ snapshots: glob: 7.2.3 optional: true - rolldown-plugin-dts@0.20.0(rolldown@1.0.0-beta.57)(typescript@5.9.3)(vue-tsc@3.2.9(typescript@5.9.3)): + rolldown-plugin-dts@0.20.0(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(typescript@5.9.3)(vue-tsc@3.2.9(typescript@5.9.3)): dependencies: '@babel/generator': 7.29.1 - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 ast-kit: 2.2.0 birpc: 4.0.0 dts-resolver: 2.1.3 get-tsconfig: 4.13.0 obug: 2.1.1 - rolldown: 1.0.0-beta.57 + rolldown: 1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optionalDependencies: typescript: 5.9.3 vue-tsc: 3.2.9(typescript@5.9.3) transitivePeerDependencies: - oxc-resolver - rolldown@1.0.0-beta.57: + rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: '@oxc-project/types': 0.103.0 '@rolldown/pluginutils': 1.0.0-beta.57 @@ -22518,9 +22499,12 @@ snapshots: '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.57 '@rolldown/binding-linux-x64-musl': 1.0.0-beta.57 '@rolldown/binding-openharmony-arm64': 1.0.0-beta.57 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.57 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.57 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.57 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' rolldown@1.0.0-beta.60(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: @@ -22573,14 +22557,14 @@ snapshots: optionalDependencies: '@babel/code-frame': 7.29.0 - rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-beta.57)(rollup@4.60.4): + rollup-plugin-visualizer@7.0.1(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rollup@4.60.4): dependencies: open: 11.0.0 picomatch: 4.0.4 source-map: 0.7.6 yargs: 18.0.0 optionalDependencies: - rolldown: 1.0.0-beta.57 + rolldown: 1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) rollup: 4.60.4 rollup-plugin-visualizer@7.0.1(rolldown@1.0.3)(rollup@4.60.4): @@ -22756,7 +22740,7 @@ snapshots: dependencies: '@img/colour': 1.0.0 detect-libc: 2.1.2 - semver: 7.8.0 + semver: 7.8.4 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -23076,13 +23060,13 @@ snapshots: dependencies: browserslist: 4.28.2 postcss: 8.5.14 - postcss-selector-parser: 7.1.1 + postcss-selector-parser: 7.1.4 stylehacks@7.0.7(postcss@8.5.8): dependencies: browserslist: 4.28.2 postcss: 8.5.8 - postcss-selector-parser: 7.1.1 + postcss-selector-parser: 7.1.4 stylehacks@8.0.1(postcss@8.5.15): dependencies: @@ -23266,15 +23250,15 @@ snapshots: cac: 6.7.14 defu: 6.1.7 empathic: 2.0.0 - hookable: 6.1.0 + hookable: 6.1.1 import-without-cache: 0.2.5 obug: 2.1.1 picomatch: 4.0.4 - rolldown: 1.0.0-beta.57 - rolldown-plugin-dts: 0.20.0(rolldown@1.0.0-beta.57)(typescript@5.9.3)(vue-tsc@3.2.9(typescript@5.9.3)) - semver: 7.8.0 - tinyexec: 1.1.2 - tinyglobby: 0.2.16 + rolldown: 1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + rolldown-plugin-dts: 0.20.0(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(typescript@5.9.3)(vue-tsc@3.2.9(typescript@5.9.3)) + semver: 7.8.4 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 tree-kill: 1.2.2 unconfig-core: 7.4.2 unrun: 0.2.25(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) @@ -23363,7 +23347,7 @@ snapshots: rollup: 4.55.1 rollup-plugin-dts: 6.3.0(rollup@4.55.1)(typescript@5.9.3) scule: 1.3.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 untyped: 2.0.0 optionalDependencies: typescript: 5.9.3 @@ -23438,16 +23422,16 @@ snapshots: pkg-types: 2.3.1 scule: 1.3.0 strip-literal: 3.1.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 unplugin: 2.3.11 unplugin-utils: 0.3.1 - unimport@6.3.0(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57): + unimport@6.3.0(oxc-parser@0.128.0)(rolldown@1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)): dependencies: acorn: 8.16.0 escape-string-regexp: 5.0.0 estree-walker: 3.0.3 - local-pkg: 1.1.2 + local-pkg: 1.2.1 magic-string: 0.30.21 mlly: 1.8.2 pathe: 2.0.3 @@ -23455,19 +23439,19 @@ snapshots: pkg-types: 2.3.1 scule: 1.3.0 strip-literal: 3.1.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 unplugin: 3.0.0 unplugin-utils: 0.3.1 optionalDependencies: oxc-parser: 0.128.0 - rolldown: 1.0.0-beta.57 + rolldown: 1.0.0-beta.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) unimport@6.3.0(oxc-parser@0.128.0)(rolldown@1.0.3): dependencies: acorn: 8.16.0 escape-string-regexp: 5.0.0 estree-walker: 3.0.3 - local-pkg: 1.1.2 + local-pkg: 1.2.1 magic-string: 0.30.21 mlly: 1.8.2 pathe: 2.0.3 @@ -23475,7 +23459,7 @@ snapshots: pkg-types: 2.3.1 scule: 1.3.0 strip-literal: 3.1.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 unplugin: 3.0.0 unplugin-utils: 0.3.1 optionalDependencies: @@ -23487,7 +23471,7 @@ snapshots: acorn: 8.16.0 escape-string-regexp: 5.0.0 estree-walker: 3.0.3 - local-pkg: 1.1.2 + local-pkg: 1.2.1 magic-string: 0.30.21 mlly: 1.8.2 pathe: 2.0.3 @@ -23495,7 +23479,7 @@ snapshots: pkg-types: 2.3.1 scule: 1.3.0 strip-literal: 3.1.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 unplugin: 3.0.0 unplugin-utils: 0.3.1 optionalDependencies: @@ -23592,7 +23576,7 @@ snapshots: mlly: 1.8.2 obug: 2.1.1 picomatch: 4.0.4 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 unplugin: 3.0.0 unplugin-utils: 0.3.1 vue: 3.5.34(typescript@5.9.3) @@ -23829,7 +23813,7 @@ snapshots: picomatch: 4.0.4 proper-lockfile: 4.1.2 tiny-invariant: 1.3.3 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 vite: 7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0) vscode-uri: 3.1.0 optionalDependencies: @@ -23848,7 +23832,7 @@ snapshots: picomatch: 4.0.4 proper-lockfile: 4.1.2 tiny-invariant: 1.3.3 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 vite: 7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0) vscode-uri: 3.1.0 optionalDependencies: @@ -23917,9 +23901,9 @@ snapshots: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.14 + postcss: 8.5.15 rollup: 4.60.4 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.8.0 fsevents: 2.3.3 @@ -23934,9 +23918,9 @@ snapshots: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.14 + postcss: 8.5.15 rollup: 4.60.4 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 25.9.3 fsevents: 2.3.3 @@ -24048,7 +24032,7 @@ snapshots: eslint-visitor-keys: 5.0.1 espree: 11.2.0 esquery: 1.7.0 - semver: 7.8.0 + semver: 7.8.4 transitivePeerDependencies: - supports-color @@ -24074,30 +24058,7 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.4 scule: 1.3.0 - tinyglobby: 0.2.16 - unplugin: 3.0.0 - unplugin-utils: 0.3.1 - vue: 3.5.34(typescript@5.9.3) - yaml: 2.8.2 - optionalDependencies: - '@vue/compiler-sfc': 3.5.31 - - vue-router@5.0.7(@vue/compiler-sfc@3.5.31)(vue@3.5.34(typescript@5.9.3)): - dependencies: - '@babel/generator': 8.0.0-rc.5 - '@vue-macros/common': 3.1.2(vue@3.5.34(typescript@5.9.3)) - '@vue/devtools-api': 8.1.1 - ast-walker-scope: 0.8.3 - chokidar: 5.0.0 - json5: 2.2.3 - local-pkg: 1.1.2 - magic-string: 0.30.21 - mlly: 1.8.2 - muggle-string: 0.4.1 - pathe: 2.0.3 - picomatch: 4.0.4 - scule: 1.3.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 unplugin: 3.0.0 unplugin-utils: 0.3.1 vue: 3.5.34(typescript@5.9.3) @@ -24105,29 +24066,6 @@ snapshots: optionalDependencies: '@vue/compiler-sfc': 3.5.31 - vue-router@5.0.7(@vue/compiler-sfc@3.5.38)(vue@3.5.34(typescript@5.9.3)): - dependencies: - '@babel/generator': 8.0.0-rc.5 - '@vue-macros/common': 3.1.2(vue@3.5.34(typescript@5.9.3)) - '@vue/devtools-api': 8.1.1 - ast-walker-scope: 0.8.3 - chokidar: 5.0.0 - json5: 2.2.3 - local-pkg: 1.1.2 - magic-string: 0.30.21 - mlly: 1.8.2 - muggle-string: 0.4.1 - pathe: 2.0.3 - picomatch: 4.0.4 - scule: 1.3.0 - tinyglobby: 0.2.16 - unplugin: 3.0.0 - unplugin-utils: 0.3.1 - vue: 3.5.34(typescript@5.9.3) - yaml: 2.8.2 - optionalDependencies: - '@vue/compiler-sfc': 3.5.38 - vue-router@5.1.0(@vue/compiler-sfc@3.5.31)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3)): dependencies: '@babel/generator': 8.0.0-rc.5 @@ -24136,14 +24074,14 @@ snapshots: ast-walker-scope: 0.9.0 chokidar: 5.0.0 json5: 2.2.3 - local-pkg: 1.1.2 + local-pkg: 1.2.1 magic-string: 0.30.21 mlly: 1.8.2 muggle-string: 0.4.1 pathe: 2.0.3 picomatch: 4.0.4 scule: 1.3.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 unplugin: 3.0.0 unplugin-utils: 0.3.1 vue: 3.5.34(typescript@5.9.3) @@ -24151,7 +24089,6 @@ snapshots: optionalDependencies: '@vue/compiler-sfc': 3.5.31 vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0) - optional: true vue-router@5.1.0(@vue/compiler-sfc@3.5.38)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.34(typescript@5.9.3)): dependencies: @@ -24161,14 +24098,14 @@ snapshots: ast-walker-scope: 0.9.0 chokidar: 5.0.0 json5: 2.2.3 - local-pkg: 1.1.2 + local-pkg: 1.2.1 magic-string: 0.30.21 mlly: 1.8.2 muggle-string: 0.4.1 pathe: 2.0.3 picomatch: 4.0.4 scule: 1.3.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 unplugin: 3.0.0 unplugin-utils: 0.3.1 vue: 3.5.34(typescript@5.9.3) @@ -24176,7 +24113,6 @@ snapshots: optionalDependencies: '@vue/compiler-sfc': 3.5.38 vite: 8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0) - optional: true vue-router@5.1.0(@vue/compiler-sfc@3.5.38)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.45.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.38(typescript@5.9.3)): dependencies: @@ -24186,14 +24122,14 @@ snapshots: ast-walker-scope: 0.9.0 chokidar: 5.0.0 json5: 2.2.3 - local-pkg: 1.1.2 + local-pkg: 1.2.1 magic-string: 0.30.21 mlly: 1.8.2 muggle-string: 0.4.1 pathe: 2.0.3 picomatch: 4.0.4 scule: 1.3.0 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 unplugin: 3.0.0 unplugin-utils: 0.3.1 vue: 3.5.38(typescript@5.9.3) diff --git a/src/module.ts b/src/module.ts index e9860ee8c..f3cee20e9 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,4 +1,4 @@ -import { stat } from 'node:fs/promises' +import { mkdir, stat } from 'node:fs/promises' import { defineNuxtModule, createResolver, @@ -33,6 +33,7 @@ import { findPreset } from './presets' import type { Manifest } from './types/manifest' import { setupPreview, setupPreviewWithAPI, shouldEnablePreview } from './utils/preview/module' import { parseSourceBase } from './utils/source' +import { createAfterParseHandler, DEFAULT_ASSET_EXTENSIONS, discoverAndCopyAssets, matchImageSizeHints, setAssetExtensions, type AssetIndex, type UnresolvedIndex } from './utils/assets' import { databaseVersion, getLocalDatabase, refineDatabaseConfig, resolveDatabaseAdapter } from './utils/database' import type { ParsedContentFile } from './types' import { initiateValidatorsContext } from './utils/dependencies' @@ -64,6 +65,13 @@ const moduleDefaults: Partial = { delimiter: ',', json: true, }, + assets: { + enabled: true, + imageSize: 'style', + blankLinks: true, + prefix: '', + debug: false, + }, }, experimental: { nativeSqlite: false, @@ -121,6 +129,35 @@ export default defineNuxtModule({ const { collections } = await loadContentConfig(nuxt, options) manifest.collections = collections + // Resolve asset options once; `null` disables the feature. + const assetsConfig = options.build?.assets + const assets = assetsConfig === false || assetsConfig?.enabled === false + ? null + : { + imageSizes: matchImageSizeHints(assetsConfig?.imageSize), + extensions: assetsConfig?.extensions || DEFAULT_ASSET_EXTENSIONS, + blankLinks: assetsConfig?.blankLinks !== false, + prefix: assetsConfig?.prefix || '', + debug: !!assetsConfig?.debug, + publicDir: join(nuxt.options.buildDir, 'content', 'assets', 'public'), + } + const assetIndex: AssetIndex = new Map() + const unresolvedAssets: UnresolvedIndex = new Map() + if (assets) { + setAssetExtensions(assets.extensions) + nuxt.hook('content:file:afterParse', createAfterParseHandler(assetIndex, { + imageSizes: assets.imageSizes, + blankLinks: assets.blankLinks, + unresolved: unresolvedAssets, + })) + + if (hasNuxtModule('@nuxt/image', nuxt)) { + await mkdir(assets.publicDir, { recursive: true }) + const image = ((nuxt.options as unknown as { image?: { dirs?: string[] } }).image ??= {}) + image.dirs = [...(image.dirs || []), assets.publicDir] + } + } + nuxt.options.vite.optimizeDeps = defu(nuxt.options.vite.optimizeDeps, { exclude: ['@sqlite.org/sqlite-wasm'], }) @@ -192,6 +229,14 @@ export default defineNuxtModule({ const preset = findPreset(nuxt) await preset.setupNitro(config, { manifest, resolver, moduleOptions: options, nuxt }) + // Serve copied assets; pushed last so the user's `public/` wins a collision. + if (assets) { + config.publicAssets ||= [] + if (!config.publicAssets.some(asset => asset?.dir === assets.publicDir)) { + config.publicAssets.push({ dir: assets.publicDir }) + } + } + const sqliteConnector = options.experimental?.sqliteConnector || (options.experimental?.nativeSqlite ? 'native' : undefined) const resolveOptions = { resolver, sqliteConnector } config.alias ||= {} @@ -216,6 +261,7 @@ export default defineNuxtModule({ nuxt, moduleOptions: options, manifest, + assets: assets ? { ...assets, index: assetIndex, unresolved: unresolvedAssets } : null, })) } }) @@ -254,7 +300,29 @@ export default defineNuxtModule({ // Generate collections and sql dump to update templates local database // `modules:done` is triggered for all environments nuxt.hook('modules:done', async () => { - const fest = await processCollectionItems(nuxt, manifest.collections, options) + // Discover and copy co-located assets before parsing, so the index is ready + // when the `content:file:afterParse` hook rewrites references. + let assetsHash = '' + if (assets) { + const { index } = await discoverAndCopyAssets(nuxt, manifest.collections, { + publicDir: assets.publicDir, + extensions: assets.extensions, + imageSizes: assets.imageSizes, + prefix: assets.prefix, + debug: assets.debug, + }) + index.forEach((value, key) => assetIndex.set(key, value)) + // Fold the asset state into the parse cache key so an asset change + // (size, addition, removal) re-parses dependent content. + assetsHash = hash({ + config: { imageSizes: assets.imageSizes, blankLinks: assets.blankLinks, prefix: assets.prefix }, + index: [...assetIndex.entries()] + .map(([key, entry]) => [key, entry.publicSrc, entry.width, entry.height]) + .sort((a, b) => String(a[0]).localeCompare(String(b[0]))), + }) + } + + const fest = await processCollectionItems(nuxt, manifest.collections, options, assetsHash) // Update manifest manifest.checksumStructure = fest.checksumStructure @@ -282,7 +350,7 @@ export default defineNuxtModule({ }, }) -async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollection[], options: ModuleOptions) { +async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollection[], options: ModuleOptions, assetsHash = '') { const collectionDump: Record = {} const collectionChecksum: Record = {} const collectionChecksumStructure: Record = {} @@ -296,6 +364,7 @@ async function processCollectionItems(nuxt: Nuxt, collections: ResolvedCollectio const configHash = hash({ mdcHighlight: (nuxt.options as unknown as { mdc: MDCModuleOptions }).mdc?.highlight, contentBuild: options.build?.markdown, + assets: assetsHash, }) const infoCollection = collections.find(c => c.name === 'info')! diff --git a/src/runtime/plugins/websocket.dev.ts b/src/runtime/plugins/websocket.dev.ts index a9317c5c1..f2501ccc6 100644 --- a/src/runtime/plugins/websocket.dev.ts +++ b/src/runtime/plugins/websocket.dev.ts @@ -1,12 +1,21 @@ import { defineNuxtPlugin } from 'nuxt/app' import { refreshNuxtData } from '#imports' -type HotEvent = (event: 'nuxt-content:update', callback: (data: { collection: string, key: string, queries: string[] }) => void) => void +interface ContentUpdate { collection: string, key: string, queries: string[] } +interface AssetUpdate { event: 'update' | 'remove', src: string, width?: number, height?: number } + +interface ContentHot { + on(event: 'nuxt-content:update', callback: (data: ContentUpdate) => void): void + on(event: 'nuxt-content:assets:update', callback: (data: AssetUpdate) => void): void +} + export default defineNuxtPlugin(() => { if (!import.meta.hot || !import.meta.client) return + const hot = import.meta.hot as unknown as ContentHot + import('../internal/database.client').then(({ loadDatabaseAdapter }) => { - ;(import.meta.hot as unknown as { on: HotEvent }).on('nuxt-content:update', async (data) => { + hot.on('nuxt-content:update', async (data) => { if (!data || !data.collection || !Array.isArray(data.queries)) return try { const db = await loadDatabaseAdapter(data.collection) @@ -25,4 +34,34 @@ export default defineNuxtPlugin(() => { } }) }) + + hot.on('nuxt-content:assets:update', (data) => { + if (!data || !data.src) return + const isUpdate = data.event === 'update' + const { src, width, height } = data + const safeSrc = src.replace(/["\\]/g, '\\$&') + document + .querySelectorAll(`:is(img, video, source, embed, iframe):where([src^="${safeSrc}"])`) + .forEach((node) => { + const element = node as HTMLImageElement + element.style.opacity = isUpdate ? '1' : '0.2' + if (!isUpdate) return + + const params = new URLSearchParams(element.getAttribute('src')!.split('?')[1] || '') + params.set('time', String(Date.now())) + + if (width && height) { + element.addEventListener('load', function onLoad() { + if (element.hasAttribute('width')) element.setAttribute('width', String(width)) + if (element.hasAttribute('height')) element.setAttribute('height', String(height)) + if (element.style.aspectRatio) { + element.style.aspectRatio = `${width} / ${height}` + } + element.removeEventListener('load', onLoad) + }) + } + + element.setAttribute('src', `${src}?${params.toString()}`) + }) + }) }) diff --git a/src/types/module.ts b/src/types/module.ts index 502c270d7..926149f25 100644 --- a/src/types/module.ts +++ b/src/types/module.ts @@ -194,6 +194,52 @@ export interface ModuleOptions { json?: boolean delimiter?: string } + /** + * Relative asset resolution. + * + * Nuxt Content resolves relative paths to images, video and documents + * co-located with your content, in markdown, MDC components and frontmatter. + * Matching files are copied next to the built site and the references are + * rewritten to absolute URLs. + * + * On by default. Set `enabled` to `false` to opt out. + */ + assets?: false | { + /** + * Whether to resolve relative asset paths. + * @default true + */ + enabled?: boolean + /** + * Inject image dimensions to avoid layout shift. + * - `'style'`: adds `style="aspect-ratio:W/H"` to `` (default) + * - `'attrs'`: adds `width` / `height` attributes to `` + * - `'src'` / `'url'`: adds a `?width=W&height=H` query (frontmatter values) + * - `''` or `[]`: inject nothing + * @default 'style' + */ + imageSize?: '' | string | Array<'style' | 'attrs' | 'src' | 'url'> + /** + * File extensions treated as assets, hence excluded from content collections. + * @default + */ + extensions?: string[] + /** + * Open links that point to an asset in a new tab. + * @default true + */ + blankLinks?: boolean + /** + * URL prefix prepended to served asset paths (namespacing / collision avoidance). + * @default '' + */ + prefix?: string + /** + * Output debug messages while discovering and copying assets. + * @default false + */ + debug?: boolean + } } experimental?: { diff --git a/src/utils/assets/discover.ts b/src/utils/assets/discover.ts new file mode 100644 index 000000000..326dcb0fe --- /dev/null +++ b/src/utils/assets/discover.ts @@ -0,0 +1,96 @@ +import { cp, mkdir, readFile, rm } from 'node:fs/promises' +import { dirname, join } from 'pathe' +import { withLeadingSlash } from 'ufo' +import { glob } from 'tinyglobby' +import { imageSize } from 'image-size' +import type { Nuxt } from '@nuxt/schema' +import type { ResolvedCollection } from '../../types/collection' +import { getExcludedSourcePaths, parseSourceBase } from '../source' +import { logger } from '../dev' +import { removeOrdering } from './paths' +import { isImage, type AssetIndex, type ImageSizeHint } from './shared' + +export function assetPublicUrl(relativeKey: string, sourcePrefix: string, prefix: string): string { + return withLeadingSlash(join(prefix, sourcePrefix || '', removeOrdering(relativeKey))) +} + +export function assetIndexKey(absolutePath: string): string { + return removeOrdering(absolutePath) +} + +export async function getAssetSize(absolutePath: string): Promise<{ width?: number, height?: number }> { + try { + const { width, height } = imageSize(await readFile(absolutePath)) + return { width, height } + } + catch { + logger.warn(`Could not read image size of "${absolutePath}"`) + return {} + } +} + +export interface DiscoverAssetsOptions { + publicDir: string + extensions: string[] + imageSizes: ImageSizeHint[] + prefix: string + debug?: boolean +} + +// Runs before content is parsed so the index is ready when `content:file:afterParse` fires. +export async function discoverAndCopyAssets( + nuxt: Nuxt, + collections: ResolvedCollection[], + options: DiscoverAssetsOptions, +): Promise<{ index: AssetIndex, count: number }> { + const index: AssetIndex = new Map() + const { publicDir, extensions, imageSizes, prefix } = options + const detectSizes = imageSizes.length > 0 + const pattern = `**/*.{${extensions.join(',')}}` + + await rm(publicDir, { recursive: true, force: true }) + + for (const collection of collections) { + if (!collection.source) { + continue + } + for (const source of collection.source) { + // @ts-expect-error - `__rootDir` is a private property storing the layer's cwd + const rootDir = collection.__rootDir || nuxt.options.rootDir + await source.prepare?.({ rootDir }) + const cwd = source.cwd + if (!cwd) { + continue + } + + const { fixed } = parseSourceBase(source) + const scanDir = join(cwd, fixed) + const keys = await glob(pattern, { + cwd: scanDir, + ignore: getExcludedSourcePaths(source), + dot: true, + expandDirectories: false, + }).catch((): string[] => []) + + for (const key of keys) { + const absoluteSource = join(scanDir, key) + const indexKey = assetIndexKey(absoluteSource) + if (index.has(indexKey)) { + continue + } + const url = assetPublicUrl(key, source.prefix || '', prefix) + const target = join(publicDir, url) + await mkdir(dirname(target), { recursive: true }) + await cp(absoluteSource, target) + + const { width, height } = detectSizes && isImage(absoluteSource) ? await getAssetSize(absoluteSource) : {} + index.set(indexKey, { publicSrc: url, width, height, content: [] }) + } + } + } + + if (options.debug) { + logger.info(`Copied ${index.size} content asset(s) to ${publicDir}`) + } + return { index, count: index.size } +} diff --git a/src/utils/assets/index.ts b/src/utils/assets/index.ts new file mode 100644 index 000000000..49d899c44 --- /dev/null +++ b/src/utils/assets/index.ts @@ -0,0 +1,5 @@ +export * from './state' +export * from './paths' +export * from './shared' +export * from './discover' +export * from './rewrite' diff --git a/src/utils/assets/paths.ts b/src/utils/assets/paths.ts new file mode 100644 index 000000000..9aaea104e --- /dev/null +++ b/src/utils/assets/paths.ts @@ -0,0 +1,58 @@ +import { extname } from 'pathe' +import { getAssetExtensions } from './state' + +const ORDERING_RE = /^\d+\./ + +// Strip ordering prefixes (`1.`, `01.`) from every segment. Copy and resolution +// both apply it so URLs and references agree. File names are kept verbatim. +export function removeOrdering(path: string): string { + return path.split('/').map(segment => segment.replace(ORDERING_RE, '')).join('/') +} + +export function parseQuery(path: string): string { + return path.match(/\?.+$/)?.[0] || '' +} + +export function removeQuery(path: string): string { + return path.replace(/\?.*$/, '') +} + +export function getExtension(path: string): string { + return extname(removeQuery(path)).substring(1).toLowerCase() +} + +export function isAssetExtension(path: string): boolean { + return getAssetExtensions().includes(getExtension(path)) +} + +export function isRelative(path: string): boolean { + return !/^(?:\/|#|[a-z][a-z0-9+.-]*:)/i.test(path) +} + +export function isRelativeAsset(value: unknown): value is string { + return typeof value === 'string' && value.length > 0 && isRelative(value) && isAssetExtension(value) +} + +export function buildStyle(...expressions: string[]): string { + return expressions + .map(expression => expression.replace(/^[; ]+|[; ]+$/g, '')) + .filter(Boolean) + .join('; ') + .replace(/\s*;\s*/g, '; ') + ';' +} + +// Merge a leading path with `key=value` params into a single query string. +export function buildQuery(...expressions: string[]): string { + const parts = expressions + .map(expression => expression.replace(/^[?&]+|&+$/g, '')) + .filter(Boolean) + if (!parts.length) { + return '' + } + const [first, ...rest] = parts + const isParam = (expression: string) => /^[^?]+=[^=]+$/.test(expression) + if (isParam(first!)) { + return '?' + parts.join('&') + } + return rest.length ? first + (first!.includes('?') ? '&' : '?') + rest.join('&') : first! +} diff --git a/src/utils/assets/rewrite.ts b/src/utils/assets/rewrite.ts new file mode 100644 index 000000000..9adc65cb6 --- /dev/null +++ b/src/utils/assets/rewrite.ts @@ -0,0 +1,181 @@ +import { dirname, resolve } from 'pathe' +import { visit } from 'minimark' +import type { MinimarkElement, MinimarkNode, MinimarkTree } from 'minimark' +import type { FileAfterParseHook } from '../../types' +import { buildQuery, buildStyle, isRelativeAsset, parseQuery, removeOrdering, removeQuery } from './paths' +import type { AssetIndex, AssetIndexEntry, ImageSizeHint, UnresolvedIndex } from './shared' + +interface RewriteContext { + index: AssetIndex + unresolved: UnresolvedIndex | null + fileDir: string + id: string + path: string + collection: string + imageSizes: ImageSizeHint[] + blankLinks: boolean +} + +export interface AfterParseHandlerOptions { + imageSizes: ImageSizeHint[] + blankLinks: boolean + unresolved?: UnresolvedIndex +} + +// Top-level fields that are structural or already resolved. Everything else is +// walked for relative asset references (`isRelativeAsset` filters non-assets). +const SKIP_TOP_LEVEL = new Set(['id', 'body', 'meta', '__metadata', 'stem', 'extension', 'path', 'rawbody']) + +function resolvedAssetKey(fileDir: string, value: string): string { + return removeOrdering(resolve(fileDir, removeQuery(value))) +} + +function resolveAssetFromContent(index: AssetIndex, fileDir: string, value: string): AssetIndexEntry | undefined { + return index.get(resolvedAssetKey(fileDir, value)) +} + +function registerContent(entry: AssetIndexEntry, ctx: RewriteContext): void { + if (ctx.id && !entry.content.some(reference => reference.id === ctx.id)) { + entry.content.push({ id: ctx.id, path: ctx.path, collection: ctx.collection }) + } +} + +// Record a reference whose target doesn't exist yet, so dev HMR can re-parse once it appears. +function recordUnresolved(ctx: RewriteContext, value: string): void { + if (!ctx.unresolved) { + return + } + const key = resolvedAssetKey(ctx.fileDir, value) + const list = ctx.unresolved.get(key) + if (!list) { + ctx.unresolved.set(key, [{ id: ctx.id, path: ctx.path, collection: ctx.collection }]) + } + else if (!list.some(reference => reference.id === ctx.id)) { + list.push({ id: ctx.id, path: ctx.path, collection: ctx.collection }) + } +} + +function applyImageSize(props: Record, entry: AssetIndexEntry, imageSizes: ImageSizeHint[]): void { + const { width, height } = entry + if (!width || !height) { + return + } + if (imageSizes.includes('attrs')) { + props.width = width + props.height = height + } + if (imageSizes.includes('style')) { + const ratio = `${width}/${height}` + if (typeof props.style === 'string') { + props.style = buildStyle(props.style, `aspect-ratio: ${ratio}`) + } + else if (props.style && typeof props.style === 'object') { + (props.style as Record).aspectRatio = ratio + } + else { + props.style = `aspect-ratio: ${ratio};` + } + } +} + +function rewriteBody(body: MinimarkTree, ctx: RewriteContext): void { + visit(body, (node: MinimarkNode) => Array.isArray(node), (node: MinimarkNode) => { + const [tag, props] = node as MinimarkElement + if (!props || typeof props !== 'object') { + return undefined + } + for (const [key, value] of Object.entries(props)) { + // `:prop` are MDC dynamic bindings (expressions), not literal paths. + if (key.startsWith(':') || typeof value !== 'string' || !isRelativeAsset(value)) { + continue + } + const entry = resolveAssetFromContent(ctx.index, ctx.fileDir, value) + if (!entry) { + recordUnresolved(ctx, value) + continue + } + registerContent(entry, ctx) + props[key] = entry.publicSrc + parseQuery(value) + + if (tag === 'img' || tag === 'nuxt-img') { + applyImageSize(props, entry, ctx.imageSizes) + } + else if (tag === 'a' && ctx.blankLinks) { + props.target ??= '_blank' + } + } + return undefined + }) +} + +function walkValue(value: unknown, rewriteLeaf: (value: string) => string): unknown { + if (typeof value === 'string') { + return rewriteLeaf(value) + } + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + value[i] = walkValue(value[i], rewriteLeaf) + } + return value + } + if (value && typeof value === 'object') { + const record = value as Record + for (const key of Object.keys(record)) { + record[key] = walkValue(record[key], rewriteLeaf) + } + } + return value +} + +function rewriteMeta(content: Record, ctx: RewriteContext): void { + const rewriteLeaf = (value: string): string => { + if (!isRelativeAsset(value)) { + return value + } + const entry = resolveAssetFromContent(ctx.index, ctx.fileDir, value) + if (!entry) { + recordUnresolved(ctx, value) + return value + } + registerContent(entry, ctx) + if (entry.width && entry.height && (ctx.imageSizes.includes('src') || ctx.imageSizes.includes('url'))) { + return buildQuery(entry.publicSrc, parseQuery(value), `width=${entry.width}&height=${entry.height}`) + } + return entry.publicSrc + parseQuery(value) + } + + if (content.meta && typeof content.meta === 'object') { + walkValue(content.meta, rewriteLeaf) + } + for (const key of Object.keys(content)) { + if (!SKIP_TOP_LEVEL.has(key)) { + content[key] = walkValue(content[key], rewriteLeaf) + } + } +} + +export function createAfterParseHandler(index: AssetIndex, options: AfterParseHandlerOptions) { + return (ctx: FileAfterParseHook): void => { + const fileDir = ctx.file.dirname || (ctx.file.path ? dirname(ctx.file.path) : '') + if (!fileDir) { + return + } + const content = ctx.content as unknown as Record + const rewriteCtx: RewriteContext = { + index, + unresolved: options.unresolved || null, + fileDir, + id: String(content.id ?? ''), + path: ctx.file.path || '', + collection: ctx.collection?.name || '', + imageSizes: options.imageSizes, + blankLinks: options.blankLinks, + } + + const body = content.body + if (body && typeof body === 'object' && Array.isArray((body as MinimarkTree).value)) { + rewriteBody(body as MinimarkTree, rewriteCtx) + } + rewriteMeta(content, rewriteCtx) + } +} diff --git a/src/utils/assets/shared.ts b/src/utils/assets/shared.ts new file mode 100644 index 000000000..a56c8f8cf --- /dev/null +++ b/src/utils/assets/shared.ts @@ -0,0 +1,60 @@ +import { getExtension } from './paths' + +export const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'avif', 'bmp', 'cur', 'apng', 'tiff', 'tif', 'heic', 'heif', 'jxl'] + +// Content extensions (md, mdc, json, yml, yaml, csv) are excluded on purpose: +// in v3 those can be legitimate `data` collection content. +export const DEFAULT_ASSET_EXTENSIONS = [ + ...IMAGE_EXTENSIONS, + // video + 'mp4', 'mov', 'webm', 'ogv', 'avi', 'flv', 'mkv', 'm4v', 'mpg', 'mpeg', '3gp', + // audio + 'mp3', 'm4a', 'wav', 'ogg', 'oga', 'weba', 'opus', 'flac', 'aac', 'mid', 'midi', + // subtitles / tracks + 'vtt', 'srt', + // embeddable documents and pages + 'html', 'htm', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'rtf', + // archives + 'zip', 'tar', 'gz', 'tgz', 'bz2', 'rar', '7z', + // fonts + 'woff', 'woff2', 'ttf', 'otf', 'eot', + // other web assets + 'glb', 'gltf', 'wasm', +] + +export function isImage(path: string): boolean { + return IMAGE_EXTENSIONS.includes(getExtension(path)) +} + +export type ImageSizeHint = 'style' | 'attrs' | 'src' | 'url' + +export function matchImageSizeHints(value: string | string[] | false | undefined): ImageSizeHint[] { + if (!value) { + return [] + } + const list = Array.isArray(value) ? value : [value] + const tokens = list.flatMap(item => (typeof item === 'string' ? item.match(/[^\s,|]+/g) || [] : [])) + return [...new Set(tokens)] as ImageSizeHint[] +} + +export interface ContentReference { + id: string + /** Absolute source path, used to re-parse the file on HMR. */ + path: string + collection: string +} + +export interface AssetIndexEntry { + /** Public URL the relative reference is rewritten to, e.g. `/media/photo.png`. */ + publicSrc: string + width?: number + height?: number + /** Content files referencing this asset (reverse index, used by dev HMR). */ + content: ContentReference[] +} + +// Both indexes are keyed by the absolute source path with ordering stripped. +export type AssetIndex = Map + +// References to a not-yet-existing asset, so dev HMR can re-parse once it appears. +export type UnresolvedIndex = Map diff --git a/src/utils/assets/state.ts b/src/utils/assets/state.ts new file mode 100644 index 000000000..a4ae58731 --- /dev/null +++ b/src/utils/assets/state.ts @@ -0,0 +1,10 @@ +// Isolated module to avoid an import cycle between `source.ts` and `discover.ts`. +let assetExtensions: string[] = [] + +export function setAssetExtensions(extensions: string[]): void { + assetExtensions = [...extensions] +} + +export function getAssetExtensions(): string[] { + return [...assetExtensions] +} diff --git a/src/utils/dev.ts b/src/utils/dev.ts index 8ef4b7efd..2283e8088 100644 --- a/src/utils/dev.ts +++ b/src/utils/dev.ts @@ -1,8 +1,8 @@ import { createUnplugin } from 'unplugin' import type { ViteDevServer } from 'vite' import crypto from 'node:crypto' -import { readFile } from 'node:fs/promises' -import { join, resolve } from 'pathe' +import { cp, mkdir, readFile, rm } from 'node:fs/promises' +import { dirname, join, resolve } from 'pathe' import type { Nuxt } from '@nuxt/schema' import { isIgnored, updateTemplates, useLogger } from '@nuxt/kit' import type { ConsolaInstance } from 'consola' @@ -16,25 +16,41 @@ import { generateCollectionInsert } from './collection' import { createParser } from './content' import { moduleTemplates } from './templates' import { getExcludedSourcePaths, parseSourceBase } from './source' +import { assetIndexKey, assetPublicUrl, getAssetSize } from './assets/discover' +import { isAssetExtension } from './assets/paths' +import { isImage, type AssetIndex, type ContentReference, type ImageSizeHint, type UnresolvedIndex } from './assets/shared' import { createHooks } from 'hookable' export const logger: ConsolaInstance = useLogger('@nuxt/content') export const contentHooks = createHooks<{ 'hmr:content:update': (data: { key: string, collection: string, queries: string[] }) => void + 'hmr:assets:update': (data: { event: 'update' | 'remove', src: string, width?: number, height?: number }) => void }>() +export interface DevAssetsConfig { + index: AssetIndex + unresolved: UnresolvedIndex + publicDir: string + imageSizes: ImageSizeHint[] + prefix: string + blankLinks: boolean + extensions: string[] + debug: boolean +} + interface HMRPluginOptions { nuxt: Nuxt moduleOptions: ModuleOptions manifest: Manifest + assets: DevAssetsConfig | null } export const NuxtContentHMRUnplugin = createUnplugin((opts: HMRPluginOptions) => { - const { nuxt, moduleOptions, manifest } = opts + const { nuxt, moduleOptions, manifest, assets } = opts const componentsTemplatePath = join(nuxt.options.buildDir, 'content/components.ts') - watchContents(nuxt, moduleOptions, manifest) + watchContents(nuxt, moduleOptions, manifest, assets) watchComponents(nuxt) return { @@ -54,12 +70,20 @@ export const NuxtContentHMRUnplugin = createUnplugin((opts: HMRPluginOptions) => data, }) }) + + contentHooks.hook('hmr:assets:update', (data) => { + server.ws.send({ + type: 'custom', + event: 'nuxt-content:assets:update', + data, + }) + }) }, }, } }) -export function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest: Manifest) { +export function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest: Manifest, assets: DevAssetsConfig | null = null) { const collectionParsers = {} as Record>> const collections = manifest.collections @@ -115,6 +139,12 @@ export function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest: Mani } // resolve path using `pathe.resolve` to use `/` instead of `\` on windows, otherwise `micromatch` will not match const absolutePath = resolve(pathOrError as string) + // Handle assets before the content match: a `**` source would otherwise + // match them as content. + if (assets && isAssetExtension(absolutePath)) { + await onAssetChange('update', absolutePath, assets) + return + } const matches = sourceMap.filter(({ source, cwd }) => { if (cwd && absolutePath.startsWith(cwd)) { return micromatch.isMatch(absolutePath.substring(cwd.length), source!.include, { ignore: source!.exclude || [], dot: true }) @@ -164,7 +194,7 @@ export function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest: Mani collectionType: collection.type, }).then(result => JSON.stringify(result)) - db.insertDevelopmentCache(keyInCollection, checksum, parsedContent) + db.insertDevelopmentCache(keyInCollection, parsedContent, checksum) } const { queries: insertQuery } = generateCollectionInsert(collection, JSON.parse(parsedContent)) @@ -179,6 +209,10 @@ export function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest: Mani } // resolve path using `pathe.resolve` to use `/` instead of `\` on windows, otherwise `micromatch` will not match const absolutePath = resolve(pathOrError as string) + if (assets && isAssetExtension(absolutePath)) { + await onAssetChange('remove', absolutePath, assets) + return + } const matches = sourceMap.filter(({ source, cwd }) => { if (cwd && absolutePath.startsWith(cwd)) { return micromatch.isMatch(absolutePath.substring(cwd.length), source!.include, { ignore: source!.exclude || [], dot: true }) @@ -239,6 +273,80 @@ export function watchContents(nuxt: Nuxt, options: ModuleOptions, manifest: Mani }) } + async function reparseReferencingContent(references: ContentReference[]) { + const db = await getDb() + const seen = new Set() + for (const reference of references) { + if (seen.has(reference.id)) { + continue + } + seen.add(reference.id) + const collection = collections.find(c => c.name === reference.collection) + if (!collection) { + continue + } + const body = await readFile(reference.path, 'utf8').catch((): string => '') + if (!body) { + continue + } + if (!collectionParsers[collection.name]) { + collectionParsers[collection.name] = await createParser(collection, nuxt) + } + const parsed = await collectionParsers[collection.name]!({ + id: reference.id, + body, + path: reference.path, + collectionType: collection.type, + }).then(result => JSON.stringify(result)) + db.insertDevelopmentCache(reference.id, parsed, getContentChecksum(body)) + const { queries } = generateCollectionInsert(collection, JSON.parse(parsed)) + await broadcast(collection, reference.id, queries) + } + } + + async function onAssetChange(event: 'update' | 'remove', absolutePath: string, assets: DevAssetsConfig) { + const match = sourceMap.find(({ cwd }) => cwd && absolutePath.startsWith(cwd)) + if (!match) { + return + } + const { source } = match + const { fixed } = parseSourceBase(source) + const scanDir = join(match.cwd, fixed) + const relativeKey = absolutePath.substring(scanDir.length).replace(/^\//, '') + const indexKey = assetIndexKey(absolutePath) + const url = assetPublicUrl(relativeKey, source.prefix || '', assets.prefix) + const target = join(assets.publicDir, url) + + if (event === 'remove') { + await rm(target, { force: true }).catch(() => {}) + assets.index.delete(indexKey) + contentHooks.callHook('hmr:assets:update', { event: 'remove', src: url }) + return + } + + const previous = assets.index.get(indexKey) + await mkdir(dirname(target), { recursive: true }) + await cp(absolutePath, target) + const { width, height } = assets.imageSizes.length && isImage(absolutePath) ? await getAssetSize(absolutePath) : {} + assets.index.set(indexKey, { publicSrc: url, width, height, content: previous?.content || [] }) + + // Re-parse content affected by this asset: a resize changes injected + // dimensions, and a first appearance resolves earlier dangling references. + const toReparse: ContentReference[] = [] + if (previous && (previous.width !== width || previous.height !== height)) { + toReparse.push(...previous.content) + } + const pending = assets.unresolved.get(indexKey) + if (pending?.length) { + toReparse.push(...pending) + assets.unresolved.delete(indexKey) + } + if (toReparse.length) { + await reparseReferencingContent(toReparse) + } + contentHooks.callHook('hmr:assets:update', { event: 'update', src: url, width, height }) + } + nuxt.hook('close', async () => { if (watcher) { watcher.removeAllListeners() diff --git a/src/utils/source.ts b/src/utils/source.ts index 71aadcc0b..afb8867e6 100644 --- a/src/utils/source.ts +++ b/src/utils/source.ts @@ -6,6 +6,7 @@ import { glob } from 'tinyglobby' import type { CollectionSource, ResolvedCollectionSource } from '../types/collection' import { downloadGitRepository } from './git' import { logger } from './dev' +import { isAssetExtension } from './assets/paths' import gitUrlParse from 'git-url-parse' export function getExcludedSourcePaths(source: CollectionSource) { @@ -39,7 +40,8 @@ export function defineLocalSource(source: CollectionSource | ResolvedCollectionS getKeys: async () => { const _keys = await glob(source.include, { cwd: resolvedSource.cwd, ignore: getExcludedSourcePaths(source), dot: true, expandDirectories: false }) .catch((): [] => []) - return _keys.map(key => key.substring(fixed.length)) + // Keep co-located assets out of content collections (no-op when off). + return _keys.map(key => key.substring(fixed.length)).filter(key => !isAssetExtension(key)) }, getItem: async (key) => { const fullPath = join(resolvedSource.cwd, fixed, key) diff --git a/test/assets.test.ts b/test/assets.test.ts new file mode 100644 index 000000000..5a2f54d15 --- /dev/null +++ b/test/assets.test.ts @@ -0,0 +1,85 @@ +import fs from 'node:fs/promises' +import { createResolver } from '@nuxt/kit' +import { setup, $fetch } from '@nuxt/test-utils' +import { afterAll, describe, expect, test } from 'vitest' +import { initiateValidatorsContext } from '../src/utils/dependencies' + +const resolver = createResolver(import.meta.url) + +function findNode(value: unknown[], tag: string): [string, Record, ...unknown[]] | undefined { + for (const node of value) { + if (Array.isArray(node) && typeof node[0] === 'string') { + if (node[0] === tag) { + return node as [string, Record, ...unknown[]] + } + const found = findNode(node.slice(2), tag) + if (found) { + return found + } + } + } + return undefined +} + +async function cleanup() { + await fs.rm(resolver.resolve('./fixtures/assets/node_modules'), { recursive: true, force: true }) + await fs.rm(resolver.resolve('./fixtures/assets/.nuxt'), { recursive: true, force: true }) + await fs.rm(resolver.resolve('./fixtures/assets/.data'), { recursive: true, force: true }) +} + +describe('assets', async () => { + await initiateValidatorsContext() + + await cleanup() + afterAll(async () => { + await cleanup() + }) + + await setup({ + rootDir: resolver.resolve('./fixtures/assets'), + dev: true, + }) + + test('serves the copied asset', async () => { + const png = await $fetch('/media/photo.png', { responseType: 'arrayBuffer' }) + expect(png.byteLength).toBeGreaterThan(0) + }) + + test('extracts image dimensions from disk and injects the aspect-ratio', async () => { + const doc = await $fetch('/api/content/get?path=/') as { body: { value: unknown[] } } + const img = findNode(doc.body.value, 'img')! + expect(img[1].src).toBe('/media/photo.png') + expect(img[1].style).toBe('aspect-ratio: 8/4;') + }) + + test('rewrites an asset link and opens it in a new tab', async () => { + const doc = await $fetch('/api/content/get?path=/') as { body: { value: unknown[] } } + const link = findNode(doc.body.value, 'a')! + expect(link[1].href).toBe('/files/doc.pdf') + expect(link[1].target).toBe('_blank') + }) + + test('rewrites a frontmatter asset path', async () => { + const doc = await $fetch('/api/content/get?path=/') as { featured: string } + expect(doc.featured).toBe('/media/photo.png') + }) + + test('rewrites an array of frontmatter asset paths', async () => { + const doc = await $fetch('/api/content/get?path=/') as { gallery: string[] } + expect(doc.gallery).toEqual(['/media/photo.png', '/media/photo.png']) + }) + + test('rewrites iframe and embed sources and serves non-image assets', async () => { + const doc = await $fetch('/api/content/get?path=/') as { body: { value: unknown[] } } + expect(findNode(doc.body.value, 'iframe')![1].src).toBe('/media/page.html') + expect(findNode(doc.body.value, 'embed')![1].src).toBe('/files/doc.pdf') + + const html = await $fetch('/media/page.html', { responseType: 'text' }) + expect(html.length).toBeGreaterThan(0) + }) + + test('resolves a parent-folder relative path', async () => { + const doc = await $fetch('/api/content/get?path=/posts/nested') as { body: { value: unknown[] } } + expect(findNode(doc.body.value, 'img')![1].src).toBe('/media/photo.png') + }) +}) diff --git a/test/fixtures/assets/app/app.vue b/test/fixtures/assets/app/app.vue new file mode 100644 index 000000000..09f935bbb --- /dev/null +++ b/test/fixtures/assets/app/app.vue @@ -0,0 +1,6 @@ + diff --git a/test/fixtures/assets/content.config.ts b/test/fixtures/assets/content.config.ts new file mode 100644 index 000000000..a44b68b7a --- /dev/null +++ b/test/fixtures/assets/content.config.ts @@ -0,0 +1,15 @@ +import { defineCollection, defineContentConfig } from '@nuxt/content' +import { z } from 'zod' + +export default defineContentConfig({ + collections: { + content: defineCollection({ + type: 'page', + source: '**', + schema: z.object({ + featured: z.string().optional(), + gallery: z.array(z.string()).optional(), + }), + }), + }, +}) diff --git a/test/fixtures/assets/content/files/doc.pdf b/test/fixtures/assets/content/files/doc.pdf new file mode 100644 index 000000000..dbf091df9 Binary files /dev/null and b/test/fixtures/assets/content/files/doc.pdf differ diff --git a/test/fixtures/assets/content/index.md b/test/fixtures/assets/content/index.md new file mode 100644 index 000000000..0ecdbe98e --- /dev/null +++ b/test/fixtures/assets/content/index.md @@ -0,0 +1,17 @@ +--- +title: Assets +featured: media/photo.png +gallery: + - media/photo.png + - media/photo.png +--- + +# Assets + +![mountains](media/photo.png) + +[the document](files/doc.pdf) + + + + diff --git a/test/fixtures/assets/content/media/page.html b/test/fixtures/assets/content/media/page.html new file mode 100644 index 000000000..19e5e0458 --- /dev/null +++ b/test/fixtures/assets/content/media/page.html @@ -0,0 +1,17 @@ + + + + + Sample + + + + +

Yay, Assets!

+

This page is displaying in an iframe

+ + diff --git a/test/fixtures/assets/content/media/photo.png b/test/fixtures/assets/content/media/photo.png new file mode 100644 index 000000000..a022d39f7 Binary files /dev/null and b/test/fixtures/assets/content/media/photo.png differ diff --git a/test/fixtures/assets/content/posts/nested.md b/test/fixtures/assets/content/posts/nested.md new file mode 100644 index 000000000..a8d4c49e7 --- /dev/null +++ b/test/fixtures/assets/content/posts/nested.md @@ -0,0 +1,7 @@ +--- +title: Nested +--- + +# Nested + +![parent folder](../media/photo.png) diff --git a/test/fixtures/assets/nuxt.config.ts b/test/fixtures/assets/nuxt.config.ts new file mode 100644 index 000000000..597da38c9 --- /dev/null +++ b/test/fixtures/assets/nuxt.config.ts @@ -0,0 +1,8 @@ +import { defineNuxtConfig } from 'nuxt/config' + +export default defineNuxtConfig({ + modules: [ + '@nuxt/content', + ], + compatibilityDate: '2025-09-03', +}) diff --git a/test/fixtures/assets/package.json b/test/fixtures/assets/package.json new file mode 100644 index 000000000..010e3445c --- /dev/null +++ b/test/fixtures/assets/package.json @@ -0,0 +1,7 @@ +{ + "name": "nuxt-content-test-assets", + "private": true, + "scripts": { + "dev": "nuxi dev" + } +} diff --git a/test/fixtures/assets/server/api/content/get.get.ts b/test/fixtures/assets/server/api/content/get.get.ts new file mode 100644 index 000000000..d8c8bb8b2 --- /dev/null +++ b/test/fixtures/assets/server/api/content/get.get.ts @@ -0,0 +1,8 @@ +import { eventHandler, getQuery } from 'h3' + +export default eventHandler(async (event) => { + const rawPath = getQuery(event).path + const path = Array.isArray(rawPath) ? rawPath[0] : (rawPath || '/') + + return await queryCollection(event, 'content').path(path).first() +}) diff --git a/test/unit/assets.ignore.test.ts b/test/unit/assets.ignore.test.ts new file mode 100644 index 000000000..a38c6ebd1 --- /dev/null +++ b/test/unit/assets.ignore.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'pathe' +import { defineLocalSource } from '../../src/utils/source' +import { setAssetExtensions } from '../../src/utils/assets/state' +import { DEFAULT_ASSET_EXTENSIONS } from '../../src/utils/assets/shared' + +describe('assets exclusion from content collection keys', () => { + let dir: string + + beforeAll(async () => { + dir = await mkdtemp(join(tmpdir(), 'nc-assets-')) + await writeFile(join(dir, 'post.md'), '# hi') + await writeFile(join(dir, 'data.json'), '{}') + await writeFile(join(dir, 'photo.jpg'), 'x') + await writeFile(join(dir, 'clip.mp4'), 'x') + }) + + afterAll(async () => { + setAssetExtensions(DEFAULT_ASSET_EXTENSIONS) + await rm(dir, { recursive: true, force: true }) + }) + + it('keeps content files and drops assets when the feature is enabled', async () => { + setAssetExtensions(DEFAULT_ASSET_EXTENSIONS) + const source = defineLocalSource({ include: '**', cwd: dir }) + await source.prepare!({ rootDir: dir }) + const keys = await source.getKeys!() + expect(keys.sort()).toEqual(['data.json', 'post.md']) + }) + + it('does not filter anything when the feature is disabled', async () => { + setAssetExtensions([]) + const source = defineLocalSource({ include: '**', cwd: dir }) + await source.prepare!({ rootDir: dir }) + const keys = await source.getKeys!() + expect(keys.sort()).toEqual(['clip.mp4', 'data.json', 'photo.jpg', 'post.md']) + }) +}) diff --git a/test/unit/assets.parser.test.ts b/test/unit/assets.parser.test.ts new file mode 100644 index 000000000..0be7b0b22 --- /dev/null +++ b/test/unit/assets.parser.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, it } from 'vitest' +import { z } from 'zod' +import { defineCollection } from '../../src/utils' +import { resolveCollection } from '../../src/utils/collection' +import { initiateValidatorsContext } from '../../src/utils/dependencies' +import { parseContent } from '../utils/content' +import { createAfterParseHandler } from '../../src/utils/assets/rewrite' +import { setAssetExtensions } from '../../src/utils/assets/state' +import { DEFAULT_ASSET_EXTENSIONS, type AssetIndex, type AssetIndexEntry } from '../../src/utils/assets/shared' +import type { FileAfterParseHook } from '../../src/types' + +type Element = [string, Record, ...unknown[]] + +function flatten(value: unknown[], out: Element[] = []): Element[] { + for (const node of value) { + if (Array.isArray(node) && typeof node[0] === 'string') { + out.push(node as Element) + flatten(node.slice(2), out) + } + } + return out +} + +function entry(publicSrc: string, width?: number, height?: number): AssetIndexEntry { + return { publicSrc, width, height, content: [] } +} + +// Only covers cases that depend on the shape the real parser produces (MDC +// components/bindings, raw HTML, fenced code, `./` + query). Pure rewrite logic +// (URL rewrite, target, imageSizes, frontmatter) is unit-tested in assets.rewrite.test.ts. +describe('assets rewrite (real parser)', async () => { + await initiateValidatorsContext() + setAssetExtensions(DEFAULT_ASSET_EXTENSIONS) + + const collection = resolveCollection('content', defineCollection({ + type: 'page', + source: 'content/**', + schema: z.object({ featured: z.string().optional(), gallery: z.array(z.string()).optional() }), + }))! + + const index: AssetIndex = new Map([ + ['/invalid/media/photo.png', entry('/media/photo.png', 8, 4)], + ['/invalid/files/doc.pdf', entry('/files/doc.pdf')], + ['/invalid/media/clip.mp4', entry('/media/clip.mp4')], + ['/invalid/media/poster.jpg', entry('/media/poster.jpg', 16, 9)], + ['/invalid/media/example.html', entry('/media/example.html')], + ['/invalid/media/raw.png', entry('/media/raw.png')], + ['/invalid/media/a.png', entry('/media/a.png')], + ['/invalid/media/b.png', entry('/media/b.png')], + ['/invalid/media/incode.png', entry('/media/incode.png')], + ]) + + async function parse(imageSize: ('style' | 'attrs' | 'src' | 'url')[]) { + const handler = createAfterParseHandler(index, { imageSizes: imageSize, blankLinks: true }) + const nuxtMock = { + callHook(hook: string, ctx: FileAfterParseHook) { + if (hook === 'content:file:afterParse') handler(ctx) + }, + } + const parsed = await parseContent('content/index.md', `--- +featured: media/photo.png +gallery: + - media/a.png + - media/b.png +--- + +![markdown image](media/photo.png) + +[a link](files/doc.pdf) + +:video{src="media/clip.mp4" poster="media/poster.jpg"} + +:img{:src="featured" alt="bound"} + + + + + +\`\`\`ts +const x = "media/incode.png" +\`\`\` +`, collection, nuxtMock as never) + return parsed as unknown as { body: { value: unknown[] }, featured?: unknown, gallery?: unknown } + } + + it('MDC component literal attributes (src and poster) are rewritten', async () => { + const video = flatten((await parse([])).body.value).find(e => e[0] === 'video')! + expect(video[1].src).toBe('/media/clip.mp4') + expect(video[1].poster).toBe('/media/poster.jpg') + }) + + it('MDC dynamic bindings (`:src`, `:images`) are left untouched', async () => { + const els = flatten((await parse([])).body.value) + expect(els.find(e => e[1][':src'] === 'featured')).toBeTruthy() + }) + + it('raw HTML iframe and img are rewritten', async () => { + const els = flatten((await parse([])).body.value) + expect(els.find(e => e[0] === 'iframe' && e[1].src === '/media/example.html')).toBeTruthy() + expect(els.find(e => e[0] === 'img' && e[1].src === '/media/raw.png')).toBeTruthy() + }) + + it('paths inside fenced code are never rewritten', async () => { + const json = JSON.stringify((await parse([])).body.value) + expect(json).not.toContain('/media/incode.png') + }) + + it('preserves an existing query string and resolves a `./` prefix', async () => { + const handler = createAfterParseHandler(index, { imageSizes: [], blankLinks: true }) + const parsed = await parseContent('content/index.md', '![q](media/photo.png?v=2)\n\n![dot](./media/photo.png)', collection, { + callHook(hook: string, ctx: FileAfterParseHook) { + if (hook === 'content:file:afterParse') handler(ctx) + }, + } as never) + const srcs = flatten((parsed.body as { value: unknown[] }).value).filter(e => e[0] === 'img').map(e => e[1].src) + expect(srcs).toEqual(['/media/photo.png?v=2', '/media/photo.png']) + }) + + it('external and absolute references are never touched', async () => { + const md = `![x](https://example.com/a.png)\n\n![y](/public/b.png)` + const handler = createAfterParseHandler(index, { imageSizes: [], blankLinks: true }) + const parsed = await parseContent('content/index.md', md, collection, { + callHook(hook: string, ctx: FileAfterParseHook) { + if (hook === 'content:file:afterParse') handler(ctx) + }, + } as never) + const imgs = flatten((parsed.body as { value: unknown[] }).value).filter(e => e[0] === 'img') + expect(imgs.map(e => e[1].src)).toEqual(['https://example.com/a.png', '/public/b.png']) + }) +}) diff --git a/test/unit/assets.paths.test.ts b/test/unit/assets.paths.test.ts new file mode 100644 index 000000000..5f9347f77 --- /dev/null +++ b/test/unit/assets.paths.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import { buildQuery, buildStyle, isRelativeAsset, parseQuery, removeOrdering, removeQuery } from '../../src/utils/assets/paths' +import { setAssetExtensions } from '../../src/utils/assets/state' +import { DEFAULT_ASSET_EXTENSIONS } from '../../src/utils/assets/shared' + +describe('assets/paths', () => { + beforeAll(() => setAssetExtensions(DEFAULT_ASSET_EXTENSIONS)) + + it('removeOrdering strips `NN.` from every segment, including the file name', () => { + expect(removeOrdering('1.foo/bar.jpg')).toBe('foo/bar.jpg') + expect(removeOrdering('1.foo/2.bar/baz.jpg')).toBe('foo/bar/baz.jpg') + expect(removeOrdering('foo/bar.jpg')).toBe('foo/bar.jpg') + expect(removeOrdering('foo/1.bar.jpg')).toBe('foo/bar.jpg') + }) + + it('removeQuery / parseQuery', () => { + expect(removeQuery('a/b.png?x=1')).toBe('a/b.png') + expect(removeQuery('a/b.png')).toBe('a/b.png') + expect(parseQuery('a/b.png?x=1')).toBe('?x=1') + expect(parseQuery('a/b.png')).toBe('') + }) + + it('isRelativeAsset accepts relative asset paths only', () => { + expect(isRelativeAsset('media/photo.png')).toBe(true) + expect(isRelativeAsset('./media/photo.png')).toBe(true) + expect(isRelativeAsset('../media/photo.png')).toBe(true) + expect(isRelativeAsset('files/doc.pdf')).toBe(true) + expect(isRelativeAsset('/media/photo.png')).toBe(false) + expect(isRelativeAsset('https://example.com/a.png')).toBe(false) + expect(isRelativeAsset('data:image/png;base64,xxxx')).toBe(false) + expect(isRelativeAsset('#anchor')).toBe(false) + expect(isRelativeAsset('media/article.md')).toBe(false) + expect(isRelativeAsset(42)).toBe(false) + }) + + it('buildStyle joins independent declarations', () => { + expect(buildStyle('color: red', 'aspect-ratio: 16/9')).toBe('color: red; aspect-ratio: 16/9;') + expect(buildStyle('', 'aspect-ratio: 1/1')).toBe('aspect-ratio: 1/1;') + }) + + it('buildQuery merges path and params', () => { + expect(buildQuery('/img.png', 'width=10&height=20')).toBe('/img.png?width=10&height=20') + expect(buildQuery('/img.png?a=1', 'width=10')).toBe('/img.png?a=1&width=10') + expect(buildQuery('width=10', 'height=20')).toBe('?width=10&height=20') + }) +}) diff --git a/test/unit/assets.rewrite.test.ts b/test/unit/assets.rewrite.test.ts new file mode 100644 index 000000000..bacd4c73d --- /dev/null +++ b/test/unit/assets.rewrite.test.ts @@ -0,0 +1,119 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import { createAfterParseHandler } from '../../src/utils/assets/rewrite' +import { setAssetExtensions } from '../../src/utils/assets/state' +import { DEFAULT_ASSET_EXTENSIONS, type AssetIndex, type ImageSizeHint, type UnresolvedIndex } from '../../src/utils/assets/shared' +import type { FileAfterParseHook } from '../../src/types' + +function makeIndex(): AssetIndex { + const index: AssetIndex = new Map() + index.set('/abs/content/real-content/media/photo.png', { publicSrc: '/real-content/media/photo.png', width: 640, height: 480, content: [] }) + index.set('/abs/content/real-content/files/doc.pdf', { publicSrc: '/real-content/files/doc.pdf', content: [] }) + return index +} + +function run(imageSize: ImageSizeHint[]) { + const index = makeIndex() + const content: Record = { + id: 'content/real-content/post.md', + cover: 'media/01.photo.png', + body: { + type: 'minimark', + value: [ + ['p', {}, ['img', { src: 'media/01.photo.png', alt: 'photo' }]], + ['a', { href: 'files/doc.pdf' }, 'download'], + ['img', { src: 'https://example.com/external.png' }], + ], + }, + meta: { + featured: 'media/01.photo.png', + gallery: ['media/01.photo.png'], + note: 'just some text', + }, + } + const ctx = { + file: { id: content.id, path: '/abs/content/1.real-content/post.md', dirname: '/abs/content/1.real-content' }, + content, + collection: { name: 'content', type: 'page' }, + } as unknown as FileAfterParseHook + + createAfterParseHandler(index, { imageSizes: imageSize, blankLinks: true })(ctx) + return { content, index } +} + +describe('assets/rewrite', () => { + beforeAll(() => setAssetExtensions(DEFAULT_ASSET_EXTENSIONS)) + + it('rewrites relative paths to public URLs and strips ordering', () => { + const { content } = run([]) + const body = content.body as { value: unknown[] } + const img = (body.value[0] as unknown[])[2] as [string, Record] + expect(img[1].src).toBe('/real-content/media/photo.png') + + const link = body.value[1] as [string, Record] + expect(link[1].href).toBe('/real-content/files/doc.pdf') + expect(link[1].target).toBe('_blank') + + expect((content.meta as Record).featured).toBe('/real-content/media/photo.png') + expect(((content.meta as Record).gallery as string[])[0]).toBe('/real-content/media/photo.png') + expect(content.cover).toBe('/real-content/media/photo.png') + }) + + it('leaves external and non-asset values untouched', () => { + const { content } = run([]) + const body = content.body as { value: unknown[] } + const external = body.value[2] as [string, Record] + expect(external[1].src).toBe('https://example.com/external.png') + expect((content.meta as Record).note).toBe('just some text') + }) + + it('injects aspect-ratio style on img with imageSize "style"', () => { + const { content } = run(['style']) + const body = content.body as { value: unknown[] } + const img = (body.value[0] as unknown[])[2] as [string, Record] + expect(img[1].style).toBe('aspect-ratio: 640/480;') + }) + + it('injects width/height attrs on img with imageSize "attrs"', () => { + const { content } = run(['attrs']) + const body = content.body as { value: unknown[] } + const img = (body.value[0] as unknown[])[2] as [string, Record] + expect(img[1].width).toBe(640) + expect(img[1].height).toBe(480) + }) + + it('encodes size in frontmatter src query with imageSize "src"', () => { + const { content } = run(['src']) + expect((content.meta as Record).featured).toBe('/real-content/media/photo.png?width=640&height=480') + }) + + it('builds a reverse index of referencing content ids', () => { + const { index } = run(['style']) + expect(index.get('/abs/content/real-content/media/photo.png')!.content.map(reference => reference.id)).toContain('content/real-content/post.md') + }) + + it('records references to a not-yet-existing asset, but not resolved ones', () => { + const index = makeIndex() + const unresolved: UnresolvedIndex = new Map() + const content: Record = { + id: 'content/real-content/post.md', + body: { + type: 'minimark', + value: [ + ['img', { src: 'media/missing.png' }], + ['img', { src: 'media/photo.png' }], + ], + }, + meta: {}, + } + const ctx = { + file: { id: content.id, path: '/abs/content/1.real-content/post.md', dirname: '/abs/content/1.real-content' }, + content, + collection: { name: 'content', type: 'page' }, + } as unknown as FileAfterParseHook + + createAfterParseHandler(index, { imageSizes: [], blankLinks: true, unresolved })(ctx) + + expect(unresolved.get('/abs/content/real-content/media/missing.png')?.map(reference => reference.id)).toContain('content/real-content/post.md') + expect(unresolved.has('/abs/content/real-content/media/photo.png')).toBe(false) + }) +}) diff --git a/test/unit/devCache.test.ts b/test/unit/devCache.test.ts new file mode 100644 index 000000000..1fb79d4ab --- /dev/null +++ b/test/unit/devCache.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect, afterAll } from 'vitest' +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'pathe' +import { getLocalDatabase } from '../../src/utils/database' + +// Guards the argument order of `insertDevelopmentCache(id, value, checksum)`, +// which the dev HMR re-parse path was previously calling with value/checksum swapped. +describe('development cache', () => { + let dir: string + let db: Awaited> + + afterAll(async () => { + await db?.close?.() + if (dir) { + await rm(dir, { recursive: true, force: true }) + } + }) + + it('stores value and checksum in their own columns', async () => { + dir = await mkdtemp(join(tmpdir(), 'nc-cache-')) + db = await getLocalDatabase({ type: 'sqlite', filename: join(dir, 'contents.sqlite') }, { nativeSqlite: true }) + + await db.insertDevelopmentCache('content/index.md', '{"parsed":true}', 'checksum-123') + const row = await db.fetchDevelopmentCacheForKey('content/index.md') + + expect(row?.value).toBe('{"parsed":true}') + expect(row?.checksum).toBe('checksum-123') + }) +})