1
0
mirror of synced 2025-12-19 18:10:59 -05:00
Files
docs/script/content-migrations/use-short-versions.js
Mike Surowiec 9386571aa4 fix: use named export for all-versions (#20478)
* fix: use named export for all-versions

* run prettier
2021-07-22 19:29:00 +00:00

268 lines
9.8 KiB
JavaScript
Executable File

#!/usr/bin/env node
import fs from 'fs'
import walk from 'walk-sync'
import path from 'path'
import { escapeRegExp } from 'lodash-es'
import { Tokenizer } from 'liquidjs'
import frontmatter from '../../lib/read-frontmatter.js'
import { allVersions } from '../../lib/all-versions.js'
import { deprecated, oldestSupported } from '../../lib/enterprise-server-releases.js'
const allVersionKeys = Object.values(allVersions)
const dryRun = ['-d', '--dry-run'].includes(process.argv[2])
const walkFiles = (pathToWalk, ext) => {
return walk(path.posix.join(process.cwd(), pathToWalk), {
includeBasePath: true,
directories: false,
}).filter((file) => file.endsWith(ext) && !file.endsWith('README.md'))
}
const markdownFiles = walkFiles('content', '.md').concat(walkFiles('data', '.md'))
const yamlFiles = walkFiles('data', '.yml')
const operatorsMap = {
// old: new
'==': '=',
ver_gt: '>',
ver_lt: '<',
'!=': '!=', // noop
}
// [start-readme]
//
// Run this script to convert long form Liquid conditionals (e.g., {% if currentVersion == "free-pro-team" %}) to
// the new custom tag (e.g., {% ifversion fpt %}) and also use the short names in versions frontmatter.
//
// [end-readme]
async function main() {
if (dryRun)
console.log('This is a dry run! The script will not write any files. Use for debugging.\n')
// 1. UPDATE MARKDOWN FILES (CONTENT AND REUSABLES)
console.log('Updating Liquid conditionals and versions frontmatter in Markdown files...\n')
for (const file of markdownFiles) {
// A. UPDATE LIQUID CONDITIONALS IN CONTENT
// Create an { old: new } conditionals object so we can get the replacements and
// make the replacements separately and not do both in nested loops.
const content = fs.readFileSync(file, 'utf8')
const contentReplacements = getLiquidReplacements(content, file)
const newContent = makeLiquidReplacements(contentReplacements, content)
// B. UPDATE FRONTMATTER VERSIONS PROPERTY
const { data } = frontmatter(newContent)
if (data.versions && typeof data.versions !== 'string') {
Object.entries(data.versions).forEach(([plan, value]) => {
// Update legacy versioning while we're here
const valueToUse = value
.replace('2.23', '3.0')
.replace(`>=${oldestSupported}`, '*')
.replace(/>=?2\.20/, '*')
.replace(/>=?2\.19/, '*')
// Find the relevant version from the master list so we can access the short name.
const versionObj = allVersionKeys.find(
(version) => version.plan === plan || version.shortName === plan
)
if (!versionObj) {
console.error(`can't find supported version for ${plan}`)
process.exit(1)
}
delete data.versions[plan]
data.versions[versionObj.shortName] = valueToUse
})
}
if (dryRun) {
console.log(contentReplacements)
} else {
fs.writeFileSync(file, frontmatter.stringify(newContent, data, { lineWidth: 10000 }))
}
}
// 2. UPDATE LIQUID CONDITIONALS IN DATA YAML FILES
console.log('Updating Liquid conditionals in YAML files...\n')
for (const file of yamlFiles) {
const yamlContent = fs.readFileSync(file, 'utf8')
const yamlReplacements = getLiquidReplacements(yamlContent, file)
// Update any `versions` properties in the YAML as well (learning tracks, etc.)
const newYamlContent = makeLiquidReplacements(yamlReplacements, yamlContent)
.replace(/("|')?free-pro-team("|')?:/g, 'fpt:')
.replace(/("|')?enterprise-server("|')?:/g, 'ghes:')
.replace(/("|')?github-ae("|')?:/g, 'ghae:')
if (dryRun) {
console.log(yamlReplacements)
} else {
fs.writeFileSync(file, newYamlContent)
}
}
}
main().then(
() => {
console.log('Done!')
},
(err) => {
console.error(err)
process.exit(1)
}
)
// Convenience function to help with readability by removing this large but unneded property.
function removeInputProps(arrayOfObjects) {
return arrayOfObjects.map((obj) => {
delete obj.input || delete obj.token.input
return obj
})
}
function makeLiquidReplacements(replacementsObj, text) {
let newText = text
Object.entries(replacementsObj).forEach(([oldCond, newCond]) => {
const oldCondRegex = new RegExp(`({%-?)\\s*?${escapeRegExp(oldCond)}\\s*?(-?%})`, 'g')
newText = newText
.replace(oldCondRegex, `$1 ${newCond} $2`)
// Content files use an old-school hack to ensure our old regex deprecation script DTRT, for example:
// `if enterpriseServerVersions contains currentVersion and currentVersion ver_gt "enterprise-server@2.21"`
// This script will change the above to `if ghes and ghes > 2.21`.
// But we don't need the hack for the new deprecation script, because it will change `if ghes > 2.21` to `if ghes`.
// So we can update this to the simpler `{% if ghes > 2.21 %}`.
.replace(/ghes and ghes/g, 'ghes')
})
return newText
}
// Versions map:
// if currentVersion == "myVersion@myRelease" -> ifversion myVersionShort OR ifversion myVersionShort = @myRelease
// if currentVersion != "myVersion@myRelease" -> ifversion not myVersionShort OR ifversion myVersionShort != @myRelease
// if currentVersion ver_gt "myVersion@myRelease -> ifversion myVersionShort > myRelease
// if currentVersion ver_lt "myVersion@myRelease -> ifversion myVersionShort < myRelease
// if enterpriseServerVersions contains currentVersion -> ifversion ghes
function getLiquidReplacements(content, file) {
const replacements = {}
const tokenizer = new Tokenizer(content)
const tokens = removeInputProps(tokenizer.readTopLevelTokens())
tokens
.filter(
(token) =>
(token.name === 'if' || token.name === 'elsif') && token.content.includes('currentVersion')
)
.map((token) => token.content)
.forEach((token) => {
const newToken = token.startsWith('if') ? ['ifversion'] : ['elsif']
// Everything from here on pushes to the `newToken` array to construct the new conditional.
token
.replace(/(if|elsif) /, '')
.split(/ (or|and) /)
.forEach((op) => {
if (op === 'or' || op === 'and') {
newToken.push(op)
return
}
// This string will always resolve to `ifversion ghes`.
if (op.includes('enterpriseServerVersions contains currentVersion')) {
newToken.push('ghes')
return
}
// For the rest, we need to check the release string.
// E.g., [ 'currentVersion', '==', '"enterprise-server@3.0"'].
const opParts = op.split(' ')
if (!(opParts.length === 3 && opParts[0] === 'currentVersion')) {
console.error(`Something went wrong with ${token} in ${file}`)
process.exit(1)
}
const operator = opParts[1]
// Remove quotes around the version and then split it on the at sign.
const [plan, release] = opParts[2].slice(1, -1).split('@')
// Find the relevant version from the master list so we can access the short name.
const versionObj = allVersionKeys.find((version) => version.plan === plan)
if (!versionObj) {
console.error(`Couldn't find a version for ${plan} in "${token}" in ${file}`)
process.exit(1)
}
// Handle numbered releases!
if (versionObj.hasNumberedReleases) {
const newOperator = operatorsMap[operator]
if (!newOperator) {
console.error(
`Couldn't find an operator that corresponds to ${operator} in "${token} in "${file}`
)
process.exit(1)
}
// Account for this one weird version included in a couple content files
deprecated.push('1.19')
// E.g., ghes > 2.20
const availableInAllGhes = deprecated.includes(release) && newOperator === '>'
// We can change > deprecated releases, like ghes > 2.19, to just ghes.
// These are now available for all ghes releases.
if (availableInAllGhes) {
newToken.push(versionObj.shortName)
return
}
// E.g., ghes < 2.20
const lessThanDeprecated = deprecated.includes(release) && newOperator === '<'
// E.g., ghes < 2.21
const lessThanOldestSupported = release === oldestSupported && newOperator === '<'
// E.g., ghes = 2.20
const equalsDeprecated = deprecated.includes(release) && newOperator === '='
const hasDeprecatedContent =
lessThanDeprecated || lessThanOldestSupported || equalsDeprecated
// Remove these by hand.
if (hasDeprecatedContent) {
console.error(`Found content that needs to be removed! See "${token} in "${file}`)
process.exit(1)
}
// Override for legacy 2.23, which should be 3.0
const releaseToUse = release === '2.23' ? '3.0' : release
newToken.push(`${versionObj.shortName} ${newOperator} ${releaseToUse}`)
return
}
// Turn != into nots, now that we can assume this is not a numbered release.
if (operator === '!=') {
newToken.push(`not ${versionObj.shortName}`)
return
}
// We should only have equality conditionals left.
if (operator !== '==') {
console.error(`Expected == but found ${operator} in "${op}" in ${token}`)
process.exit(1)
}
// Handle `latest`!
if (release === 'latest') {
newToken.push(versionObj.shortName)
return
}
// Handle all other non-standard releases, like github-ae@next and github-ae@issue-12345
newToken.push(`${versionObj.shortName}-${release}`)
})
replacements[token] = newToken.join(' ')
})
return replacements
}