Skip to content

Conversation

@dschmidt
Copy link
Contributor

@dschmidt dschmidt commented Sep 30, 2025

Description

make relative imports in amd/cjs relative to the current module instead of the baseURI.

Fixes #20860

I've added an override to customRelativeUrlMechanisms to avoid modifying the copied code from rollup.
As far as I can tell rollup itself uses module.uri to replace import.meta.url in amd modules. So I assume it's okay to use it.
https://github.com/rollup/rollup/blob/ce6cb93098850a46fa242e37b74a919e99a5de28/src/ast/nodes/MetaProperty.ts#L209

An alternative could be to use resolve.toUrl(module.id) but it seems unneccessary if other parts of the code already rely on module.uri.

FWIW I've verified that import.meta.url gets transpiled to new URL(module.uri, document.baseURI).href in my setup with amd modules. Using import.meta.url directly for the imports here does not work as it happens too late in the transpilation process (getting errors that module may not be used outside of module scope).

I'm a bit lost regarding a regression test as I couldn't find a test for the relative path behavior that I could simply adjust... if you require me to implement one, could you give me a few pointers how to implement this specific one?

After creating a proper reproducer and discussions with @lukastaegert of rollup fame and @sapphi-red it turned out, joining the path with the baseURI is actually correct.
It works in rollup because modules request require to be injected. This local require instance has the correct base path and so toUrl returns a correct absolute path.
In vite this was missing, so we were falling back to the global require, so toUrl used baseUri as base instead of the current module's path.

This PR adds require to the dependencies of amd modules.

Unfortunately this change is breaking for a specific edge case: when a module already contains a local variable require the added parameter leads to an error.
The completeSystemJsWrapPlugin has a similar problem.

@dschmidt dschmidt force-pushed the fix/amd-relative-imports branch from 951ec02 to 835e172 Compare September 30, 2025 11:58
@sapphi-red
Copy link
Member

Would you provide concrete examples of require.toUrl(relativePath), module.uri, and document.baseURI?
Rollup has been using the same code for years, so I guess there's a reason.
Have you tried setting the baseUrl option?

@dschmidt
Copy link
Contributor Author

Sure, no problem. I've used this code:

const customRelativeUrlMechanisms = {
  ...relativeUrlMechanisms,
  // override amd to use module.uri instead of document.baseURI
  amd: (relativePath) => {
    if (relativePath[0] !== '.') relativePath = './' + relativePath
    return getResolveUrl(
      `require.toUrl('${escapeId(relativePath)}'), (() => {
        console.log('relativePath', '${relativePath}')
        console.log('require.toUrl(relativePath)', require.toUrl('${relativePath}'))
        console.log('module.id', module.id)
        console.log('module.uri', module.uri)
        console.log('document.baseURI', document.baseURI)

        return new URL(module.uri, document.baseURI).href
      })()`,
    )
  },
  'worker-iife': (relativePath) =>
    getResolveUrl(
      `'${escapeId(partialEncodeURIPath(relativePath))}', self.location.href`,
    ),
} as const satisfies Record<string, (relativePath: string) => string>

to produce this output:

relativePath ../assets/zip-web-worker-CqnRs-V2.js
unzip.js:4681 require.toUrl(relativePath) ./../assets/zip-web-worker-CqnRs-V2.js
unzip.js:4682 module.id https://host.docker.internal:9200/assets/apps/unzip/js/unzip.js
unzip.js:4683 module.uri https://host.docker.internal:9200/assets/apps/unzip/js/unzip.js
unzip.js:4684 document.baseURI https://host.docker.internal:9200/

@dschmidt
Copy link
Contributor Author

I'm not sure about the baseUrl - is it possible to have multiple instances of requirejs?
We have multiple apps with individual baseUrls so to speak.
If I do

requirejs.config({
    baseUrl: '/assets/apps/unzip/'
  });

that's only valid for a single app but not for all others.

No idea why rollup implemented it this way. Maybe module.uri wasn't always available.
Usually it probably just doesn't matter. I have to admit my use case is a bit of an edge case - but it's one that is supported by system and es modules.
I think it's a valid way to define "relative paths" to be relative to some base uri, but then all module systems should implement it that way.

@dschmidt
Copy link
Contributor Author

If you insist, I can also send a PR to Rollup and we can check their feedback 😅

@sapphi-red sapphi-red added p2-edge-case Bug, but has workaround or limited in scope (priority) feat: build labels Oct 1, 2025
@sapphi-red
Copy link
Member

Thank you for the examples and the PR to Rollup. I think it makes sense. Just in case, I'd like to wait for a week for the feedback on Rollup side.

@dschmidt
Copy link
Contributor Author

dschmidt commented Oct 1, 2025

Makes sense. I understand that while being a small change in terms of LoC, the change could potentlally be delicate and I certainly don't want to break others.
If we can fix it in rollup and vite and keep the code in sync, that would of course be the best outcome.

If rollup people see a problem, I'm happy to discuss alternatives with you and them.
Until then we can certainly wait. As I wrote above it's an issue we have a workaround for, so there's absolutely no urgency to fix it... but it would be so much nicer if we could just use ?url without manual tinkering with the url (and ?worker and import other assets... )

@dschmidt
Copy link
Contributor Author

I've tried to upstream it to rollup, but afaict now it's not applicable there, because rollup does not have the relative base feature https://vite.dev/guide/build.html#relative-base: relative paths are always relative to the baseURI not to the including module.

