Skip to content

Commit

Permalink
Drop Node 14 support (#5782)
Browse files Browse the repository at this point in the history
* chore: Update engines field

* fix(deps): Remove node-fetch

* feat(polyfills): Remove node-fetch for undici

* feat(webapi): Remove node-fetch from the webapis polyfills for undici

* feat(core): Remove node-fetch for undici in Astro core

* feat(telemetry): Remove node-fetch for undici

* feat(node): Remove node-fetch for undici in node integration

* feat(vercel): Remove node-fetch for undici in Vercel integration

* chore: update lockfile

* chore: update lockfile

* chore: changeset

* fix(set): Fix set directives not streaming correctly on Node 16

* Try another approach

* Debugging

* Debug fetch

* Use global fetch if there is one

* changeset for lit

* Remove web-streams-polyfill

* Remove web-streams-polyfill license note

* Update .changeset/stupid-wolves-explain.md

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

Co-authored-by: Matthew Phillips <matthew@skypack.dev>
Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
  • Loading branch information
3 people committed Jan 9, 2023
1 parent 9bb08bf commit 1f92d64
Show file tree
Hide file tree
Showing 36 changed files with 147 additions and 387 deletions.
5 changes: 5 additions & 0 deletions .changeset/chatty-rivers-camp.md
@@ -0,0 +1,5 @@
---
'@astrojs/lit': patch
---

Only shim fetch if not already present
16 changes: 16 additions & 0 deletions .changeset/curvy-beds-warn.md
@@ -0,0 +1,16 @@
---
'astro': major
'@astrojs/prism': major
'create-astro': major
'@astrojs/mdx': minor
'@astrojs/node': major
'@astrojs/preact': major
'@astrojs/react': major
'@astrojs/solid-js': major
'@astrojs/svelte': major
'@astrojs/vercel': major
'@astrojs/vue': major
'@astrojs/telemetry': major
---

Remove support for Node 14. Minimum supported Node version is now >=16.12.0
7 changes: 7 additions & 0 deletions .changeset/stupid-wolves-explain.md
@@ -0,0 +1,7 @@
---
'@astrojs/webapi': major
---

Replace node-fetch's polyfill with undici.

Since `undici` does not support it, this change also removes custom support for the `file:` protocol
2 changes: 1 addition & 1 deletion packages/astro-prism/package.json
Expand Up @@ -35,6 +35,6 @@
"@types/prismjs": "1.26.0"
},
"engines": {
"node": "^14.18.0 || >=16.12.0"
"node": ">=16.12.0"
}
}
2 changes: 1 addition & 1 deletion packages/astro/astro.js
Expand Up @@ -50,7 +50,7 @@ async function main() {
// it's okay to hard-code the valid Node versions here since they will not change over time.
if (typeof require === 'undefined') {
console.error(`\nNode.js v${version} is not supported by Astro!
Please upgrade to a version of Node.js with complete ESM support: "^14.18.0 || >=16.12.0"\n`);
Please upgrade to a supported version of Node.js: ">=16.12.0"\n`);
}

// Not supported: Report the most helpful error message possible.
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/package.json
Expand Up @@ -199,7 +199,6 @@
"eol": "^0.9.1",
"memfs": "^3.4.7",
"mocha": "^9.2.2",
"node-fetch": "^3.2.5",
"node-mocks-http": "^1.11.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
Expand All @@ -208,10 +207,11 @@
"rollup": "^3.9.0",
"sass": "^1.52.2",
"srcset-parse": "^1.1.0",
"undici": "^5.14.0",
"unified": "^10.1.2"
},
"engines": {
"node": "^14.18.0 || >=16.12.0",
"node": ">=16.12.0",
"npm": ">=6.14.0"
}
}
21 changes: 16 additions & 5 deletions packages/astro/src/runtime/server/escape.ts
@@ -1,4 +1,5 @@
import { escape } from 'html-escaper';
import { streamAsyncIterator } from './util.js';

// Leverage the battle-tested `html-escaper` npm package.
export const escapeHTML = escape;
Expand Down Expand Up @@ -58,9 +59,19 @@ export function isHTMLBytes(value: any): value is HTMLBytes {
return Object.prototype.toString.call(value) === '[object HTMLBytes]';
}

async function* unescapeChunksAsync(iterable: AsyncIterable<Uint8Array>): any {
for await (const chunk of iterable) {
yield unescapeHTML(chunk as BlessedType);
function hasGetReader(obj: unknown): obj is ReadableStream {
return typeof (obj as any).getReader === 'function';
}

async function* unescapeChunksAsync(iterable: ReadableStream | string): any {
if (hasGetReader(iterable)) {
for await (const chunk of streamAsyncIterator(iterable)) {
yield unescapeHTML(chunk as BlessedType);
}
} else {
for await (const chunk of iterable) {
yield unescapeHTML(chunk as BlessedType);
}
}
}

Expand All @@ -82,7 +93,7 @@ export function unescapeHTML(
}
// If a response, stream out the chunks
else if (str instanceof Response && str.body) {
const body = str.body as unknown as AsyncIterable<Uint8Array>;
const body = str.body;
return unescapeChunksAsync(body);
}
// If a promise, await the result and mark that.
Expand All @@ -92,7 +103,7 @@ export function unescapeHTML(
});
} else if (Symbol.iterator in str) {
return unescapeChunks(str);
} else if (Symbol.asyncIterator in str) {
} else if (Symbol.asyncIterator in str || hasGetReader(str)) {
return unescapeChunksAsync(str);
}
}
Expand Down
10 changes: 6 additions & 4 deletions packages/astro/src/runtime/server/response.ts
@@ -1,3 +1,5 @@
import { streamAsyncIterator } from './util.js';

const isNodeJS =
typeof process === 'object' && Object.prototype.toString.call(process) === '[object process]';

Expand All @@ -21,9 +23,9 @@ function createResponseClass() {
async text(): Promise<string> {
if (this.#isStream && isNodeJS) {
let decoder = new TextDecoder();
let body = this.#body as AsyncIterable<Uint8Array>;
let body = this.#body;
let out = '';
for await (let chunk of body) {
for await (let chunk of streamAsyncIterator(body)) {
out += decoder.decode(chunk);
}
return out;
Expand All @@ -33,10 +35,10 @@ function createResponseClass() {

async arrayBuffer(): Promise<ArrayBuffer> {
if (this.#isStream && isNodeJS) {
let body = this.#body as AsyncIterable<Uint8Array>;
let body = this.#body;
let chunks: Uint8Array[] = [];
let len = 0;
for await (let chunk of body) {
for await (let chunk of streamAsyncIterator(body)) {
chunks.push(chunk);
len += chunk.length;
}
Expand Down
14 changes: 14 additions & 0 deletions packages/astro/src/runtime/server/util.ts
Expand Up @@ -31,3 +31,17 @@ export function serializeListValue(value: any) {
export function isPromise<T = any>(value: any): value is Promise<T> {
return !!value && typeof value === 'object' && typeof value.then === 'function';
}

export async function* streamAsyncIterator(stream: ReadableStream) {
const reader = stream.getReader();

try {
while (true) {
const { done, value } = await reader.read();
if (done) return;
yield value;
}
} finally {
reader.releaseLock();
}
}
4 changes: 2 additions & 2 deletions packages/astro/test/ssr-api-route.test.js
@@ -1,7 +1,7 @@
import { expect } from 'chai';
import { loadFixture } from './test-utils.js';
import { File, FormData } from 'undici';
import testAdapter from './test-adapter.js';
import { FormData, File } from 'node-fetch';
import { loadFixture } from './test-utils.js';

describe('API routes in SSR', () => {
/** @type {import('./test-utils').Fixture} */
Expand Down
10 changes: 5 additions & 5 deletions packages/astro/test/streaming.test.js
@@ -1,7 +1,7 @@
import { isWindows, loadFixture } from './test-utils.js';
import { expect } from 'chai';
import testAdapter from './test-adapter.js';
import * as cheerio from 'cheerio';
import testAdapter from './test-adapter.js';
import { isWindows, loadFixture, streamAsyncIterator } from './test-utils.js';

describe('Streaming', () => {
if (isWindows) return;
Expand Down Expand Up @@ -32,7 +32,7 @@ describe('Streaming', () => {
it('Body is chunked', async () => {
let res = await fixture.fetch('/');
let chunks = [];
for await (const bytes of res.body) {
for await (const bytes of streamAsyncIterator(res.body)) {
let chunk = bytes.toString('utf-8');
chunks.push(chunk);
}
Expand Down Expand Up @@ -61,7 +61,7 @@ describe('Streaming', () => {
const response = await app.render(request);
let chunks = [];
let decoder = new TextDecoder();
for await (const bytes of response.body) {
for await (const bytes of streamAsyncIterator(response.body)) {
let chunk = decoder.decode(bytes);
chunks.push(chunk);
}
Expand Down Expand Up @@ -102,7 +102,7 @@ describe('Streaming disabled', () => {
it('Body is chunked', async () => {
let res = await fixture.fetch('/');
let chunks = [];
for await (const bytes of res.body) {
for await (const bytes of streamAsyncIterator(res.body)) {
let chunk = bytes.toString('utf-8');
chunks.push(chunk);
}
Expand Down
16 changes: 15 additions & 1 deletion packages/astro/test/test-utils.js
Expand Up @@ -19,7 +19,7 @@ polyfill(globalThis, {
});

/**
* @typedef {import('node-fetch').Response} Response
* @typedef {import('undici').Response} Response
* @typedef {import('../src/core/dev/dev').DedvServer} DevServer
* @typedef {import('../src/@types/astro').AstroConfig} AstroConfig
* @typedef {import('../src/core/preview/index').PreviewServer} PreviewServer
Expand Down Expand Up @@ -303,3 +303,17 @@ export const isWindows = os.platform() === 'win32';
export function fixLineEndings(str) {
return str.replace(/\r\n/g, '\n');
}

export async function* streamAsyncIterator(stream) {
const reader = stream.getReader();

try {
while (true) {
const { done, value } = await reader.read();
if (done) return;
yield value;
}
} finally {
reader.releaseLock();
}
}
2 changes: 1 addition & 1 deletion packages/create-astro/package.json
Expand Up @@ -54,6 +54,6 @@
"uvu": "^0.5.3"
},
"engines": {
"node": "^14.18.0 || >=16.12.0"
"node": ">=16.12.0"
}
}
9 changes: 8 additions & 1 deletion packages/integrations/lit/server-shim.js
@@ -1,5 +1,12 @@
import { installWindowOnGlobal } from '@lit-labs/ssr/lib/dom-shim.js';
installWindowOnGlobal();

if(typeof fetch === 'function') {
const _fetch = fetch;
installWindowOnGlobal();
globalThis.fetch = window.fetch = _fetch;
} else {
installWindowOnGlobal();
}

window.global = window;
document.getElementsByTagName = () => [];
Expand Down
2 changes: 1 addition & 1 deletion packages/integrations/mdx/package.json
Expand Up @@ -71,6 +71,6 @@
"vite": "^4.0.3"
},
"engines": {
"node": "^14.18.0 || >=16.12.0"
"node": ">=16.12.0"
}
}
4 changes: 2 additions & 2 deletions packages/integrations/node/package.json
Expand Up @@ -37,12 +37,12 @@
"astro": "workspace:^2.0.0-beta.0"
},
"devDependencies": {
"@types/node-fetch": "^2.6.2",
"@types/send": "^0.17.1",
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"chai": "^4.3.6",
"mocha": "^9.2.2",
"node-mocks-http": "^1.11.0"
"node-mocks-http": "^1.11.0",
"undici": "^5.14.0"
}
}
2 changes: 1 addition & 1 deletion packages/integrations/node/src/response-iterator.ts
Expand Up @@ -4,7 +4,7 @@
* - https://github.com/apollographql/apollo-client/blob/main/src/utilities/common/responseIterator.ts
*/

import type { Response as NodeResponse } from 'node-fetch';
import type { Response as NodeResponse } from 'undici';
import { Readable as NodeReadableStream } from 'stream';

interface NodeStreamIterator<T> {
Expand Down
2 changes: 1 addition & 1 deletion packages/integrations/preact/package.json
Expand Up @@ -47,6 +47,6 @@
"preact": "^10.6.5"
},
"engines": {
"node": "^14.18.0 || >=16.12.0"
"node": ">=16.12.0"
}
}
2 changes: 1 addition & 1 deletion packages/integrations/react/package.json
Expand Up @@ -52,6 +52,6 @@
"@types/react-dom": "^17.0.17 || ^18.0.6"
},
"engines": {
"node": "^14.18.0 || >=16.12.0"
"node": ">=16.12.0"
}
}
2 changes: 1 addition & 1 deletion packages/integrations/solid/package.json
Expand Up @@ -44,6 +44,6 @@
"solid-js": "^1.4.3"
},
"engines": {
"node": "^14.18.0 || >=16.12.0"
"node": ">=16.12.0"
}
}
2 changes: 1 addition & 1 deletion packages/integrations/svelte/package.json
Expand Up @@ -47,6 +47,6 @@
"astro": "workspace:^2.0.0-beta.0"
},
"engines": {
"node": "^14.18.0 || >=16.12.0"
"node": ">=16.12.0"
}
}
11 changes: 1 addition & 10 deletions packages/integrations/vercel/src/serverless/entrypoint.ts
Expand Up @@ -3,21 +3,12 @@ import type { SSRManifest } from 'astro';
import { App } from 'astro/app';
import type { IncomingMessage, ServerResponse } from 'node:http';

import * as requestTransformLegacy from './request-transform/legacy.js';
import * as requestTransformNode18 from './request-transform/node18.js';
import { getRequest, setResponse } from './request-transform';

polyfill(globalThis, {
exclude: 'window document',
});

// Node 18+ has a new API for request/response, while older versions use node-fetch
// When we drop support for Node 14, we can remove the legacy code by switching to undici

const nodeVersion = parseInt(process.version.split('.')[0].slice(1)); // 'v14.17.0' -> 14

const { getRequest, setResponse } =
nodeVersion >= 18 ? requestTransformNode18 : requestTransformLegacy;

export const createExports = (manifest: SSRManifest) => {
const app = new App(manifest);

Expand Down

0 comments on commit 1f92d64

Please sign in to comment.