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

@vitejs/plugin-legacy wrong work in nuxt #15464

Closed
xtoolkit opened this issue Nov 13, 2022 · 16 comments
Closed

@vitejs/plugin-legacy wrong work in nuxt #15464

xtoolkit opened this issue Nov 13, 2022 · 16 comments

Comments

@xtoolkit
Copy link
Contributor

xtoolkit commented Nov 13, 2022

Environment


  • Operating System: Linux
  • Node Version: v16.17.1
  • Nuxt Version: 3.0.0-rc.13
  • Nitro Version: 0.6.1
  • Package Manager: pnpm@7.15.0
  • Builder: vite
  • User Config: runtimeConfig, css, vite
  • Build Modules: -

Reproduction

Nuxt

Describe the bug

Problem is polyfills load after entry file and System variable is undefined!

i install legacy plugin in pure vite project and don't see this problem. i think nuxt problem!

Additional context

No response

Logs

Uncaught ReferenceError: System is not defined `entry-legacy.4e3349cb.js:1:12341`
@marssantoso
Copy link

Hi. Are there any updates or a known workaround for this?

@lehni
Copy link

lehni commented Mar 15, 2023

As a workaround, we ended up configuring config.vite.build.target for legacy JS support, and are using a self-hosed https://polyfill.io/ for the polyfills. We're self-hosting it because the online version is stuck in v3 for now, see polyfillpolyfill/polyfill-service#2734.

To make configuring config.vite.build.target easier, we're using browserslist along with esbuild-plugin-browserslist:

import browserslist from 'browserslist'
import { resolveToEsbuildTarget } from 'esbuild-plugin-browserslist'
import { defineNuxtConfig } from 'nuxt/config'

export default defineNuxtConfig({
  
  vite: {
    build: {
      target: getBuildTarget([
        '>0.1% and supports es6-module and not ios < 12 and not opera > 0',
        'node >= 18.13.0'
      ])
    }
  }
})

function getBuildTarget(browsers) {
  return resolveToEsbuildTarget(browserslist(browsers), {
    printUnknownTargets: false
  })
}

@marssantoso
Copy link

marssantoso commented Mar 15, 2023

As a workaround, we ended up configuring config.vite.build.target for legacy JS support, and are using a self-hosed https://polyfill.io/ for the polyfills. We're self-hosting it because the online version is stuck in v3 for now, see Financial-Times/polyfill-service#2734.

To make configuring config.vite.build.target easier, we're using browserslist along with esbuild-plugin-browserslist:

import browserslist from 'browserslist'
import { resolveToEsbuildTarget } from 'esbuild-plugin-browserslist'
import { defineNuxtConfig } from 'nuxt/config'

export default defineNuxtConfig({
  
  vite: {
    build: {
      target: getBuildTarget([
        '>0.1% and supports es6-module and not ios < 12 and not opera > 0',
        'node >= 18.13.0'
      ])
    }
  }
})

function getBuildTarget(browsers) {
  return resolveToEsbuildTarget(browserslist(browsers), {
    printUnknownTargets: false
  })
}

Thanks for the quick response and for sharing your workaround, @lehni.

That might worth a try, but I want to make sure one thing. How do you integrate polyfill.io (or you self-hosted version of it), into your config above? Simply by inserting your self-hosted polyfill url into a script tag?

Edit: cleared up the question

@lehni
Copy link

lehni commented Mar 15, 2023

You just need to add a script tag to your app that loads the polyfill with the desired options. We have something like this in our default layout:

  setup() {
    const script = []
    const { polyfill } = useConfig()
    if (polyfill) {
      const version = polyfill.version || 1
      const features = polyfill.features?.join(',') || ''
      const flags = polyfill.flags?.join(',') || ''
      script.push({
        src: `/polyfill?v=${version}&features=${features}&flags=${flags}`
      })
    }
    
    useHead({ script })
  }

