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

fix: fix rollup plugin compatibility when getModuleInfo() is used #6811

Closed

Conversation

joaotavora
Copy link

@joaotavora joaotavora commented Feb 8, 2022

Description

Hello. This PR addresses and fixes #6766. @aleclarson requested I present it. In the issue, I show how a Rollup plugin such as this:

//vite-config.js
import path from 'path';
export default {
    plugins: [
        {
            enforce: 'pre',
            async resolveId(id) {
                let [file, query] = id.split('?');
                const probe = query && query.match(/xoption=([^&]+)/)
                const x = probe && Number(probe[1])
                if (x) return {id: path.resolve(file), meta: {x}}
            },
            async transform(blob, id) {
                const x = this.getModuleInfo(id)?.meta?.x
                if (x) return `window.THE_X = ${x}\n` + blob;
            }
        }
    ]
}

... exhibits different behaviour when exercised via vite build and vite dev. The two small files main.js and foo.js can help understand the context.

//main.js
import fn from './foo.js?xoption=21'
document.querySelector('#app').innerHTML = `<h1>Hello Vite: ${fn()}</h1>`
//foo.js
export default function () {return 21 + window.THE_X;}

Additional context

This change improves the parity of behaviour between vite build and vite dev when Rollup plugins retuning non-null meta properties from resolveId() hooks are used.

If one takes the Rollup behavior as reference in this case (I think it's well inside of this criteria) then this is fixing a small bug in Vite.

That bug was in a very large part already fixed by PR #5465 which @aleclarson authored. Upgrading to a recent Vite version containing the PR is my prime motivation for this PR.

Here's an explanation of the faulty behaviour:

The normalizeUrl() helper calls user plugins's resolveId() twice.

  • Once with the URL string containing a query suffix. Here it ignores the meta reply.

  • Again, with the URL no longer containing said suffix. Here it doesn't ignore the meta reply. It is stored in the moduleGraph node.

As can be seen, if the user's plugin meta reply depends on the query suffix being passed in the imported URL, that meta reply won't be registered correctly in the module graph and is not retrievable via getModuleInfo() later on.

This change makes it so that the first call doesn't ignore the meta reply. This makes Vite's dev server match Rollup's plugin-calling behaviour and fixes #6766.

What is the purpose of this pull request?

  • Bug fix
  • New Feature
  • Documentation update
  • Other

Before submitting the PR, please make sure you do the following

  • Read the Contributing Guidelines.
  • Read the Pull Request Guidelines and follow the Commit Convention.
  • Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
  • Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123).
  • Ideally, include relevant tests that fail without this PR but pass with it.

@joaotavora
Copy link
Author

It the general direction of the PR is agreeable, I can probably include some automatic tests that fail without this PR and pass with it.

@joaotavora joaotavora marked this pull request as draft February 16, 2022 11:46
@joaotavora
Copy link
Author

joaotavora commented Feb 16, 2022

I've converted this into a draft as I've noticed that there's still an example where vite build works and vite dev doesn't. It's even simpler:

//main.js
import fn from '@foo.js?xoption=21'
document.querySelector('#app').innerHTML = `<h1>Hello Vite: ${fn()}</h1>`
//vite.config.js
const virtualId = "@foo.js"
import path from 'path';
export default {
  plugins: [
    {
      enforce: 'pre',
      async resolveId(id) {
        let [prequery, query] = id.split('?');
        if (prequery !== virtualId) return;
        const probe = query && query.match(/xoption=([^&]+)/)
        const x = probe && Number(probe[1])
        if (x) {
          return {id: prequery, meta: {x}}
        }
      },
      async load(id) {
        if (id === virtualId) {
          const x = this.getModuleInfo(id)?.meta?.x
          if (!x) throw new Error('OOPS')
          return `export default function () {return 21 + ${x};}`
        }
      }
    }
  ]
}

vite dev throws, vite build doesn't

@joaotavora joaotavora changed the title Fix/fix meta get module info bug fix: fix rollup plugin compatibility when getModuleInfo() is used Feb 16, 2022
@joaotavora joaotavora marked this pull request as ready for review February 16, 2022 12:23
This new helper, unused but by the locus where it was extracted from,
will help a subsequent commit fix a bug.

* packages/vite/src/node/server/moduleGraph.ts
(ensureEntryFromResolved): New helper.
(ensureEntryFromUrl): Use new helper.
This commit improves the parity of behaviour between 'vite build' and
'vite dev' when Rollup plugins retuning non-null 'meta' properties
from resolveId() hooks are used.

If one takes the Rollup behaviour to be any kind of reference in these
cases, this is fixing a small bug in Vite, which was in a very large
part already fixed by PR vitejs#5465.

Here's an explanation of the faulty behaviour:

The normalizeUrl() helper calls user plugins's resolveId() twice.

- Once with the URL string containing a query suffix.  Here it
  _ignores_ the meta reply.

- Again, with the URL no longer containing said suffix.  Here it
  doesn't ignore the meta reply.  It is stored in the moduleGraph
  node.

As can be seen, if the user's plugin meta reply depends on the query
suffix being passed in the imported URL, that meta reply won't be
registered correctly in the module graph and is not retrievable via
getModuleInfo() later on.

