Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nuxt): client only page #25037

Merged
merged 58 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
0b653bc
refactor: allow wraping route component into `ClientOnly`
logotip4ik Jan 3, 2024
e3d63de
test: add tests for client only page
logotip4ik Jan 3, 2024
a0e28b9
chore: add `clientOnly` option to `PageMeta` interface
logotip4ik Jan 3, 2024
821a9c8
docs: mention `clientOnly` in definePageMeta
logotip4ik Jan 3, 2024
44e0750
fix ? wrap transition into client only
logotip4ik Jan 3, 2024
2a62ce0
test: type errors array and push only error text
logotip4ik Jan 3, 2024
4a568f8
Update docs/2.guide/2.directory-structure/1.pages.md
logotip4ik Jan 4, 2024
6f69fbc
Update packages/nuxt/src/pages/runtime/composables.ts
logotip4ik Jan 4, 2024
16e2761
Revert "fix ? wrap transition into client only"
logotip4ik Jan 4, 2024
39eb07f
wip rexport page component wrapped in createClientOnly
logotip4ik Jan 4, 2024
7221021
Merge branch 'main' into feat/client-only-page
logotip4ik Jan 14, 2024
d4c58fa
Merge branch 'main' into feat/client-only-page
logotip4ik Jan 18, 2024
b15125a
Reapply "fix ? wrap transition into client only"
logotip4ik Jan 19, 2024
ff16d81
is it really that broken ?
logotip4ik Jan 19, 2024
93bf622
Merge branch 'main' into feat/client-only-page
logotip4ik Jan 19, 2024
eaba14f
chore: invert if check
logotip4ik Jan 19, 2024
51167ff
refactor: reexport page component
logotip4ik Jan 20, 2024
8488b56
fix: export undefined if page source is empty
logotip4ik Jan 20, 2024
ba79c79
fix: only retest for client only page if not seen one yet
logotip4ik Jan 20, 2024
17caed5
fix: check macro page imports for definePageMeta macro
logotip4ik Jan 20, 2024
03703c4
refactor: remove not needed query for page component import
logotip4ik Jan 20, 2024
f8f5d9e
test: add little delay to account for vue transition
logotip4ik Jan 20, 2024
48a37cf
Merge branch 'main' into feat/client-only-page
logotip4ik Jan 20, 2024
0625b61
chore: pretend we are import from a file
logotip4ik Jan 20, 2024
f1598b1
chore: stub page wrapper file
logotip4ik Jan 20, 2024
a2fcafd
test: bump a bit delay
logotip4ik Jan 20, 2024
d462b0c
fix: actually add page wrapper stub file only for webpack
logotip4ik Jan 20, 2024
ef82a21
Merge remote-tracking branch 'origin/main' into feat/client-only-page
danielroe Jan 29, 2024
bcb046f
test: rename index client only page to index.client
logotip4ik Feb 1, 2024
7aff470
test: remove not needed `definePageMeta` call
logotip4ik Feb 1, 2024
994831b
refactor: add mode to page meta type
logotip4ik Feb 1, 2024
a273877
refactor: strip `client` extension from page segment and apply client…
logotip4ik Feb 1, 2024
6ae06e1
refactor: pass nuxt pages to page wrapper plugin to check for client …
logotip4ik Feb 1, 2024
5fc8063
refactor: use route meta to check if client mode is enabled
logotip4ik Feb 1, 2024
dd52fec
refactor: more consistent plugin naming
logotip4ik Feb 1, 2024
b9b648e
Merge branch 'main' of github.com:logotip4ik/nuxt into feat/client-on…
logotip4ik Feb 1, 2024
ad9797e
refactor: try to reexport page wrapped in createClienOnly
logotip4ik Feb 3, 2024
70409eb
chore: Remove not used plugin
logotip4ik Feb 3, 2024
ad55085
chore: less diff
logotip4ik Feb 3, 2024
c610822
chore: even less diff
logotip4ik Feb 3, 2024
4fadacf
chore: allow all page modes beeing the query value
logotip4ik Feb 3, 2024
d1417f5
Merge branch 'main' into feat/client-only-page
logotip4ik Feb 21, 2024
e831dca
Merge branch 'main' of github.com:logotip4ik/nuxt into feat/client-on…
logotip4ik Mar 1, 2024
f778228
fix: finally it works
logotip4ik Mar 1, 2024
087e038
chore: remove not used plugin
logotip4ik Mar 1, 2024
a3e7ef2
chore: cleanup type
logotip4ik Mar 1, 2024
3834257
chore: cleanup imports
logotip4ik Mar 1, 2024
68feb8b
chore: better description wording
logotip4ik Mar 1, 2024
3cefe8f
chore: run lint
logotip4ik Mar 1, 2024
5738bfe
chore: add no side effects to createClientPage
logotip4ik Mar 1, 2024
8e6ecd8
docs: add client only section in pages
logotip4ik Mar 1, 2024
da85070
docs: cleanup meta section
logotip4ik Mar 1, 2024
899ff0f
fix: allow passing down page props
logotip4ik Mar 2, 2024
6d240ce
chore: revert server replacement control flow changes
logotip4ik Mar 2, 2024
6f2620a
chore: use else-if to prevent double check
logotip4ik Mar 2, 2024
1a28d8d
Merge remote-tracking branch 'origin/main' into feat/client-only-page
danielroe Mar 6, 2024
86de839
style: lint
danielroe Mar 6, 2024
66fa33f
docs: add additional line
danielroe Mar 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/2.guide/2.directory-structure/1.pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@ You may define a name for this page's route.

