Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add API to get available/current version from JavaScript #9875

Open
davidism opened this issue Jan 9, 2023 · 6 comments
Open

add API to get available/current version from JavaScript #9875

davidism opened this issue Jan 9, 2023 · 6 comments
Labels
Needed: more information A reply from issue author is required

Comments

@davidism
Copy link

davidism commented Jan 9, 2023

In Flask's docs, I show a banner that floats at the top of the window with a warning if it is an old version or the development version, with a link to the page for the most recent version. However, it is currently rendered while building the docs, which means that all versions need to be rebuilt when a new version is released.

I would like to be able to implement this version banner in JavaScript, so that it always points to the latest version without requiring rebuilds. To do this, I need to be able to fetch the list of available versions and current version, just like I can with the info injected into the context during build.

@ericholscher
Copy link
Member

Hey David,

We actually have an extension that does most of this: https://github.com/humitos/sphinx-version-warning -- it hasn't been heavily used, but it uses our existing versions API to get the list of versions for the project, and sort by semver the highest. Could be a reasonable approach to try that, or something similar. I know in the past we had a couple issues around CORS with this, which we're working to resolve, but do let us know if you hit a similar issue.

@davidism
Copy link
Author

davidism commented Jan 13, 2023

Cool! I could have sworn there wasn't a version API last time I checked, but then again that might have been a few years ago now 😅 Opened this because we had discussed it, without actually checking the API again, sorry.

We do have a custom domain, so I'll need to verify that CORS works.

@humitos
Copy link
Member

humitos commented Jul 24, 2023

Ideally, this will be supported with the new addons we are building at https://github.com/readthedocs/addons

@davidism
Copy link
Author

davidism commented Jul 24, 2023

For what it's worth, I ended up writing some JavaScript that only relies on the version configured in Sphinx and the versions fetched from the PyPI API. This assumes normalized PEP 440 version markers (a more complicated regex could be used if needed), and discards non-final versions (a, b, rc, post, dev). If the docs have the version "2.2" or "2.2.x", everything under 2.2 like 2.2.1 will count towards 2.2, only 2.3 would be considered newer to show the banner. If the docs version is higher than any released version on PyPI, then it is considered the dev version.

Implementation
const versionRe = new RegExp([
  "^",
  "(?:(?<epoch>[1-9][0-9]*)!)?)",
  "(?<version>(?:0|[1-9][0-9]*)(?:\\.(?:0|[1-9][0-9]*))*)",
  "(?:(?<preL>a|b|rc)(?<preN>0|[1-9][0-9]*))?",
  "(?:\\.post(?<postN>0|[1-9][0-9]*))?",
  "(?:\\.dev(?<devN>0|[1-9][0-9]*))?",
  "$",
].join(""))

function parseVersion(value) {
  const {groups: {epoch, version, preL, preN, postN, devN}} = versionRe.exec(value)

  const parts = []
  let keep = false
  version.split(".").map(Number.parseInt).reverse().forEach(p => {
    if (keep) {
      parts.push(p)
    } else if (p > 0) {
      parts.push(p)
      keep = true
    }
  })
  parts.push(Number.parseInt(epoch) || 0)

  return {
    version: parts.reverse(),
    isPre: Boolean(preL),
    preL: preL || "",
    preN: Number.parseInt(preN) || 0,
    isPost: Boolean(postN),
    postN: Number.parseInt(postN) || 0,
    isDev: Boolean(devN),
    devN: Number.parseInt(devN) || 0,
  }
}

function compareVersions(a, b) {
  for (const [i, an] of a.entries()) {
    const bn = i < b.length ? b[i] : 0

    if (an < bn) {
      return -1
    } else if (an > bn) {
      return 1
    }
  }

  if (a.length < b.length) {
    return -1
  }

  return 0
}

async function getReleasedVersions(name) {
  const response = await fetch(
    `https://pypi.org/simple/${name}/`,
    {"headers": {"Accept": "application/vnd.pypi.simple.v1+json"}}
  )
  const data = await response.json()
  return data["versions"]
    .map(parseVersion)
    .filter(v => !(v.isPre || v.isDev))
    .map(v => v.version)
    .sort(compareVersions)
    .reverse()
}

async function describeVersion(name, value) {
  if (value.endsWith(".x")) {
    value = value.slice(0, -2)
  }

  const currentVersion = parseVersion(value).version
  const releasedVersions = await getReleasedVersions(name)

  if (releasedVersions.length === 0) {
    return "dev"
  }

  const latestVersion = releasedVersions[0]

  while (latestVersion.length < currentVersion.length) {
    latestVersion.push(0)
  }

  const compared = compareVersions(currentVersion, latestVersion)

  if (compared === 1) {
    return ["dev", latestVersion]
  }

  if (currentVersion.every((n, i) => n === latestVersion[i])) {
    return ["latest", latestVersion]
  }

  return ["old", latestVersion]
}

An example:

async function addVersionBanner() {
  const [desc, latestVersion] = await describeVersion("flask", "2.2.x")
  
  if (desc === "old") {
    // show "This is an old version" banner.
  } else if (desc === "dev") {
    // show "This is the dev version" banner.
  }
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", addVersionBanner)
} else {
  addVersionBanner()
}

@humitos
Copy link
Member

humitos commented Feb 21, 2024

I did an initial implementation to expose all the Read the Docs data used to generate the flyout as a JavaScript object in readthedocs/addons#64. I'd like to receive feedback about it since I should be useful to solve this problem 👍🏼

@humitos
Copy link
Member

humitos commented Feb 21, 2024

This assumes normalized PEP 440 version markers (a more complicated regex could be used if needed), and discards non-final versions (a, b, rc, post, dev). If the docs have the version "2.2" or "2.2.x", everything under 2.2 like 2.2.1 will count towards 2.2, only 2.3 would be considered newer to show the banner

With the implementation of #11069 this will be configurable per project and even with a customized pattern. Let me know if that will help here as well.

@humitos humitos added the Needed: more information A reply from issue author is required label Feb 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needed: more information A reply from issue author is required
Projects
None yet
Development

No branches or pull requests

3 participants