In vite on the other hand the code I modified/overrode is only used when the relative base feature (with base ./) is used and we have relative paths from the including module to the included module.
So I assume that's the reason the code in Rollup is what it is and why the change makes sense in vite only.

c.f. rollup/rollup#6129 (comment)

@sapphi-red Thoughts?

@sapphi-red
Copy link
Member

I think the relativePath passed to the relativeUrlMechanisms is the same value.
https://github.com/rollup/rollup/blob/55a8fd5a70820f274921edf394efbbaa620f0962/src/ast/nodes/MetaProperty.ts#L96

path.posix.relative(path.dirname(importer), filename),

So if there's a problem with Rollup, I guess we have the same problem with Vite.

@sapphi-red
Copy link
Member

I think I need a reproduction to see what is happening.

@dschmidt
Copy link
Contributor Author

In my tests it wasn't the same value but I'll try to setup a reproducer which does (not) work with vite/rollup
Then you and @lukastaegert can look at it again and maybe we find a solution that works everywhere

It's not completely trivial, so I'm not sure how soon I'll get to it...

@dschmidt
Copy link
Contributor Author

https://github.com/dschmidt/vite-amd-relative-import-example

This shows my issue in vite for starters...

@sapphi-red
Copy link
Member

sapphi-red commented Oct 21, 2025

I found the difference. With Rollup, the following code is generated:

define(["require"], (function(require) {
  "use strict";
  const fooTxtUrl = new URL(require.toUrl("../assets/foo.txt"), document.baseURI).href;
  function main() {
    console.log("fooTxtUrl", fooTxtUrl);
    return "OK";
  }
  return main;
}));

On the other hand, Vite generates:

define((function() {
  "use strict";
  const fooTxtUrl = "" + new URL(require.toUrl("../assets/foo.txt"), document.baseURI).href;
  function main() {
    console.log("fooTxtUrl", fooTxtUrl);
    return "OK";
  }
  return main;
}));

(the code I used)

The problem here is that Vite is not generating ["require"] and require in the parameter. This leads to require referencing the global one instead. We need to add a plugin like this one: https://github.com/vitejs/vite/blob/2443e056941d63ea5988293ca1a1351af017e59d/packages/vite/src/node/plugins/completeSystemWrap.ts

@dschmidt
Copy link
Contributor Author

That makes a lot of sense! I can confirm that adjusting the generated plugin.js to include the require parameter works.
Thanks a lot for digging into this!

... to avoid falling back to the global require, which can break
relative import urls.
@dschmidt dschmidt changed the title fix(build): make relative imports in amd/cjs relative to the current module fix(build): ensure amd bundles request require to be injected Oct 21, 2025
Copy link
Member

@sapphi-red sapphi-red left a comment

Choose a reason for hiding this comment

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

Thank you. The plugin implementation looks good to me.

I've added some minor comments. Also would you add a comment here like the one for system format?

amd: (relativePath) => {
if (relativePath[0] !== '.') relativePath = './' + relativePath
return getResolveUrl(
`require.toUrl('${escapeId(relativePath)}'), document.baseURI`,
)
},

// NOTE: make sure rollup generate `module` params

@dschmidt dschmidt force-pushed the fix/amd-relative-imports branch from 1c64a6e to 6f6d3c4 Compare October 22, 2025 18:57
sapphi-red
sapphi-red previously approved these changes Oct 23, 2025
Copy link
Member

@sapphi-red sapphi-red left a comment

Choose a reason for hiding this comment

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

Thanks, I finished up the playground test.

@dschmidt
Copy link
Contributor Author

Thanks a bunch!

Feel free to squash the commits before or when merging.

@userquin
Copy link
Contributor

@sapphi-red can we run ecosystem-ci with this PR? iirc PWA plugin will generate some define when building a custom service worker (type classic)

Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason this file is in public/ and not in the root?

Copy link
Member

Choose a reason for hiding this comment

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

It is because this file is not part of the build (the entrypoint is index.ts) and won't be copied to dist if it's in the root.

Copy link
Member

Choose a reason for hiding this comment

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

Alright, but it feels a bit weird to be building a Vite app this way. Usually the build output format doesn't affect dev, but I suppose it's fine as a test setup for now.

@sapphi-red
Copy link
Member

/ecosystem-ci run

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 24, 2025

Open in StackBlitz

npm i https://pkg.pr.new/vite@20861

commit: 74c9da4

@vite-ecosystem-ci
Copy link

📝 Ran ecosystem CI on 1a8ee07: Open

suite result latest scheduled
analogjs failure success
react-router success ⏹️ cancelled
laravel failure failure
qwik failure failure
storybook failure success
vite-environment-examples failure success
vite-plugin-rsc failure failure
vite-plugin-cloudflare failure success
vitest failure success
waku success failure

astro, ladle, nuxt, marko, one, histoire, quasar, sveltekit, unocss, rakkas, vike, vite-plugin-vue, vite-plugin-react, vite-plugin-pwa, vite-plugin-svelte, vite-setup-catalogue, vuepress, vitepress

@sapphi-red
Copy link
Member

All the failures are known ones. @userquin plugin-pwa passes fine with this PR 👍

@sapphi-red sapphi-red merged commit bb85bd7 into vitejs:main Oct 24, 2025
18 checks passed
@dschmidt
Copy link
Contributor Author

Thanks everyone for the input and especially to @sapphi-red for taking the PR over the finish line :)

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

Labels

feat: build p2-edge-case Bug, but has workaround or limited in scope (priority) trigger: preview

Projects

None yet

Development

Successfully merging this pull request may close these issues.

amd: relative base is relative to baseURI not to current module

4 participants