Files
shields/core/base-service/loader.js
chris48s 388459a8ca fail to start server if there are duplicate service names (#9099)
* fail to start server if there are duplicate service names

* update class names in loader test fixtures

---------

Co-authored-by: repo-ranger[bot] <39074581+repo-ranger[bot]@users.noreply.github.com>
2023-05-01 18:07:13 +00:00

117 lines
3.1 KiB
JavaScript

import path from 'path'
import { fileURLToPath } from 'url'
import { globSync } from 'glob'
import countBy from 'lodash.countby'
import categories from '../../services/categories.js'
import BaseService from './base.js'
import { assertValidServiceDefinitionExport } from './service-definitions.js'
const serviceDir = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..',
'..',
'services'
)
function toUnixPath(path) {
// glob does not allow \ as a path separator
// see https://github.com/isaacs/node-glob/blob/main/changelog.md#80
// so we need to convert to use / for use with glob
return path.replace(/\\/g, '/')
}
class InvalidService extends Error {
constructor(message) {
super(message)
this.name = 'InvalidService'
}
}
function getServicePaths(pattern) {
return globSync(toUnixPath(path.join(serviceDir, '**', pattern))).sort()
}
function assertNamesUnique(names, { message }) {
const duplicates = {}
Object.entries(countBy(names))
.filter(([name, count]) => count > 1)
.forEach(([name, count]) => {
duplicates[name] = count
})
if (Object.keys(duplicates).length) {
throw new Error(`${message}: ${JSON.stringify(duplicates, undefined, 2)}`)
}
}
async function loadServiceClasses(servicePaths) {
if (!servicePaths) {
servicePaths = getServicePaths('*.service.js')
}
const serviceClasses = []
for await (const servicePath of servicePaths) {
const currentServiceClasses = Object.values(
await import(`file://${servicePath}`)
).flatMap(element =>
typeof element === 'object' ? Object.values(element) : element
)
if (currentServiceClasses.length === 0) {
throw new InvalidService(
`Expected ${servicePath} to export a service or a collection of services`
)
}
currentServiceClasses.forEach(serviceClass => {
if (serviceClass && serviceClass.prototype instanceof BaseService) {
// Decorate each service class with the directory that contains it.
serviceClass.serviceFamily = servicePath
.replace(serviceDir, '')
.split(path.sep)[1]
serviceClass.validateDefinition()
return serviceClasses.push(serviceClass)
}
throw new InvalidService(
`Expected ${servicePath} to export a service or a collection of services; one of them was ${serviceClass}`
)
})
}
assertNamesUnique(
serviceClasses.map(({ name }) => name),
{
message: 'Duplicate service names found',
}
)
return serviceClasses
}
async function collectDefinitions() {
const services = (await loadServiceClasses())
// flatMap.
.map(ServiceClass => ServiceClass.getDefinition())
.reduce((accum, these) => accum.concat(these), [])
const result = { schemaVersion: '0', categories, services }
assertValidServiceDefinitionExport(result)
return result
}
async function loadTesters() {
return Promise.all(
getServicePaths('*.tester.js').map(
async path => await import(`file://${path}`)
)
)
}
export {
InvalidService,
loadServiceClasses,
getServicePaths,
collectDefinitions,
loadTesters,
}