Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"dev": "nuxt dev playground",
"dev:ssl": "nuxt dev playground --https",
"dev:prepare": "pnpm -r dev:prepare && nuxt prepare && nuxt prepare playground && pnpm prepare:fixtures",
"prepare:fixtures": "nuxt prepare test/fixtures/basic && nuxt prepare test/fixtures/cdn && nuxt prepare test/fixtures/extend-registry && nuxt prepare test/fixtures/partytown && nuxt prepare test/fixtures/first-party && nuxt prepare test/fixtures/linkedin-insight && nuxt prepare test/fixtures/linkedin-insight-cdn && nuxt prepare test/fixtures/tiktok-pixel && nuxt prepare test/fixtures/calendly && nuxt prepare test/fixtures/calendly-cdn && nuxt prepare test/fixtures/ahrefs-analytics && nuxt prepare test/fixtures/ahrefs-analytics-cdn && nuxt prepare test/fixtures/usercentrics && nuxt prepare test/fixtures/speedcurve && nuxt prepare test/fixtures/maplibre",
"prepare:fixtures": "nuxt prepare test/fixtures/basic && nuxt prepare test/fixtures/cdn && nuxt prepare test/fixtures/extend-registry && nuxt prepare test/fixtures/partytown && nuxt prepare test/fixtures/first-party && nuxt prepare test/fixtures/linkedin-insight && nuxt prepare test/fixtures/linkedin-insight-cdn && nuxt prepare test/fixtures/tiktok-pixel && nuxt prepare test/fixtures/calendly && nuxt prepare test/fixtures/calendly-cdn && nuxt prepare test/fixtures/ahrefs-analytics && nuxt prepare test/fixtures/ahrefs-analytics-cdn && nuxt prepare test/fixtures/usercentrics && nuxt prepare test/fixtures/speedcurve && nuxt prepare test/fixtures/maplibre && nuxt prepare test/fixtures/map-hydration",
"typecheck": "pnpm --filter @nuxt/scripts-cli typecheck && nuxt typecheck",
"release": "pnpm build && bumpp -r --output=CHANGELOG.md",
"lint": "eslint .",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
import { watch } from 'vue'
import { bindGoogleMapsEvents, useGoogleMapsResource } from './useGoogleMapsResource'

// Renders no DOM of its own. A render function that returns `null` gives a
// comment node on the server and the client. A template that holds only a
// comment, or nothing, renders nothing on the server, so hydration mismatches.
defineOptions({ render: () => null })

