Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,27 @@ export const E_INVALID_HOOKS_VALUE = createError<[eventName: string, value: stri
'Expected hooks for event "%s" to be an array of dynamic imports. Instead received "%s"',
'E_INVALID_HOOKS_VALUE'
)

/**
* The exception is raised when the presets property is not an array
*/
export const E_INVALID_PRESETS_VALUE = createError<[value: string]>(
'Expected presets to be an array of functions. Instead received "%s"',
'E_INVALID_PRESETS_VALUE'
)

/**
* The exception is raised when a preset is not a function
*/
export const E_INVALID_PRESET_FUNCTION = createError<[index: number, value: string]>(
'Expected preset at index %s to be a function. Instead received "%s"',
'E_INVALID_PRESET_FUNCTION'
)

/**
* The exception is raised when a preset throws an error during execution
*/
export const E_PRESET_EXECUTION_ERROR = createError<[index: number, message: string]>(
'Preset at index %s failed to execute: %s',
'E_PRESET_EXECUTION_ERROR'
)
39 changes: 37 additions & 2 deletions src/rc_file/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ import globParent from 'glob-parent'

import * as errors from '../errors.js'
import { directories } from '../directories.js'
import type { AppEnvironments, MetaFileNode, PreloadNode, ProviderNode, RcFile } from '../types.js'
import type {
AppEnvironments,
MetaFileNode,
PreloadNode,
PresetFn,
ProviderNode,
RcFile,
} from '../types.js'

const KNOWN_ASSEMBLER_HOOKS = [
'buildStarting',
Expand Down Expand Up @@ -198,11 +205,37 @@ export class RcFileParser {
})
}

/**
* Apply presets functions to the raw rcFile before parsing
*/
#applyPresets(): void {
const presets: PresetFn[] = this.#rcFile.raw?.presets || []
if (presets.length === 0) return

if (!Array.isArray(presets)) {
throw new errors.E_INVALID_PRESETS_VALUE([inspect(presets)])
}

