Skip to content

Commit c53960f

Browse files
authored
v3 improvements & fixes (#26)
* Better error handling & switch to fetch instead of axios * switch to cross-fetch * protocol -> scheme breaking change * better type protection around short url creation * lightweight builds * Adjust cjs and browser builds * v3.0.0-rc2 * use .mjs suffix * v3.0.0-rc3 * remove esm warning * bump version * More precise typing for toFile
1 parent 1ecba65 commit c53960f

File tree

8 files changed

+160
-77
lines changed

8 files changed

+160
-77
lines changed

README.md

+2-4
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ myChart.setConfig({
2828
});
2929
```
3030

31-
If you are using ESM modules, import the ESM build directly with: `import QuickChart from "quickchart-js/build/quickchart.esm.js"`
32-
3331
Use `getUrl()` on your quickchart object to get the encoded URL that renders your chart:
3432

3533
```js
@@ -123,9 +121,9 @@ Creates a binary buffer that contains your chart image.
123121

124122
Returns a base 64 data URL beginning with `data:image/png;base64`.
125123

126-
### toFile(pathOrDescriptor: string): Promise
124+
### toFile(pathOrDescriptor: PathLike | FileHandle): Promise
127125

128-
Creates a file containing your chart image.
126+
Given a filepath string or a writable file handle, creates a file containing your chart image.
129127

130128
## More examples
131129

examples/to_file_example.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ async function saveChart() {
1616
}
1717
saveChart();
1818

19-
console.log('Written to /tmp/chart.png');
19+
console.log('Written to /tmp/chart.png');

package.json

+22-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
{
22
"name": "quickchart-js",
3-
"version": "2.0.3",
3+
"version": "3.0.0",
44
"description": "Javascript client for QuickChart.io",
55
"main": "build/quickchart.cjs.js",
6-
"module": "build/quickchart.esm.js",
6+
"module": "build/quickchart.mjs",
77
"browser": "build/quickchart.js",
88
"types": "build/typescript/index.d.ts",
9+
"exports": {
10+
".": {
11+
"require": "./build/quickchart.cjs.js",
12+
"import": "./build/quickchart.mjs",
13+
"types": "build/typescript/index.d.ts"
14+
}
15+
},
916
"repository": "https://github.com/typpo/quickchart-js",
1017
"author": "Ian Webster",
1118
"license": "MIT",
@@ -14,13 +21,13 @@
1421
"test": "jest --testPathIgnorePatterns=examples/",
1522
"format": "prettier --single-quote --trailing-comma all --print-width 100 --write \"**/*.{js,ts}\"",
1623
"build": "tsc && yarn build:browser && yarn build:esm && yarn build:cjs",
17-
"build:browser": "esbuild src/index.cjs.ts --bundle --sourcemap --external:fs --external:crypto --target=es2015 --global-name=QuickChart --tsconfig=tsconfig.json --outfile=build/quickchart.js",
18-
"build:cjs": "esbuild src/index.cjs.ts --bundle --sourcemap --platform=node --target=node10.4 --global-name=QuickChart --tsconfig=tsconfig.json --outfile=build/quickchart.cjs.js",
19-
"build:esm": "esbuild src/index.ts --bundle --sourcemap --platform=node --target=node10.4 --global-name=QuickChart --tsconfig=tsconfig.json --format=esm --outfile=build/quickchart.esm.js",
20-
"build:watch": "esbuild src/index.cjs.ts --watch --bundle --sourcemap --platform=node --target=node10.4 --global-name=QuickChart --tsconfig=tsconfig.json --outfile=build/quickchart.cjs.js"
24+
"build:browser": "esbuild src/index.ts --bundle --sourcemap --external:fs --external:crypto --target=es2015 --global-name=QuickChart --tsconfig=tsconfig.json --footer:js=\"QuickChart = QuickChart.default\" --outfile=build/quickchart.js",
25+
"build:cjs": "esbuild src/index.ts --format=cjs --sourcemap --tree-shaking=true --platform=node --target=node10.4 --global-name=QuickChart --tsconfig=tsconfig.json --footer:js=\"module.exports = module.exports.default;\" --outfile=build/quickchart.cjs.js",
26+
"build:esm": "esbuild src/index.ts --sourcemap --tree-shaking=true --platform=node --target=node10.4 --global-name=QuickChart --tsconfig=tsconfig.json --format=esm --outfile=build/quickchart.mjs",
27+
"build:watch": "esbuild src/index.ts --watch --sourcemap --tree-shaking=true --platform=node --target=node10.4 --global-name=QuickChart --tsconfig=tsconfig.json --footer:js=\"module.exports = module.exports.default;\" --outfile=build/quickchart.cjs.js"
2128
},
2229
"dependencies": {
23-
"axios": "^0.24.0",
30+
"cross-fetch": "^3.1.5",
2431
"javascript-stringify": "^2.1.0"
2532
},
2633
"devDependencies": {
@@ -30,7 +37,15 @@
3037
"@types/jest": "^27.0.3",
3138
"esbuild": "^0.14.1",
3239
"jest": "^27.4.3",
40+
"jest-fetch-mock": "^3.0.3",
3341
"prettier": "^2.3.2",
3442
"typescript": "^4.5.2"
43+
},
44+
"jest": {
45+
"automock": false,
46+
"resetMocks": false,
47+
"setupFiles": [
48+
"./test/setupJest.js"
49+
]
3550
}
3651
}

