Skip to content

Commit fb7799e

Browse files
authored
Merge pull request #672 from highcharts/enhancement/highcharts-npm-scripts
enhancement/highcharts-npm-scripts
2 parents 15421d3 + 02d93b3 commit fb7799e

15 files changed

Lines changed: 182 additions & 74 deletions

.env.sample

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ PUPPETEER_TEMP_DIR = ./tmp/
44
# HIGHCHARTS CONFIG
55
HIGHCHARTS_VERSION = latest
66
HIGHCHARTS_CDN_URL = https://code.highcharts.com/
7+
HIGHCHARTS_USE_NPM = false
78
HIGHCHARTS_CORE_SCRIPTS =
89
HIGHCHARTS_MODULE_SCRIPTS =
910
HIGHCHARTS_INDICATOR_SCRIPTS =
File renamed without changes.

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# 5.1.0
2+
3+
_New Features:_
4+
5+
- Added the `useNpm` option to load Highcharts scripts from the NPM package instead of the CDN.
6+
17
# 5.0.0
28

39
_Breaking Changes:_

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ The format, along with its default values, is as follows (using the recommended
114114
"highcharts": {
115115
"version": "latest",
116116
"cdnURL": "https://code.highcharts.com/",
117+
"useNpm": false,
117118
"coreScripts": [
118119
"highcharts",
119120
"highcharts-more",
@@ -307,6 +308,7 @@ These variables are set in your environment and take precedence over options fro
307308

308309
- `HIGHCHARTS_VERSION`: Highcharts version to use (defaults to `latest`).
309310
- `HIGHCHARTS_CDN_URL`: Highcharts CDN URL of scripts to be used (defaults to `https://code.highcharts.com/`).
311+
- `HIGHCHARTS_USE_NPM`: The flag that determines whether to use Highcharts scripts from CDN or NPM package (defaults to `false`).
310312
- `HIGHCHARTS_CORE_SCRIPTS`: Highcharts core scripts to fetch (defaults to ``).
311313
- `HIGHCHARTS_MODULE_SCRIPTS`: Highcharts module scripts to fetch (defaults to ``).
312314
- `HIGHCHARTS_INDICATOR_SCRIPTS`: Highcharts indicator scripts to fetch (defaults to ``).
@@ -412,6 +414,7 @@ To supply command line arguments, add them as flags when running the application
412414

413415
_Available options:_
414416

417+
- `--useNpm`: The flag that determines whether to use Highcharts scripts from CDN or NPM package (defaults to `false`).
415418
- `--infile`: The input file should include a name and a type (**.json** or **.svg**) and must be a correctly formatted JSON or SVG file (defaults to `false`).
416419
- `--instr`: An input in a form of a stringified JSON or SVG file. Overrides the `--infile` option (defaults to `false`).
417420
- `--options`: An alias for the `--instr` option (defaults to `false`).
@@ -562,6 +565,10 @@ curl -H 'hc-auth: 12345' -X POST 127.0.0.1:7801/change_hc_version/10.3.3
562565

563566
This is useful to e.g. upgrade to the latest HC version without downtime.
564567

568+
IMPORTANT NOTE:
569+
570+
This is not possible when using the Highcharts dependency package directly (by setting the `useNpm` to **true**).
571+
565572
# Node.js Module
566573

567574
Finally, the Export Server can also be used as a Node.js module to simplify integrations:
@@ -709,6 +716,10 @@ Samples and tests for every mentioned export method can be found in the `./sampl
709716

710717
# Tips, Tricks & Notes
711718

719+
## Note About Highcharts Version
720+
721+
When `useNpm` is set to **true**, Highcharts uses the version specified in `package.json`. The `version` option or switching the Highcharts version on the server at runtime will have no effect. To change the version of local Highcharts scripts, update `package.json` directly.
722+
712723
## Note about Deprecated Options
713724

714725
At some point during the transition process from the `PhantomJS` solution, certain options were deprecated. Here is a list of options that no longer work with the server based on `Puppeteer`:

dist/index.cjs

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

dist/index.esm.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.esm.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/cache.js

Lines changed: 115 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@ See LICENSE file in root for details.
1717
// before starting the service
1818

1919
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
20-
import { join } from 'path';
20+
import { join, resolve, sep } from 'path';
2121

2222
import { HttpsProxyAgent } from 'https-proxy-agent';
2323

2424
import { getOptions } from './config.js';
2525
import { envs } from './envs.js';
2626
import { fetch } from './fetch.js';
2727
import { log } from './logger.js';
28-
import { __dirname } from './utils.js';
28+
import { __dirname, __highchartsDir } from './utils.js';
2929

3030
import ExportError from './errors/ExportError.js';
3131

@@ -52,12 +52,14 @@ export const extractVersion = (cache) => {
5252

5353
/**
5454
* Extracts the Highcharts module name based on the scriptPath.
55+
*
56+
* @param {string} scriptPath - The path to the module.
57+
*
58+
* @returns {string} The name of a module.
5559
*/
5660
export const extractModuleName = (scriptPath) => {
57-
return scriptPath.replace(
58-
/(.*)\/|(.*)modules\/|stock\/(.*)indicators\/|maps\/(.*)modules\//gi,
59-
''
60-
);
61+
// Normalize slashes, get part after the last '/' and remove .js extension
62+
return scriptPath.replace(/\\/g, '/').split('/').pop().replace(/\.js$/i, '');
6163
};
6264

6365
/**
@@ -102,8 +104,10 @@ export const saveConfigToManifest = async (config, fetchedModules) => {
102104
* to use for a request.
103105
* @param {Object} fetchedModules - An object which tracks which Highcharts
104106
* modules have been fetched.
105-
* @param {boolean} shouldThrowError - A flag to indicate if the error should be
106-
* thrown. This should be used only for the core scripts.
107+
* @param {boolean} [useNpm=false] - A flag to indicate if the script should be
108+
* get from NPM package or fetched from CDN. The default value is `false`.
109+
* @param {boolean} [shouldThrowError=false] - A flag to indicate if the error
110+
* should be thrown. This should be used only for the core scripts.
107111
*
108112
* @returns {Promise<string>} A Promise resolving to the text representation
109113
* of the fetched script.
@@ -115,36 +119,71 @@ export const fetchAndProcessScript = async (
115119
script,
116120
requestOptions,
117121
fetchedModules,
122+
useNpm = false,
118123
shouldThrowError = false
119124
) => {
120-
// Get rid of the .js from the custom strings
121-
if (script.endsWith('.js')) {
122-
script = script.substring(0, script.length - 3);
125+
let response;
126+
127+
// Add the missing .js to the strings
128+
if (!script.endsWith('.js')) {
129+
script = `${script}.js`;
123130
}
124131

125-
log(4, `[cache] Fetching script - ${script}.js`);
132+
// Whether to use NPM package scripts or fetch it from CDN
133+
if (useNpm) {
134+
try {
135+
// Log fetched script
136+
log(
137+
4,
138+
`[cache] Fetching script from NPM - ${join('node_modules', 'highcharts', script)}`
139+
);
140+
141+
// Sanitize and validate path
142+
const resolvedScriptPath = resolve(__highchartsDir, script);
143+
if (!resolvedScriptPath.startsWith(resolve(__highchartsDir) + sep)) {
144+
throw new ExportError(
145+
`[cache] Invalid script path detected for '${script}'. Directory traversal attempt or bad version input.`,
146+
403
147+
);
148+
}
126149

127-
// Fetch the script
128-
const response = await fetch(`${script}.js`, requestOptions);
150+
// Fetch the script from NPM
151+
response = readFileSync(resolvedScriptPath, 'utf8');
129152

130-
// If OK, return its text representation
131-
if (response.statusCode === 200 && typeof response.text == 'string') {
132-
if (fetchedModules) {
133-
const moduleName = extractModuleName(script);
134-
fetchedModules[moduleName] = 1;
153+
// If OK, return its text representation
154+
if (fetchedModules && response) {
155+
fetchedModules[extractModuleName(script)] = 1;
156+
}
157+
return response;
158+
} catch {
159+
// Proceed
135160
}
161+
} else {
162+
// Log fetched script
163+
log(4, `[cache] Fetching script from CDN - ${script}`);
136164

137-
return response.text;
165+
// Fetch the script from CDN
166+
response = await fetch(script, requestOptions);
167+
168+
// If OK, return its text representation
169+
if (response.statusCode === 200 && typeof response.text == 'string') {
170+
if (fetchedModules) {
171+
fetchedModules[extractModuleName(script)] = 1;
172+
}
173+
return response.text;
174+
}
138175
}
139176

177+
// Based on the `shouldThrowError` flag, decide how to serve error message
140178
if (shouldThrowError) {
141179
throw new ExportError(
142-
`Could not fetch the ${script}.js. The script might not exist in the requested version (status code: ${response.statusCode}).`
143-
).setError(response);
180+
`[cache] Could not fetch the mandatory ${script}. The script might not exist in the requested version.`,
181+
404
182+
);
144183
} else {
145184
log(
146185
2,
147-
`[cache] Could not fetch the ${script}.js. The script might not exist in the requested version.`
186+
`[cache] Could not fetch the ${script}. The script might not exist in the requested version.`
148187
);
149188
}
150189

@@ -154,10 +193,7 @@ export const fetchAndProcessScript = async (
154193
/**
155194
* Fetches Highcharts scripts and customScripts from the given CDNs.
156195
*
157-
* @param {string} coreScripts - Array of Highcharts core scripts to fetch.
158-
* @param {string} moduleScripts - Array of Highcharts modules to fetch.
159-
* @param {string} customScripts - Array of custom script paths to fetch
160-
* (full URLs).
196+
* @param {Object} highchartsOptions - Object containing all highcharts options.
161197
* @param {object} proxyOptions - Options for the proxy agent to use for
162198
* a request.
163199
* @param {object} fetchedModules - An object which tracks which Highcharts
@@ -166,12 +202,22 @@ export const fetchAndProcessScript = async (
166202
* @returns {Promise<string>} The fetched scripts content joined.
167203
*/
168204
export const fetchScripts = async (
169-
coreScripts,
170-
moduleScripts,
171-
customScripts,
205+
highchartsOptions,
172206
proxyOptions,
173207
fetchedModules
174208
) => {
209+
const version = highchartsOptions.version;
210+
const hcVersion = version === 'latest' || !version ? '' : `${version}/`;
211+
const cdnURL = highchartsOptions.cdnURL || cache.cdnURL;
212+
213+
log(
214+
3,
215+
`[cache] Updating cache version to Highcharts: ${hcVersion || 'latest'}.`
216+
);
217+
218+
// Whether to use NPM or CDN
219+
const useNpm = highchartsOptions.useNpm;
220+
175221
// Configure proxy if exists
176222
let proxyAgent;
177223
const { host, port, username, password } = proxyOptions;
@@ -199,26 +245,49 @@ export const fetchScripts = async (
199245
}
200246
: {};
201247

202-
const allFetchPromises = [
203-
...coreScripts.map((script) =>
204-
fetchAndProcessScript(`${script}`, requestOptions, fetchedModules, true)
248+
const fetchedScripts = await Promise.all([
249+
...highchartsOptions.coreScripts.map((c) =>
250+
fetchAndProcessScript(
251+
(useNpm && c) || `${cdnURL}${hcVersion}${c}`,
252+
requestOptions,
253+
fetchedModules,
254+
useNpm,
255+
true
256+
)
205257
),
206-
...moduleScripts.map((script) =>
207-
fetchAndProcessScript(`${script}`, requestOptions, fetchedModules)
258+
...highchartsOptions.moduleScripts.map((m) =>
259+
fetchAndProcessScript(
260+
(useNpm && join('modules', m)) ||
261+
(m === 'map'
262+
? `${cdnURL}maps/${hcVersion}modules/${m}`
263+
: `${cdnURL}${hcVersion}modules/${m}`),
264+
requestOptions,
265+
fetchedModules,
266+
useNpm
267+
)
268+
),
269+
...highchartsOptions.indicatorScripts.map((i) =>
270+
fetchAndProcessScript(
271+
(useNpm && join('indicators', i)) ||
272+
`${cdnURL}stock/${hcVersion}indicators/${i}`,
273+
requestOptions,
274+
fetchedModules,
275+
useNpm
276+
)
208277
),
209-
...customScripts.map((script) =>
210-
fetchAndProcessScript(`${script}`, requestOptions)
278+
...highchartsOptions.customScripts.map((c) =>
279+
fetchAndProcessScript(`${c}`, requestOptions)
211280
)
212-
];
281+
]);
213282

214-
const fetchedScripts = await Promise.all(allFetchPromises);
215283
return fetchedScripts.join(';\n');
216284
};
217285

218286
/**
219287
* Updates the local cache with Highcharts scripts and their versions.
220288
*
221-
* @param {Object} options - Object containing all options.
289+
* @param {Object} highchartsOptions - Object containing all options from
290+
* the highcharts section.
222291
* @param {string} sourcePath - The path to the source file in the cache.
223292
*
224293
* @returns {Promise<object>} A Promise resolving to an object representing
@@ -232,40 +301,22 @@ export const updateCache = async (
232301
proxyOptions,
233302
sourcePath
234303
) => {
235-
const version = highchartsOptions.version;
236-
const hcVersion = version === 'latest' || !version ? '' : `${version}/`;
237-
const cdnURL = highchartsOptions.cdnURL || cache.cdnURL;
238-
239-
log(
240-
3,
241-
`[cache] Updating cache version to Highcharts: ${hcVersion || 'latest'}.`
242-
);
243-
244-
const fetchedModules = {};
245304
try {
305+
const fetchedModules = {};
306+
307+
// Get sources
246308
cache.sources = await fetchScripts(
247-
[
248-
...highchartsOptions.coreScripts.map((c) => `${cdnURL}${hcVersion}${c}`)
249-
],
250-
[
251-
...highchartsOptions.moduleScripts.map((m) =>
252-
m === 'map'
253-
? `${cdnURL}maps/${hcVersion}modules/${m}`
254-
: `${cdnURL}${hcVersion}modules/${m}`
255-
),
256-
...highchartsOptions.indicatorScripts.map(
257-
(i) => `${cdnURL}stock/${hcVersion}indicators/${i}`
258-
)
259-
],
260-
highchartsOptions.customScripts,
309+
highchartsOptions,
261310
proxyOptions,
262311
fetchedModules
263312
);
264313

314+
// Get sources version
265315
cache.hcVersion = extractVersion(cache);
266316

267317
// Save the fetched modules into caches' source JSON
268318
writeFileSync(sourcePath, cache.sources);
319+
269320
return fetchedModules;
270321
} catch (error) {
271322
throw new ExportError(

lib/envs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export const Config = z.object({
141141
})
142142
)
143143
.transform((value) => (value !== '' ? value : undefined)),
144+
HIGHCHARTS_USE_NPM: v.boolean(),
144145
HIGHCHARTS_CORE_SCRIPTS: v.array(scriptsNames.core),
145146
HIGHCHARTS_MODULE_SCRIPTS: v.array(scriptsNames.modules),
146147
HIGHCHARTS_INDICATOR_SCRIPTS: v.array(scriptsNames.indicators),

lib/schemas/config.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const scriptsNames = {
4545
'geoheatmap',
4646
'pyramid3d',
4747
'networkgraph',
48-
'overlapping-datalabels',
48+
// 'overlapping-datalabels',
4949
'pareto',
5050
'pattern-fill',
5151
'pictorial',
@@ -168,6 +168,12 @@ export const defaultConfig = {
168168
envLink: 'HIGHCHARTS_CDN_URL',
169169
description: 'The CDN URL for Highcharts scripts to be used.'
170170
},
171+
useNpm: {
172+
value: false,
173+
type: 'boolean',
174+
envLink: 'HIGHCHARTS_USE_NPM',
175+
description: 'Flag to use Highcharts scripts from NPM package'
176+
},
171177
coreScripts: {
172178
value: scriptsNames.core,
173179
type: 'string[]',
@@ -753,6 +759,12 @@ export const promptsConfig = {
753759
message: 'The URL of CDN',
754760
initial: defaultConfig.highcharts.cdnURL.value
755761
},
762+
{
763+
type: 'toggle',
764+
name: 'useNpm',
765+
message: 'Flag to use Highcharts scripts from NPM package',
766+
initial: defaultConfig.highcharts.useNpm.value
767+
},
756768
{
757769
type: 'multiselect',
758770
name: 'coreScripts',

0 commit comments

Comments
 (0)