presets.forEach((preset, index) => {
if (typeof preset !== 'function') {
throw new errors.E_INVALID_PRESET_FUNCTION([index, inspect(preset)])
}

try {
preset({ rcFile: this.#rcFile })
} catch (error) {
throw new errors.E_PRESET_EXECUTION_ERROR([index, error.message])
}
})
}

/**
* Parse and validate file contents and merge them with defaults
*/
parse(): RcFile {
return {
this.#applyPresets()

const rcFile = {
typescript: this.#rcFile.typescript,
preloads: this.#getPreloads(),
metaFiles: this.#getMetaFiles(),
Expand All @@ -219,5 +252,7 @@ export class RcFileParser {
experimental: this.#rcFile.experimental,
raw: this.#rcFile.raw,
}

return rcFile
}
}
10 changes: 10 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ export type RcFile = {
* RcFile input is the partial copy of the RcFile
*/
export interface RcFileInput {
/**
* List of presets to apply to the configuration
*/
presets?: PresetFn[]

typescript?: RcFile['typescript']
directories?: Partial<DirectoriesNode> & { [key: string]: string }
preloads?: (PreloadNode | PreloadNode['file'])[]
Expand Down Expand Up @@ -283,3 +288,8 @@ export interface ContainerProviderContract {
* will call this function
*/
export type Importer = (moduleIdentifier: string, options?: ImportCallOptions) => any

/**
* AdonisRC Preset function type
*/
export type PresetFn = (options: { rcFile: RcFile }) => void
191 changes: 191 additions & 0 deletions tests/rc_file/preset.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* @adonisjs/application
*
* (c) AdonisJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { test } from '@japa/runner'
import { directories } from '../../src/directories.js'
import { RcFileParser } from '../../src/rc_file/parser.js'
import { type PresetFn } from '../../src/types.ts'

test.group('Rc Parser | presets', () => {
test('mutate the RcFile using the preset function', ({ assert }) => {
const provider = async () => ({})
const onBuildCompleted = async () => ({}) as any
const commands = async () => {}

function bunPreset(): PresetFn {
return function ({ rcFile }) {
rcFile.hooks ??= {}
rcFile.hooks.buildFinished = [...(rcFile.hooks?.buildFinished || []), onBuildCompleted]

rcFile.commands.push(commands)
rcFile.providers.push({ environment: ['web'], file: provider })
rcFile.tests.timeout = 3000
}
}

const preset = bunPreset()
const parser = new RcFileParser({ presets: [preset] })

assert.deepEqual(parser.parse(), {
raw: { presets: [preset] },
typescript: true,
preloads: [],
directories,
metaFiles: [],
commands: [commands],
commandsAliases: {},
providers: [{ environment: ['web'], file: provider }],
experimental: {},
hooks: {
buildFinished: [onBuildCompleted],
},
tests: {
suites: [],
timeout: 3000,
forceExit: true,
},
})
})

test('apply multiple presets in order', ({ assert }) => {
const provider1 = async () => ({})
const provider2 = async () => ({})
const command1 = async () => {}
const command2 = async () => {}

function firstPreset(): PresetFn {
return function ({ rcFile }) {
rcFile.commands.push(command1)
rcFile.providers.push({ file: provider1, environment: ['web'] })
rcFile.typescript = false
}
}

function secondPreset(): PresetFn {
return function ({ rcFile }) {
rcFile.commands.push(command2)
rcFile.providers.push({ file: provider2, environment: ['console'] })
rcFile.tests.forceExit = false
}
}

const preset1 = firstPreset()
const preset2 = secondPreset()
const parser = new RcFileParser({ presets: [preset1, preset2] })

assert.deepEqual(parser.parse(), {
raw: { presets: [preset1, preset2] },
typescript: false,
preloads: [],
directories,
metaFiles: [],
commands: [command1, command2],
commandsAliases: {},
providers: [
{ environment: ['web'], file: provider1 },
{ environment: ['console'], file: provider2 },
],
experimental: {},
hooks: {},
tests: {
suites: [],
timeout: 2000,
forceExit: false,
},
})
})

test('preset modifications get also validated', ({ assert }) => {
function invalidPreset(): PresetFn {
return function ({ rcFile }) {
// This should throw during validation because file is not a function
rcFile.providers.push({ file: 'invalid-string' as any, environment: ['web'] })
}
}

const preset = invalidPreset()
const parser = new RcFileParser({ presets: [preset] })

const fn = () => parser.parse()
assert.throws(
fn,
"Invalid provider entry \"{ file: 'invalid-string', environment: [ 'web' ] }\". The file property must be a function"
)
})

test('multiple presets can modify same property arrays', ({ assert }) => {
const provider1 = async () => ({})
const provider2 = async () => ({})
const command1 = async () => {}
const command2 = async () => {}

function firstPreset(): PresetFn {
return function ({ rcFile }) {
rcFile.providers.push({ file: provider1, environment: ['web'] })
rcFile.commands.push(command1)
}
}

function secondPreset(): PresetFn {
return function ({ rcFile }) {
rcFile.providers.push({ file: provider2, environment: ['console'] })
rcFile.commands.push(command2)
}
}

const preset1 = firstPreset()
const preset2 = secondPreset()
const parser = new RcFileParser({
presets: [preset1, preset2],
providers: [async () => ({}) as any],
commands: [async () => {}],
})

const result = parser.parse()

assert.lengthOf(result.providers, 3)
assert.lengthOf(result.commands, 3)
})

test('throw error when presets is not an array', ({ assert }) => {
const parser = new RcFileParser({ presets: 'invalid' })

const fn = () => parser.parse()
assert.throws(
fn,
'Expected presets to be an array of functions. Instead received "\'invalid\'"'
)
})

test('throw error when preset is not a function', ({ assert }) => {
const parser = new RcFileParser({ presets: ['invalid', () => {}] })

const fn = () => parser.parse()
assert.throws(fn, 'Expected preset at index 0 to be a function. Instead received "\'invalid\'"')
})

test('throw error with correct index when multiple presets and one fails', ({ assert }) => {
function workingPreset(): PresetFn {
return function () {}
}

function failingPreset(): PresetFn {
return function () {
throw new Error('Second preset failed')
}
}

const preset1 = workingPreset()
const preset2 = failingPreset()
const parser = new RcFileParser({ presets: [preset1, preset2] })

const fn = () => parser.parse()
assert.throws(fn, 'Preset at index 1 failed to execute: Second preset failed')
})
})