Skip to content
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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ https://github.highcharts.com/master/highcharts-gantt.src.js

### esbuild Mode

For **Highcharts v13+ branches**, esbuild is used automatically. Version 13 introduced dynamic `import()`, ES2020 module syntax, and a restructured module system that the legacy Highcharts Assembler cannot handle, so the server detects these branches and routes them through esbuild without any configuration.

For **older branches**, append `?esbuild` to any request URL to opt in manually:

```
http://localhost:8080/master/highcharts.src.js?esbuild
```

To confirm that esbuild was used, check the response header:

```
X-Built-With: esbuild
```

Add `?esbuild` to any request to use esbuild compilation instead of the standard TypeScript + assembler pipeline:

```
Expand Down
2 changes: 1 addition & 1 deletion app/dashboards.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ async function dashboardsHandler (req, res, next) {
return handleQueueError(error)
})

res.status(201)
res.status(200)

return serveIfExists(res, obj.assembled)
},
Expand Down
60 changes: 57 additions & 3 deletions app/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
'use strict'

// Import dependencies, sorted by path name.
const { secureToken, repo } = require('../config.json')
const { secureToken, repo, token } = require('../config.json')
const { downloadFile, downloadSourceFolder, urlExists, getBranchInfo, getCommitInfo, isRateLimited } = require('./download.js')
const { log } = require('./utilities')

Expand Down Expand Up @@ -41,6 +41,51 @@ const { existsSync } = require('node:fs')
const { shouldUseWebpack, compileWebpack } = require('./utils.js')
const { compileWithEsbuild } = require('./esbuild.js')

const esbuildCache = new Map()
const ESBUILD_DETECTION_FILE = 'ts/masters/highcharts-autoload.src.ts'
Comment on lines +44 to +45
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

esbuildCache is an unbounded Map and will grow indefinitely with unique refs/SHAs, which can become a long-lived memory leak in a server process. Consider switching to a bounded LRU/TTL cache (e.g., cap entries and/or expire after N minutes), or only caching for known branch names (not every commit SHA).

Copilot uses AI. Check for mistakes.

/**
* Check if a branch requires esbuild by looking for the autoload master file.
* @param {string} ref - Branch name or commit SHA
* @returns {Promise<boolean>} true if the branch needs esbuild
*/
async function branchNeedsEsbuild (ref) {
if (esbuildCache.has(ref)) {
return esbuildCache.get(ref)
}

if (/\.\./.test(ref)) {
esbuildCache.set(ref, false)
return false
}

try {
const url = `https://raw.githubusercontent.com/${repo}/${ref}/${ESBUILD_DETECTION_FILE}`
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)

try {
const res = await fetch(url, {
method: 'HEAD',
signal: controller.signal,
headers: token ? { Authorization: `token ${token}` } : {}
})
if (!res.ok && res.status !== 404) {
console.warn(`branchNeedsEsbuild: GitHub returned ${res.status} for ${ref}`)
}
const result = res.ok
esbuildCache.set(ref, result)
return result
} finally {
clearTimeout(timeout)
}
} catch (e) {
console.warn(`branchNeedsEsbuild: failed to check ${ref}:`, e.message)
esbuildCache.set(ref, false)
return false
}
}

// Constants
const PATH_TMP_DIRECTORY = join(__dirname, '../tmp')
const URL_DOWNLOAD = `https://raw.githubusercontent.com/${repo}/`
Expand Down Expand Up @@ -146,18 +191,20 @@ async function handlerDefault (req, res) {
}

// Check for esbuild mode
const useEsbuild = /[?&]esbuild(?:[&=]|$)/.test(req.url)
let useEsbuild = /[?&]esbuild(?:[&=]|$)/.test(req.url)

let branch = await getBranch(req.path)
let url = req.url
let useGitDownloader = branch === 'master' || /^\/v[0-9]/.test(req.path) // version tags
const originalBranch = branch

// If we can get it, save by commit sha
// This also means we can use the degit downloader
// (only works on latest commit in a branch)
// Only `v8.0.0` gets saved by their proper names
const { commit } = await getBranchInfo(branch)
if (commit) {
url = url.replace(`/${originalBranch}/`, `/${commit.sha}/`)
branch = commit.sha
useGitDownloader = true
}
Expand All @@ -168,11 +215,18 @@ async function handlerDefault (req, res) {
if (!useGitDownloader) {
const { sha } = await getCommitInfo(branch)
if (sha) {
url = url.replace(branch, sha)
url = url.replace(`/${branch}/`, `/${sha}/`)
branch = sha
}
}

if (!useEsbuild) {
const needsEsbuild = await branchNeedsEsbuild(branch)
if (needsEsbuild) {
useEsbuild = true
}
}

// Handle esbuild mode
if (useEsbuild) {
const result = await serveEsbuildFile(branch, url, useGitDownloader)
Expand Down
Loading
Loading