src/index.cjs.ts

-3
This file was deleted.

src/index.ts

+34-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import axios from 'axios';
1+
import fetch from 'cross-fetch';
22
import { stringify } from 'javascript-stringify';
33

4+
import type { PathLike } from 'fs';
5+
import type { FileHandle } from 'fs/promises';
46
import type { ChartConfiguration } from 'chart.js';
7+
import type { Response } from 'cross-fetch';
58

69
const SPECIAL_FUNCTION_REGEX: RegExp = /['"]__BEGINFUNCTION__(.*?)__ENDFUNCTION__['"]/g;
710

@@ -34,9 +37,17 @@ function doStringify(chartConfig: ChartConfiguration): string | undefined {
3437
return str.replace(SPECIAL_FUNCTION_REGEX, '$1');
3538
}
3639

40+
function postJson(url: string, payload: PostData): Promise<Response> {
41+
return fetch(url, {
42+
method: 'POST',
43+
headers: { 'Content-Type': 'application/json' },
44+
body: JSON.stringify(payload),
45+
});
46+
}
47+
3748
class QuickChart {
3849
private host: string;
39-
private protocol: string;
50+
private scheme: string;
4051
private baseUrl: string;
4152
private width: number;
4253
private height: number;
@@ -54,8 +65,8 @@ class QuickChart {
5465
this.accountId = accountId;
5566

5667
this.host = 'quickchart.io';
57-
this.protocol = 'https';
58-
this.baseUrl = `${this.protocol}://${this.host}`;
68+
this.scheme = 'https';
69+
this.baseUrl = `${this.scheme}://${this.host}`;
5970

6071
this.chart = undefined;
6172
this.width = 500;
@@ -193,13 +204,18 @@ class QuickChart {
193204
throw new Error('Short URLs must use quickchart.io host');
194205
}
195206

196-
const resp = await axios.post(`${this.baseUrl}/chart/create`, this.getPostData());
197-
if (resp.status !== 200) {
198-
throw `Bad response code ${resp.status} from chart shorturl endpoint`;
199-
} else if (!resp.data.success) {
200-
throw 'Received failure response from chart shorturl endpoint';
207+
const resp = await postJson(`${this.baseUrl}/chart/create`, this.getPostData());
208+
if (!resp.ok) {
209+
const quickchartError = resp.headers.get('x-quickchart-error');
210+
const details = quickchartError ? `\n${quickchartError}` : '';
211+
throw new Error(`Chart shorturl creation failed with status code ${resp.status}${details}`);
212+
}
213+
214+
const json = (await resp.json()) as undefined | { success?: boolean; url?: string };
215+
if (!json || !json.success || !json.url) {
216+
throw new Error('Received failure response from chart shorturl endpoint');
201217
} else {
202-
return resp.data.url;
218+
return json.url;
203219
}
204220
}
205221

@@ -208,13 +224,14 @@ class QuickChart {
208224
throw new Error('You must call setConfig before getUrl');
209225
}
210226

211-
const resp = await axios.post(`${this.baseUrl}/chart`, this.getPostData(), {
212-
responseType: 'arraybuffer',
213-
});
214-
if (resp.status !== 200) {
215-
throw `Bad response code ${resp.status} from chart shorturl endpoint`;
227+
const resp = await postJson(`${this.baseUrl}/chart`, this.getPostData());
228+
if (!resp.ok) {
229+
const quickchartError = resp.headers.get('x-quickchart-error');
230+
const details = quickchartError ? `\n${quickchartError}` : '';
231+
throw new Error(`Chart creation failed with status code ${resp.status}${details}`);
216232
}
217-
return Buffer.from(resp.data, 'binary');
233+
const data = await resp.arrayBuffer();
234+
return Buffer.from(data);
218235
}
219236

220237
async toDataUrl(): Promise<string> {
@@ -224,7 +241,7 @@ class QuickChart {
224241
return `data:image/${type};base64,${b64buf}`;
225242
}
226243

227-
async toFile(pathOrDescriptor: string): Promise<void> {
244+
async toFile(pathOrDescriptor: PathLike | FileHandle): Promise<void> {
228245
const fs = require('fs');
229246
const buf = await this.toBinary();
230247
fs.writeFileSync(pathOrDescriptor, buf);

test/index.test.ts

+53-38
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import axios from 'axios';
1+
import fetchMock from 'jest-fetch-mock';
22

33
import QuickChart from '../src/index';
44

5-
jest.mock('axios');
6-
75
test('basic chart, no auth', () => {
86
const qc = new QuickChart();
97
qc.setConfig({
@@ -249,82 +247,100 @@ test('postdata for js chart', () => {
249247

250248
test('getShortUrl for chart, no auth', async () => {
251249
const mockResp = {
252-
status: 200,
253-
data: {
254-
success: true,
255-
url: 'https://quickchart.io/chart/render/9a560ba4-ab71-4d1e-89ea-ce4741e9d232',
256-
},
250+
success: true,
251+
url: 'https://quickchart.io/chart/render/9a560ba4-ab71-4d1e-89ea-ce4741e9d232',
257252
};
258-
(axios.post as jest.Mock).mockImplementationOnce(() => Promise.resolve(mockResp));
253+
fetchMock.mockResponseOnce(JSON.stringify(mockResp));
254+
255+
const qc = new QuickChart();
256+
qc.setConfig({
257+
type: 'bar',
258+
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
259+
});
260+
261+
await expect(qc.getShortUrl()).resolves.toEqual(mockResp.url);
262+
});
263+
264+
test('getShortUrl for chart js error', async () => {
265+
fetchMock.mockResponseOnce(() => {
266+
throw new Error('Request timed out');
267+
});
259268

260269
const qc = new QuickChart();
261270
qc.setConfig({
262271
type: 'bar',
263272
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
264273
});
265274

266-
await expect(qc.getShortUrl()).resolves.toEqual(mockResp.data.url);
267-
expect(axios.post).toHaveBeenCalled();
275+
await expect(qc.getShortUrl()).rejects.toThrow('Request timed out');
268276
});
269277

270278
test('getShortUrl for chart bad status code', async () => {
271-
const mockResp = {
279+
fetchMock.mockResponseOnce('', {
272280
status: 502,
273-
};
274-
(axios.post as jest.Mock).mockImplementationOnce(() => Promise.resolve(mockResp));
281+
});
282+
283+
const qc = new QuickChart();
284+
qc.setConfig({
285+
type: 'bar',
286+
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
287+
});
288+
289+
await expect(qc.getShortUrl()).rejects.toThrow('failed with status code');
290+
});
291+
292+
test('getShortUrl for chart bad status code with error detail', async () => {
293+
fetchMock.mockResponseOnce('', {
294+
status: 400,
295+
headers: {
296+
'x-quickchart-error': 'foo bar',
297+
},
298+
});
275299

276300
const qc = new QuickChart();
277301
qc.setConfig({
278302
type: 'bar',
279303
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
280304
});
281305

282-
await expect(qc.getShortUrl()).rejects.toContain('Bad response code');
283-
expect(axios.post).toHaveBeenCalled();
306+
await expect(qc.getShortUrl()).rejects.toThrow('foo bar');
284307
});
285308

286309
test('getShortUrl api failure', async () => {
287-
const mockResp = {
288-
status: 200,
289-
data: {
310+
fetchMock.mockResponseOnce(
311+
JSON.stringify({
290312
success: false,
291-
},
292-
};
293-
(axios.post as jest.Mock).mockImplementationOnce(() => Promise.resolve(mockResp));
313+
}),
314+
);
294315

295316
const qc = new QuickChart();
296317
qc.setConfig({
297318
type: 'bar',
298319
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
299320
});
300321

301-
await expect(qc.getShortUrl()).rejects.toContain('failure response');
302-
expect(axios.post).toHaveBeenCalled();
322+
await expect(qc.getShortUrl()).rejects.toThrow('failure response');
323+
expect(fetch).toHaveBeenCalled();
303324
});
304325

305326
test('toBinary, no auth', async () => {
306-
const mockResp = {
307-
status: 200,
308-
data: Buffer.from('bWVvdw==', 'base64'),
309-
};
310-
(axios.post as jest.Mock).mockImplementationOnce(() => Promise.resolve(mockResp));
327+
const mockData = Buffer.from('bWVvdw==', 'base64');
328+
// https://github.com/jefflau/jest-fetch-mock/issues/218
329+
fetchMock.mockResponseOnce(() => Promise.resolve({ body: mockData as unknown as string }));
311330

312331
const qc = new QuickChart();
313332
qc.setConfig({
314333
type: 'bar',
315334
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
316335
});
317336

318-
await expect(qc.toBinary()).resolves.toEqual(mockResp.data);
319-
expect(axios.post).toHaveBeenCalled();
337+
await expect(qc.toBinary()).resolves.toEqual(mockData);
320338
});
321339

322-
test('toBinary, no auth', async () => {
323-
const mockResp = {
324-
status: 200,
325-
data: Buffer.from('bWVvdw==', 'base64'),
326-
};
327-
(axios.post as jest.Mock).mockImplementationOnce(() => Promise.resolve(mockResp));
340+
test('toDataUrl, no auth', async () => {
341+
fetchMock.mockResponseOnce(() =>
342+
Promise.resolve({ body: Buffer.from('bWVvdw==', 'base64') as unknown as string }),
343+
);
328344

329345
const qc = new QuickChart();
330346
qc.setConfig({
@@ -333,7 +349,6 @@ test('toBinary, no auth', async () => {
333349
});
334350

335351
await expect(qc.toDataUrl()).resolves.toEqual('data:image/png;base64,bWVvdw==');
336-
expect(axios.post).toHaveBeenCalled();
337352
});
338353

339354
test('no chart specified throws error', async () => {

test/setupJest.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const fetchMock = require('jest-fetch-mock');
2+
fetchMock.enableMocks();
3+
jest.setMock('cross-fetch', fetchMock);

0 commit comments

Comments
 (0)