mirror of
https://github.com/actions/configure-pages.git
synced 2025-12-08 08:06:09 +00:00
437 lines
15 KiB
JavaScript
437 lines
15 KiB
JavaScript
const fs = require('fs')
|
|
const espree = require('espree')
|
|
const core = require('@actions/core')
|
|
|
|
/*
|
|
Parse a JavaScript based configuration file and inject arbitrary key/value in it.
|
|
This is used to make sure most static site generators can automatically handle
|
|
Pages's path based routing (and work).
|
|
|
|
Supported configuration initializations:
|
|
|
|
(1) Direct default export:
|
|
|
|
export default {
|
|
// configuration object here
|
|
}
|
|
|
|
(2) Direct module export:
|
|
|
|
module.exports = {
|
|
// configuration object here
|
|
}
|
|
|
|
(3) Indirect default export:
|
|
|
|
const config = {
|
|
// configuration object here
|
|
}
|
|
export default config
|
|
|
|
(4) Indirect module export:
|
|
|
|
const config = {
|
|
// configuration object here
|
|
}
|
|
module.exports = config
|
|
|
|
(5) Direct default export with wrapping call:
|
|
|
|
export default defineConfig({
|
|
// configuration object here
|
|
})
|
|
|
|
(6) Direct module export with wrapping call:
|
|
|
|
module.exports = defineConfig({
|
|
// configuration object here
|
|
})
|
|
|
|
(7) Indirect default export with wrapping call at the definition:
|
|
|
|
const config = defineConfig({
|
|
// configuration object here
|
|
})
|
|
export default config
|
|
|
|
(8) Indirect default export with wrapping call at the export:
|
|
|
|
const config = {
|
|
// configuration object here
|
|
}
|
|
export default defineConfig(config)
|
|
|
|
(9) Indirect module export with wrapping call at the definition:
|
|
|
|
const config = defineConfig({
|
|
// configuration object here
|
|
})
|
|
module.exports = config
|
|
|
|
(10) Indirect module export with wrapping call at the export:
|
|
|
|
const config = {
|
|
// configuration object here
|
|
}
|
|
module.exports = defineConfig(config)
|
|
*/
|
|
|
|
class ConfigParser {
|
|
// Ctor
|
|
// - configurationFile: path to the configuration file
|
|
// - blankConfigurationFile: a blank configuration file to use if non was previously found
|
|
constructor({ configurationFile, blankConfigurationFile, allowWrappingCall = false, properties }) {
|
|
// Save field
|
|
this.configurationFile = configurationFile
|
|
this.allowWrappingCall = allowWrappingCall === true
|
|
this.properties = properties
|
|
|
|
// If the configuration file does not exist, initialize it with the blank configuration file
|
|
if (!fs.existsSync(this.configurationFile)) {
|
|
core.info('Using default blank configuration')
|
|
const blankConfiguration = fs.readFileSync(blankConfigurationFile, 'utf8')
|
|
fs.writeFileSync(this.configurationFile, blankConfiguration, {
|
|
encoding: 'utf8'
|
|
})
|
|
}
|
|
|
|
// Read the configuration file
|
|
this.configuration = fs.readFileSync(this.configurationFile, 'utf8')
|
|
}
|
|
|
|
findTopLevelVariableDeclarator(ast, identifierName) {
|
|
let targetDeclarator
|
|
ast.body.find(
|
|
node =>
|
|
node.type === 'VariableDeclaration' &&
|
|
node.declarations &&
|
|
node.declarations.length > 0 &&
|
|
node.declarations.find(declarator => {
|
|
if (
|
|
declarator.type === 'VariableDeclarator' &&
|
|
declarator.id &&
|
|
declarator.id.type === 'Identifier' &&
|
|
declarator.id.name === identifierName
|
|
) {
|
|
targetDeclarator = declarator
|
|
return true
|
|
}
|
|
})
|
|
)
|
|
return targetDeclarator
|
|
}
|
|
|
|
// Find the configuration object in an AST.
|
|
// Look for, in order:
|
|
// - a direct default export
|
|
// - a direct default export with a wrapping call
|
|
// - an indirect default export
|
|
// - an indirect default export with a wrapping call at the definition
|
|
// - an indirect default export with a wrapping call at the export
|
|
// - a direct module export
|
|
// - a direct module export with a wrapping call
|
|
// - an indirect module export
|
|
// - an indirect module export with a wrapping call at the definition
|
|
// - an indirect module export with a wrapping call at the export
|
|
//
|
|
// Return the configuration object or null.
|
|
findConfigurationObject(ast, allowWrappingCall = false) {
|
|
// Try to find a default export
|
|
var defaultExport = ast.body.find(node => node.type === 'ExportDefaultDeclaration')
|
|
|
|
// Direct default export
|
|
if (defaultExport && defaultExport.declaration.type === 'ObjectExpression') {
|
|
core.info('Found configuration object in direct default export declaration')
|
|
return defaultExport.declaration
|
|
}
|
|
|
|
// Direct default export with a wrapping call
|
|
else if (
|
|
allowWrappingCall &&
|
|
defaultExport &&
|
|
defaultExport.declaration.type === 'CallExpression' &&
|
|
defaultExport.declaration.arguments.length > 0 &&
|
|
defaultExport.declaration.arguments[0] &&
|
|
defaultExport.declaration.arguments[0].type === 'ObjectExpression'
|
|
) {
|
|
core.info('Found configuration object in direct default export declaration with a wrapping call')
|
|
return defaultExport.declaration.arguments[0]
|
|
}
|
|
|
|
// Indirect default export
|
|
else if (defaultExport && defaultExport.declaration.type === 'Identifier') {
|
|
const identifierName = defaultExport.declaration.name
|
|
const identifierDeclarator = this.findTopLevelVariableDeclarator(ast, identifierName)
|
|
const identifierInitialization = identifierDeclarator && identifierDeclarator.init
|
|
if (identifierInitialization && identifierInitialization.type === 'ObjectExpression') {
|
|
core.info('Found configuration object in indirect default export declaration')
|
|
return identifierInitialization
|
|
}
|
|
// Indirect default export with a wrapping call at the definition
|
|
else if (
|
|
allowWrappingCall &&
|
|
identifierInitialization &&
|
|
identifierInitialization.type === 'CallExpression' &&
|
|
identifierInitialization.arguments.length > 0 &&
|
|
identifierInitialization.arguments[0] &&
|
|
identifierInitialization.arguments[0].type === 'ObjectExpression'
|
|
) {
|
|
core.info(
|
|
'Found configuration object in indirect default export declaration with a wrapping call at the definition'
|
|
)
|
|
return identifierInitialization.arguments[0]
|
|
}
|
|
}
|
|
|
|
// Indirect default export with a wrapping call at the export
|
|
else if (
|
|
allowWrappingCall &&
|
|
defaultExport &&
|
|
defaultExport.declaration.type === 'CallExpression' &&
|
|
defaultExport.declaration.arguments.length > 0 &&
|
|
defaultExport.declaration.arguments[0] &&
|
|
defaultExport.declaration.arguments[0].type === 'Identifier'
|
|
) {
|
|
const identifierName = defaultExport.declaration.arguments[0].name
|
|
const identifierDeclarator = this.findTopLevelVariableDeclarator(ast, identifierName)
|
|
const identifierInitialization = identifierDeclarator && identifierDeclarator.init
|
|
if (identifierInitialization && identifierInitialization.type === 'ObjectExpression') {
|
|
core.info(
|
|
'Found configuration object in indirect default export declaration with a wrapping call at the export'
|
|
)
|
|
return identifierInitialization
|
|
}
|
|
}
|
|
|
|
// Try to find a module export
|
|
var moduleExport = ast.body.find(
|
|
node =>
|
|
node.type === 'ExpressionStatement' &&
|
|
node.expression.type === 'AssignmentExpression' &&
|
|
node.expression.operator === '=' &&
|
|
node.expression.left.type === 'MemberExpression' &&
|
|
node.expression.left.object.type === 'Identifier' &&
|
|
node.expression.left.object.name === 'module' &&
|
|
node.expression.left.property.type === 'Identifier' &&
|
|
node.expression.left.property.name === 'exports'
|
|
)
|
|
|
|
// Direct module export
|
|
if (moduleExport && moduleExport.expression.right.type === 'ObjectExpression') {
|
|
core.info('Found configuration object in direct module export')
|
|
return moduleExport.expression.right
|
|
}
|
|
|
|
// Direct default export with a wrapping call
|
|
else if (
|
|
allowWrappingCall &&
|
|
moduleExport &&
|
|
moduleExport.expression.right.type === 'CallExpression' &&
|
|
moduleExport.expression.right.arguments.length > 0 &&
|
|
moduleExport.expression.right.arguments[0] &&
|
|
moduleExport.expression.right.arguments[0].type === 'ObjectExpression'
|
|
) {
|
|
core.info('Found configuration object in direct module export with a wrapping call')
|
|
return moduleExport.expression.right.arguments[0]
|
|
}
|
|
|
|
// Indirect module export
|
|
else if (moduleExport && moduleExport.expression.right.type === 'Identifier') {
|
|
const identifierName = moduleExport && moduleExport.expression.right.name
|
|
const identifierDeclarator = this.findTopLevelVariableDeclarator(ast, identifierName)
|
|
const identifierInitialization = identifierDeclarator && identifierDeclarator.init
|
|
if (identifierInitialization && identifierInitialization.type === 'ObjectExpression') {
|
|
core.info('Found configuration object in indirect module export')
|
|
return identifierInitialization
|
|
}
|
|
// Indirect module export with a wrapping call at the definition
|
|
else if (
|
|
allowWrappingCall &&
|
|
identifierInitialization &&
|
|
identifierInitialization.type === 'CallExpression' &&
|
|
identifierInitialization.arguments.length > 0 &&
|
|
identifierInitialization.arguments[0] &&
|
|
identifierInitialization.arguments[0].type === 'ObjectExpression'
|
|
) {
|
|
core.info('Found configuration object in indirect module export with a wrapping call at the definition')
|
|
return identifierInitialization.arguments[0]
|
|
}
|
|
}
|
|
|
|
// Indirect module export with a wrapping call at the export
|
|
else if (
|
|
allowWrappingCall &&
|
|
moduleExport &&
|
|
moduleExport.expression.right.type === 'CallExpression' &&
|
|
moduleExport.expression.right.arguments.length > 0 &&
|
|
moduleExport.expression.right.arguments[0] &&
|
|
moduleExport.expression.right.arguments[0].type === 'Identifier'
|
|
) {
|
|
const identifierName = moduleExport.expression.right.arguments[0].name
|
|
const identifierDeclarator = this.findTopLevelVariableDeclarator(ast, identifierName)
|
|
const identifierInitialization = identifierDeclarator && identifierDeclarator.init
|
|
if (identifierInitialization && identifierInitialization.type === 'ObjectExpression') {
|
|
core.info('Found configuration object in indirect module export declaration with a wrapping call at the export')
|
|
return identifierInitialization
|
|
}
|
|
}
|
|
|
|
// No configuration object found
|
|
return null
|
|
}
|
|
|
|
// Find a property with a given name on a given object.
|
|
//
|
|
// Return the matching property or null.
|
|
findProperty(object, name) {
|
|
// Try to find a property matching a given name
|
|
const property =
|
|
object.type === 'ObjectExpression' &&
|
|
object.properties.find(node => node.key.type === 'Identifier' && node.key.name === name)
|
|
|
|
// Return the property's value (if found) or null
|
|
if (property) {
|
|
return property.value
|
|
}
|
|
return null
|
|
}
|
|
|
|
// Generate a (nested) property declaration.
|
|
// - properties: list of properties to generate
|
|
// - startIndex: the index at which to start in the declaration
|
|
// - propertyValue: the value of the property
|
|
//
|
|
// Return a nested property declaration as a string.
|
|
getPropertyDeclaration(properties, startIndex, propertyValue) {
|
|
if (startIndex === properties.length - 1) {
|
|
return `${properties[startIndex]}: ${JSON.stringify(propertyValue)}`
|
|
} else {
|
|
return (
|
|
`${properties[startIndex]}: {` + this.getPropertyDeclaration(properties, startIndex + 1, propertyValue) + '}'
|
|
)
|
|
}
|
|
}
|
|
|
|
// Inject all properties into the configuration
|
|
injectAll() {
|
|
for (var [propertyName, propertyValue] of Object.entries(this.properties)) {
|
|
this.inject(propertyName, propertyValue)
|
|
}
|
|
}
|
|
|
|
// Inject an arbitrary property into the configuration
|
|
// - propertyName: the name of the property (may use . to target nested objects)
|
|
// - propertyValue: the value of the property
|
|
inject(propertyName, propertyValue) {
|
|
// Logging
|
|
core.info(`Injecting property=${propertyName} and value=${propertyValue} in:`)
|
|
core.info(this.configuration)
|
|
|
|
// Parse the AST out of the configuration file
|
|
const espreeOptions = {
|
|
ecmaVersion: 'latest',
|
|
sourceType: 'module',
|
|
range: true
|
|
}
|
|
const ast = espree.parse(this.configuration, espreeOptions)
|
|
|
|
// Find the configuration object
|
|
var configurationObject = this.findConfigurationObject(ast, this.allowWrappingCall)
|
|
if (!configurationObject) {
|
|
throw 'Could not find a configuration object in the configuration file'
|
|
}
|
|
|
|
// A property may be nested in the configuration file. Split the property name with '.'
|
|
// then walk the configuration object one property at a time.
|
|
var depth = 0
|
|
const properties = propertyName.split('.')
|
|
var lastNode = configurationObject
|
|
// eslint-disable-next-line no-constant-condition
|
|
while (true) {
|
|
// Find the node for the current property
|
|
var propertyNode = this.findProperty(lastNode, properties[depth])
|
|
|
|
// Update last node
|
|
if (propertyNode != null) {
|
|
lastNode = propertyNode
|
|
depth++
|
|
}
|
|
|
|
// Exit when exiting the current configuration object
|
|
if (propertyNode == null || depth >= properties.length) {
|
|
break
|
|
}
|
|
}
|
|
|
|
// If the configuration file is defining the property we are after, update it.
|
|
if (depth == properties.length) {
|
|
// The last node identified is an object expression, so do the assignment
|
|
if (lastNode.type === 'ObjectExpression') {
|
|
this.configuration =
|
|
this.configuration.slice(0, lastNode.range[0]) +
|
|
JSON.stringify(propertyValue) +
|
|
this.configuration.slice(lastNode.range[1])
|
|
}
|
|
|
|
// A misc object was found in the configuration file (e.g. an array, a string, a boolean,
|
|
// a number, etc.), just replace the whole range by our declaration
|
|
else {
|
|
this.configuration =
|
|
this.configuration.slice(0, lastNode.range[0]) +
|
|
JSON.stringify(propertyValue) +
|
|
this.configuration.slice(lastNode.range[1])
|
|
}
|
|
}
|
|
|
|
// Create nested properties in the configuration file
|
|
else {
|
|
// Build the declaration to inject
|
|
const declaration = this.getPropertyDeclaration(properties, depth, propertyValue)
|
|
|
|
// The last node identified is an object expression, so do the assignment
|
|
if (lastNode.type === 'ObjectExpression') {
|
|
// The object is blank (no properties) so replace the whole range by a new object containing the declaration
|
|
if (lastNode.properties.length === 0) {
|
|
this.configuration =
|
|
this.configuration.slice(0, lastNode.range[0]) +
|
|
'{' +
|
|
declaration +
|
|
'}' +
|
|
this.configuration.slice(lastNode.range[1])
|
|
}
|
|
|
|
// The object contains other properties, prepend our new one at the beginning
|
|
else {
|
|
this.configuration =
|
|
this.configuration.slice(0, lastNode.properties[0].range[0]) +
|
|
declaration +
|
|
',' +
|
|
this.configuration.slice(lastNode.properties[0].range[0])
|
|
}
|
|
}
|
|
|
|
// A misc object was found in the configuration file (e.g. an array, a string, a boolean,
|
|
// a number, etc.), just replace the whole range by our declaration
|
|
else {
|
|
this.configuration =
|
|
this.configuration.slice(0, lastNode.range[0]) +
|
|
'{' +
|
|
declaration +
|
|
'}' +
|
|
this.configuration.slice(lastNode.range[1])
|
|
}
|
|
}
|
|
|
|
// Logging
|
|
core.info('Injection successful, new configuration:')
|
|
core.info(this.configuration)
|
|
|
|
// Finally write the new configuration in the file
|
|
fs.writeFileSync(this.configurationFile, this.configuration, {
|
|
encoding: 'utf8'
|
|
})
|
|
}
|
|
}
|
|
|
|
module.exports = { ConfigParser }
|