Skip to content
37 changes: 37 additions & 0 deletions src/tests/webR/webr-worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,45 @@ describe('Download and install binary webR packages', () => {
expect(await pkg.toBoolean()).toEqual(true);
warnSpy.mockRestore();
});

test('Install packages from R-universe using shim', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => null);
const pkg = (await webR.evalR(`
stopifnot(! "boot" %in% installed.packages()[, "Package"])
webr::shim_install()
install.packages("boot", repos = "https://cran.r-universe.dev")
"boot" %in% installed.packages()[, "Package"]
`)) as RLogical;
expect(await pkg.toBoolean()).toEqual(true);
warnSpy.mockRestore();
});
});

describe('Download files', () => {
test('download.file() without URL redirect', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => null);
const pkg = (await webR.evalR(`
tf <- tempfile()
utils::download.file("https://cran.r-project.org/web/packages/rlang/index.html", tf)
utils::file_test("-f", tf)
`)) as RLogical;
expect(await pkg.toBoolean()).toEqual(true);
warnSpy.mockRestore();
});

test('download.file() with URL redirect', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => null);
const pkg = (await webR.evalR(`
tf <- tempfile()
utils::download.file("https://cran.r-project.org/package=rlang", tf)
utils::file_test("-f", tf)
`)) as RLogical;
expect(await pkg.toBoolean()).toEqual(true);
warnSpy.mockRestore();
});
});


describe('Test webR virtual filesystem', () => {
const testFileContents = new Uint8Array([1, 2, 4, 7, 11, 16, 22, 29, 37, 46]);
test('Upload a file to the VFS', async () => {
Expand Down
47 changes: 41 additions & 6 deletions src/webR/webr-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,9 +592,13 @@ function copyFSNode(obj: FSNode): FSNode {
return retObj;
}

function downloadFileContent(URL: string, headers: Array<string> = []): XHRResponse {
function downloadFileContent(url: string, headers: Array<string> = [], maxRedirects = 10): XHRResponse {
if (maxRedirects <= 0) {
return { status: 400, response: 'Too many redirects' };
}

const request = new XMLHttpRequest();
request.open('GET', URL, false);
request.open('GET', url, false);
request.responseType = 'arraybuffer';

try {
Expand All @@ -610,15 +614,46 @@ function downloadFileContent(URL: string, headers: Array<string> = []): XHRRespo

try {
request.send(null);
const status = IN_NODE
? (JSON.parse(String(request.status)) as { data: { statusCode: number } }).data.statusCode
: request.status;

let status: number;

if (IN_NODE) {
const parsed = JSON.parse(String(request.status)) as {
data: { statusCode: number; headers: Record<string, string> }
};
status = parsed.data.statusCode;

// Follow 3xx redirects
if (status >= 300 && status < 400) {
const location = parsed.data.headers?.location;

if (location) {
// Resolve relative URLs against the original URL
let redirectUrl: string;
try {
redirectUrl = new URL(location, url).href;
} catch (error) {
let responseText: string;
if (error instanceof TypeError) {
responseText = "Invalid redirect URL format";
} else {
responseText = "Unexpected redirect URL error";
}
console.error(responseText + ":", error);
return { status: 400, response: responseText };
}
return downloadFileContent(redirectUrl, headers, maxRedirects - 1);
}
}
} else {
status = request.status;
}

if (status >= 200 && status < 300) {
return { status: status, response: request.response as ArrayBuffer };
} else {
const responseText = new TextDecoder().decode(request.response as ArrayBuffer);
console.error(`Error fetching ${URL} - ${responseText}`);
console.error(`Error fetching ${url} - ${responseText}`);
return { status: status, response: responseText };
}
} catch {
Expand Down