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): server-only pages #24954

Merged
merged 51 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
0097fa4
wip test
huang-julien Nov 22, 2023
f35b414
feat: use current route path and remove component from preload
huang-julien Nov 26, 2023
338d479
Merge branch 'main' into feat/server-page
huang-julien Dec 3, 2023
283096d
fix: fix components.islands template
huang-julien Dec 5, 2023
a9caf7f
Merge remote-tracking branch 'origin/main' into feat/server-page
huang-julien Dec 5, 2023
2dc1242
test: rename SugarCounter to Counter
huang-julien Dec 5, 2023
a9d3ae8
Merge remote-tracking branch 'origin/main' into feat/server-page
huang-julien Dec 29, 2023
10ca60f
fix: stop loading indicator after server page end
huang-julien Dec 29, 2023
446b67f
Merge remote-tracking branch 'origin/main' into feat/server-page
huang-julien Feb 3, 2024
8afef3d
refactor: test by moving all to build time
huang-julien Feb 4, 2024
f939108
chore: lint
huang-julien Feb 4, 2024
70c9809
chore: LINNNTTT
huang-julien Feb 4, 2024
a29d5c2
chore: LIIIIIIIIIIIIIIIIIIIINNNNNNNNTTTTTTTTTTT
huang-julien Feb 4, 2024
ce525f5
fix: correctly laod server page in runtime
huang-julien Feb 4, 2024
b0f2f3b
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 4, 2024
0c2844b
fix: fix route file generation and introduce createIslandPage to hand…
huang-julien Feb 11, 2024
4a55e8c
Merge branch 'feat/server-page' of https://github.com/nuxt/nuxt into …
huang-julien Feb 11, 2024
a763824
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 11, 2024
9bc0e9b
fix: fix .server route detection
huang-julien Feb 11, 2024
6123b66
Merge branch 'feat/server-page' of https://github.com/nuxt/nuxt into …
huang-julien Feb 11, 2024
8703c28
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 11, 2024
9a5c498
Merge branch 'main' into feat/server-page
huang-julien Feb 14, 2024
807ff78
chore: cleanup
huang-julien Feb 14, 2024
b3b1947
docs: add meta data to docs
Feb 20, 2024
b69349d
Merge branch 'main' into feat/server-page
huang-julien Feb 20, 2024
ff927e1
chore: remove hook and remove spaces
huang-julien Feb 20, 2024
f0bdf48
fix: fix naming
huang-julien Feb 20, 2024
d0d0820
test: add basic test
huang-julien Feb 20, 2024
f7f8ef0
refactor: use mode like in the 25037 PR
huang-julien Feb 20, 2024
685078d
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 20, 2024
987e32c
fix(nuxt): fix serialization
huang-julien Feb 20, 2024
81b13b5
Merge branch 'feat/server-page' of https://github.com/nuxt/nuxt into …
huang-julien Feb 20, 2024
b259749
fix: remove chunk comment
huang-julien Feb 20, 2024
0e20478
chore: remove unwanted line/space changes
huang-julien Feb 21, 2024
5292d9a
chore: add back tab
huang-julien Feb 21, 2024
b673a33
Apply suggestions from code review
huang-julien Feb 24, 2024
cfb5708
fix: move url path to context
huang-julien Feb 25, 2024
f40e653
Merge branch 'feat/server-page' of https://github.com/nuxt/nuxt into …
huang-julien Feb 25, 2024
bff2a85
docs: add warning about query not being send to server pages
huang-julien Feb 25, 2024
d969135
chore: update import
danielroe Feb 25, 2024
67b697c
docs: update slightly
danielroe Feb 25, 2024
9a29f0c
perf: use single dynamic import
danielroe Feb 25, 2024
a01b82e
Merge remote-tracking branch 'origin/main' into feat/server-page
danielroe Feb 25, 2024
3207fd6
fix: allow setting 'all' as a mode
danielroe Feb 25, 2024
51a30d7
perf: simplify
danielroe Feb 25, 2024
afd8cb8
refactor: only enable server pages at build with `.server.vue` and in…
danielroe Feb 26, 2024
2bbef5b
Merge remote-tracking branch 'origin/main' into feat/server-page
danielroe Feb 26, 2024
948c374
fix: pass query for non-prerendered server pages
danielroe Feb 26, 2024
3a1262a
fix: use route, not normalised page
danielroe Feb 26, 2024
523d55e
perf: use more exact import
danielroe Feb 26, 2024
6a64292
fix: don't pass attrs/props
danielroe Feb 26, 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
1 change: 0 additions & 1 deletion packages/nuxt/src/app/components/island-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export default defineComponent({
},
setup (props) {
const component = islandComponents[props.context.name] as ReturnType<typeof defineAsyncComponent>

if (!component) {
throw createError({
statusCode: 404,
Expand Down
12 changes: 10 additions & 2 deletions packages/nuxt/src/app/components/nuxt-island.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Component } from 'vue'
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onMounted, ref, toRaw, watch, withMemo } from 'vue'
import { Fragment, Teleport, computed, createStaticVNode, createVNode, defineComponent, getCurrentInstance, h, nextTick, onErrorCaptured, onMounted, ref, toRaw, watch, withMemo } from 'vue'
import { debounce } from 'perfect-debounce'
import { hash } from 'ohash'
import { appendResponseHeader } from 'h3'
Expand All @@ -13,8 +13,10 @@ import { join } from 'pathe'
import type { NuxtIslandResponse } from '../../core/runtime/nitro/renderer'
import { useNuxtApp, useRuntimeConfig } from '../nuxt'
import { prerenderRoutes, useRequestEvent } from '../composables/ssr'
import { useRoute } from '../../app/composables/router'
huang-julien marked this conversation as resolved.
Show resolved Hide resolved
import { getFragmentHTML } from './utils'


// @ts-expect-error virtual file
import { remoteComponentIslands, selectiveClient } from '#build/nuxt.config.mjs'

Expand Down Expand Up @@ -70,6 +72,7 @@ export default defineComponent({
}
},
async setup (props, { slots, expose }) {
const route = useRoute()
let canTeleport = import.meta.server
const teleportKey = ref(0)
const key = ref(0)
Expand Down Expand Up @@ -146,6 +149,7 @@ export default defineComponent({
const cHead = ref<Record<'link' | 'style', Array<Record<string, string>>>>({ link: [], style: [] })
useHead(cHead)

onErrorCaptured((err) => { console.error(err)})
async function _fetchComponent (force = false) {
const key = `${props.name}_${hashId.value}`

Expand All @@ -157,9 +161,11 @@ export default defineComponent({
// Hint to Nitro to prerender the island component
nuxtApp.runWithContext(() => prerenderRoutes(url))
}
// TODO: Validate response

// TODO: Validate response
// $fetch handles the app.baseURL in dev
const r = await eventFetch(withQuery(((import.meta.dev && import.meta.client) || props.source) ? url : joinURL(config.app.baseURL ?? '', url), {
url: route.fullPath,
Copy link
Member

@danielroe danielroe Feb 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will make it much harder to cache server islands as the hash we create will vary by path.

Could we instead pass this as part of the context via createIslandPage instead (and maybe use route.path to skip query/hash)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should not skip query πŸ€”, userss tend to relly on it within pages composables. Maybe can we pass an object like

{
  path: string,
query?: Record<string, string>
}

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is (for example) that we will make a separate request for every query variation, which wouldn't be compatible with static sites and allow cache-busting by passing arbitrary query params or hashes ... (We should definitely not pass hash.) We could document this as a limitation of server pages... Or at a minimum, skip passing the full route if the route matches prerender routeRule?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, let's start by passing only the path in the context then πŸ€”. and iterate on it, i'm not sure what kind of side effect we can have if using fullPath with ssg

...props.context,
props: props.props ? JSON.stringify(props.props) : undefined
}))
Expand All @@ -173,6 +179,8 @@ export default defineComponent({
}
setPayload(key, result)
return result


}

async function fetchComponent (force = false) {
Expand Down
3 changes: 1 addition & 2 deletions packages/nuxt/src/components/islandsTransform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import MagicString from 'magic-string'
import { ELEMENT_NODE, parse, walk } from 'ultrahtml'
import { hash } from 'ohash'
import { resolvePath } from '@nuxt/kit'
import { isVue } from '../core/utils'

import { isVue } from '../core/utils'
interface ServerOnlyComponentTransformPluginOptions {
getComponents: () => Component[]
/**
Expand Down
25 changes: 25 additions & 0 deletions packages/nuxt/src/components/runtime/server-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,28 @@ export const createServerComponent = (name: string) => {
}
})
}

/*@__NO_SIDE_EFFECTS__*/
export const createIslandPage = (name: string) => {
return defineComponent({
name,
inheritAttrs: false,
props: { lazy: Boolean },
setup (props, { attrs, slots, expose }) {
const islandRef = ref<null | typeof NuxtIsland>(null)

expose({
refresh: () => islandRef.value?.refresh()
})

return () => {
return h('div', [h(NuxtIsland, {
name,
lazy: props.lazy,
props: attrs,
ref: islandRef
}, slots)])
}
}
})
}
11 changes: 10 additions & 1 deletion packages/nuxt/src/components/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,21 @@ export const componentsIslandsTemplate: NuxtTemplate = {
// components.islands.mjs'
getContents ({ app }) {
const components = app.components
const pages = app.pages
const islands = components.filter(component =>
component.island ||
// .server components without a corresponding .client component will need to be rendered as an island
(component.mode === 'server' && !components.some(c => c.pascalName === component.pascalName && c.mode === 'client'))
)

const pageExports = pages?.filter(p => (p.server && p.file && p.name)).map((p) => {
const comment = createImportMagicComments({
chunkName: p.file!
})

return `"${p.name}": defineAsyncComponent(${genDynamicImport(p.file!, { comment })})`
}) || []

return [
'import { defineAsyncComponent } from \'vue\'',
'export const islandComponents = import.meta.client ? {} : {',
Expand All @@ -87,7 +96,7 @@ export const componentsIslandsTemplate: NuxtTemplate = {
const comment = createImportMagicComments(c)
return ` "${c.pascalName}": defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
}
).join(',\n'),
).concat(pageExports).join(',\n'),
'}'
].join('\n')
}
Expand Down
5 changes: 4 additions & 1 deletion packages/nuxt/src/pages/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,11 @@ export default defineNuxtModule({
nuxt.hook('build:manifest', (manifest) => {
if (nuxt.options.dev) { return }
const sourceFiles = getSources(nuxt.apps.default.pages || [])

for (const key in manifest) {
if (manifest[key].src && Object.values(nuxt.apps).some(app => app.pages?.some(page => page.server && page.file === join(nuxt.options.srcDir, manifest[key].src!) ))) {
delete manifest[key]
continue
}
if (manifest[key].isEntry) {
manifest[key].dynamicImports =
manifest[key].dynamicImports?.filter(i => !sourceFiles.includes(i))
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)
/** render the page as a server island */
island?: boolean
}

declare module 'vue-router' {
Expand Down
8 changes: 4 additions & 4 deletions packages/nuxt/src/pages/runtime/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default defineComponent({
default: null
}
},
setup (props, { attrs, expose }) {
setup(props, { attrs, expose }) {
const nuxtApp = useNuxtApp()
const pageRef = ref()
const forkRoute = inject(PageRouteSymbol, null)
Expand Down Expand Up @@ -142,15 +142,15 @@ export default defineComponent({
}
})

function _mergeTransitionProps (routeProps: TransitionProps[]): TransitionProps {
function _mergeTransitionProps(routeProps: TransitionProps[]): TransitionProps {
const _props: TransitionProps[] = routeProps.map(prop => ({
...prop,
onAfterLeave: prop.onAfterLeave ? toArray(prop.onAfterLeave) : undefined
}))
return defu(..._props as [TransitionProps, TransitionProps])
}

function haveParentRoutesRendered (fork: RouteLocationNormalizedLoaded | null, newRoute: RouteLocationNormalizedLoaded, Component?: VNode) {
function haveParentRoutesRendered(fork: RouteLocationNormalizedLoaded | null, newRoute: RouteLocationNormalizedLoaded, Component?: VNode) {
if (!fork) { return false }

const index = newRoute.matched.findIndex(m => m.components?.default === Component?.type)
Expand All @@ -163,7 +163,7 @@ function haveParentRoutesRendered (fork: RouteLocationNormalizedLoaded | null, n
(Component && generateRouteKey({ route: newRoute, Component }) !== generateRouteKey({ route: fork, Component }))
}

function hasChildrenRoutes (fork: RouteLocationNormalizedLoaded | null, newRoute: RouteLocationNormalizedLoaded, Component?: VNode) {
function hasChildrenRoutes(fork: RouteLocationNormalizedLoaded | null, newRoute: RouteLocationNormalizedLoaded, Component?: VNode) {
if (!fork) { return false }

const index = newRoute.matched.findIndex(m => m.components?.default === Component?.type)
Expand Down
4 changes: 2 additions & 2 deletions packages/nuxt/src/pages/runtime/plugins/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
? nuxtApp.ssrContext!.url
: createCurrentLocation(routerBase, window.location, nuxtApp.payload.path)

const router = createRouter({
const router = createRouter({
...routerOptions,
scrollBehavior: (to, from, savedPosition) => {
if (from === START_LOCATION) {
Expand Down Expand Up @@ -249,7 +249,7 @@ const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
await nuxtApp.runWithContext(() => showError(error))
}
})

return { provide: { router } }
}
})
Expand Down
11 changes: 9 additions & 2 deletions packages/nuxt/src/pages/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import { uniqueBy } from '../core/utils'
import { toArray } from '../utils'
import { distDir } from '../dirs'

enum SegmentParserState {
initial,
Expand Down Expand Up @@ -87,6 +88,11 @@
// Array where routes should be added, useful when adding child routes
let parent = routes

if(segments[segments.length - 1].endsWith('.server')) {
huang-julien marked this conversation as resolved.
Show resolved Hide resolved
segments[segments.length - 1] = segments[segments.length - 1].replace('.server', '')
route.server = true
}

for (let i = 0; i < segments.length; i++) {
const segment = segments[i]

Expand Down Expand Up @@ -183,7 +189,7 @@
try {
extractedMeta[key] = JSON.parse(runInNewContext(`JSON.stringify(${valueString})`, {}))
} catch {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not JSON-serializable (reading \`${absolutePath}\`).`)

Check warning on line 192 in packages/nuxt/src/pages/utils.ts

View workflow job for this annotation

GitHub Actions / code

Unexpected console statement
dynamicProperties.add(key)
continue
}
Expand All @@ -196,7 +202,7 @@
continue
}
if (element.type !== 'Literal' || typeof element.value !== 'string') {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not an array of string literals (reading \`${absolutePath}\`).`)

Check warning on line 205 in packages/nuxt/src/pages/utils.ts

View workflow job for this annotation

GitHub Actions / code

Unexpected console statement
dynamicProperties.add(key)
continue
}
Expand All @@ -207,7 +213,7 @@
}

if (property.value.type !== 'Literal' || typeof property.value.value !== 'string') {
console.debug(`[nuxt] Skipping extraction of \`${key}\` metadata as it is not a string literal or array of string literals (reading \`${absolutePath}\`).`)

Check warning on line 216 in packages/nuxt/src/pages/utils.ts

View workflow job for this annotation

GitHub Actions / code

Unexpected console statement
dynamicProperties.add(key)
continue
}
Expand Down Expand Up @@ -412,6 +418,7 @@
meta: serializeRouteValue(metaFiltered, skipMeta),
alias: serializeRouteValue(toArray(page.alias), skipAlias),
redirect: serializeRouteValue(page.redirect),
server: serializeRouteValue(page.server),
}

for (const key of ['path', 'name', 'meta', 'alias', 'redirect'] satisfies NormalizedRouteKeys) {
Expand All @@ -423,7 +430,7 @@
if (page.children?.length) {
route.children = normalizeRoutes(page.children, metaImports, overrideMeta).routes
}

// Without a file, we can't use `definePageMeta` to extract route-level meta from the file
if (!page.file) {
return route
Expand All @@ -439,7 +446,7 @@
meta: `${metaImportName} || {}`,
alias: `${metaImportName}?.alias || []`,
redirect: `${metaImportName}?.redirect`,
component: genDynamicImport(file, { interopDefault: true })
component: `(${metaImportName}?.island || ${route.server}) ? ${genDynamicImport(resolve(distDir, 'components/runtime/server-component'))}.then(({ createIslandPage }) => createIslandPage(${ route.name })) : ${genDynamicImport(file, { interopDefault: true })}`
Fixed Show fixed Hide fixed
}

if (route.children != null) {
Expand Down