const props = defineProps<{
/**
* Configuration options for the circle overlay.
Expand Down Expand Up @@ -112,6 +117,3 @@ watch(() => props.options, (options) => {
}
}, { deep: true })
</script>

<template>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
import { watch } from 'vue'
import { bindGoogleMapsEvents, useGoogleMapsResource } from './useGoogleMapsResource'

// Renders no DOM of its own. A render function that returns `null` gives a
// comment node on the server and the client. A template that holds only a
// comment, or nothing, renders nothing on the server, so hydration mismatches.
defineOptions({ render: () => null })

const props = defineProps<{
/**
* The GeoJSON source. Can be a URL string or a GeoJSON object.
Expand Down Expand Up @@ -132,6 +137,3 @@ watch(() => props.style, (style) => {
dataLayer.value.setStyle(style ?? {})
}, { deep: true })
</script>

<template>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
import { watch } from 'vue'
import { bindGoogleMapsEvents, useGoogleMapsResource } from './useGoogleMapsResource'

// Renders no DOM of its own. A render function that returns `null` gives a
// comment node on the server and the client. A template that holds only a
// comment, or nothing, renders nothing on the server, so hydration mismatches.
defineOptions({ render: () => null })

const props = defineProps<{
/**
* Configuration options for the polygon overlay.
Expand Down Expand Up @@ -95,6 +100,3 @@ watch(() => props.options, (options) => {
}
}, { deep: true })
</script>

<template>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
import { watch } from 'vue'
import { bindGoogleMapsEvents, useGoogleMapsResource } from './useGoogleMapsResource'

// Renders no DOM of its own. A render function that returns `null` gives a
// comment node on the server and the client. A template that holds only a
// comment, or nothing, renders nothing on the server, so hydration mismatches.
defineOptions({ render: () => null })

const props = defineProps<{
/**
* Configuration options for the polyline overlay.
Expand Down Expand Up @@ -95,6 +100,3 @@ watch(() => props.options, (options) => {
}
}, { deep: true })
</script>

<template>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
import { watch } from 'vue'
import { bindGoogleMapsEvents, useGoogleMapsResource } from './useGoogleMapsResource'

// Renders no DOM of its own. A render function that returns `null` gives a
// comment node on the server and the client. A template that holds only a
// comment, or nothing, renders nothing on the server, so hydration mismatches.
defineOptions({ render: () => null })

const props = defineProps<{
/**
* Configuration options for the rectangle overlay.
Expand Down Expand Up @@ -102,6 +107,3 @@ watch(() => props.options, (options) => {
}
}, { deep: true })
</script>

<template>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ interface ScriptLeafletGeoJsonEmits {
layerremove: [event: Leaflet.LayerEvent]
}

// Renders no DOM of its own. A render function that returns `null` gives a
// comment node on the server and the client. A template that holds only a
// comment, or nothing, renders nothing on the server, so hydration mismatches.
defineOptions({ render: () => null })

const props = defineProps<{
/** GeoJSON object, feature, or feature collection. Replace it to update the layer. */
data: GeoJsonObject | GeoJsonObject[]
Expand Down Expand Up @@ -59,7 +64,3 @@ watch(() => props.options?.style, (style) => {

defineExpose({ geoJson })
</script>

<template>
<!-- nuxt-scripts: Leaflet GeoJSON layer -->
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ interface ScriptLeafletTileLayerEmits {
tileerror: [event: Leaflet.TileErrorEvent]
}

// Renders no DOM of its own. A render function that returns `null` gives a
// comment node on the server and the client. A template that holds only a
// comment, or nothing, renders nothing on the server, so hydration mismatches.
defineOptions({ render: () => null })

const props = defineProps<{
/** Tile URL template, for example `https://tile.openstreetmap.org/{z}/{x}/{y}.png`. */
url: string
Expand Down Expand Up @@ -50,7 +55,3 @@ watch(() => props.options, (options) => {

defineExpose({ tileLayer })
</script>

<template>
<!-- nuxt-scripts: Leaflet tile layer -->
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ interface LayerStyle {
filter: unknown
}

// Renders no DOM of its own. A render function that returns `null` gives a
// comment node on the server and the client. A template that holds only a
// comment, or nothing, renders nothing on the server, so hydration mismatches.
defineOptions({ render: () => null })

const props = defineProps<ScriptMapLibreGeoJsonProps>()

const emit = defineEmits<ScriptMapLibreGeoJsonEmits>()
Expand Down Expand Up @@ -429,7 +434,3 @@ watch(styleSignature, (signature) => {

defineExpose({ geoJson })
</script>

<template>
<!-- nuxt-scripts: MapLibre GeoJSON source and layers -->
</template>
45 changes: 45 additions & 0 deletions test/e2e/map-hydration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createResolver } from '@nuxt/kit'
import { $fetch, createPage, setup, url } from '@nuxt/test-utils/e2e'
import { describe, expect, it } from 'vitest'

const { resolve } = createResolver(import.meta.url)

/**
* A renderless map component must render the same node on the server and the
* client. A template that holds only a comment, or nothing, renders nothing on
* the server while the client expects a comment node, so hydration reports a
* mismatch.
*
* `@nuxt/test-utils` builds inside the Vitest worker, where `NODE_ENV` is `test`.
* `@vue/compiler-core` picks its development build there, and its `comments`
* option defaults to `true`, so a comment-only template cannot fail. Setting
* `comments: false` applies the production default. The probe test proves it.
*/
const pages = ['/maplibre', '/leaflet', '/google-maps']

describe('map component hydration in a production build', { timeout: 120000 }, async () => {
await setup({
rootDir: resolve('../fixtures/map-hydration'),
browser: true,
nuxtConfig: {
vue: { compilerOptions: { comments: false } },
},
})

it.each(pages)('strips template comments from %s, like a production build', async (path) => {
const html = await $fetch<string>(path)
expect(html).toContain('<div id="__nuxt">')
expect(html).not.toContain('production-build-probe')
})

it.each(pages)('hydrates %s without a mismatch', async (path) => {
const page = await createPage()
const messages: string[] = []
page.on('console', message => messages.push(`${message.type()}: ${message.text()}`))
page.on('pageerror', error => messages.push(`pageerror: ${error.message}`))
await page.goto(url(path), { waitUntil: 'hydration' })

expect(messages.filter(message => /hydrat|mismatch/i.test(message))).toEqual([])
await page.close()
})
})
3 changes: 3 additions & 0 deletions test/fixtures/map-hydration/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<NuxtPage />
</template>
17 changes: 17 additions & 0 deletions test/fixtures/map-hydration/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineNuxtConfig } from 'nuxt/config'

export default defineNuxtConfig({
modules: [
'@nuxt/scripts',
],
scripts: {
registry: {
// No page loads a map SDK. Hydration compares the server HTML with the
// client's first render, which happens before any script loads.
maplibre: { trigger: false },
leaflet: { trigger: false, bundle: false },
googleMaps: { trigger: false },
},
},
compatibilityDate: '2024-07-05',
})
1 change: 1 addition & 0 deletions test/fixtures/map-hydration/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
18 changes: 18 additions & 0 deletions test/fixtures/map-hydration/pages/google-maps.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<template>
<div>
<!-- production-build-probe: a production compile strips this comment -->
<!-- A placeholder, not a key. The SDK never loads, so Google never sees it. -->
<ScriptGoogleMaps
api-key="placeholder-not-a-key"
:trigger="false"
:width="400"
:height="300"
>
<ScriptGoogleMapsPolygon :options="{ paths: [] }" />
<ScriptGoogleMapsPolyline :options="{ path: [] }" />
<ScriptGoogleMapsCircle :options="{ radius: 10 }" />
<ScriptGoogleMapsRectangle />
<ScriptGoogleMapsGeoJson :src="{ type: 'FeatureCollection', features: [] }" />
</ScriptGoogleMaps>
</div>
</template>
20 changes: 20 additions & 0 deletions test/fixtures/map-hydration/pages/leaflet.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script setup lang="ts">
const data = { type: 'FeatureCollection', features: [] } as const
</script>

<template>
<div>
<!-- production-build-probe: a production compile strips this comment -->
<ScriptLeafletMap
:trigger="false"
:center="[0, 0]"
:width="400"
:height="300"
aria-label="Leaflet hydration map"
>
<!-- The map never loads, so no tile is requested. -->
<ScriptLeafletTileLayer url="/tiles/{z}/{x}/{y}.png" />
<ScriptLeafletGeoJson :data="data" />
</ScriptLeafletMap>
</div>
</template>
29 changes: 29 additions & 0 deletions test/fixtures/map-hydration/pages/maplibre.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { ScriptMapLibreGeoJsonLayer } from '@nuxt/scripts'
import type { StyleSpecification } from 'maplibre-gl'

// No remote sources, glyphs or sprites.
const style: StyleSpecification = {
version: 8,
sources: {},
layers: [{ id: 'background', type: 'background', paint: { 'background-color': '#ffffff' } }],
}
const data = { type: 'FeatureCollection', features: [] } as const
const layers: ScriptMapLibreGeoJsonLayer[] = [{ id: 'points', type: 'circle' }]
</script>

<template>
<div>
<!-- production-build-probe: a production compile strips this comment -->
<ScriptMapLibreMap
:trigger="false"
:map-style="style"
:center="[0, 0]"
:width="400"
:height="300"
aria-label="MapLibre hydration map"
>
<ScriptMapLibreGeoJson source-id="points" :data="data" :layers="layers" />
</ScriptMapLibreMap>
</div>
</template>
3 changes: 3 additions & 0 deletions test/fixtures/map-hydration/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "./.nuxt/tsconfig.json"
}
Loading