diff --git a/docs/src/pages/guides/data-fetching.md b/docs/src/pages/guides/data-fetching.md index 5f306a2dd..a0481f059 100644 --- a/docs/src/pages/guides/data-fetching.md +++ b/docs/src/pages/guides/data-fetching.md @@ -32,11 +32,10 @@ console.log(data); ## Using `fetch()` outside of Astro Components -If you want to use `fetch()` in a non-astro component, use the [`node-fetch`](https://github.com/node-fetch/node-fetch) library: +If you want to use `fetch()` in a non-astro component, it is also globally available: ```tsx // Movies.tsx -import fetch from 'node-fetch'; import type { FunctionalComponent } from 'preact'; import { h } from 'preact'; @@ -55,11 +54,3 @@ const Movies: FunctionalComponent = () => { export default Movies; ``` - -If you load a component using `node-fetch` [interactively](/core-concepts/component-hydration), with `client:load`, `client:visible`, etc., you'll need to either not use `node-fetch` or switch to an [isomorphic](https://en.wikipedia.org/wiki/Isomorphic_JavaScript) library that will run both at build time and on the client, as the [`node-fetch` README.md](https://github.com/node-fetch/node-fetch#motivation) recommends: - -> Instead of implementing XMLHttpRequest in Node.js to run browser-specific [Fetch polyfill](https://github.com/github/fetch), why not go from native http to fetch API directly? Hence, node-fetch, minimal code for a window.fetch compatible API on Node.js runtime. -> -> See Jason Miller's [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch) or Leonardo Quixada's [cross-fetch](https://github.com/lquixada/cross-fetch) for isomorphic usage (exports node-fetch for server-side, whatwg-fetch for client-side). - -> Quoted from https://github.com/node-fetch/node-fetch#motivation diff --git a/packages/astro/src/runtime/vite/config.ts b/packages/astro/src/runtime/vite/config.ts index 6d6e5774b..4d35f7d54 100644 --- a/packages/astro/src/runtime/vite/config.ts +++ b/packages/astro/src/runtime/vite/config.ts @@ -11,6 +11,7 @@ import astroVitePlugin from './plugin-astro.js'; import astroPostprocessVitePlugin from './plugin-astro-postprocess.js'; import markdownVitePlugin from './plugin-markdown.js'; import jsxVitePlugin from './plugin-jsx.js'; +import fetchVitePlugin from './plugin-fetch.js'; import { AstroDevServer } from '../../dev'; const require = createRequire(import.meta.url); @@ -86,6 +87,7 @@ export async function loadViteConfig( markdownVitePlugin({ config: astroConfig, devServer }), jsxVitePlugin({ config: astroConfig, logging }), astroPostprocessVitePlugin({ config: astroConfig, devServer }), + fetchVitePlugin(), ...plugins ], publicDir: fileURLToPath(astroConfig.public), diff --git a/packages/astro/src/runtime/vite/plugin-fetch.ts b/packages/astro/src/runtime/vite/plugin-fetch.ts new file mode 100644 index 000000000..a006c6503 --- /dev/null +++ b/packages/astro/src/runtime/vite/plugin-fetch.ts @@ -0,0 +1,58 @@ +import type { Plugin } from 'vite'; +import MagicString from 'magic-string'; + +// https://github.com/vitejs/vite/discussions/5109#discussioncomment-1450726 +function isSSR(options: undefined | boolean | { ssr: boolean }): boolean { + if (options === undefined) { + return false + } + if (typeof options === 'boolean') { + return options + } + if (typeof options == 'object') { + return !!options.ssr + } + return false +} + +// This matches any JS-like file (that we know of) +// See https://regex101.com/r/Cgofir/1 +const SUPPORTED_FILES = /\.(astro|svelte|vue|[cm]?js|jsx|[cm]?ts|tsx)$/; +const DEFINE_FETCH = `import fetch from 'node-fetch';\n`; + +export default function pluginFetch(): Plugin { + return { + name: '@astrojs/vite-plugin-fetch', + enforce: 'post', + async transform(code, id, opts) { + const ssr = isSSR(opts); + + // If this isn't an SSR pass, `fetch` will already be available! + if (!ssr) { + return null; + } + + // Only transform JS-like files + if (!id.match(SUPPORTED_FILES)) { + return null; + } + + // Optimization: only run on probable matches + if (!code.includes('fetch')) { + return null; + } + + const s = new MagicString(code); + s.prepend(DEFINE_FETCH); + + const result = s.toString(); + + const map = s.generateMap({ + source: id, + includeContent: true + }); + + return { code: result, map } + }, + }; +} diff --git a/packages/astro/test/fetch.test.js b/packages/astro/test/fetch.test.js index 60ecb8442..23112c479 100644 --- a/packages/astro/test/fetch.test.js +++ b/packages/astro/test/fetch.test.js @@ -1,5 +1,3 @@ -/** - * UNCOMMENT: add fetch() in component support import { expect } from 'chai'; import cheerio from 'cheerio'; import { loadFixture } from './test-utils.js'; @@ -11,14 +9,22 @@ before(async () => { await fixture.build(); }); - describe('Global Fetch', () => { - it('Is available in non-Astro components.', async () => { + it('Is available in Astro pages', async () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - expect($('#jsx').text()).to.equal('function'); + expect($('#astro-page').text()).to.equal('function', 'Fetch supported in .astro page'); + }); + it('Is available in Astro components', async () => { + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + expect($('#astro-component').text()).to.equal('function', 'Fetch supported in .astro components'); + }); + it('Is available in non-Astro components', async () => { + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + expect($('#jsx').text()).to.equal('function', 'Fetch supported in .jsx'); + expect($('#svelte').text()).to.equal('function', 'Fetch supported in .svelte'); + expect($('#vue').text()).to.equal('function', 'Fetch supported in .vue'); }); }); -*/ - -it.skip('is skipped', () => {}); diff --git a/packages/astro/test/fixtures/fetch/src/components/AstroComponent.astro b/packages/astro/test/fixtures/fetch/src/components/AstroComponent.astro new file mode 100644 index 000000000..e56cfd3cd --- /dev/null +++ b/packages/astro/test/fixtures/fetch/src/components/AstroComponent.astro @@ -0,0 +1 @@ +{typeof fetch} diff --git a/packages/astro/test/fixtures/fetch/src/components/Child.jsx b/packages/astro/test/fixtures/fetch/src/components/JsxComponent.jsx similarity index 100% rename from packages/astro/test/fixtures/fetch/src/components/Child.jsx rename to packages/astro/test/fixtures/fetch/src/components/JsxComponent.jsx diff --git a/packages/astro/test/fixtures/fetch/src/components/SvelteComponent.svelte b/packages/astro/test/fixtures/fetch/src/components/SvelteComponent.svelte new file mode 100644 index 000000000..49f32acbb --- /dev/null +++ b/packages/astro/test/fixtures/fetch/src/components/SvelteComponent.svelte @@ -0,0 +1 @@ +{ typeof fetch } diff --git a/packages/astro/test/fixtures/fetch/src/components/VueComponent.vue b/packages/astro/test/fixtures/fetch/src/components/VueComponent.vue new file mode 100644 index 000000000..1ea2e2ccc --- /dev/null +++ b/packages/astro/test/fixtures/fetch/src/components/VueComponent.vue @@ -0,0 +1,16 @@ + + {{ type }} + + + + diff --git a/packages/astro/test/fixtures/fetch/src/pages/index.astro b/packages/astro/test/fixtures/fetch/src/pages/index.astro index 66979efa3..a9e334104 100644 --- a/packages/astro/test/fixtures/fetch/src/pages/index.astro +++ b/packages/astro/test/fixtures/fetch/src/pages/index.astro @@ -1,5 +1,8 @@ --- -import Child from '../components/Child.jsx'; +import Test from '../components/AstroComponent.astro'; +import JsxComponent from '../components/JsxComponent.jsx'; +import SvelteComponent from '../components/SvelteComponent.svelte'; +import VueComponent from '../components/VueComponent.vue'; --- @@ -7,6 +10,10 @@ import Child from '../components/Child.jsx';