Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop Node 14 support #5782

Merged
merged 22 commits into from Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 changes remove support for the `file:` protocol
matthewp marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -198,7 +198,6 @@
"eol": "^0.9.1",
"memfs": "^3.4.7",
"mocha": "^9.2.2",
"node-fetch": "^3.2.5",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫡

"node-mocks-http": "^1.11.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
Expand All @@ -207,10 +206,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"
}
}
17 changes: 12 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,15 @@ 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);
async function* unescapeChunksAsync(iterable: ReadableStream | string): any {
if (iterable instanceof ReadableStream) {
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 +89,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 +99,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 || str instanceof ReadableStream) {
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any code with a while(true) gets my approval

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And a try / finally? This code has it all!

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"
}
}
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