Skip to content

Vue 3.4 alpha 4 compiler break Nuxt fixtures tests #9853

@huang-julien

Description

@huang-julien

Vue version

3.4-alpha.4

Link to minimal reproduction

https://github.com/nuxt/nuxt/actions/runs/7208581804/job/19637892933?pr=23998

Steps to reproduce

  • Start the fixtures from Nuxt with pnpm nuxi dev ./test/fixtures/basic
  • go to localhost:3000/islands
  • see Property "t" was accessed during render but is not defined on instance. in logs
  • nuxt fixtures test don't pass with pnpm test

What is expected?

  • not having any error or warn in the console
  • fixtures tests should also pass

What is actually happening?

The compiled result of the SFC has a
JSON.stringify($setup.__vforToArray(["fall", "back"]).map((t, index) => ({ t: _ctx.t }))
(source: JSON.stringify(__vforToArray(['fall', 'back']).map((t, index) => ({ t: t }))))

t shouldn't be prefixed with _ctx

Investigation

  • doing a nuxt hot reload seems to fix it
  • triggering 5 to 6 time HMR seems to fix it (some cache ?)
  • We tried to reproduce it on the SFC playground, unsuccessfully

debugging the SFC file transformation

  • the issue seems to be coming from @vue/compiler-sfc through @vitejs/plugin-vue
  • code passed to transformMain is always the same
  • the descriptor for the SFC file is not identical at first loading (_ctx prefix) and at nuxt hot reload or hmr.

looking for the vue core commit

Additionnal Infos

SFC source:

<template>
  <div data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:2:3">
    <div v-if="count > 2" data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:3:5">
      count is above 2
    </div>
    <div style="display: contents;" nuxt-ssr-slot-name="default" />
    {{ data }}
    <div id="long-async-component-count" data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:8:5">
      {{ count }}
    </div>
    {{ headers['custom-head'] }}
    <div style="display: contents;" nuxt-ssr-slot-name="test" :nuxt-ssr-slot-data="JSON.stringify([{ count: count }])"/>
    <p data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:16:5">hello world !!!</p>
    <div style="display: contents;" nuxt-ssr-slot-name="hello" :nuxt-ssr-slot-data="JSON.stringify(__vforToArray(3).map((t, index) => ({ t: t })))"><div nuxt-slot-fallback-start="hello"/><div v-for="(t, index) in 3" style="display: contents;">
      <div :key="t" data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:22:7">
        fallback slot -- index: {{ index }}
      </div>
    </div><div nuxt-slot-fallback-end/></div>

    <div style="display: contents;" nuxt-ssr-slot-name="fallback" :nuxt-ssr-slot-data="JSON.stringify(__vforToArray(['fall', 'back']).map((t, index) => ({ t: t })))"><div nuxt-slot-fallback-start="fallback"/><div v-for="(t, index) in ['fall', 'back']" style="display: contents;">
      <div :key="t" data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:32:7">
        {{ t }} slot -- index: {{ index }}
      </div>
      <div
        :key="t"
        class="fallback-slot-content" data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:35:7"
      >
        wonderful fallback
      </div>
    </div><div nuxt-slot-fallback-end/></div>
  </div>
</template>

<script setup lang="ts">
import { vforToArray as __vforToArray } from '#app/components/utils'
import { getResponseHeaders } from 'h3'
defineProps<{
  count: number
}>()

const evt = useRequestEvent()
const headers = getResponseHeaders(evt)
const { data } = await useFetch('/api/very-long-request')
</script>

vite plugin vue result:

import { withAsyncContext as _withAsyncContext, defineComponent as _defineComponent } from "vue";
import { vforToArray as __vforToArray } from "#app/components/utils";
import { getResponseHeaders } from "h3";
const _sfc_main = /* @__PURE__ */ _defineComponent({
  __name: "LongAsyncComponent",
  props: {
    count: { type: Number, required: true }
  },
  async setup(__props, { expose: __expose }) {
    __expose();
    let __temp, __restore;
    const evt = useRequestEvent();
    const headers = getResponseHeaders(evt);
    const { data } = ([__temp, __restore] = _withAsyncContext(() => useFetch("/api/very-long-request")), __temp = await __temp, __restore(), __temp);
    const __returned__ = { evt, headers, data, get __vforToArray() {
      return __vforToArray;
    } };
    Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
    return __returned__;
  }
});
import { mergeProps as _mergeProps } from "vue";
import { ssrRenderStyle as _ssrRenderStyle, ssrRenderAttr as _ssrRenderAttr, ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate, ssrRenderList as _ssrRenderList } from "vue/server-renderer";
function _sfc_ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  _push(`<div${_ssrRenderAttrs(_mergeProps({ "data-v-inspector": "test/fixtures/basic/components/islands/LongAsyncComponent.vue:2:3" }, _attrs))}>`);
  if ($props.count > 2) {
    _push(`<div data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:3:5"> count is above 2 </div>`);
  } else {
    _push(`<!---->`);
  }
  _push(`<div style="${_ssrRenderStyle({ "display": "contents" })}" nuxt-ssr-slot-name="default"></div> ${_ssrInterpolate($setup.data)} <div id="long-async-component-count" data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:8:5">${_ssrInterpolate($props.count)}</div> ${_ssrInterpolate($setup.headers["custom-head"])} <div style="${_ssrRenderStyle({ "display": "contents" })}" nuxt-ssr-slot-name="test"${_ssrRenderAttr("nuxt-ssr-slot-data", JSON.stringify([{ count: $props.count }]))}></div><p data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:16:5">hello world !!!</p><div style="${_ssrRenderStyle({ "display": "contents" })}" nuxt-ssr-slot-name="hello"${_ssrRenderAttr("nuxt-ssr-slot-data", JSON.stringify($setup.__vforToArray(3).map((t, index) => ({ t: _ctx.t }))))}><div nuxt-slot-fallback-start="hello"></div><!--[-->`);
  _ssrRenderList(3, (t, index) => {
    _push(`<div style="${_ssrRenderStyle({ "display": "contents" })}"><div data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:22:7"> fallback slot -- index: ${_ssrInterpolate(index)}</div></div>`);
  });
  _push(`<!--]--><div nuxt-slot-fallback-end></div></div><div style="${_ssrRenderStyle({ "display": "contents" })}" nuxt-ssr-slot-name="fallback"${_ssrRenderAttr("nuxt-ssr-slot-data", JSON.stringify($setup.__vforToArray(["fall", "back"]).map((t, index) => ({ t: _ctx.t }))))}><div nuxt-slot-fallback-start="fallback"></div><!--[-->`);
  _ssrRenderList(["fall", "back"], (t, index) => {
    _push(`<div style="${_ssrRenderStyle({ "display": "contents" })}"><div data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:32:7">${_ssrInterpolate(t)} slot -- index: ${_ssrInterpolate(index)}</div><div class="fallback-slot-content" data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:35:7"> wonderful fallback </div></div>`);
  });
  _push(`<!--]--><div nuxt-slot-fallback-end></div></div></div>`);
}
import { useSSRContext as __vite_useSSRContext } from "vue";
const _sfc_setup = _sfc_main.setup;
_sfc_main.setup = (props, ctx) => {
  const ssrContext = __vite_useSSRContext();
  (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("components/islands/LongAsyncComponent.vue");
  return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
};
import _export_sfc from "\0plugin-vue:export-helper";
export default /* @__PURE__ */ _export_sfc(_sfc_main, [["ssrRender", _sfc_ssrRender], ["__file", "G:/repo/nuxt/test/fixtures/basic/components/islands/LongAsyncComponent.vue"]]);

What it should be:

import { withAsyncContext as _withAsyncContext, defineComponent as _defineComponent } from "vue";
import { vforToArray as __vforToArray } from "#app/components/utils";
import { getResponseHeaders } from "h3";
const _sfc_main = /* @__PURE__ */ _defineComponent({
  __name: "LongAsyncComponent",
  props: {
    count: { type: Number, required: true }
  },
  async setup(__props, { expose: __expose }) {
    __expose();
    let __temp, __restore;
    const evt = useRequestEvent();
    const headers = getResponseHeaders(evt);
    const { data } = ([__temp, __restore] = _withAsyncContext(() => useFetch("/api/very-long-request")), __temp = await __temp, __restore(), __temp);
    const __returned__ = { evt, headers, data, get __vforToArray() {
      return __vforToArray;
    } };
    Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
    return __returned__;
  }
});
import { mergeProps as _mergeProps } from "vue";
import { ssrRenderStyle as _ssrRenderStyle, ssrRenderAttr as _ssrRenderAttr, ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate, ssrRenderList as _ssrRenderList } from "vue/server-renderer";
function _sfc_ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  _push(`<div${_ssrRenderAttrs(_mergeProps({ "data-v-inspector": "test/fixtures/basic/components/islands/LongAsyncComponent.vue:2:3" }, _attrs))}>`);
  if ($props.count > 2) {
    _push(`<div data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:3:5"> count is above 2 </div>`);
  } else {
    _push(`<!---->`);
  }
  _push(`<div style="${_ssrRenderStyle({ "display": "contents" })}" nuxt-ssr-slot-name="default"></div> ${_ssrInterpolate($setup.data)} <div id="long-async-component-count" data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:8:5">${_ssrInterpolate($props.count)}</div> ${_ssrInterpolate($setup.headers["custom-head"])} <div style="${_ssrRenderStyle({ "display": "contents" })}" nuxt-ssr-slot-name="test"${_ssrRenderAttr("nuxt-ssr-slot-data", JSON.stringify([{ count: $props.count }]))}></div><p data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:16:5">hello world !!!</p><div style="${_ssrRenderStyle({ "display": "contents" })}" nuxt-ssr-slot-name="hello"${_ssrRenderAttr("nuxt-ssr-slot-data", JSON.stringify($setup.__vforToArray(3).map((t, index) => ({ t }))))}><div nuxt-slot-fallback-start="hello"></div><!--[-->`);
  _ssrRenderList(3, (t, index) => {
    _push(`<div style="${_ssrRenderStyle({ "display": "contents" })}"><div data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:22:7"> fallback slot -- index: ${_ssrInterpolate(index)}</div></div>`);
  });
  _push(`<!--]--><div nuxt-slot-fallback-end></div></div><div style="${_ssrRenderStyle({ "display": "contents" })}" nuxt-ssr-slot-name="fallback"${_ssrRenderAttr("nuxt-ssr-slot-data", JSON.stringify($setup.__vforToArray(["fall", "back"]).map((t, index) => ({ t }))))}><div nuxt-slot-fallback-start="fallback"></div><!--[-->`);
  _ssrRenderList(["fall", "back"], (t, index) => {
    _push(`<div style="${_ssrRenderStyle({ "display": "contents" })}"><div data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:32:7">${_ssrInterpolate(t)} slot -- index: ${_ssrInterpolate(index)}</div><div class="fallback-slot-content" data-v-inspector="test/fixtures/basic/components/islands/LongAsyncComponent.vue:35:7"> wonderful fallback </div></div>`);
  });
  _push(`<!--]--><div nuxt-slot-fallback-end></div></div></div>`);
}
import { useSSRContext as __vite_useSSRContext } from "vue";
const _sfc_setup = _sfc_main.setup;
_sfc_main.setup = (props, ctx) => {
  const ssrContext = __vite_useSSRContext();
  (ssrContext.modules || (ssrContext.modules = /* @__PURE__ */ new Set())).add("components/islands/LongAsyncComponent.vue");
  return _sfc_setup ? _sfc_setup(props, ctx) : void 0;
};
import _export_sfc from "\0plugin-vue:export-helper";
export default /* @__PURE__ */ _export_sfc(_sfc_main, [["ssrRender", _sfc_ssrRender], ["__file", "G:/repo/nuxt/test/fixtures/basic/components/islands/LongAsyncComponent.vue"]]);

System Info

No response

Any additional comments?

  • It is really strange that triggering HMR multiple times (around 5 times) seems to correctly compile the sfc template. A reload of nuxt also fix it.
  • also, not every .map((t, index) => ()) are prefixed with _ctx

cc @danielroe

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions