Cleanup the whole config-parser class

This commit is contained in:
Yoann Chaudet
2022-07-18 16:42:45 -07:00
parent 3efd613ed2
commit f830cbcb66
8 changed files with 321 additions and 247 deletions

View File

@@ -0,0 +1,2 @@
// Default Pages configuration for Gatsby
module.exports = {}

View File

@@ -0,0 +1,3 @@
// Default Pages configuration for Next
const nextConfig = {}
module.exports = nextConfig

View File

@@ -0,0 +1,2 @@
// Default Pages configuration for Nuxt
export default {}

View File

@@ -1,209 +1,281 @@
const fs = require('fs') const fs = require('fs')
const espree = require('espree') const espree = require('espree')
const format = require('string-format')
const prettier = require('prettier') const prettier = require('prettier')
const core = require('@actions/core') const core = require('@actions/core')
// Parse the AST /*
const espreeOptions = { Parse a JavaScript based configuration file and initialize or update a given property.
ecmaVersion: 6, This is used to make sure most static site generators can automatically handle
sourceType: 'module', Pages's path based routing.
range: true
} Supported configuration initializations:
(1) Default export:
export default {
// configuration object here
}
(2) Direct module export:
module.exports = {
// configuration object here
}
(3) Indirect module export:
const config = // configuration object here
module.exports = config
*/
class ConfigParser { class ConfigParser {
constructor(staticSiteConfig) { // Ctor
this.pathPropertyNuxt = `router: { base: '{0}' }` // - configurationFile: path to the configuration file
this.pathPropertyNext = `basePath: '{0}'` // - propertyName: name of the property to update (or set)
this.pathPropertyGatsby = `pathPrefix: '{0}'` // - propertyValue: value of the property to update (or set)
this.configskeleton = `export default { {0} }` // - blankConfigurationFile: a blank configuration file to use if non was previously found
this.staticSiteConfig = staticSiteConfig constructor({
this.config = fs.existsSync(this.staticSiteConfig.filePath) configurationFile,
? fs.readFileSync(this.staticSiteConfig.filePath, 'utf8') propertyName,
: null propertyValue,
this.validate() blankConfigurationFile
} }) {
// Save fields
this.configurationFile = configurationFile
this.propertyName = propertyName
this.propertyValue = propertyValue
validate() { // If the configuration file does not exist, initialize it with the blank configuration file
if (!this.config) { if (!fs.existsSync(this.configurationFile)) {
core.info(`original raw configuration was empty:\n${this.config}`) core.info('Use default blank configuration')
core.info('Generating a default configuration to start from...') const blankConfiguration = fs.readFileSync(blankConfigurationFile, 'utf8')
fs.writeFileSync(this.configurationFile, blankConfiguration, {
// Update the `config` property with a default configuration file encoding: 'utf8'
this.config = this.generateConfigFile() })
} }
// Read the configuration file
core.info('Read existing configuration')
this.configuration = fs.readFileSync(this.configurationFile, 'utf8')
} }
generateConfigFile() { // Find the configuration object in an AST.
switch (this.staticSiteConfig.type) { // Look for a default export, a direct module export or an indirect module
case 'nuxt': // export (in that order).
return format( //
this.configskeleton, // Return the configuration object or null.
format(this.pathPropertyNuxt, this.staticSiteConfig.newPath) findConfigurationObject(ast) {
) // Try to find a default export
case 'next': var defaultExport = ast.body.find(
return format( node =>
this.configskeleton, node.type === 'ExportDefaultDeclaration' &&
format(this.pathPropertyNext, this.staticSiteConfig.newPath) node.declaration.type === 'ObjectExpression'
) )
case 'gatsby': if (defaultExport) {
return format( core.info('Found configuration object in default export declaration')
this.configskeleton, return defaultExport.declaration
format(this.pathPropertyGatsby, this.staticSiteConfig.newPath)
)
default:
throw 'Unknown config type'
} }
// 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
}
// Indirect module export
else if (
moduleExport &&
moduleExport.expression.right.type === 'Identifier'
) {
const identifierName = moduleExport && moduleExport.expression.right.name
const identifierDefinition = ast.body.find(
node =>
node.type === 'VariableDeclaration' &&
node.declarations.length == 1 &&
node.declarations[0].type === 'VariableDeclarator' &&
node.declarations[0].id.type === 'Identifier' &&
node.declarations[0].id.name === identifierName &&
node.declarations[0].init.type === 'ObjectExpression'
)
if (identifierDefinition) {
core.info('Found configuration object in indirect module export')
return identifierDefinition.declarations[0].init
}
}
// No configuration object found
return null
} }
generateConfigProperty() { // Find a property with a given name on a given object.
switch (this.staticSiteConfig.type) { //
case 'nuxt': // Return the matching property or null.
return format(this.pathPropertyNuxt, this.staticSiteConfig.newPath) findProperty(object, name) {
case 'next': // Try to find a property matching a given name
return format(this.pathPropertyNext, this.staticSiteConfig.newPath) const property =
case 'gatsby': object.type === 'ObjectExpression' &&
return format(this.pathPropertyGatsby, this.staticSiteConfig.newPath) object.properties.find(
default: node => node.key.type === 'Identifier' && node.key.name === name
throw 'Unknown config type' )
// 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
//
// Return a nested property declaration as a string.
getPropertyDeclaration(properties, startIndex) {
if (startIndex === properties.length - 1) {
return `${properties[startIndex]}: "${this.propertyValue}"`
} else {
return (
`${properties[startIndex]}: {` +
this.getPropertyDeclaration(properties, startIndex + 1) +
'}'
)
} }
} }
parse() { parse() {
// Print current configuration // Logging
core.info(`original configuration:\n${this.config}`) core.info(`Parsing configuration:\n${this.configuration}`)
// Parse the AST // Parse the AST out of the configuration file
const ast = espree.parse(this.config, espreeOptions) const espreeOptions = {
ecmaVersion: 6,
sourceType: 'module',
range: true
}
const ast = espree.parse(this.configuration, espreeOptions)
// Find the default export declaration node // Find the configuration object
var exportNode = ast.body.find(node => node.type === 'ExpressionStatement') var configurationObject = this.findConfigurationObject(ast)
if (exportNode) { if (!configurationObject) {
var property = this.getPropertyModuleExport(exportNode) throw 'Could not find a configuration object in the configuration file'
} else {
exportNode = ast.body.find(
node => node.type === 'ExportDefaultDeclaration'
)
if (!exportNode) throw 'Unable to find default export'
var property = this.getPropertyExportDefault(exportNode)
} }
if (property) { // A property may be nested in the configuration file. Split the property name with `.`
switch (this.staticSiteConfig.type) { // then walk the configuration object one property at a time.
case 'nuxt': var depth = 0
this.parseNuxt(property) const properties = this.propertyName.split('.')
break var lastNode = configurationObject
case 'next': while (1) {
case 'gatsby': // Find the node for the current property
this.parseNextGatsby(property) var propertyNode = this.findProperty(lastNode, properties[depth])
break
default: // Update last node
throw 'Unknown config type' if (propertyNode != null) {
lastNode = propertyNode
depth++
}
// Exit when exiting the current configuration object
if (propertyNode == null || depth >= properties.length) {
break
} }
} }
// Write down the updated configuration // If the configuration file is defining the property we are after, update it.
core.info(`parsed configuration:\n${this.config}`) if (depth == properties.length) {
fs.writeFileSync(this.staticSiteConfig.filePath, this.config) // The last node identified is an object expression, so do the assignment
if (lastNode.type === 'ObjectExpression') {
this.configuration =
this.configuration.slice(0, lastNode.value.range[0]) +
`"${this.propertyValue}"` +
this.configuration.slice(lastNode.value.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]) +
`"${this.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)
// 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])
}
}
// Format the updated configuration with prettier's default settings // Format the updated configuration with prettier's default settings
this.config = prettier.format(this.config, { this.configuration = prettier.format(this.configuration, {
filePath: this.staticSiteConfig.filePath, parser: 'espree',
parser: 'babel' /* default ot javascript for when filePath is nil */
// Matching this repo's prettier configuration
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
trailingComma: 'none',
bracketSpacing: false,
arrowParens: 'avoid'
}) })
// Return the new configuration // Logging
return this.config core.info(`Parsing configuration:\n${this.configuration}`)
}
getPropertyModuleExport(exportNode) { // Finally write the new configuration in the file
var propertyNode = exportNode.expression.right.properties.find( fs.writeFileSync(this.configurationFile, this.configuration, {
node => encoding: 'utf8'
node.key.type === 'Identifier' && })
node.key.name === this.staticSiteConfig.pathName
)
if (!propertyNode) {
core.info(
'Unable to find property, insert it : ' +
this.staticSiteConfig.pathName
)
if (exportNode.expression.right.properties.length > 0) {
this.config =
this.config.slice(
0,
exportNode.expression.right.properties[0].range[0]
) +
this.generateConfigProperty() +
',' +
this.config.slice(exportNode.expression.right.properties[0].range[0])
core.info('new config = \n' + this.config)
} else {
this.config =
this.config.slice(0, exportNode.expression.right.range[0] + 1) +
this.generateConfigProperty() +
this.config.slice(exportNode.expression.right.range[1] - 1)
core.info('new config = \n' + this.config)
}
}
return propertyNode
}
getPropertyExportDefault(exportNode) {
var propertyNode = exportNode.declaration.properties.find(
node =>
node.key.type === 'Identifier' &&
node.key.name === this.staticSiteConfig.pathName
)
if (!propertyNode) {
core.info(
'Unable to find property, insert it ' + this.staticSiteConfig.pathName
)
if (exportNode.declaration.properties.length > 0) {
this.config =
this.config.slice(0, exportNode.declaration.properties[0].range[0]) +
this.generateConfigProperty() +
',' +
this.config.slice(exportNode.declaration.properties[0].range[0])
core.info('new config = \n' + this.config)
} else {
this.config =
this.config.slice(0, exportNode.declaration.range[0] + 1) +
this.generateConfigProperty() +
this.config.slice(exportNode.declaration.range[1] - 1)
core.info('new config = \n' + this.config)
}
}
return propertyNode
}
parseNuxt(propertyNode) {
// Find the base node
if (propertyNode && propertyNode.value.type === 'ObjectExpression') {
var baseNode = propertyNode.value.properties.find(
node =>
node.key.type === 'Identifier' &&
node.key.name === this.staticSiteConfig.subPathName
) //'base')
if (baseNode) {
// Swap the base value by a hardcoded string and print it
this.config =
this.config.slice(0, baseNode.value.range[0]) +
`'${this.staticSiteConfig.newPath}'` +
this.config.slice(baseNode.value.range[1])
}
}
}
parseNextGatsby(pathNode) {
if (pathNode) {
this.config =
this.config.slice(0, pathNode.value.range[0]) +
`'${this.staticSiteConfig.newPath}'` +
this.config.slice(pathNode.value.range[1])
}
} }
} }

View File

@@ -12,76 +12,73 @@ const cases = [
[ [
'next.config.js', 'next.config.js',
{ {
filePath: `${tmpFolder}/next.config.js`, configurationFile: `${tmpFolder}/next.config.js`,
type: 'next', propertyName: 'basePath',
pathName: 'basePath', propertyValue: repoPath,
newPath: repoPath blankConfigurationFile: `${process.cwd()}/src/blank-configurations/next.js`
} }
], ],
[ [
'next.config.old.js', 'next.config.old.js',
{ {
filePath: `${tmpFolder}/next.config.old.js`, configurationFile: `${tmpFolder}/next.config.old.js`,
type: 'next', propertyName: 'basePath',
pathName: 'basePath', propertyValue: repoPath,
newPath: repoPath blankConfigurationFile: `${process.cwd()}/src/blank-configurations/next.js`
} }
], ],
[ [
'next.config.old.missing.js', 'next.config.old.missing.js',
{ {
filePath: `${tmpFolder}/next.config.old.missing.js`, configurationFile: `${tmpFolder}/next.config.old.missing.js`,
type: 'next', propertyName: 'basePath',
pathName: 'basePath', propertyValue: repoPath,
newPath: repoPath blankConfigurationFile: `${process.cwd()}/src/blank-configurations/next.js`
} }
], ],
[ [
'gatsby-config.js', 'gatsby-config.js',
{ {
filePath: `${tmpFolder}/gatsby-config.js`, configurationFile: `${tmpFolder}/gatsby-config.js`,
type: 'gatsby', propertyName: 'pathPrefix',
pathName: 'pathPrefix', propertyValue: repoPath,
newPath: repoPath blankConfigurationFile: `${process.cwd()}/src/blank-configurations/gatsby.js`
} }
], ],
[ [
'gatsby-config.old.js', 'gatsby-config.old.js',
{ {
filePath: `${tmpFolder}/gatsby-config.old.js`, configurationFile: `${tmpFolder}/gatsby-config.old.js`,
type: 'gatsby', propertyName: 'pathPrefix',
pathName: 'pathPrefix', propertyValue: repoPath,
newPath: repoPath blankConfigurationFile: `${process.cwd()}/src/blank-configurations/gatsby.js`
} }
], ],
[ [
'nuxt.config.js', 'nuxt.config.js',
{ {
filePath: `${tmpFolder}/nuxt.config.js`, configurationFile: `${tmpFolder}/nuxt.config.js`,
type: 'nuxt', propertyName: 'router.base',
pathName: 'router', propertyValue: repoPath,
subPathName: 'base', blankConfigurationFile: `${process.cwd()}/src/blank-configurations/nuxt.js`
newPath: repoPath
} }
], ],
[ [
'nuxt.config.missing.js', 'nuxt.config.missing.js',
{ {
filePath: `${tmpFolder}/nuxt.config.missing.js`, configurationFile: `${tmpFolder}/nuxt.config.missing.js`,
type: 'nuxt', propertyName: 'router.base',
pathName: 'router', propertyValue: repoPath,
subPathName: 'base', blankConfigurationFile: `${process.cwd()}/src/blank-configurations/nuxt.js`
newPath: repoPath
} }
], ],
[ [
'nuxt.config.old.js', 'nuxt.config.old.js',
{ {
filePath: `${tmpFolder}/nuxt.config.old.js`, configurationFile: `${tmpFolder}/nuxt.config.old.js`,
type: 'nuxt', propertyName: 'router.base',
pathName: 'router', propertyValue: repoPath,
subPathName: 'base', blankConfigurationFile: `${process.cwd()}/src/blank-configurations/nuxt.js`
newPath: repoPath
} }
] ]
] ]

View File

@@ -1,9 +1,7 @@
import {resolve} from 'path' import {resolve} from 'path'
export default { export default {
router: { router: {base: '/amazing-new-repo/'},
base: '/amazing-new-repo/'
},
alias: { alias: {
style: resolve(__dirname, './assets/style') style: resolve(__dirname, './assets/style')
}, },

3
src/fixtures/x.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
pathPrefix: "hello",
};

View File

@@ -1,41 +1,38 @@
const core = require('@actions/core') const core = require('@actions/core')
const axios = require('axios')
const {ConfigParser} = require('./config-parser') const {ConfigParser} = require('./config-parser')
function getParserConfiguration(staticSiteGenerator, path) {
switch (staticSiteGenerator) {
case 'nuxt':
return {
configurationFile: './nuxt.config.js',
propertyName: 'router.base',
propertyValue: path,
blankConfigurationFile: `${process.cwd()}/blank-configurations/nuxt.js`
}
case 'next':
return {
configurationFile: './next.config.js',
propertyName: 'basePath',
propertyValue: path,
blankConfigurationFile: `${process.cwd()}/blank-configurations/next.js`
}
case 'gatsby':
return {
configurationFile: './gatsby-config.js',
propertyName: 'pathPrefix',
propertyValue: path,
blankConfigurationFile: `${process.cwd()}/blank-configurations/gatsby.js`
}
default:
throw `Unsupported static site generator: ${staticSiteGenerator}`
}
}
async function setPagesPath({staticSiteGenerator, path}) { async function setPagesPath({staticSiteGenerator, path}) {
try { try {
switch (staticSiteGenerator) { // Parse/mutate the configuration file
case 'nuxt': new ConfigParser(getParserConfiguration(staticSiteGenerator, path)).parse()
var ssConfig = {
filePath: './nuxt.config.js',
type: 'nuxt',
pathName: 'router',
subPathName: 'base',
newPath: path
}
break
case 'next':
var ssConfig = {
filePath: './next.config.js',
type: 'next',
pathName: 'basePath',
newPath: path
}
break
case 'gatsby':
var ssConfig = {
filePath: './gatsby-config.js',
type: 'gatsby',
pathName: 'pathPrefix',
newPath: path
}
break
default:
throw 'Unknown config type'
}
let configParser = new ConfigParser(ssConfig)
configParser.parse()
} catch (error) { } catch (error) {
core.warning( core.warning(
`We were unable to determine how to inject the site metadata into your config. Generated URLs may be incorrect. The base URL for this site should be ${path}. Please ensure your framework is configured to generate relative links appropriately.`, `We were unable to determine how to inject the site metadata into your config. Generated URLs may be incorrect. The base URL for this site should be ${path}. Please ensure your framework is configured to generate relative links appropriately.`,