withastro / docs

Astro documentation
https://docs.astro.build/
MIT License
1.31k stars 1.47k forks source link

Imports guide recommends a buggy use of `import.meta.url` #7785

Open delucis opened 6 months ago

delucis commented 6 months ago

đź“š Subject area/topic

Imports

đź“‹ Page(s) affected (or suggested, for new content)

https://docs.astro.build/en/guides/imports/#node-builtins

đź“‹ Description of content that is out-of-date or incorrect

The “Node Builtins” section in the Imports guide is three years old and probably misleading.

It suggests building a relative path to another file like this:

const url = new URL('../../package.json', import.meta.url);

However, import.meta.url’s value varies between dev and build, for example during dev, a page at src/pages/dir/index.astro has an import.meta.url of src/pages/dir/index.astro. However, during a build this might be something like dist/chunks/prerender_CX4g21Zw.mjs instead.

In simple cases this technique can appear to work. For example, in src/pages/index.astro the folder depth is usually the same between dev and build and if you’re loading something at the root level like package.json, you will end up in the right place.

But in pages or components in subdirectories, or when trying to reference siblings in the file structure, you’re very likely to hit inconsistencies between dev and build (not to mention SSR uses of node:fs where you might even be referencing a file outside your project which is missing completely when deployed).

🖥️ Reproduction in StackBlitz (if reporting incorrect content or code samples)

https://stackblitz.com/edit/github-9wjfwu?file=src%2Fpages%2Findex.astro

This example shows the documented technique apparently working in dev mode.

If you quit the dev server (Ctrl + C) and preview the build instead by running npm run build && npm run preview, the values in the sub directory page will be off.

sarah11918 commented 5 months ago

Do we know what the proper guidance here is? Or just that what we have might set people up for problems?

Would love some tech guidance on this one!

sarah11918 commented 3 months ago

Pinging for freshness, in case anyone wants to pick this up!

Fryuni commented 3 months ago

So, what we are recommending is what Vite recommends for client-side code. For SSR Vite has the following warning at the very bottom of this page:

[!WARNING] Does not work with SSR

This pattern does not work if you are using Vite for Server-Side Rendering, because import.meta.url have different semantics in browsers vs. Node.js. The server bundle also cannot determine the client host URL ahead of time.

Recommending something here is tricky because the appropriate solution will depend on the intended use case.

  1. If the intent is to read the file's content at runtime on the server, there is no portable way of doing this across adapters. The instruction would have to be on a per adapter basis and wouldn't even be allowed in all of them (Cloudflare wouldn't allow it without enabling compatibilities, for example)
  2. If the intent is to load the file's content, but the expected content is present during build-time, then the solution would be to import it using ?raw, which returns the file's content. So instead of this:

    import fs from 'node:fs/promises';
    
    const url = new URL('../../package.json', import.meta.url);
    const json = await fs.readFile(url, 'utf-8');
    const data = JSON.parse(json);

    It would be this:

    import packageJson from '../../package.json?raw';
    
    const data = JSON.parse(packageJson);

    Some file types (like JSON) can be imported directly, and Vite does the parsing, so that could even be shortened to import data from '../../package.json';. However, the general solution for any file type given this intended use would be the ?raw suffix.

  3. If the intent is to get the final path to a file so it can be downloaded by the client, like a "download the code for this component" on a blog post, then the solution would be to import it using ?url:
    // This is a string containing the path this file will be served on
    import fileLink from '../path/to/file?url';

    The value of that path will differ between dev and build, but the path returned will still serve the content. On dev, it will be the local path, and on build, the file will be copied into /_astro and served statically regardless of the adapter being used.

  4. If the intent is to get the path to a folder and then walk its tree at runtime, that is also very much not portable, even less so than the intent 1.
Fryuni commented 3 months ago

Related to #8417

sarah11918 commented 1 week ago
  1. Alright, so what about making our statement more strong, instead of

Our aim is to provide Astro alternatives to common Node.js builtins. However, no such alternatives exist today. So, if you really need to use these builtin modules we don’t want to stop you. Astro supports Node.js builtins using Node’s newer node: prefix. If you want to read a file, for example, you can do so like this:

Something more like:

Astro supports Node.js builtins, with some limitations, using Node’s newer node: prefix. Note that there may be differences between development and production, and some features may be incompatible with on-demand rendering.

and scrap the example, or add an example that is something that really would predictably and reliably work well?