And the config (retrieved by useConfig(), our own composable for site config):

    
    polyfill: {
      version: 2, // Only used for cache busting.
      features: [
        'default',
        'globalThis',
        'es2015',
        'es2016',
        'es2017',
        'es2018',
        'es2019',
        'es2020',
        'es2021',
        'es2022',
        'es2023'
      ],
      flags: ['gated']
    }
    

@zhancheng
Copy link

wanna help for this issue @danielroe

Copy link
Member

@zhancheng help very welcome 😊

@xtoolkit
Copy link
Contributor Author

xtoolkit commented Mar 30, 2023

The temporary solution for this problem is to transfer the entry-legacy file to the end of the manifest file. You can solve the problem using a Nuxt hook in a module file:

// modules/fixViteLegacyPlugin.ts
import {defineNuxtModule} from '@nuxt/kit';

export default defineNuxtModule({
  setup(_option, nuxt) {
    nuxt.hook('build:manifest', manifest => {
      const keys = Object.keys(manifest);

      // detect vite plugin added
      if (!keys.some(key => key.includes('polyfills-legacy'))) {
        return;
      }

      const entryKey = keys.find(key => key.includes('entry-legacy')) as string;
      const entryValue = manifest[entryKey];

      // remove entry
      delete manifest[entryKey];

      // add entry end of manifest
      manifest[entryKey] = entryValue;

      // fix legacy module attributes
      for (const item in manifest) {
        manifest[item].module = item.includes('polyfills-legacy')
          ? false
          : !item.includes('-legacy.js');
      }
    });
  }
});

@IlyaSemenov
Copy link

IlyaSemenov commented Apr 25, 2023

Just in case if anyone lands here trying to make Nuxt work in Chrome 49 (Windows XP), I managed to achieve that by removing defer from the script tags:

