vite: Importing public files with `/* @vite-ignore */` dynamic imports throws an error

Description

My project generates JavaScript templates for non-technical users. Before Vite, I published a zip file that looked like this:

  • index.html
  • config.js
  • bundles/
    • mount.js

I’m trying to port this project to Vite.

To prevent Vite from minifying the source and mangling the name, I’ve tried putting config.js in public. However, when I run vite serve, I’m met with this error message:

Failed to load url /config.js (resolved id: /config.js). This file is in /public and will be copied 
as-is during build without going through the plugin transforms, and therefore should not be 
imported from source code. It can only be referenced via HTML tags.

Reproduction

While I respect the effort to prevent people from making mistakes, there ought to be the ability to say “no really, this is on purpose.”

In short, I want to be able to tell Vite “Treat this JS file like any other asset. Don’t change its name or its contents, and allow it to be imported from the devserver.”

Suggested solution

The easiest solution from both an implementation and a user’s POV would be a config flag, like server.allowDangerousPublicDirImports. If it’s set, Vite skips throwing the error and serves the requested file as if it’s any other asset.

This gives the user the final decision about what is and is not permissible in their architecture, without resorting to crazy backdoors like writing a plugin to serve JS as an assetInclude.

The term Dangerous in the flag name warns people to not set it without consideration. Using a flag gives it an obvious place to live in the documentation to make it easier for people to find. (A nod could also be included in ERR_LOAD_PUBLIC_URL.) I’m not convinced that the risk of serving files from publicDir is so high that they need to individually be whitelisted.

Alternative

Whitelisting, either by:

  1. an inline comment, like import(/* vite-treat-as-asset */ 'config.js), or
  2. a config field, like jsModulesInPublicDir: ['**/*config.js'].

The comment is less discoverable than a config field, but easier to use. You explicitly grant permission at the call site, so there is nothing to maintain. (Imagine if the file name changes.)

A config field is DRYer (for instance, if many packages in a monorepo share a vite.config, one setting repairs the whole project).

Additional context

config.js is effectively a JSON with generous commenting and some dependency injection to provide functionality like shuffling and centering.

It’s designed to be hand-edited by people who aren’t technical enough to create or maintain a web app, but are capable of editing some strings. All the complexity is in mount.js, which is just a blob to them. They can edit the config file, drag it to a file server, and have an app that meets their needs without touching the command line. Moreover, config can be generated by a web app after mount has already been minted.

Validations

About this issue

  • Original URL
  • State: open
  • Created 8 months ago
  • Reactions: 3
  • Comments: 20 (15 by maintainers)

Most upvoted comments

I was able to work around this issue by creating a custom import function for when I want to import a js file from the public folder. Works for me with vite: ^5.0.8.

/**
 * This is a patched version of the native `import` method that works with vite bundling.
 * Allows you to import a javascript file from the public directory.
 */
function importStatic(modulePath) {
    if (import.meta.env.DEV) {
        return import(/* @vite-ignore */ `${modulePath}?${Date.now()}`);
    } else {
        return import(/* @vite-ignore */ modulePath);
    }
}

const path = '/content/foo.js`; //  -> `/public/content/foo.js`
const module = await importStatic(path);

Edit: @appsforartists raised a point that the ${Date.now()} suffix is not required and a simple ? suffix will work, but the full ?${Date.now()} may help avoid any caching issues when running in dev mode.

Edit 2: Also worth mentioning, if you want to do relative imports for static files you’ll have to make sure vite is bundling JS files into the root of the dist/ folder and not the dist/assets/ folder like it was for me. You can do this by adjusting the rollup config

export default defineConfig({
    build: {
        rollupOptions: {
            output: {
                chunkFileNames: '[name]-[hash].js',
                entryFileNames: '[name]-[hash].js',
                assetFileNames: '[name]-[hash][extname]',
            }
        }
    }
})

Dammit - just tried upgrading to Vite@5.0.4 and this error is back:

Failed to load url /experiment-config.js (resolved id: /experiment-config.js). This file is in 
../passthrough and will be copied as-is during build without going through the plugin transforms, 
and therefore should not be imported from source code. It can only be referenced via HTML tags.

Yeah. I wouldn’t say it’s “required,” but it can be nice to ensure you’re getting unique, uncached URLs on each load.

I haven’t played with it in this context to see how well it plays with HMR. It’s just an old habit to avoid caching.

Here’s how I solved it for my little use case in a vite react app…

I added 2 files to a subfolder in public.

  • public/app/
    • config.js
    • importer.js
// config.js
export const myPlugin = {
  id: "yolo",
  main(id) {
    console.log("Yolo!", id);
  },
};
// importer.js
function importNative(path) {
  return import(path);
}

Then in my index.html file I added <script src="/app/importer.js"></script> to the <head> area.

To test it in my src/main.tsx file I did:

const importNative = (path: string) => {
  // path = import.meta.env.BASE_URL + path; // IF you have a base path
  const inative = window["importNative"] as (path: string) => any;
  return inative(path);
};

importNative("/app/config.js").then(config => {
  const myPlugin = config?.myPlugin;
  if (!myPlugin) { console.log("Config?", config); return; }
  myPlugin.main(myPlugin.id);
});