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

[api-minor] Update the minimum supported Node.js version to 20, and only support the Fetch API for "remote" PDF documents in Node.js #18959

Merged
merged 4 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [18, lts/*, 22, 23]
Snuffleupagus marked this conversation as resolved.
Show resolved Hide resolved
node-version: [20, 22, 23]

steps:
- name: Checkout repository
Expand Down
4 changes: 2 additions & 2 deletions gulpfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const ENV_TARGETS = [
"Chrome >= 103",
"Firefox ESR",
"Safari >= 16.4",
"Node >= 18",
"Node >= 20",
"> 1%",
"not IE > 0",
"not dead",
Expand Down Expand Up @@ -2271,7 +2271,7 @@ function packageJson() {
url: `git+${DIST_GIT_URL}`,
},
engines: {
node: ">=18",
node: ">=20",
},
scripts: {},
};
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"url": "git://github.com/mozilla/pdf.js.git"
},
"engines": {
"node": ">=18"
"node": ">=20"
},
"license": "Apache-2.0"
}
32 changes: 14 additions & 18 deletions src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ import {
NodeCanvasFactory,
NodeCMapReaderFactory,
NodeFilterFactory,
NodePackages,
NodeStandardFontDataFactory,
} from "display-node_utils";
import { CanvasGraphics } from "./canvas.js";
Expand Down Expand Up @@ -451,15 +450,20 @@ function getDocument(src = {}) {
PDFJSDev.test("GENERIC") &&
isNodeJS
) {
const isFetchSupported =
typeof fetch !== "undefined" &&
typeof Response !== "undefined" &&
"body" in Response.prototype;

NetworkStream =
isFetchSupported && isValidFetchUrl(url)
? PDFFetchStream
: PDFNodeStream;
if (isValidFetchUrl(url)) {
if (
typeof fetch === "undefined" ||
typeof Response === "undefined" ||
!("body" in Response.prototype)
) {
throw new Error(
"getDocument - the Fetch API was disabled in Node.js, see `--no-experimental-fetch`."
);
}
NetworkStream = PDFFetchStream;
} else {
NetworkStream = PDFNodeStream;
}
} else {
NetworkStream = isValidFetchUrl(url)
? PDFFetchStream
Expand Down Expand Up @@ -2137,14 +2141,6 @@ class PDFWorker {
* @type {Promise<void>}
*/
get promise() {
if (
typeof PDFJSDev !== "undefined" &&
PDFJSDev.test("GENERIC") &&
isNodeJS
) {
// Ensure that all Node.js packages/polyfills have loaded.
return Promise.all([NodePackages.promise, this._readyCapability.promise]);
}
return this._readyCapability.promise;
}

Expand Down
172 changes: 34 additions & 138 deletions src/display/node_stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* globals process */

import { AbortException, assert, MissingPDFException } from "../shared/util.js";
import {
createHeaders,
extractFilenameFromHeader,
validateRangeRequestCapabilities,
} from "./network_utils.js";
import { NodePackages } from "./node_utils.js";

if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
throw new Error(
Expand All @@ -33,28 +28,18 @@ function parseUrlOrPath(sourceUrl) {
if (urlRegex.test(sourceUrl)) {
return new URL(sourceUrl);
}
const url = NodePackages.get("url");
const url = process.getBuiltinModule("url");
return new URL(url.pathToFileURL(sourceUrl));
}

function createRequest(url, headers, callback) {
if (url.protocol === "http:") {
const http = NodePackages.get("http");
return http.request(url, { headers }, callback);
}
const https = NodePackages.get("https");
return https.request(url, { headers }, callback);
}

class PDFNodeStream {
constructor(source) {
this.source = source;
this.url = parseUrlOrPath(source.url);
this.isHttp =
this.url.protocol === "http:" || this.url.protocol === "https:";
// Check if url refers to filesystem.
this.isFsUrl = this.url.protocol === "file:";
this.headers = createHeaders(this.isHttp, source.httpHeaders);
assert(
this.url.protocol === "file:",
"PDFNodeStream only supports file:// URLs."
);

this._fullRequestReader = null;
this._rangeRequestReaders = [];
Expand All @@ -69,19 +54,15 @@ class PDFNodeStream {
!this._fullRequestReader,
"PDFNodeStream.getFullReader can only be called once."
);
this._fullRequestReader = this.isFsUrl
? new PDFNodeStreamFsFullReader(this)
: new PDFNodeStreamFullReader(this);
this._fullRequestReader = new PDFNodeStreamFsFullReader(this);
return this._fullRequestReader;
}

getRangeReader(start, end) {
if (end <= this._progressiveDataLength) {
return null;
}
const rangeReader = this.isFsUrl
? new PDFNodeStreamFsRangeReader(this, start, end)
: new PDFNodeStreamRangeReader(this, start, end);
const rangeReader = new PDFNodeStreamFsRangeReader(this, start, end);
this._rangeRequestReaders.push(rangeReader);
return rangeReader;
}
Expand All @@ -95,7 +76,7 @@ class PDFNodeStream {
}
}

class BaseFullReader {
class PDFNodeStreamFsFullReader {
constructor(stream) {
this._url = stream.url;
this._done = false;
Expand All @@ -118,6 +99,24 @@ class BaseFullReader {
this._readableStream = null;
this._readCapability = Promise.withResolvers();
this._headersCapability = Promise.withResolvers();

const fs = process.getBuiltinModule("fs");
fs.promises.lstat(this._url).then(
stat => {
// Setting right content length.
this._contentLength = stat.size;

this._setReadableStream(fs.createReadStream(this._url));
this._headersCapability.resolve();
},
error => {
if (error.code === "ENOENT") {
error = new MissingPDFException(`Missing PDF "${this._url}".`);
}
this._storedError = error;
this._headersCapability.reject(error);
}
);
}

get headersReady() {
Expand Down Expand Up @@ -210,8 +209,8 @@ class BaseFullReader {
}
}

class BaseRangeReader {
constructor(stream) {
class PDFNodeStreamFsRangeReader {
constructor(stream, start, end) {
this._url = stream.url;
this._done = false;
this._storedError = null;
Expand All @@ -221,6 +220,11 @@ class BaseRangeReader {
this._readCapability = Promise.withResolvers();
const source = stream.source;
this._isStreamingSupported = !source.disableStream;

const fs = process.getBuiltinModule("fs");
this._setReadableStream(
fs.createReadStream(this._url, { start, end: end - 1 })
);
}

get isStreamingSupported() {
Expand Down Expand Up @@ -288,112 +292,4 @@ class BaseRangeReader {
}
}

class PDFNodeStreamFullReader extends BaseFullReader {
constructor(stream) {
super(stream);

// Node.js requires the `headers` to be a regular Object.
const headers = Object.fromEntries(stream.headers);

const handleResponse = response => {
if (response.statusCode === 404) {
const error = new MissingPDFException(`Missing PDF "${this._url}".`);
this._storedError = error;
this._headersCapability.reject(error);
return;
}
this._headersCapability.resolve();
this._setReadableStream(response);

const responseHeaders = new Headers(this._readableStream.headers);

const { allowRangeRequests, suggestedLength } =
validateRangeRequestCapabilities({
responseHeaders,
isHttp: stream.isHttp,
rangeChunkSize: this._rangeChunkSize,
disableRange: this._disableRange,
});

this._isRangeSupported = allowRangeRequests;
// Setting right content length.
this._contentLength = suggestedLength || this._contentLength;

this._filename = extractFilenameFromHeader(responseHeaders);
};

this._request = createRequest(this._url, headers, handleResponse);

this._request.on("error", reason => {
this._storedError = reason;
this._headersCapability.reject(reason);
});
// Note: `request.end(data)` is used to write `data` to request body
// and notify end of request. But one should always call `request.end()`
// even if there is no data to write -- (to notify the end of request).
this._request.end();
}
}

class PDFNodeStreamRangeReader extends BaseRangeReader {
constructor(stream, start, end) {
super(stream);

// Node.js requires the `headers` to be a regular Object.
const headers = Object.fromEntries(stream.headers);
headers.Range = `bytes=${start}-${end - 1}`;

const handleResponse = response => {
if (response.statusCode === 404) {
const error = new MissingPDFException(`Missing PDF "${this._url}".`);
this._storedError = error;
return;
}
this._setReadableStream(response);
};

this._request = createRequest(this._url, headers, handleResponse);

this._request.on("error", reason => {
this._storedError = reason;
});
this._request.end();
}
}

class PDFNodeStreamFsFullReader extends BaseFullReader {
constructor(stream) {
super(stream);

const fs = NodePackages.get("fs");
fs.promises.lstat(this._url).then(
stat => {
// Setting right content length.
this._contentLength = stat.size;

this._setReadableStream(fs.createReadStream(this._url));
this._headersCapability.resolve();
},
error => {
if (error.code === "ENOENT") {
error = new MissingPDFException(`Missing PDF "${this._url}".`);
}
this._storedError = error;
this._headersCapability.reject(error);
}
);
}
}

class PDFNodeStreamFsRangeReader extends BaseRangeReader {
constructor(stream, start, end) {
super(stream);

const fs = NodePackages.get("fs");
this._setReadableStream(
fs.createReadStream(this._url, { start, end: end - 1 })
);
}
}

export { PDFNodeStream };
Loading