[Next] fetch support (#1563)

* fix: polyfill fetch in every ssr scenario

* test(fetch): update fetch tests

* docs: update data fetching guide to remove caveats about `fetch` and isomorphic usage

* refactor: update regex for clarity
This commit is contained in:
Nate Moore 2021-10-15 14:00:38 -05:00 committed by Drew Powers
parent 7749e18066
commit 62106902bb
9 changed files with 103 additions and 21 deletions

View file

@ -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

View file

@ -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),

View file

@ -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 }
},
};
}

View file

@ -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', () => {});

View file

@ -0,0 +1 @@
<span id="astro-component">{typeof fetch}</span>

View file

@ -0,0 +1 @@
<span id="svelte">{ typeof fetch }</span>

View file

@ -0,0 +1,16 @@
<template>
<span id="vue">{{ type }}</span>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
return {
type: typeof fetch
}
}
})
</script>

View file

@ -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';
---
<html lang="en">
@ -7,6 +10,10 @@ import Child from '../components/Child.jsx';
<title>Global fetch</title>
</head>
<body>
<Child />
<span id="astro-page">{typeof fetch}</span>
<Test />
<JsxComponent />
<SvelteComponent />
<VueComponent />
</body>
</html>
</html>