This change makes it so that the first call doesn't ignore the meta
reply. This makes Vite's dev server match Rollup's plugin-calling behaviour
and fixes vitejs#6766.

Fix: vitejs#6766

* packages/vite/src/node/importGlob.ts (ResolvedUrl): Import it.
(transformImportGlob): Use it.

* packages/vite/src/node/plugins/importAnalysis.ts
(ResolvedUrl): Import it.
(normalizeUrl): Use it. Always return meta.
(transform): Ensure moduleGraph is correctly built.
* packages/vite/src/node/server/moduleGraph.ts
(ensureEntryFromResolved): Fix.
@joaotavora
Copy link
Author

I've converted this into a draft as I've noticed that there's still an example where vite build works and vite dev doesn't. It's even simpler:

I've fixed this second problem.

I've also added an automated test that fails without this PR and passes with it. Rebased the PR so that the failing test comes first, then three commits which fix the failing test.

Ping @patak-dev @aleclarson, please have a look at this PR.

@aleclarson
Copy link
Member

Can you make a Stackblitz for each of the failing plugins? You can start from https://vite.new/react

@joaotavora
Copy link
Author

@joaotavora
Copy link
Author

Friendly ping. Any news here? Should I also make a Rollup "repl.it" recipe to show that this behaves nicely on Rollup?

In the `normalizeUrl` function of the vite:import-analysis plugin, we already have the resolvedId associated with the normalized URL, as well as resolved metadata. So let's use it instead of running `resolveId` hooks again.
@aleclarson aleclarson force-pushed the fix/fix-meta-getModuleInfo-bug branch from c7d9195 to f0c4e1f Compare March 2, 2022 20:28
@@ -259,7 +260,11 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
// its last updated timestamp to force the browser to fetch the most
// up-to-date version of this module.
try {
const depModule = await moduleGraph.ensureEntryFromUrl(url, ssr)
Copy link
Member

@aleclarson aleclarson Mar 2, 2022

Choose a reason for hiding this comment

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

Why did I change this? This was the final spot where the resolved metadata was getting lost, so I've reverted the change you made inside the ensureEntryFromResolved method.

Copy link
Author

Choose a reason for hiding this comment

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

Indeed, it was getting lost here because the url, passed to it was already query-trimmed. So I agree with your change here, and yes, I think it means you can revert this change in moduleGraph.ts.

       fileMappedModules.add(mod)
+    } else if (mod.id !== resolvedId) {
+      this.idToModuleMap.set(resolvedId, mod)
+      if (meta) mod.meta = { ...mod.meta, ...meta }
     }
     return mo

But should you? Is not that change conceptually correct in the protocol of ensureEntryFromResolved? Depends on what one takes to be "key" inside resolvedUrl triplet. If you say it's only the URL, you're right to revert it. If you assume its the URL/id combination, you're not.

Copy link
Member

@aleclarson aleclarson Mar 2, 2022

Choose a reason for hiding this comment

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

The rationale for removing that block goes like this:

The resolution of a module URL is assumed to be idempotent, which means the resolvedId and meta values are not expected to change between resolveId invocations. This assumption is made clear by the ModuleNode object having only a single id string, rather than an array of them.

Copy link
Author

Choose a reason for hiding this comment

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

The resolution of a module URL is assumed to be idempotent, which means the resolvedId and meta values are not expected to change between resolveId invocations.

Makes sense. And you now made it so that that expectation is fulfilled where it wasn't. However, they are still three arguments to the function that will sometimes ignore the latter two if they vary. This means that even with this new somewhat cleaner helper, it could perhaps be even cleaner and take only one argument. Not for this PR though, and maybe just stating what you said in a comment/docstring would probably be more than enough.

@aleclarson aleclarson force-pushed the fix/fix-meta-getModuleInfo-bug branch from f0c4e1f to d284c7d Compare March 2, 2022 20:31
@aleclarson aleclarson force-pushed the fix/fix-meta-getModuleInfo-bug branch from d284c7d to 5493c29 Compare March 2, 2022 20:31
@@ -128,7 +128,7 @@ export class ModuleGraph {
for (const imported of importedModules) {
const dep =
typeof imported === 'string'
? await this.ensureEntryFromUrl(imported, ssr)
Copy link
Member

@aleclarson aleclarson Mar 2, 2022

Choose a reason for hiding this comment

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

Why did I change this? By this point, the imported URL is resolved, so using ensureEntryFromUrl is overkill. It pointlessly reruns the resolveId hook chain.

@patak-dev patak-dev added the p3-minor-bug An edge case that only affects very specific usage (priority) label Mar 11, 2022
@patak-dev
Copy link
Member

We talked about this issue with the team today. Once CI issues and conflicts are resolved, we are good to merge it 👍🏼

@joaotavora
Copy link
Author

joaotavora commented Mar 25, 2022 via email

@aleclarson
Copy link
Member

Closing in favor of #7477

@aleclarson aleclarson closed this Mar 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
p3-minor-bug An edge case that only affects very specific usage (priority)
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Support for moduleInfo.meta in resolveId hook still not equivalent in vite dev and vite build
3 participants