From 8732720221c636843e212de3c9d01f9503d6eb61 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Thu, 9 Mar 2023 02:32:24 +1100 Subject: [PATCH] perf(head): drop `@vueuse/head` dependency (#19519) --- docs/1.getting-started/5.seo-meta.md | 12 ++-- docs/3.api/1.composables/use-head.md | 8 +-- packages/nuxt/build.config.ts | 2 +- packages/nuxt/package.json | 5 +- packages/nuxt/src/core/nitro.ts | 2 +- packages/nuxt/src/head/module.ts | 16 +++-- packages/nuxt/src/head/runtime/composables.ts | 4 +- packages/nuxt/src/head/runtime/index.ts | 2 +- ...vueuse-head.plugin.ts => unhead.plugin.ts} | 12 ++-- .../lib/vueuse-head-polyfill.plugin.ts | 8 +++ packages/schema/src/config/experimental.ts | 10 ++- pnpm-lock.yaml | 72 +++++++++---------- test/basic.test.ts | 15 +++- test/bundle.test.ts | 5 +- test/fixtures/basic/nuxt.config.ts | 6 +- test/fixtures/basic/pages/vueuse-head.vue | 24 +++++++ 16 files changed, 130 insertions(+), 73 deletions(-) rename packages/nuxt/src/head/runtime/lib/{vueuse-head.plugin.ts => unhead.plugin.ts} (69%) create mode 100644 packages/nuxt/src/head/runtime/lib/vueuse-head-polyfill.plugin.ts create mode 100644 test/fixtures/basic/pages/vueuse-head.vue diff --git a/docs/1.getting-started/5.seo-meta.md b/docs/1.getting-started/5.seo-meta.md index c0e2ec533a2..d010b979c86 100644 --- a/docs/1.getting-started/5.seo-meta.md +++ b/docs/1.getting-started/5.seo-meta.md @@ -46,7 +46,8 @@ export default defineNuxtConfig({ ## Composable: `useHead` -The `useHead` composable function allows you to manage your head tags in a programmatic and reactive way, powered by [@vueuse/head](https://github.com/vueuse/head). +The `useHead` composable function allows you to manage your head tags in a programmatic and reactive way, +powered by [Unhead](https://unhead.harlanzw.com/). As with all composables, it can only be used with a components `setup` and lifecycle hooks. @@ -62,7 +63,7 @@ useHead({ bodyAttrs: { class: 'test' }, - script: [ { children: 'console.log(\'Hello world\')' } ] + script: [ { innerHTML: 'console.log(\'Hello world\')' } ] }) ``` @@ -153,6 +154,7 @@ The below is the non-reactive types used for `useHead`, `app.head` and component interface MetaObject { title?: string titleTemplate?: string | ((title?: string) => string) + templateParams?: Record> base?: Base link?: Link[] meta?: Meta[] @@ -164,7 +166,7 @@ interface MetaObject { } ``` -See [zhead](https://github.com/harlan-zw/zhead/tree/main/packages/schema/src) for more detailed types. +See [@unhead/schema](https://github.com/unjs/unhead/blob/main/packages/schema/src/schema.ts) for more detailed types. ## Features @@ -228,7 +230,7 @@ Now, if you set the title to `My Page` with `useHead` on another page of your si ### Body Tags -You can use the `body: true` option on the `link` and `script` meta tags to append them to the end of the `` tag. +You can use the `tagPosition: 'bodyClose'` option on applicable tags to append them to the end of the `` tag. For example: @@ -238,7 +240,7 @@ useHead({ script: [ { src: 'https://third-party-script.com', - body: true + tagPosition: 'bodyClose' // valid options are: 'head' | 'bodyClose' | 'bodyOpen' } ] }) diff --git a/docs/3.api/1.composables/use-head.md b/docs/3.api/1.composables/use-head.md index a891f614070..754b9109099 100644 --- a/docs/3.api/1.composables/use-head.md +++ b/docs/3.api/1.composables/use-head.md @@ -4,9 +4,7 @@ description: useHead customizes the head properties of individual pages of your # `useHead` -Nuxt provides the `useHead` composable to add and customize the head properties of individual pages of your Nuxt app. - -`useHead` is powered by [@vueuse/head](https://github.com/vueuse/head), you can find more in-depth documentation [here](https://unhead.harlanzw.com/) +The `useHead` composable function allows you to manage your head tags in a programmatic and reactive way, powered by [Unhead](https://unhead.harlanzw.com/). ::ReadMore{link="/docs/getting-started/seo-meta"} :: @@ -17,7 +15,7 @@ Nuxt provides the `useHead` composable to add and customize the head properties useHead(meta: MaybeComputedRef): void ``` -Below are the non-reactive types for `useHead`. See [zhead](https://github.com/harlan-zw/zhead/tree/main/packages/schema/src) for more detailed types. +Below are the non-reactive types for `useHead`. ```ts interface MetaObject { @@ -34,6 +32,8 @@ interface MetaObject { } ``` +See [@unhead/schema](https://github.com/unjs/unhead/blob/main/packages/schema/src/schema.ts) for more detailed types. + ::alert{type=info} The properties of `useHead` can be dynamic, accepting `ref`, `computed` and `reactive` properties. `meta` parameter can also accept a function returning an object to make the entire object reactive. :: diff --git a/packages/nuxt/build.config.ts b/packages/nuxt/build.config.ts index 5b6de355eba..5fd53e9e209 100644 --- a/packages/nuxt/build.config.ts +++ b/packages/nuxt/build.config.ts @@ -26,6 +26,6 @@ export default defineBuildConfig({ 'nuxt/schema', '@vue/reactivity', '@vue/shared', - '@vueuse/head' + '@unhead/vue' ] }) diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 9a74c2b473b..ca0223d76d0 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -52,10 +52,10 @@ "@nuxt/telemetry": "^2.1.10", "@nuxt/ui-templates": "^1.1.1", "@nuxt/vite-builder": "3.2.3", - "@unhead/ssr": "^1.1.16", + "@unhead/ssr": "^1.1.20", + "@unhead/vue": "^1.1.20", "@vue/reactivity": "^3.2.47", "@vue/shared": "^3.2.47", - "@vueuse/head": "^1.1.15", "chokidar": "^3.5.3", "cookie-es": "^0.5.0", "defu": "^6.1.2", @@ -82,7 +82,6 @@ "ufo": "^1.1.1", "unctx": "^2.1.2", "unenv": "^1.2.1", - "unhead": "^1.1.16", "unimport": "^3.0.2", "unplugin": "^1.1.0", "untyped": "^1.2.2", diff --git a/packages/nuxt/src/core/nitro.ts b/packages/nuxt/src/core/nitro.ts index e4c94f5d63f..709d3937254 100644 --- a/packages/nuxt/src/core/nitro.ts +++ b/packages/nuxt/src/core/nitro.ts @@ -7,7 +7,7 @@ import escapeRE from 'escape-string-regexp' import { defu } from 'defu' import fsExtra from 'fs-extra' import { dynamicEventHandler } from 'h3' -import { createHeadCore } from 'unhead' +import { createHeadCore } from '@unhead/vue' import { renderSSRHead } from '@unhead/ssr' import { distDir } from '../dirs' import { ImportProtectionPlugin } from './plugins/import-protection' diff --git a/packages/nuxt/src/head/module.ts b/packages/nuxt/src/head/module.ts index cb275d9b270..f613db69b21 100644 --- a/packages/nuxt/src/head/module.ts +++ b/packages/nuxt/src/head/module.ts @@ -1,5 +1,5 @@ import { resolve } from 'pathe' -import { addComponent, addPlugin, defineNuxtModule } from '@nuxt/kit' +import { addComponent, addPlugin, defineNuxtModule, tryResolveModule } from '@nuxt/kit' import { distDir } from '../dirs' const components = ['NoScript', 'Link', 'Base', 'Title', 'Meta', 'Style', 'Head', 'Html', 'Body'] @@ -11,8 +11,8 @@ export default defineNuxtModule({ setup (options, nuxt) { const runtimeDir = nuxt.options.alias['#head'] || resolve(distDir, 'head/runtime') - // Transpile @nuxt/meta and @vueuse/head - nuxt.options.build.transpile.push('@vueuse/head') + // Transpile @unhead/vue + nuxt.options.build.transpile.push('@unhead/vue') // Add #head alias nuxt.options.alias['#head'] = runtimeDir @@ -30,8 +30,14 @@ export default defineNuxtModule({ kebabName: componentName }) } + // Opt-out feature allowing dependencies using @vueuse/head to work + if (nuxt.options.experimental.polyfillVueUseHead) { + // backwards compatibility + nuxt.options.alias['@vueuse/head'] = tryResolveModule('@unhead/vue') || '@unhead/vue' + addPlugin({ src: resolve(runtimeDir, 'lib/vueuse-head-polyfill.plugin') }) + } - // Add library specific plugin - addPlugin({ src: resolve(runtimeDir, 'lib/vueuse-head.plugin') }) + // Add library-specific plugin + addPlugin({ src: resolve(runtimeDir, 'lib/unhead.plugin') }) } }) diff --git a/packages/nuxt/src/head/runtime/composables.ts b/packages/nuxt/src/head/runtime/composables.ts index 6c3e21041c3..de2c7c2bc1e 100644 --- a/packages/nuxt/src/head/runtime/composables.ts +++ b/packages/nuxt/src/head/runtime/composables.ts @@ -1,5 +1,5 @@ -import type { HeadEntryOptions, UseHeadInput, ActiveHeadEntry } from '@vueuse/head' -import { useSeoMeta as _useSeoMeta } from '@vueuse/head' +import type { HeadEntryOptions, UseHeadInput, ActiveHeadEntry } from '@unhead/vue' +import { useSeoMeta as _useSeoMeta } from '@unhead/vue' import type { HeadAugmentations } from 'nuxt/schema' import { useNuxtApp } from '#app/nuxt' diff --git a/packages/nuxt/src/head/runtime/index.ts b/packages/nuxt/src/head/runtime/index.ts index b39ad2a547a..5eddec2dafc 100644 --- a/packages/nuxt/src/head/runtime/index.ts +++ b/packages/nuxt/src/head/runtime/index.ts @@ -1,4 +1,4 @@ -import type { UseHeadInput } from '@vueuse/head' +import type { UseHeadInput } from '@unhead/vue' import type { HeadAugmentations } from 'nuxt/schema' export * from './composables' diff --git a/packages/nuxt/src/head/runtime/lib/vueuse-head.plugin.ts b/packages/nuxt/src/head/runtime/lib/unhead.plugin.ts similarity index 69% rename from packages/nuxt/src/head/runtime/lib/vueuse-head.plugin.ts rename to packages/nuxt/src/head/runtime/lib/unhead.plugin.ts index ec1d2f96ab4..3c44ac6e637 100644 --- a/packages/nuxt/src/head/runtime/lib/vueuse-head.plugin.ts +++ b/packages/nuxt/src/head/runtime/lib/unhead.plugin.ts @@ -1,4 +1,4 @@ -import { createHead, useHead } from '@vueuse/head' +import { createHead, useHead } from '@unhead/vue' import { renderSSRHead } from '@unhead/ssr' import { defineNuxtPlugin } from '#app/nuxt' // @ts-expect-error untyped @@ -16,25 +16,25 @@ export default defineNuxtPlugin((nuxtApp) => { const unpauseDom = () => { pauseDOMUpdates = false // triggers dom update - head.internalHooks.callHook('entries:updated', head.unhead) + head.hooks.callHook('entries:updated', head) } - head.internalHooks.hook('dom:beforeRender', (context) => { context.shouldRender = !pauseDOMUpdates }) + head.hooks.hook('dom:beforeRender', (context) => { context.shouldRender = !pauseDOMUpdates }) nuxtApp.hooks.hook('page:start', () => { pauseDOMUpdates = true }) // wait for new page before unpausing dom updates (triggered after suspense resolved) nuxtApp.hooks.hook('page:finish', unpauseDom) nuxtApp.hooks.hook('app:mounted', unpauseDom) } - // useHead does not depend on a vue component context, we keep it on the nuxtApp for backwards compatibility + // support backwards compatibility, remove at some point nuxtApp._useHead = useHead if (process.server) { nuxtApp.ssrContext!.renderMeta = async () => { - const meta = await renderSSRHead(head.unhead) + const meta = await renderSSRHead(head) return { ...meta, bodyScriptsPrepend: meta.bodyTagsOpen, - // resolves naming difference with NuxtMeta and @vueuse/head + // resolves naming difference with NuxtMeta and Unhead bodyScripts: meta.bodyTags } } diff --git a/packages/nuxt/src/head/runtime/lib/vueuse-head-polyfill.plugin.ts b/packages/nuxt/src/head/runtime/lib/vueuse-head-polyfill.plugin.ts new file mode 100644 index 00000000000..57d1a15bc4c --- /dev/null +++ b/packages/nuxt/src/head/runtime/lib/vueuse-head-polyfill.plugin.ts @@ -0,0 +1,8 @@ +// @ts-expect-error ts failing with type +import { polyfillAsVueUseHead } from '@unhead/vue/polyfill' +import { defineNuxtPlugin } from '#app/nuxt' + +export default defineNuxtPlugin((nuxtApp) => { + // avoid breaking ecosystem dependencies using low-level @vueuse/head APIs + polyfillAsVueUseHead(nuxtApp.vueApp._context.provides.usehead) +}) diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index c860fa8fda2..b9f23e5aee6 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -147,6 +147,14 @@ export default defineUntypedSchema({ * * @see https://github.com/nuxt/nuxt/issues/15592 */ - configSchema: true + configSchema: true, + + /** + * Whether or not to add a compatibility layer for modules, plugins or user code relying on the old + * `@vueuse/head` API. + * + * This can be disabled for most Nuxt sites to reduce the client-side bundle by ~0.5kb. + */ + polyfillVueUseHead: true } }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 751a6124293..b3e8178f7b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -418,10 +418,10 @@ importers: '@nuxt/vite-builder': workspace:* '@types/fs-extra': ^11.0.1 '@types/hash-sum': ^1.0.0 - '@unhead/ssr': ^1.1.16 + '@unhead/ssr': ^1.1.20 + '@unhead/vue': ^1.1.20 '@vue/reactivity': ^3.2.47 '@vue/shared': ^3.2.47 - '@vueuse/head': ^1.1.15 chokidar: ^3.5.3 cookie-es: ^0.5.0 defu: ^6.1.2 @@ -449,7 +449,6 @@ importers: unbuild: ^1.1.2 unctx: ^2.1.2 unenv: ^1.2.1 - unhead: ^1.1.16 unimport: ^3.0.2 unplugin: ^1.1.0 untyped: ^1.2.2 @@ -464,10 +463,10 @@ importers: '@nuxt/telemetry': 2.1.10 '@nuxt/ui-templates': 1.1.1 '@nuxt/vite-builder': link:../vite - '@unhead/ssr': 1.1.16 + '@unhead/ssr': 1.1.20 + '@unhead/vue': 1.1.20_vue@3.2.47 '@vue/reactivity': 3.2.47 '@vue/shared': 3.2.47 - '@vueuse/head': 1.1.15_vue@3.2.47 chokidar: 3.5.3 cookie-es: 0.5.0 defu: 6.1.2 @@ -494,7 +493,6 @@ importers: ufo: 1.1.1 unctx: 2.1.2 unenv: 1.2.1 - unhead: 1.1.16 unimport: 3.0.2 unplugin: 1.1.0 untyped: 1.2.2 @@ -2304,11 +2302,11 @@ packages: eslint-visitor-keys: 3.3.0 dev: true - /@unhead/dom/1.1.16: - resolution: {integrity: sha512-4JlwF4pNVynZsxT5687ntvS2yafGtWg7Y76IAPmLSOVrZ6fpYqF/vUn5+eCbMb0Og4C9ECrkbD9NSIqAiQ/0/Q==} + /@unhead/dom/1.1.20: + resolution: {integrity: sha512-LKd3Wq1myQmhea/5ysa9LsWWYFgtRhJ8D75PQfsIz9SOvjAUs6gNJi2Tv9adXr5gfFGXd0s2EMTLk/0IPTBwdg==} dependencies: - '@unhead/schema': 1.1.16 - '@unhead/shared': 1.1.16 + '@unhead/schema': 1.1.20 + '@unhead/shared': 1.1.20 dev: false /@unhead/schema/1.1.16: @@ -2316,29 +2314,37 @@ packages: dependencies: hookable: 5.4.2 zhead: 2.0.4 + dev: true - /@unhead/shared/1.1.16: - resolution: {integrity: sha512-Fien3han5vZ4sIaH89Tk9o7eensIDEdRynbwnik+WNo0PTfmaN/LCa3EYsacOdNYeZ3eMjjYpmQt6r+FJCqzSA==} + /@unhead/schema/1.1.20: + resolution: {integrity: sha512-XAMPCJjE+AhOW+HCYgxwNT7KX1Ch0y/byduJvM3kdwk4oA5lHRxF7M+s98FTZPpGJmlJr07CdW3l3w+qxmaJLQ==} dependencies: - '@unhead/schema': 1.1.16 + hookable: 5.4.2 + zhead: 2.0.4 dev: false - /@unhead/ssr/1.1.16: - resolution: {integrity: sha512-SCay47uCx7jHL30iX/D0chxOLLY5m2/WCU5bepCQlzFO25OFC6cEEYtG6/BQOIRVnyN8a2KvOU38jgL9xQZN9A==} + /@unhead/shared/1.1.20: + resolution: {integrity: sha512-8fJ1hB8bRw7JM9Lq98OSmLeDmajHHA9ymg8y7aDBC/R5lq+/WWSMtmXs+JNs4rsl+1gHPdl4YbjdEHp6OvcnpA==} dependencies: - '@unhead/schema': 1.1.16 - '@unhead/shared': 1.1.16 + '@unhead/schema': 1.1.20 + dev: false + + /@unhead/ssr/1.1.20: + resolution: {integrity: sha512-h+5rH4dMe+SorWJv2JiBVE8uchdXFbuSUfXtKBw/VmlBE69DXlZMEcIa2BDmp12wRiU9W8MtWOSvvPuBQ2Hm3g==} + dependencies: + '@unhead/schema': 1.1.20 + '@unhead/shared': 1.1.20 dev: false - /@unhead/vue/1.1.16_vue@3.2.47: - resolution: {integrity: sha512-EbE0kqJHDmebF+X6fL7HqGnbTa+b6h6SzIvE+D0i0/IwhD2lXRrPcBy0XyLV9CLBpAbLVG4OByxnBX+7Kk/vUA==} + /@unhead/vue/1.1.20_vue@3.2.47: + resolution: {integrity: sha512-I6TaAcO+B+czD/W6I6HQ2EFu/aJ7r0/EEz4Ltlg/YrdKL1beqh2nDY+R1Xz43EqKS0yvxGTlJHJ9cBmtSP2b0g==} peerDependencies: vue: '>=2.7 || >=3' dependencies: - '@unhead/schema': 1.1.16 - '@unhead/shared': 1.1.16 + '@unhead/schema': 1.1.20 + '@unhead/shared': 1.1.20 hookable: 5.4.2 - unhead: 1.1.16 + unhead: 1.1.20 vue: 3.2.47 dev: false @@ -2794,18 +2800,6 @@ packages: - vue dev: true - /@vueuse/head/1.1.15_vue@3.2.47: - resolution: {integrity: sha512-LJqvb7dpSqnsdn6YWUxv97vWCnn/s6IfBrE4ih5kRlh8XQXr/HjXJ8IyIxxp0X7QDr3FhOsjRDpJSiQbDYbBdQ==} - peerDependencies: - vue: '>=2.7 || >=3' - dependencies: - '@unhead/dom': 1.1.16 - '@unhead/schema': 1.1.16 - '@unhead/ssr': 1.1.16 - '@unhead/vue': 1.1.16_vue@3.2.47 - vue: 3.2.47 - dev: false - /@vueuse/integrations/9.12.0_focus-trap@7.3.1: resolution: {integrity: sha512-bu0hOQAqg7A8S33RHpr49LuzVQJ4tK4oyimEfhPFGUVqmz/MMcwPH8Lde+MbVXvfYh2hrtwNv9S38pCmonRx4w==} peerDependencies: @@ -8199,12 +8193,12 @@ packages: node-fetch-native: 1.0.2 pathe: 1.1.0 - /unhead/1.1.16: - resolution: {integrity: sha512-m2cPqEkgwHnA/di9P17TRS21p4aAJw4QolwTyXM+gzrlnFPe8gqOYuwEVnLRMS/NgoNWH7iPDOGyf5bbbrnTaw==} + /unhead/1.1.20: + resolution: {integrity: sha512-fic5YOINP2BD5PvyF05FIayIRaRYjujhuJ2uo2dIpLItAIBHYmBsTEkljsrwC+EULNfeC3Rf4UHxmr8u0EewCA==} dependencies: - '@unhead/dom': 1.1.16 - '@unhead/schema': 1.1.16 - '@unhead/shared': 1.1.16 + '@unhead/dom': 1.1.20 + '@unhead/schema': 1.1.20 + '@unhead/shared': 1.1.20 hookable: 5.4.2 dev: false diff --git a/test/basic.test.ts b/test/basic.test.ts index 84e26d953cf..132051fb74e 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -355,7 +355,7 @@ describe('nuxt links', () => { }) describe('head tags', () => { - it('should render tags', async () => { + it('SSR should render tags', async () => { const headHtml = await $fetch('/head') expect(headHtml).toContain('Using a dynamic component - Title Template Fn Change') @@ -377,6 +377,19 @@ describe('head tags', () => { expect(indexHtml).toContain('Basic fixture') }) + it('SPA should render appHead tags', async () => { + const headHtml = await $fetch('/head', { headers: { 'x-nuxt-no-ssr': '1' } }) + + expect(headHtml).toContain('') + expect(headHtml).toContain('') + expect(headHtml).toContain('') + }) + + it('legacy vueuse/head works', async () => { + const headHtml = await $fetch('/vueuse-head') + expect(headHtml).toContain('using provides usehead and updateDOM - VueUse head polyfill test') + }) + it('should render http-equiv correctly', async () => { const html = await $fetch('/head') // http-equiv should be rendered kebab case diff --git a/test/bundle.test.ts b/test/bundle.test.ts index 5a43c03f3fb..0592a4a7944 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -40,10 +40,10 @@ describe.skipIf(isWindows)('minimal nuxt application', () => { it('default server bundle size', async () => { stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) - expect(stats.server.totalBytes).toBeLessThan(93000) + expect(stats.server.totalBytes).toBeLessThan(94400) const modules = await analyzeSizes('node_modules/**/*', serverDir) - expect(modules.totalBytes).toBeLessThan(2722000) + expect(modules.totalBytes).toBeLessThan(2713000) const packages = modules.files .filter(m => m.endsWith('package.json')) @@ -55,7 +55,6 @@ describe.skipIf(isWindows)('minimal nuxt application', () => { "@unhead/dom", "@unhead/shared", "@unhead/ssr", - "@unhead/vue", "@vue/compiler-core", "@vue/compiler-dom", "@vue/compiler-ssr", diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index 9cefb047696..c249f3bfc67 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -18,7 +18,11 @@ export default defineNuxtConfig({ head: { charset: 'utf-8', link: [undefined], - meta: [{ name: 'viewport', content: 'width=1024, initial-scale=1' }, { charset: 'utf-8' }] + meta: [ + { name: 'viewport', content: 'width=1024, initial-scale=1' }, + { charset: 'utf-8' }, + { name: 'description', content: 'Nuxt Fixture' } + ] } }, buildDir: process.env.NITRO_BUILD_DIR, diff --git a/test/fixtures/basic/pages/vueuse-head.vue b/test/fixtures/basic/pages/vueuse-head.vue new file mode 100644 index 00000000000..af6b9b34997 --- /dev/null +++ b/test/fixtures/basic/pages/vueuse-head.vue @@ -0,0 +1,24 @@ + +