Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

feat(nuxt3): add support for definePageMeta macro #2678

Merged
merged 28 commits into from
Jan 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c438a5a
feat(nuxt3): add support for `definePageMeta` macro
danielroe Jan 11, 2022
8ff35ea
feat(nuxt3): add support for defining `transition` and `layout` via r…
danielroe Jan 11, 2022
a2e2e04
perf(vite): do not resolve `module` field for packages (#2636)
antfu Jan 11, 2022
98d9f72
fix: export empty default for hmr
danielroe Jan 11, 2022
c36641a
docs: update layout example
danielroe Jan 11, 2022
390a28a
docs: update comment
danielroe Jan 11, 2022
07df171
fix: allow disabling transition
danielroe Jan 11, 2022
8d46d8f
Merge branch 'main' into feat/route-meta-macro
danielroe Jan 11, 2022
bc3ec2b
fix: extract non-word chars
danielroe Jan 11, 2022
72feb43
fix: only replace comments on a line by themselves
danielroe Jan 13, 2022
88e259a
[release edge]
pi0 Jan 12, 2022
2c0c626
feat(bridge): upgrade unplugin-vue2-script-setup (#2687)
antfu Jan 13, 2022
232f67c
fix(vite): build fails with ssr turned off (#2652)
blazmrak Jan 13, 2022
1010975
chore(deps): update all non-major dependencies (#2676)
renovate[bot] Jan 13, 2022
958ef64
ignore vite upgrade [release]
pi0 Jan 13, 2022
fbadf57
chore(deps): update all non-major dependencies (#2707)
renovate[bot] Jan 13, 2022
c7205de
chore: migrate to vitest from mocha (#2694)
antfu Jan 13, 2022
99f9186
chore(deps): update all non-major dependencies (#2714)
renovate[bot] Jan 13, 2022
0f3ba14
chore(deps): update dependency globby to v12 (#2659)
renovate[bot] Jan 13, 2022
6c9f677
Merge remote-tracking branch 'origin/main' into feat/route-meta-macro
danielroe Jan 13, 2022
dcd668d
fix: explicitly export only macros
danielroe Jan 13, 2022
3d2f581
Update packages/nuxt3/src/pages/runtime/composables.ts
danielroe Jan 14, 2022
0226991
refactor: use `findExports` from `mlly`
danielroe Jan 17, 2022
429c31c
Merge remote-tracking branch 'origin/main' into feat/route-meta-macro
danielroe Jan 17, 2022
e51c4fc
Revert "perf(vite): do not resolve `module` field for packages (#2636)"
danielroe Jan 17, 2022
0759996
Merge branch 'main' into feat/route-meta-macro
danielroe Jan 17, 2022
b2eab92
Merge branch 'main' into feat/route-meta-macro
pi0 Jan 17, 2022
1160770
update lockfile
pi0 Jan 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 23 additions & 36 deletions docs/content/3.docs/2.directory-structure/6.layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,20 @@ Given the example above, you can use a custom layout like this:

```vue
<script>
export default {
// This will also work in `<script setup>`
definePageMeta({
layout: "custom",
};
});
</script>
```

::alert{type=info}
Learn more about [defining page meta](/docs/directory-structure/pages#page-metadata).
::

## Example: using with slots

You can also take full control (for example, with slots) by using the `<NuxtLayout>` component (which is globally available throughout your application) and set `layout: false` in your component options.
You can also take full control (for example, with slots) by using the `<NuxtLayout>` component (which is globally available throughout your application) by setting `layout: false`.

```vue
<template>
Expand All @@ -53,51 +58,33 @@ You can also take full control (for example, with slots) by using the `<NuxtLayo
</NuxtLayout>
</template>

<script>
export default {
<script setup>
definePageMeta({
layout: false,
};
});
</script>
```

## Example: using with `<script setup>`

If you are utilizing Vue `<script setup>` [compile-time syntactic sugar](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup), you can use a secondary `<script>` tag to set `layout` options as needed.

::alert{type=info}
Learn more about [`<script setup>` and `<script>` tags co-existing](https://v3.vuejs.org/api/sfc-script-setup.html#usage-alongside-normal-script) in the Vue docs.
::

Assuming this directory structure:

```bash
-| layouts/
---| custom.vue
-| pages/
---| my-page.vue
```
## Example: changing the layout

And this `custom.vue` layout:
You can also use a ref or computed property for your layout.

```vue
<template>
<div>
Some shared layout content:
<slot />
<button @click="enableCustomLayout">Update layout</button>
</div>
</template>
```

You can set a page layout in `my-page.vue` β€” alongside the `<script setup>` tag β€” like this:

```vue
<script>
export default {
layout: "custom",
};
</script>

<script setup>
// your setup script
const route = useRoute()
function enableCustomLayout () {
// Note: because it's within a ref, it will persist if
// you navigate away and then back to the page.
route.layout.value = "custom"
}
definePageMeta({
layout: ref(false),
});
</script>
```
49 changes: 49 additions & 0 deletions docs/content/3.docs/2.directory-structure/9.pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,52 @@ To display theΒ `child.vue`Β component, you have to insert theΒ `<NuxtNestedPage
</div>
</template>
```

## Page Metadata

You might want to define metadata for each route in your app. You can do this using the `definePageMeta` macro, which will work both in `<script>` and in `<script setup>`:

```vue
<script setup>
definePageMeta({
title: 'My home page'
})
```

This data can then be accessed throughout the rest of your app from the `route.meta` object.

```vue
<script setup>
const route = useRoute()
console.log(route.meta.title) // My home page
</script>
```

If you are using nested routes, the page metadata from all these routes will be merged into a single object. For more on route meta, see the [vue-router docs](https://next.router.vuejs.org/guide/advanced/meta.html#route-meta-fields).

Much like `defineEmits` or `defineProps` (see [Vue docs](https://v3.vuejs.org/api/sfc-script-setup.html#defineprops-and-defineemits)), `definePageMeta` is a **compiler macro**. It will be compiled away so you cannot reference it within your component. Instead, the metadata passed to it will be hoisted out of the component. Therefore, the page meta object cannot reference the component (or values defined on the component). However, it can reference imported bindings.

```vue
<script setup>
import { someData } from '~/utils/example'

const title = ref('')

definePageMeta({
title,
someData
})
</script>
```

### Special Metadata

Of course, you are welcome to define metadata for your own use throughout your app. But some metadata defined with `definePageMeta` has a particular purpose:

#### `layout`

You can define the layout used to render the route. This can be either false (to disable any layout), a string or a ref/computed, if you want to make it reactive in some way. [More about layouts](/docs/directory-structure/layouts).

#### `transition`

You can define transition properties for the `<transition>` component that wraps your pages, or pass `false` to disable the `<transition>` wrapper for that route. [More about transitions](https://v3.vuejs.org/guide/transitions-overview.html).
4 changes: 2 additions & 2 deletions examples/with-layouts/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
</div>
</template>

<script>
export default defineNuxtComponent({
<script setup>
definePageMeta({
layout: 'custom'
})
</script>
4 changes: 3 additions & 1 deletion examples/with-layouts/pages/manual.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
</template>

<script>
definePageMeta({
layout: false
})
export default {
layout: false,
data: () => ({
layout: 'custom'
})
Expand Down
4 changes: 2 additions & 2 deletions examples/with-layouts/pages/same.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</template>

<script>
export default {
definePageMeta({
layout: 'custom'
}
})
</script>
4 changes: 2 additions & 2 deletions packages/nuxt3/src/auto-imports/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const TransformPlugin = createUnplugin((ctx: AutoImportContext) => {
enforce: 'post',
transformInclude (id) {
const { pathname, search } = parseURL(id)
const { type } = parseQuery(search)
const { type, macro } = parseQuery(search)

// Exclude node_modules by default
if (ctx.transform.exclude.some(pattern => id.match(pattern))) {
Expand All @@ -47,7 +47,7 @@ export const TransformPlugin = createUnplugin((ctx: AutoImportContext) => {
// vue files
if (
pathname.endsWith('.vue') &&
(type === 'template' || type === 'script' || !search)
(type === 'template' || type === 'script' || macro || !search)
) {
return true
}
Expand Down
92 changes: 92 additions & 0 deletions packages/nuxt3/src/pages/macros.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { createUnplugin } from 'unplugin'
import { parseQuery, parseURL, withQuery } from 'ufo'
import { findStaticImports, findExports } from 'mlly'

export interface TransformMacroPluginOptions {
macros: Record<string, string>
}

export const TransformMacroPlugin = createUnplugin((options: TransformMacroPluginOptions) => {
return {
name: 'nuxt-pages-macros-transform',
enforce: 'post',
transformInclude (id) {
// We only process SFC files for macros
return parseURL(id).pathname.endsWith('.vue')
},
transform (code, id) {
const { search } = parseURL(id)

// Tree-shake out any runtime references to the macro.
// We do this first as it applies to all files, not just those with the query
for (const macro in options.macros) {
const match = code.match(new RegExp(`\\b${macro}\\s*\\(\\s*`))?.[0]
if (match) {
code = code.replace(match, `/*#__PURE__*/ false && ${match}`)
}
}

if (!parseQuery(search).macro) { return code }

// [webpack] Re-export any imports from script blocks in the components
// with workaround for vue-loader bug: https://github.com/vuejs/vue-loader/pull/1911
const scriptImport = findStaticImports(code).find(i => parseQuery(i.specifier.replace('?macro=true', '')).type === 'script')
pi0 marked this conversation as resolved.
Show resolved Hide resolved
if (scriptImport) {
const specifier = withQuery(scriptImport.specifier.replace('?macro=true', ''), { macro: 'true' })
return `export { meta } from "${specifier}"`
}

const currentExports = findExports(code)
for (const match of currentExports) {
if (match.type !== 'default') { continue }
if (match.specifier && match._type === 'named') {
// [webpack] Export named exports rather than the default (component)
return code.replace(match.code, `export {${Object.values(options.macros).join(', ')}} from "${match.specifier}"`)
} else {
// ensure we tree-shake any _other_ default exports out of the macro script
code = code.replace(match.code, '/*#__PURE__*/ false &&')
code += '\nexport default {}'
}
}

for (const macro in options.macros) {
// Skip already-processed macros
if (currentExports.some(e => e.name === options.macros[macro])) { continue }

const { 0: match, index = 0 } = code.match(new RegExp(`\\b${macro}\\s*\\(\\s*`)) || {} as RegExpMatchArray
const macroContent = match ? extractObject(code.slice(index + match.length)) : 'undefined'

code += `\nexport const ${options.macros[macro]} = ${macroContent}`
}

return code
}
}
})

const starts = {
'{': '}',
'[': ']',
'(': ')',
'<': '>',
'"': '"',
"'": "'"
}

function extractObject (code: string) {
// Strip comments
code = code.replace(/^\s*\/\/.*$/gm, '')

const stack = []
let result = ''
do {
if (stack[0] === code[0] && result.slice(-1) !== '\\') {
stack.shift()
} else if (code[0] in starts) {
stack.unshift(starts[code[0]])
}
result += code[0]
code = code.slice(1)
} while (stack.length && code.length)
return result
}
19 changes: 15 additions & 4 deletions packages/nuxt3/src/pages/module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { existsSync } from 'fs'
import { defineNuxtModule, addTemplate, addPlugin, templateUtils } from '@nuxt/kit'
import { defineNuxtModule, addTemplate, addPlugin, templateUtils, addVitePlugin, addWebpackPlugin } from '@nuxt/kit'
import { resolve } from 'pathe'
import { distDir } from '../dirs'
import { resolveLayouts, resolvePagesRoutes, addComponentToRoutes } from './utils'
import { resolveLayouts, resolvePagesRoutes, normalizeRoutes } from './utils'
import { TransformMacroPlugin, TransformMacroPluginOptions } from './macros'

export default defineNuxtModule({
meta: {
Expand Down Expand Up @@ -41,8 +42,18 @@ export default defineNuxtModule({
const composablesFile = resolve(runtimeDir, 'composables')
autoImports.push({ name: 'useRouter', as: 'useRouter', from: composablesFile })
autoImports.push({ name: 'useRoute', as: 'useRoute', from: composablesFile })
autoImports.push({ name: 'definePageMeta', as: 'definePageMeta', from: composablesFile })
})

// Extract macros from pages
const macroOptions: TransformMacroPluginOptions = {
macros: {
definePageMeta: 'meta'
}
}
addVitePlugin(TransformMacroPlugin.vite(macroOptions))
addWebpackPlugin(TransformMacroPlugin.webpack(macroOptions))

// Add router plugin
addPlugin(resolve(runtimeDir, 'router'))

Expand All @@ -52,8 +63,8 @@ export default defineNuxtModule({
async getContents () {
const pages = await resolvePagesRoutes(nuxt)
await nuxt.callHook('pages:extend', pages)
const serializedRoutes = addComponentToRoutes(pages)
return `export default ${templateUtils.serialize(serializedRoutes)}`
const { routes: serializedRoutes, imports } = normalizeRoutes(pages)
return [...imports, `export default ${templateUtils.serialize(serializedRoutes)}`].join('\n')
}
})

Expand Down
27 changes: 27 additions & 0 deletions packages/nuxt3/src/pages/runtime/composables.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ComputedRef, /* KeepAliveProps, */ Ref, TransitionProps } from 'vue'
import type { Router, RouteLocationNormalizedLoaded } from 'vue-router'
import { useNuxtApp } from '#app'

Expand All @@ -8,3 +9,29 @@ export const useRouter = () => {
export const useRoute = () => {
return useNuxtApp()._route as RouteLocationNormalizedLoaded
}

export interface PageMeta {
[key: string]: any
transition?: false | TransitionProps
layout?: false | string | Ref<false | string> | ComputedRef<false | string>
// TODO: https://github.com/vuejs/vue-next/issues/3652
// keepalive?: false | KeepAliveProps
}

declare module 'vue-router' {
interface RouteMeta extends PageMeta {}
}

const warnRuntimeUsage = (method: string) =>
console.warn(
`${method}() is a compiler-hint helper that is only usable inside ` +
'<script setup> of a single file component. Its arguments should be ' +
'compiled away and passing it at runtime has no effect.'
)

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const definePageMeta = (meta: PageMeta): void => {
if (process.dev) {
warnRuntimeUsage('definePageMeta')
}
}
6 changes: 3 additions & 3 deletions packages/nuxt3/src/pages/runtime/layout.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { defineComponent, h } from 'vue'
import { defineComponent, h, Ref } from 'vue'
// @ts-ignore
import layouts from '#build/layouts'

export default defineComponent({
props: {
name: {
type: [String, Boolean],
type: [String, Boolean, Object] as unknown as () => string | false | Ref<string | false>,
default: 'default'
}
},
setup (props, context) {
return () => {
const layout = props.name
const layout = (props.name && typeof props.name === 'object' ? props.name.value : props.name) ?? 'default'
if (!layouts[layout]) {
if (process.dev && layout && layout !== 'default') {
console.warn(`Invalid layout \`${layout}\` selected.`)
Expand Down
Loading