diff --git a/dist/runtime.mjs b/dist/runtime.mjs
index 29154e86eed636881eabc2c7fd01e9883e9e0404..736cbd0cd5dab57e71fd5dea427afc99717c4909 100644
--- a/dist/runtime.mjs
+++ b/dist/runtime.mjs
@@ -162,8 +162,7 @@ function renderScripts(ssrContext, rendererContext) {
   return Object.values(scripts).map((resource) => renderScriptToString({
     type: resource.module ? "module" : null,
     src: rendererContext.buildAssetsURL(resource.file),
-    defer: resource.module ? null : "",
-    crossorigin: ""
+    crossorigin: resource.module ? "" : null
   })).join("");
 }
 function createRenderer(createApp, renderOptions) {

and then to fix this: [nuxt] error caught during app initialization Error: Context conflict the workaround is to remove modern chunks completely:

// modules/vite-legacy-patch.ts
import { defineNuxtModule } from '@nuxt/kit'
import { pick } from 'lodash'

export default defineNuxtModule({
  setup(_option, nuxt) {
    nuxt.hook('build:manifest', manifest => {
      // copy of manifest where polyfill is moved to 1st position
      const manifest_copy: typeof manifest = {
        ...pick(manifest, 'vite/legacy-polyfills-legacy'),
        ...manifest,
      }
      // clear manifest
      for (const key of Object.keys(manifest)) {
        delete manifest[key]
      }
      // fill manifest again from the copy
      Object.assign(manifest, manifest_copy)

      // remove module attributes from legacy chunks
      for (const key of Object.keys(manifest)) {
        if (key.match(/-legacy(\.|$)/)) {
          manifest[key].module = false
        } else if (manifest[key].module) {
          // remove modern chunks completely, otherwise it conflicts in modern Chrome:
          // [nuxt] error caught during app initialization Error: Context conflict
          //
          // that could be related to defer being removed from legacy chunks
          // see: patches/vue-bundle-renderer@1.0.3.patch
          // but with that patch Chrome 49 wouldn't run any scripts at all
          delete manifest[key]
        }
      }
    })
  },
})

and then some polyfills:

export default defineNuxtConfig({
  vite: {
    plugins: [
      // for Windows XP
      // see also modules/vite-legacy-patch.ts
      legacy({
        targets: ['chrome 49'],
        additionalLegacyPolyfills: [
          'intersection-observer',
          'mdn-polyfills/Element.prototype.getAttributeNames',
        ],
      }),
    ],
  }
})

This makes it work in Windows XP.

This is all (without a doubt) very dirty and unpleasant, so if there are better workarounds please weigh in!

UPDATE: see better working solution below.

@fabis94
Copy link

fabis94 commented May 9, 2023

The problem is that the polyfill bundle is loaded after the entry bundle, cause polyfills should always come first. I was inspired by Ilya's comment to create a solution like this:

// nuxt.config.ts
  hooks: {
    'build:manifest': (manifest) => {
      // kinda hacky, vite polyfills are incorrectly being loaded last so we have to move them to appear first in the object.
      // we can't replace `manifest` entirely, cause then we're only mutating a local variable, not the actual manifest
      // which is why we have to mutate the reference.
      // since ES2015 object string property order is more or less guaranteed - the order is chronological
      const polyfillKey = 'vite/legacy-polyfills'
      const polyfillEntry = manifest[polyfillKey]
      if (!polyfillEntry) return

      const oldManifest = { ...manifest }
      delete oldManifest[polyfillKey]

      for (const key in manifest) {
        delete manifest[key]
      }

      manifest[polyfillKey] = polyfillEntry
      for (const key in oldManifest) {
        manifest[key] = oldManifest[key]
      }
    }
  }

That's the only change I needed to make, no need to adjust any build artifacts or anything.

Note: The polyfill bundle key might be different depending on the settings of the legacy plugin. I'm only using it to generate polyfills for modern bundles (renderLegacyChunks: false & modernPolyfills: true)

@IlyaSemenov
Copy link

IlyaSemenov commented May 9, 2023

Just a heads up, I managed to deliver isomorphic build which runs both legacy and non-legacy code:

1. Put the polyfills chunk to 1st position and remove module attribute from legacy chunks

modules/vite-legacy.ts:

import { defineNuxtModule } from '@nuxt/kit'
import { pick } from 'lodash'

// Fix vite-legacy build, see https://github.com/nuxt/nuxt/issues/15464
export default defineNuxtModule({
  setup(_option, nuxt) {
    nuxt.hook('build:manifest', manifest => {
      if (!manifest['vite/legacy-polyfills-legacy']) {
        return
      }

      // Copy of manifest where polyfill is moved to 1st position.
      const manifest_copy: typeof manifest = {
        ...pick(manifest, 'vite/legacy-polyfills-legacy'),
        ...manifest,
      }
      // Clear manifest.
      for (const key of Object.keys(manifest)) {
        delete manifest[key]
      }
      // Fill manifest again from the copy.
      Object.assign(manifest, manifest_copy)

      // Remove module attribute from legacy chunks.
      for (const key of Object.keys(manifest)) {
        if (key.match(/-legacy(\.|$)/)) {
          manifest[key].module = false
        }
      }
    })
  },
})

2. Mark legacy chunks as nomodule and remove defer from them

server/plugins/vite-legacy.ts:

import { defineNitroPlugin } from 'nitropack/dist/runtime/plugin'

// Make vite-legacy build operational, see https://github.com/nuxt/nuxt/issues/15464
export default defineNitroPlugin(nitroApp => {
  nitroApp.hooks.hook('render:response', response => {
    // Mark legacy chunks as nomodule (prevents modern browsers from loading them)
    // At the same time, unmark them as defer (otherwise System.register() in the legacy entry doesn't actually execute the code)
    response.body = response.body.replace(
      /(<script src="[^"]+\-legacy\.[^>]+") defer/g,
      '$1 nomodule',
    )

    // Remove legacy chunks preload (fixes warnings in modern browsers)
    response.body = response.body.replace(
      /<link rel="preload" as="script" href="[^"]+\-legacy\..*?>/g,
      '',
    )

    // The other option would be NOT to remove defer from legacy chunks,
    // but start them from a nomodule HTML script:
    //
    // response.body += `<script nomodule>document.querySelector("script[src*='/entry-legacy.']").onload=function(){System.import(this.src)}</script>`
    //
    // This is similar to what vite-legacy-plugin does in vanilla vite.
  })
})