You may define a path matcher, if you have a more complex pattern than can be expressed with the file name. See [the `vue-router` docs](https://router.vuejs.org/guide/essentials/route-matching-syntax.html#custom-regex-in-params) for more information.

#### `clientOnly`

Will wrap your page in [the Nuxt `<ClientOnly>` component](/docs/api/components/client-only) if you set `clientOnly: true` in your `definePageMeta`. This might be useful if you trying to import library that depends on `window` in page file.
logotip4ik marked this conversation as resolved.
Show resolved Hide resolved

### Typing Custom Metadata

If you add custom metadata for your pages, you may wish to do so in a type-safe way. It is possible to augment the type of the object accepted by `definePageMeta`:
Expand Down
2 changes: 2 additions & 0 deletions packages/nuxt/src/pages/runtime/composables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export interface PageMeta {
path?: string
/** Set to `false` to avoid scrolling to top on page navigations */
scrollToTop?: boolean | ((to: RouteLocationNormalizedLoaded, from: RouteLocationNormalizedLoaded) => boolean)
/** Make this page run on client only */
logotip4ik marked this conversation as resolved.
Show resolved Hide resolved
clientOnly?: boolean
}

declare module 'vue-router' {
Expand Down
48 changes: 26 additions & 22 deletions packages/nuxt/src/pages/runtime/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { toArray } from './utils'
import type { RouterViewSlotProps } from './utils'
import { generateRouteKey, wrapInKeepAlive } from './utils'
import { RouteProvider } from '#app/components/route-provider'
import ClientOnly from '#app/components/client-only'
logotip4ik marked this conversation as resolved.
Show resolved Hide resolved
import { useNuxtApp } from '#app/nuxt'
import { _wrapIf } from '#app/components/utils'
import { LayoutMetaSymbol, PageRouteSymbol } from '#app/components/injections'
Expand Down Expand Up @@ -90,36 +91,39 @@ export default defineComponent({
const key = generateRouteKey(routeProps, props.pageKey)

const hasTransition = !!(props.transition ?? routeProps.route.meta.pageTransition ?? defaultPageTransition)
const keepaliveConfig = props.keepalive ?? routeProps.route.meta.keepalive ?? (defaultKeepaliveConfig as KeepAliveProps)
const clientOnlyConfig = routeProps.route.meta.clientOnly;
const transitionProps = hasTransition && _mergeTransitionProps([
props.transition,
routeProps.route.meta.pageTransition,
defaultPageTransition,
{ onAfterLeave: () => { nuxtApp.callHook('page:transition:finish', routeProps.Component) } }
].filter(Boolean))

const keepaliveConfig = props.keepalive ?? routeProps.route.meta.keepalive ?? (defaultKeepaliveConfig as KeepAliveProps)
vnode = _wrapIf(Transition, hasTransition && transitionProps,
wrapInKeepAlive(keepaliveConfig, h(Suspense, {
suspensible: true,
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
onResolve: () => { nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component).then(() => nuxtApp.callHook('page:loading:end')).finally(done)) }
}, {
default: () => {
const providerVNode = h(RouteProvider, {
key: key || undefined,
vnode: routeProps.Component,
route: routeProps.route,
renderKey: key || undefined,
trackRootNodes: hasTransition,
vnodeRef: pageRef
})
if (import.meta.client && keepaliveConfig) {
(providerVNode.type as any).name = (routeProps.Component.type as any).name || (routeProps.Component.type as any).__name || 'RouteProvider'
vnode = _wrapIf(ClientOnly, clientOnlyConfig,
_wrapIf(Transition, hasTransition && transitionProps,
wrapInKeepAlive(keepaliveConfig, h(Suspense, {
suspensible: true,
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
onResolve: () => { nextTick(() => nuxtApp.callHook('page:finish', routeProps.Component).then(() => nuxtApp.callHook('page:loading:end')).finally(done)) }
}, {
default: () => {
const providerVNode = h(RouteProvider, {
key: key || undefined,
vnode: routeProps.Component,
route: routeProps.route,
renderKey: key || undefined,
trackRootNodes: hasTransition,
vnodeRef: pageRef
})
if (import.meta.client && keepaliveConfig) {
(providerVNode.type as any).name = (routeProps.Component.type as any).name || (routeProps.Component.type as any).__name || 'RouteProvider'
}
return providerVNode
}
return providerVNode
}
})
)).default()
})
))
).default()

return vnode
}
Expand Down
67 changes: 67 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,73 @@ describe('pages', () => {
expect(response).not.toContain('don\'t look at this')
expect(response).toContain('OH NNNNNNOOOOOOOOOOO')
})

it('client only page', async () => {
const response = await fetch('/client-only').then(r => r.text())

// Should not contain rendered page on initial request
expect(response).not.toContain('"hasAccessToWindow": true')
expect(response).not.toContain('"isServer": false')

const errors: string[] = []
const { page: clientInitialPage } = await renderPage('/client-only-page')

clientInitialPage.on('console', (message) => {
const type = message.type()
if (type === 'error' || type === 'warning') {
errors.push(message.text())
}
})

// But after hydration element should appear and contain this object
expect(await clientInitialPage.locator('#state').textContent()).toMatchInlineSnapshot(`
"{
"hasAccessToWindow": true,
"isServer": false
}"
`)

expect(await clientInitialPage.locator('#server-rendered').textContent()).toMatchInlineSnapshot(`"false"`)

// Then go to non client only page
await clientInitialPage.click('a');

// that page should be client rendered
expect(await clientInitialPage.locator('#server-rendered').textContent()).toMatchInlineSnapshot(`"false"`)
// and not contain any errors or warnings
expect(errors.length).toBe(0);

await clientInitialPage.close();
errors.length = 0;

const { page: normalInitialPage } = await renderPage('/client-only-page/normal')

normalInitialPage.on('console', (message) => {
const type = message.type()
if (type === 'error' || type === 'warning') {
errors.push(message.text())
}
})

// Now non client only page should be sever rendered
expect(await normalInitialPage.locator('#server-rendered').textContent()).toMatchInlineSnapshot(`"true"`)

// Go to client only page
await normalInitialPage.click('a')

// and expect same object to be present
expect(await normalInitialPage.locator('#state').textContent()).toMatchInlineSnapshot(`
"{
"hasAccessToWindow": true,
"isServer": false
}"
`)

// also there should not be any errors
expect(errors.length).toBe(0);

await normalInitialPage.close()
})
})

describe('nuxt composables', () => {
Expand Down
36 changes: 36 additions & 0 deletions test/fixtures/basic/pages/client-only-page/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
definePageMeta({ clientOnly: true })

const state = useState('test', () => {
let hasAccessToWindow = null as null | boolean;

try {
hasAccessToWindow = Object.keys(window).at(0) ? true : false
} catch {
hasAccessToWindow = null
}

return {
hasAccessToWindow,
isServer: import.meta.server
}
})

const serverRendered = useState(() => import.meta.server)
</script>

<template>
<div>
<NuxtLink to="/client-only-page/normal">
normal
</NuxtLink>

<p id="state">
{{ state }}
</p>

<p id="server-rendered">
{{ serverRendered }}
</p>
</div>
</template>
15 changes: 15 additions & 0 deletions test/fixtures/basic/pages/client-only-page/normal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script setup lang="ts">
const renderedOnServer = useState(() => import.meta.server)
</script>

<template>
<div>
<NuxtLink to="/client-only-page">
to client only page
</NuxtLink>

<p id="server-rendered">
{{ renderedOnServer }}
</p>
</div>
</template>