Skip to content

Commit

Permalink
feat: make the lib isomorphic (APIDevTools#280)
Browse files Browse the repository at this point in the history
  • Loading branch information
alanpoulain authored Oct 12, 2022
1 parent 135bf4f commit 138f648
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 85 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ extends: "@jsdevtools"
env:
node: true
browser: true
rules:
"@typescript-eslint/no-explicit-any": ["off"]
22 changes: 12 additions & 10 deletions .github/workflows/CI-CD.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ name: CI-CD

on:
push:
branches:
- main
pull_request:
schedule:
- cron: "0 0 1 * *"
Expand All @@ -25,16 +27,16 @@ jobs:
- macos-latest
- windows-latest
node:
- 10
- 12
- 14
- lts/*
- current

steps:
- name: Checkout source
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Install Node ${{ matrix.node }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}

Expand Down Expand Up @@ -69,12 +71,12 @@ jobs:

steps:
- name: Checkout source
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Install Node
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: 12
node-version: lts/*

- name: Install dependencies
run: npm ci
Expand Down Expand Up @@ -120,10 +122,10 @@ jobs:
- node_tests
- browser_tests
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 12
node-version: lts/*

- name: Install dependencies
run: npm ci
Expand Down
1 change: 1 addition & 0 deletions .mocharc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ spec: test/specs/**/*.spec.js
bail: true
recursive: true
async-only: true
require: ./test/fixtures/polyfill.js
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ When using a transpiler such as [Babel](https://babeljs.io/) or [TypeScript](htt
import $RefParser from "@apidevtools/json-schema-ref-parser";
```

If you are using Node.js < 18, you'll need a polyfill for `fetch`, like [node-fetch](https://github.com/node-fetch/node-fetch):
```javascript
import fetch from "node-fetch";

globalThis.fetch = fetch;
```



Browser support
Expand Down
113 changes: 45 additions & 68 deletions lib/resolvers/http.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"use strict";

const http = require("http");
const https = require("https");
const { ono } = require("@jsdevtools/ono");
const url = require("../util/url");
const { ResolverError } = require("../util/errors");
Expand Down Expand Up @@ -70,12 +68,12 @@ module.exports = {
* @param {object} file - An object containing information about the referenced file
* @param {string} file.url - The full URL of the referenced file
* @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
* @returns {Promise<Buffer>}
* @returns {Promise<string>}
*/
read (file) {
let u = url.parse(file.url);

if (process.browser && !u.protocol) {
if (typeof window !== "undefined" && !u.protocol) {
// Use the protocol of the current page
u.protocol = url.parse(location.href).protocol;
}
Expand All @@ -91,42 +89,40 @@ module.exports = {
* @param {object} httpOptions - The `options.resolve.http` object
* @param {number} [redirects] - The redirect URLs that have already been followed
*
* @returns {Promise<Buffer>}
* @returns {Promise<string>}
* The promise resolves with the raw downloaded data, or rejects if there is an HTTP error.
*/
function download (u, httpOptions, redirects) {
return new Promise(((resolve, reject) => {
u = url.parse(u);
redirects = redirects || [];
redirects.push(u.href);

get(u, httpOptions)
.then((res) => {
if (res.statusCode >= 400) {
throw ono({ status: res.statusCode }, `HTTP ERROR ${res.statusCode}`);
u = url.parse(u);
redirects = redirects || [];
redirects.push(u.href);

return get(u, httpOptions)
.then((res) => {
if (res.statusCode >= 400) {
throw ono({ status: res.statusCode }, `HTTP ERROR ${res.statusCode}`);
}
else if (res.statusCode >= 300) {
if (redirects.length > httpOptions.redirects) {
throw new ResolverError(ono({ status: res.statusCode },
`Error downloading ${redirects[0]}. \nToo many redirects: \n ${redirects.join(" \n ")}`));
}
else if (res.statusCode >= 300) {
if (redirects.length > httpOptions.redirects) {
reject(new ResolverError(ono({ status: res.statusCode },
`Error downloading ${redirects[0]}. \nToo many redirects: \n ${redirects.join(" \n ")}`)));
}
else if (!res.headers.location) {
throw ono({ status: res.statusCode }, `HTTP ${res.statusCode} redirect with no location header`);
}
else {
// console.log('HTTP %d redirect %s -> %s', res.statusCode, u.href, res.headers.location);
let redirectTo = url.resolve(u, res.headers.location);
download(redirectTo, httpOptions, redirects).then(resolve, reject);
}
else if (!res.headers.location) {
throw ono({ status: res.statusCode }, `HTTP ${res.statusCode} redirect with no location header`);
}
else {
resolve(res.body || Buffer.alloc(0));
// console.log('HTTP %d redirect %s -> %s', res.statusCode, u.href, res.headers.location);
let redirectTo = url.resolve(u, res.headers.location);
return download(redirectTo, httpOptions, redirects);
}
})
.catch((err) => {
reject(new ResolverError(ono(err, `Error downloading ${u.href}`), u.href));
});
}));
}
else {
return res.text();
}
})
.catch((err) => {
throw new ResolverError(ono(err, `Error downloading ${u.href}`), u.href);
});
}

/**
Expand All @@ -139,42 +135,23 @@ function download (u, httpOptions, redirects) {
* The promise resolves with the HTTP Response object.
*/
function get (u, httpOptions) {
return new Promise(((resolve, reject) => {
// console.log('GET', u.href);

let protocol = u.protocol === "https:" ? https : http;
let req = protocol.get({
hostname: u.hostname,
port: u.port,
path: u.path,
auth: u.auth,
protocol: u.protocol,
headers: httpOptions.headers || {},
withCredentials: httpOptions.withCredentials
});
let controller;
let timeoutId;
if (httpOptions.timeout) {
controller = new AbortController();
timeoutId = setTimeout(() => controller.abort(), httpOptions.timeout);
}

if (typeof req.setTimeout === "function") {
req.setTimeout(httpOptions.timeout);
return fetch(u, {
method: "GET",
headers: httpOptions.headers || {},
credentials: httpOptions.withCredentials ? "include" : "same-origin",
signal: controller ? controller.signal : null,
}).then(response => {
if (timeoutId) {
clearTimeout(timeoutId);
}

req.on("timeout", () => {
req.abort();
});

req.on("error", reject);

req.once("response", (res) => {
res.body = Buffer.alloc(0);

res.on("data", (data) => {
res.body = Buffer.concat([res.body, Buffer.from(data)]);
});

res.on("error", reject);

res.on("end", () => {
resolve(res);
});
});
}));
return response;
});
}
8 changes: 4 additions & 4 deletions lib/util/url.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";

let isWindows = /^win/.test(process.platform),
let isWindows = /^win/.test(globalThis.process?.platform),
forwardSlashPattern = /\//g,
protocolPattern = /^(\w{2,}):\/\//i,
url = module.exports,
Expand All @@ -22,7 +22,7 @@ let urlDecodePatterns = [
/\%40/g, "@"
];

exports.parse = require("url").parse;
exports.parse = (u) => new URL(u);

/**
* Returns resolved target URL relative to a base URL in a manner similar to that of a Web browser resolving an anchor tag HREF.
Expand All @@ -45,7 +45,7 @@ exports.resolve = function resolve (from, to) {
* @returns {string}
*/
exports.cwd = function cwd () {
if (process.browser) {
if (typeof window !== "undefined") {
return location.href;
}

Expand Down Expand Up @@ -144,7 +144,7 @@ exports.isHttp = function isHttp (path) {
}
else if (protocol === undefined) {
// There is no protocol. If we're running in a browser, then assume it's HTTP.
return process.browser;
return typeof window !== "undefined";
}
else {
// It's some other protocol, such as "ftp://", "mongodb://", etc.
Expand Down
Loading

0 comments on commit 138f648

Please sign in to comment.