This leaves incompatibility window for legacy browsers that do support modules but don't support modern features such as async generators (based on caniuse that would be e.g. Chrome 61-62). Vanilla vite-legacy-plugin injects special detection scripts into SSR HTML: https://github.com/vitejs/vite/blob/535795a8286e4a9525acd2340e1d1d1adfd70acf/packages/plugin-legacy/src/snippets.ts

Ideally, Nuxt should adopt that approach fully.

@danielroe
Copy link
Member

We've discussed as a team and I think we shouldn't adopt within Nuxt itself as the incompatibility window is small and decreasing with time.

However, a module implementation would very much be welcome. Feel free to ping me if there's anything you need or Nuxt does not provide as part of doing this.

@danielroe danielroe closed this as not planned Won't fix, can't repro, duplicate, stale Jun 19, 2023
@fabis94
Copy link

fabis94 commented Jun 19, 2023

the incompatibility window is small and decreasing with time.

@danielroe Can you elaborate on this? It seems like this is always going to be a problem, cause the polyfill bundle is always going to be loaded after the nuxt bundle.

The resolution is quite hacky as well (having to re-order the build manifest in a nuxt hook).

Would be nice if there would just be a nuxt config API that would allow you to control the load order of bundles.

@danielroe
Copy link
Member

@fabis94 Would you open a new issue to track the enhancement you're talking about, perhaps?

@IlyaSemenov
Copy link

FWIW, I published my recipe above as a Nuxt module:

https://www.npmjs.com/package/nuxt-vite-legacy

This collection of hacks is not something I'm exactly proud of, but it works. :)

@bitbytebit1
Copy link

This is something that I feel should be documented somewhere. I have prior experience with Vite and polyfills from another project and expected to be able to add the legacy plugin to the the vite.plugins property in the nuxt.config.ts file without issue. I went so far as writing the documentation for using this property in Nuxt before finding this issue.

Quite shocked that we need to update the response on every request using the render:response, feels icky IMHO.

Honestly this is something I'd like to see first class support however I understand there's very little sense in re-inventing the wheel.

@Laevatein29
Copy link

The problem is that the polyfill bundle is loaded after the entry bundle, cause polyfills should always come first. I was inspired by Ilya's comment to create a solution like this:

// nuxt.config.ts
  hooks: {
    'build:manifest': (manifest) => {
      // kinda hacky, vite polyfills are incorrectly being loaded last so we have to move them to appear first in the object.
      // we can't replace `manifest` entirely, cause then we're only mutating a local variable, not the actual manifest
      // which is why we have to mutate the reference.
      // since ES2015 object string property order is more or less guaranteed - the order is chronological
      const polyfillKey = 'vite/legacy-polyfills'
      const polyfillEntry = manifest[polyfillKey]
      if (!polyfillEntry) return

      const oldManifest = { ...manifest }
      delete oldManifest[polyfillKey]

      for (const key in manifest) {
        delete manifest[key]
      }

      manifest[polyfillKey] = polyfillEntry
      for (const key in oldManifest) {
        manifest[key] = oldManifest[key]
      }
    }
  }

That's the only change I needed to make, no need to adjust any build artifacts or anything.

Note: The polyfill bundle key might be different depending on the settings of the legacy plugin. I'm only using it to generate polyfills for modern bundles (renderLegacyChunks: false & modernPolyfills: true)

It works for me,thx~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants