configorama 0.6.3 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -12
- package/cli.js +10 -3
- package/index.d.ts +45 -0
- package/package.json +12 -3
- package/src/index.js +18 -9
- package/src/main.js +369 -108
- package/src/parsers/esm.js +69 -0
- package/src/parsers/index.js +3 -1
- package/src/parsers/ini.js +51 -0
- package/src/parsers/ini.test.js +133 -0
- package/src/parsers/json5.js +1 -0
- package/src/parsers/typescript.js +154 -0
- package/src/parsers/yaml.test.js +1 -1
- package/src/resolvers/valueFromCron.js +252 -0
- package/src/resolvers/valueFromCron.test.js +132 -0
- package/src/resolvers/valueFromEval.js +37 -0
- package/src/resolvers/valueFromEval.test.js +44 -0
- package/src/resolvers/valueFromGit.js +6 -4
- package/src/types.d.ts +112 -0
- package/src/utils/cleanVariable.js +67 -3
- package/src/utils/createEditorLink.js +23 -0
- package/src/utils/find-nested-variables.js +10 -1
- package/src/utils/logs.js +2 -1
- package/src/utils/parse.js +40 -0
- package/src/utils/resolveAlias.js +152 -0
- package/src/utils/resolveAlias.test.js +98 -0
- package/src/utils/resolveAliasOld.js +65 -0
- package/src/utils/textUtils.js +2 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Execute ESM file and return its export using jiti
|
|
5
|
+
* @param {string} filePath - Full path to the ESM file
|
|
6
|
+
* @param {Object} opts - Additional options including dynamicArgs
|
|
7
|
+
* @returns {Promise<*>} The result of executing the ESM file
|
|
8
|
+
*/
|
|
9
|
+
async function executeESMFile(filePath, opts = {}) {
|
|
10
|
+
try {
|
|
11
|
+
// Use require for now since ESM dynamic import in async context is complex
|
|
12
|
+
// We'll use jiti to handle ESM syntax
|
|
13
|
+
const { createJiti } = require('jiti')
|
|
14
|
+
const jiti = createJiti(__filename, {
|
|
15
|
+
interopDefault: true
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// Load the ESM file - resolve to absolute path first
|
|
19
|
+
const resolvedPath = path.resolve(filePath)
|
|
20
|
+
let esmModule = jiti(resolvedPath)
|
|
21
|
+
|
|
22
|
+
// Handle different export patterns - jiti returns { default: Function } for ESM default exports
|
|
23
|
+
if (esmModule && typeof esmModule === 'object' && esmModule.default) {
|
|
24
|
+
esmModule = esmModule.default
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// For ESM files, we just return the module (object or function)
|
|
28
|
+
// The calling code will determine whether to execute it or not
|
|
29
|
+
return esmModule
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throw new Error(`Failed to load ESM file ${filePath}: ${err.message}`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Synchronous ESM file execution using jiti
|
|
37
|
+
* @param {string} filePath - Full path to the ESM file
|
|
38
|
+
* @param {Object} opts - Additional options including dynamicArgs
|
|
39
|
+
* @returns {*} The result of executing the ESM file
|
|
40
|
+
*/
|
|
41
|
+
function executeESMFileSync(filePath, opts = {}) {
|
|
42
|
+
try {
|
|
43
|
+
// Use jiti to handle ESM syntax synchronously
|
|
44
|
+
const { createJiti } = require('jiti')
|
|
45
|
+
const jiti = createJiti(__filename, {
|
|
46
|
+
interopDefault: true
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Load the ESM file - resolve to absolute path first
|
|
50
|
+
const resolvedPath = path.resolve(filePath)
|
|
51
|
+
let esmModule = jiti(resolvedPath)
|
|
52
|
+
|
|
53
|
+
// Handle different export patterns - jiti returns { default: Function } for ESM default exports
|
|
54
|
+
if (esmModule && typeof esmModule === 'object' && esmModule.default) {
|
|
55
|
+
esmModule = esmModule.default
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// For ESM files, we just return the module (object or function)
|
|
59
|
+
// The calling code will determine whether to execute it or not
|
|
60
|
+
return esmModule
|
|
61
|
+
} catch (err) {
|
|
62
|
+
throw new Error(`Failed to load ESM file ${filePath}: ${err.message}`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
executeESMFile,
|
|
68
|
+
executeESMFileSync
|
|
69
|
+
}
|
package/src/parsers/index.js
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const INI = require('ini')
|
|
2
|
+
const YAML = require('./yaml')
|
|
3
|
+
const JSON5 = require('./json5')
|
|
4
|
+
|
|
5
|
+
function parse(contents) {
|
|
6
|
+
let object
|
|
7
|
+
try {
|
|
8
|
+
object = JSON.parse(JSON.stringify(INI.parse(contents)))
|
|
9
|
+
} catch (e) {
|
|
10
|
+
throw new Error(e)
|
|
11
|
+
}
|
|
12
|
+
return object
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function dump(object) {
|
|
16
|
+
let ini
|
|
17
|
+
try {
|
|
18
|
+
ini = INI.stringify(object)
|
|
19
|
+
} catch (e) {
|
|
20
|
+
throw new Error(e)
|
|
21
|
+
}
|
|
22
|
+
return ini
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toYaml(iniContents) {
|
|
26
|
+
let yml
|
|
27
|
+
try {
|
|
28
|
+
yml = YAML.dump(parse(iniContents))
|
|
29
|
+
} catch (e) {
|
|
30
|
+
throw new Error(e)
|
|
31
|
+
}
|
|
32
|
+
return yml
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toJson(iniContents) {
|
|
36
|
+
let json
|
|
37
|
+
try {
|
|
38
|
+
json = JSON.stringify(parse(iniContents))
|
|
39
|
+
} catch (e) {
|
|
40
|
+
throw new Error(e)
|
|
41
|
+
}
|
|
42
|
+
return json
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
parse: parse,
|
|
47
|
+
dump: dump,
|
|
48
|
+
toYaml: toYaml,
|
|
49
|
+
toYml: toYaml,
|
|
50
|
+
toJson: toJson
|
|
51
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/* eslint-disable no-template-curly-in-string */
|
|
2
|
+
const { test } = require('uvu')
|
|
3
|
+
const assert = require('uvu/assert')
|
|
4
|
+
const ini = require('./ini')
|
|
5
|
+
|
|
6
|
+
function normalize(obj) {
|
|
7
|
+
return JSON.parse(JSON.stringify(obj))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
test('ini parse basic', () => {
|
|
11
|
+
const iniContent = `
|
|
12
|
+
key=value
|
|
13
|
+
number=123
|
|
14
|
+
boolean=true
|
|
15
|
+
|
|
16
|
+
[section]
|
|
17
|
+
sectionKey=sectionValue
|
|
18
|
+
`
|
|
19
|
+
const result = ini.parse(iniContent)
|
|
20
|
+
console.log('result', result)
|
|
21
|
+
|
|
22
|
+
assert.is(result.key, 'value', 'key should be value')
|
|
23
|
+
assert.is(result.number, '123', 'number should be 123')
|
|
24
|
+
assert.is(result.boolean, true, 'boolean should be true')
|
|
25
|
+
assert.equal(normalize(result.section), normalize({
|
|
26
|
+
sectionKey: 'sectionValue'
|
|
27
|
+
}), 'section should be sectionValue')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('ini dump basic', () => {
|
|
31
|
+
const obj = {
|
|
32
|
+
key: 'value',
|
|
33
|
+
number: 123,
|
|
34
|
+
section: {
|
|
35
|
+
sectionKey: 'sectionValue'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const result = ini.dump(obj)
|
|
40
|
+
assert.ok(result.includes('key=value'))
|
|
41
|
+
assert.ok(result.includes('number=123'))
|
|
42
|
+
assert.ok(result.includes('[section]'))
|
|
43
|
+
assert.ok(result.includes('sectionKey=sectionValue'))
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('ini toYaml', () => {
|
|
47
|
+
const iniContent = `
|
|
48
|
+
key=value
|
|
49
|
+
[section]
|
|
50
|
+
sectionKey=sectionValue
|
|
51
|
+
`
|
|
52
|
+
|
|
53
|
+
const result = ini.toYaml(iniContent)
|
|
54
|
+
assert.ok(result.includes('key: value'))
|
|
55
|
+
assert.ok(result.includes('section:'))
|
|
56
|
+
assert.ok(result.includes('sectionKey: sectionValue'))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('ini toJson', () => {
|
|
60
|
+
const iniContent = `
|
|
61
|
+
key=value
|
|
62
|
+
[section]
|
|
63
|
+
sectionKey=sectionValue
|
|
64
|
+
`
|
|
65
|
+
|
|
66
|
+
const result = ini.toJson(iniContent)
|
|
67
|
+
console.log('result', result)
|
|
68
|
+
|
|
69
|
+
const parsed = JSON.parse(result)
|
|
70
|
+
assert.is(parsed.key, 'value')
|
|
71
|
+
assert.equal(parsed.section,{
|
|
72
|
+
sectionKey: 'sectionValue'
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test.skip('ini toYaml complex', () => {
|
|
77
|
+
const iniContent = `
|
|
78
|
+
# Top level values
|
|
79
|
+
string=hello world
|
|
80
|
+
number=42
|
|
81
|
+
boolean=true
|
|
82
|
+
float=3.14
|
|
83
|
+
|
|
84
|
+
[main]
|
|
85
|
+
nestedString=test
|
|
86
|
+
nestedNumber=123
|
|
87
|
+
nestedBoolean=false
|
|
88
|
+
|
|
89
|
+
[main.subsection]
|
|
90
|
+
deeplyNested=value
|
|
91
|
+
array=1,2,3,4,5
|
|
92
|
+
|
|
93
|
+
[another]
|
|
94
|
+
key=value
|
|
95
|
+
`
|
|
96
|
+
|
|
97
|
+
const result = ini.toYaml(iniContent)
|
|
98
|
+
console.log('complex yaml result', result)
|
|
99
|
+
|
|
100
|
+
// Check top level values
|
|
101
|
+
assert.ok(result.includes('string: hello world'), 'string should be hello world')
|
|
102
|
+
assert.ok(result.includes('number: 42'), 'number should be 42')
|
|
103
|
+
assert.ok(result.includes('boolean: true'), 'boolean should be true')
|
|
104
|
+
assert.ok(result.includes('float: 3.14'), 'float should be 3.14')
|
|
105
|
+
|
|
106
|
+
// Check nested structure
|
|
107
|
+
assert.ok(result.includes('main:'))
|
|
108
|
+
assert.ok(result.includes('nestedString: test'), 'nestedString should be test')
|
|
109
|
+
assert.ok(result.includes('nestedNumber: 123'), 'nestedNumber should be 123')
|
|
110
|
+
assert.ok(result.includes('nestedBoolean: false'), 'nestedBoolean should be false')
|
|
111
|
+
|
|
112
|
+
// Check deeply nested structure
|
|
113
|
+
assert.ok(result.includes('subsection:'), 'subsection should be present')
|
|
114
|
+
assert.ok(result.includes('deeplyNested: value'), 'deeplyNested should be value')
|
|
115
|
+
assert.ok(result.includes('array: 1,2,3,4,5'), 'array should be 1,2,3,4,5')
|
|
116
|
+
|
|
117
|
+
// Check another section
|
|
118
|
+
assert.ok(result.includes('another:'))
|
|
119
|
+
assert.ok(result.includes('key: value'))
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test.skip('ini parse error handling', () => {
|
|
123
|
+
let error
|
|
124
|
+
try {
|
|
125
|
+
const result = ini.parse('invalid ini content')
|
|
126
|
+
console.log('ini parse error handling', result)
|
|
127
|
+
} catch (e) {
|
|
128
|
+
error = e
|
|
129
|
+
}
|
|
130
|
+
assert.ok(error instanceof Error)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test.run()
|
package/src/parsers/json5.js
CHANGED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Execute TypeScript file and return its export
|
|
6
|
+
* @param {string} filePath - Full path to the TypeScript file
|
|
7
|
+
* @param {Object} opts - Additional options including dynamicArgs
|
|
8
|
+
* @returns {Promise<*>} The result of executing the TypeScript file
|
|
9
|
+
*/
|
|
10
|
+
async function executeTypeScriptFile(filePath, opts = {}) {
|
|
11
|
+
// Check if tsx is available first (preferred)
|
|
12
|
+
let useTsx = false
|
|
13
|
+
try {
|
|
14
|
+
require.resolve('tsx/cjs/api')
|
|
15
|
+
useTsx = true
|
|
16
|
+
} catch (err) {
|
|
17
|
+
// Fallback to ts-node if tsx is not available
|
|
18
|
+
try {
|
|
19
|
+
require.resolve('ts-node/register')
|
|
20
|
+
} catch (tsNodeErr) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'TypeScript support requires either "tsx" or "ts-node" to be installed. ' +
|
|
23
|
+
'Please install one of them:\n' +
|
|
24
|
+
' npm install tsx --save-dev (recommended)\n' +
|
|
25
|
+
' npm install ts-node typescript --save-dev'
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Clear require cache to ensure fresh execution
|
|
31
|
+
const resolvedPath = require.resolve(filePath)
|
|
32
|
+
delete require.cache[resolvedPath]
|
|
33
|
+
|
|
34
|
+
let tsFile
|
|
35
|
+
if (useTsx) {
|
|
36
|
+
// Use tsx for modern, fast TypeScript execution
|
|
37
|
+
const { register } = require('tsx/cjs/api')
|
|
38
|
+
const restore = register()
|
|
39
|
+
try {
|
|
40
|
+
tsFile = require(filePath)
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw new Error(`Failed to load TypeScript file: ${err.message}`)
|
|
43
|
+
} finally {
|
|
44
|
+
restore()
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
// Fallback to ts-node
|
|
48
|
+
try {
|
|
49
|
+
require('ts-node/register')
|
|
50
|
+
tsFile = require(filePath)
|
|
51
|
+
} catch (err) {
|
|
52
|
+
throw new Error(`Failed to load TypeScript file with ts-node: ${err.message}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof tsFile !== 'function') {
|
|
57
|
+
return tsFile
|
|
58
|
+
} else {
|
|
59
|
+
let tsArgs = opts.dynamicArgs || {}
|
|
60
|
+
if (tsArgs && typeof tsArgs === 'function') {
|
|
61
|
+
tsArgs = tsArgs()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const result = tsFile(tsArgs)
|
|
66
|
+
|
|
67
|
+
// Handle promises
|
|
68
|
+
if (result && typeof result.then === 'function') {
|
|
69
|
+
return await result
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return result
|
|
73
|
+
} catch (err) {
|
|
74
|
+
throw new Error(`Error executing TypeScript function: ${err.message}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Synchronous TypeScript file execution (using tsx with sync API)
|
|
81
|
+
* @param {string} filePath - Full path to the TypeScript file
|
|
82
|
+
* @param {Object} opts - Additional options including dynamicArgs
|
|
83
|
+
* @returns {*} The result of executing the TypeScript file
|
|
84
|
+
*/
|
|
85
|
+
function executeTypeScriptFileSync(filePath, opts = {}) {
|
|
86
|
+
// Check if tsx is available first (preferred)
|
|
87
|
+
let useTsx = false
|
|
88
|
+
try {
|
|
89
|
+
require.resolve('tsx/cjs/api')
|
|
90
|
+
useTsx = true
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// Fallback to ts-node if tsx is not available
|
|
93
|
+
try {
|
|
94
|
+
require.resolve('ts-node/register')
|
|
95
|
+
} catch (tsNodeErr) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
'TypeScript support requires either "tsx" or "ts-node" to be installed. ' +
|
|
98
|
+
'Please install one of them:\n' +
|
|
99
|
+
' npm install tsx --save-dev (recommended)\n' +
|
|
100
|
+
' npm install ts-node typescript --save-dev'
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Clear require cache to ensure fresh execution
|
|
106
|
+
const resolvedPath = require.resolve(filePath)
|
|
107
|
+
delete require.cache[resolvedPath]
|
|
108
|
+
|
|
109
|
+
let tsFile
|
|
110
|
+
if (useTsx) {
|
|
111
|
+
// Use tsx for modern, fast TypeScript execution
|
|
112
|
+
const { register } = require('tsx/cjs/api')
|
|
113
|
+
const restore = register()
|
|
114
|
+
try {
|
|
115
|
+
tsFile = require(filePath)
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new Error(`Failed to load TypeScript file: ${err.message}`)
|
|
118
|
+
} finally {
|
|
119
|
+
restore()
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
// Fallback to ts-node
|
|
123
|
+
try {
|
|
124
|
+
require('ts-node/register')
|
|
125
|
+
tsFile = require(filePath)
|
|
126
|
+
} catch (err) {
|
|
127
|
+
throw new Error(`Failed to load TypeScript file with ts-node: ${err.message}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (typeof tsFile !== 'function') {
|
|
132
|
+
return tsFile
|
|
133
|
+
} else {
|
|
134
|
+
let tsArgs = opts.dynamicArgs || {}
|
|
135
|
+
if (tsArgs && typeof tsArgs === 'function') {
|
|
136
|
+
tsArgs = tsArgs()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const result = tsFile(tsArgs)
|
|
141
|
+
|
|
142
|
+
// Note: For sync execution, we don't await promises
|
|
143
|
+
// If the function returns a promise, it will be resolved by the calling code
|
|
144
|
+
return result
|
|
145
|
+
} catch (err) {
|
|
146
|
+
throw new Error(`Error executing TypeScript function: ${err.message}`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
executeTypeScriptFile,
|
|
153
|
+
executeTypeScriptFileSync
|
|
154
|
+
}
|
package/src/parsers/yaml.test.js
CHANGED
|
@@ -110,7 +110,7 @@ resolvedDomainNameTwo: \${domainsTwo.\${opt:stage, "prod"}}
|
|
|
110
110
|
assert.equal(result, expected)
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
test('preProcess - should wrap variables in quotes inside array brackets', () => {
|
|
113
|
+
test('preProcess - should wrap variables in quotes inside array brackets two', () => {
|
|
114
114
|
const input = `
|
|
115
115
|
service: my-service
|
|
116
116
|
custom:
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const cronRefSyntax = RegExp(/^cron\((~?[\{\}\:\$a-zA-Z0-9._\-\/,'"\*\` ]+?)?\)/g)
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert human-readable strings to cron expressions
|
|
5
|
+
* Based on common patterns and schedules
|
|
6
|
+
*/
|
|
7
|
+
function parseCronExpression(input) {
|
|
8
|
+
if (!input || typeof input !== 'string') {
|
|
9
|
+
throw new Error('Cron input must be a non-empty string')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const normalizedInput = input.toLowerCase().trim()
|
|
13
|
+
|
|
14
|
+
// Pre-defined common cron expressions
|
|
15
|
+
const cronMap = {
|
|
16
|
+
// Every minute/hour/day patterns
|
|
17
|
+
'every minute': '* * * * *',
|
|
18
|
+
'every hour': '0 * * * *',
|
|
19
|
+
'every day': '0 0 * * *',
|
|
20
|
+
'every week': '0 0 * * 0',
|
|
21
|
+
'every month': '0 0 1 * *',
|
|
22
|
+
'every year': '0 0 1 1 *',
|
|
23
|
+
'yearly': '0 0 1 1 *',
|
|
24
|
+
'annually': '0 0 1 1 *',
|
|
25
|
+
'monthly': '0 0 1 * *',
|
|
26
|
+
'weekly': '0 0 * * 0',
|
|
27
|
+
'daily': '0 0 * * *',
|
|
28
|
+
'hourly': '0 * * * *',
|
|
29
|
+
|
|
30
|
+
// Common business schedules
|
|
31
|
+
'weekdays': '0 0 * * 1-5',
|
|
32
|
+
'weekends': '0 0 * * 0,6',
|
|
33
|
+
'business hours': '0 9-17 * * 1-5',
|
|
34
|
+
'after hours': '0 18-8 * * *',
|
|
35
|
+
|
|
36
|
+
// Specific times
|
|
37
|
+
'midnight': '0 0 * * *',
|
|
38
|
+
'noon': '0 12 * * *',
|
|
39
|
+
'morning': '0 9 * * *',
|
|
40
|
+
'evening': '0 18 * * *',
|
|
41
|
+
|
|
42
|
+
// Interval patterns
|
|
43
|
+
'every 5 minutes': '*/5 * * * *',
|
|
44
|
+
'every 10 minutes': '*/10 * * * *',
|
|
45
|
+
'every 15 minutes': '*/15 * * * *',
|
|
46
|
+
'every 30 minutes': '*/30 * * * *',
|
|
47
|
+
'every 2 hours': '0 */2 * * *',
|
|
48
|
+
'every 3 hours': '0 */3 * * *',
|
|
49
|
+
'every 6 hours': '0 */6 * * *',
|
|
50
|
+
'every 12 hours': '0 */12 * * *',
|
|
51
|
+
|
|
52
|
+
// Days of week
|
|
53
|
+
'monday': '0 0 * * 1',
|
|
54
|
+
'tuesday': '0 0 * * 2',
|
|
55
|
+
'wednesday': '0 0 * * 3',
|
|
56
|
+
'thursday': '0 0 * * 4',
|
|
57
|
+
'friday': '0 0 * * 5',
|
|
58
|
+
'saturday': '0 0 * * 6',
|
|
59
|
+
'sunday': '0 0 * * 0',
|
|
60
|
+
|
|
61
|
+
// Monthly patterns
|
|
62
|
+
'first day of month': '0 0 1 * *',
|
|
63
|
+
'last day of month': '0 0 L * *',
|
|
64
|
+
'middle of month': '0 0 15 * *',
|
|
65
|
+
|
|
66
|
+
// Special patterns
|
|
67
|
+
'never': '0 0 30 2 *', // Feb 30th (never occurs)
|
|
68
|
+
'reboot': '@reboot',
|
|
69
|
+
'startup': '@reboot'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check direct mapping first
|
|
73
|
+
if (cronMap[normalizedInput]) {
|
|
74
|
+
return cronMap[normalizedInput]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Parse "at X:XX" patterns (e.g., "at 9:30", "at 14:00")
|
|
78
|
+
const atTimeMatch = normalizedInput.match(/^at (\d{1,2}):(\d{2})(\s*(am|pm))?$/i)
|
|
79
|
+
if (atTimeMatch) {
|
|
80
|
+
let hour = parseInt(atTimeMatch[1])
|
|
81
|
+
const minute = parseInt(atTimeMatch[2])
|
|
82
|
+
const amPm = atTimeMatch[4]
|
|
83
|
+
|
|
84
|
+
if (amPm && amPm.toLowerCase() === 'pm' && hour !== 12) {
|
|
85
|
+
hour += 12
|
|
86
|
+
} else if (amPm && amPm.toLowerCase() === 'am' && hour === 12) {
|
|
87
|
+
hour = 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return `${minute} ${hour} * * *`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Parse "every X minutes/hours/days" patterns
|
|
94
|
+
const everyMatch = normalizedInput.match(/^every (\d+) (minute|minutes|hour|hours|day|days|week|weeks|month|months)s?$/i)
|
|
95
|
+
if (everyMatch) {
|
|
96
|
+
const interval = parseInt(everyMatch[1])
|
|
97
|
+
const unit = everyMatch[2].toLowerCase().replace(/s$/, '') // Remove trailing 's' if present
|
|
98
|
+
|
|
99
|
+
switch (unit) {
|
|
100
|
+
case 'minute':
|
|
101
|
+
return `*/${interval} * * * *`
|
|
102
|
+
case 'hour':
|
|
103
|
+
return `0 */${interval} * * *`
|
|
104
|
+
case 'day':
|
|
105
|
+
return `0 0 */${interval} * *`
|
|
106
|
+
case 'week':
|
|
107
|
+
return `0 0 * * 0/${interval}`
|
|
108
|
+
case 'month':
|
|
109
|
+
return `0 0 1 */${interval} *`
|
|
110
|
+
default:
|
|
111
|
+
throw new Error(`Unsupported interval unit: ${unit}`)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Parse "X minute(s)/hour(s)/day(s)" patterns (e.g., "1 minute", "5 minutes", "1 hour")
|
|
116
|
+
const intervalMatch = normalizedInput.match(/^(\d+) (minute|minutes|hour|hours|day|days|week|weeks|month|months)s?$/i)
|
|
117
|
+
if (intervalMatch) {
|
|
118
|
+
const interval = parseInt(intervalMatch[1])
|
|
119
|
+
const unit = intervalMatch[2].toLowerCase().replace(/s$/, '') // Remove trailing 's' if present
|
|
120
|
+
|
|
121
|
+
switch (unit) {
|
|
122
|
+
case 'minute':
|
|
123
|
+
return `*/${interval} * * * *`
|
|
124
|
+
case 'hour':
|
|
125
|
+
return `0 */${interval} * * *`
|
|
126
|
+
case 'day':
|
|
127
|
+
return `0 0 */${interval} * *`
|
|
128
|
+
case 'week':
|
|
129
|
+
return `0 0 * * 0/${interval}`
|
|
130
|
+
case 'month':
|
|
131
|
+
return `0 0 1 */${interval} *`
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`Unsupported interval unit: ${unit}`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Parse "on Xst/nd/rd/th of month at time" patterns (e.g., "on 1st of month at 00:00")
|
|
138
|
+
const ordinalDateMatch = normalizedInput.match(/^on (\d+)(?:st|nd|rd|th) of month at (\d{1,2}):(\d{2})(\s*(am|pm))?$/i)
|
|
139
|
+
if (ordinalDateMatch) {
|
|
140
|
+
const dayOfMonth = parseInt(ordinalDateMatch[1])
|
|
141
|
+
let hour = parseInt(ordinalDateMatch[2])
|
|
142
|
+
const minute = parseInt(ordinalDateMatch[3])
|
|
143
|
+
const amPm = ordinalDateMatch[5]
|
|
144
|
+
|
|
145
|
+
if (amPm && amPm.toLowerCase() === 'pm' && hour !== 12) {
|
|
146
|
+
hour += 12
|
|
147
|
+
} else if (amPm && amPm.toLowerCase() === 'am' && hour === 12) {
|
|
148
|
+
hour = 0
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return `${minute} ${hour} ${dayOfMonth} * *`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Parse "on weekday at time" patterns (e.g., "on monday at 9:00")
|
|
155
|
+
const weekdayTimeMatch = normalizedInput.match(/^on ((?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)(?:,(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday))*?) at (\d{1,2}):(\d{2})(\s*(am|pm))?$/i)
|
|
156
|
+
if (weekdayTimeMatch) {
|
|
157
|
+
const dayMap = {
|
|
158
|
+
'sunday': 0, 'monday': 1, 'tuesday': 2, 'wednesday': 3,
|
|
159
|
+
'thursday': 4, 'friday': 5, 'saturday': 6
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Extract all days from the match
|
|
163
|
+
const days = weekdayTimeMatch[1].split(',').map(day => day.trim())
|
|
164
|
+
const dayOfWeek = days.map(day => dayMap[day.toLowerCase()]).join(',')
|
|
165
|
+
|
|
166
|
+
let hour = parseInt(weekdayTimeMatch[2])
|
|
167
|
+
const minute = parseInt(weekdayTimeMatch[3])
|
|
168
|
+
const amPm = weekdayTimeMatch[5]
|
|
169
|
+
|
|
170
|
+
if (amPm && amPm.toLowerCase() === 'pm' && hour !== 12) {
|
|
171
|
+
hour += 12
|
|
172
|
+
} else if (amPm && amPm.toLowerCase() === 'am' && hour === 12) {
|
|
173
|
+
hour = 0
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return `${minute} ${hour} * * ${dayOfWeek}`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Parse "on weekdays/weekends at time" patterns (e.g., "on weekdays at 9:00")
|
|
180
|
+
const weekdaysTimeMatch = normalizedInput.match(/^on (weekdays|weekends) at (\d{1,2}):(\d{2})(\s*(am|pm))?$/i)
|
|
181
|
+
if (weekdaysTimeMatch) {
|
|
182
|
+
const dayRange = weekdaysTimeMatch[1].toLowerCase() === 'weekdays' ? '1-5' : '0,6'
|
|
183
|
+
let hour = parseInt(weekdaysTimeMatch[2])
|
|
184
|
+
const minute = parseInt(weekdaysTimeMatch[3])
|
|
185
|
+
const amPm = weekdaysTimeMatch[5]
|
|
186
|
+
|
|
187
|
+
if (amPm && amPm.toLowerCase() === 'pm' && hour !== 12) {
|
|
188
|
+
hour += 12
|
|
189
|
+
} else if (amPm && amPm.toLowerCase() === 'am' && hour === 12) {
|
|
190
|
+
hour = 0
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return `${minute} ${hour} * * ${dayRange}`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check if it's already a valid cron expression (5 or 6 parts)
|
|
197
|
+
const parts = normalizedInput.split(/\s+/)
|
|
198
|
+
if (parts.length === 5 || parts.length === 6) {
|
|
199
|
+
// Basic validation for cron format
|
|
200
|
+
if (parts.every(part => /^[@*\d,\-\/]+$/.test(part) || part.startsWith('@'))) {
|
|
201
|
+
return normalizedInput
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// If no pattern matches, throw an error with suggestions
|
|
206
|
+
const suggestions = Object.keys(cronMap).slice(0, 10).join(', ')
|
|
207
|
+
throw new Error(`Unrecognized cron pattern: "${input}". Supported patterns include: ${suggestions}`)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getValueFromCron(variableString) {
|
|
211
|
+
// Get value from cron(expression)
|
|
212
|
+
const cronExpression = variableString.match(/cron\((.*)\)/)[1]
|
|
213
|
+
// console.log('cronExpression', cronExpression)
|
|
214
|
+
|
|
215
|
+
if (!cronExpression || cronExpression.trim() === '') {
|
|
216
|
+
throw new Error(`Invalid variable syntax for cron reference "${variableString}".
|
|
217
|
+
|
|
218
|
+
\${cron} variable must have a pattern.
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
\${cron("every minute")}
|
|
222
|
+
\${cron("weekdays")}
|
|
223
|
+
\${cron("at 9:30")}
|
|
224
|
+
\${cron("every 5 minutes")}
|
|
225
|
+
`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Remove surrounding quotes if present
|
|
229
|
+
const cleanExpression = cronExpression.replace(/^['"`](.*)['"`]$/, '$1')
|
|
230
|
+
|
|
231
|
+
// If already a cron expression, return it
|
|
232
|
+
if (cleanExpression.match(/^[\*\/,\-\d]+$/)) {
|
|
233
|
+
return cleanExpression
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const resolvedCron = parseCronExpression(cleanExpression)
|
|
238
|
+
// console.log('resolvedCron', resolvedCron)
|
|
239
|
+
return Promise.resolve(resolvedCron)
|
|
240
|
+
} catch (error) {
|
|
241
|
+
throw new Error(`Failed to parse cron expression "${cleanExpression}": ${error.message}`)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
type: 'cron',
|
|
247
|
+
prefix: 'cron',
|
|
248
|
+
match: cronRefSyntax,
|
|
249
|
+
resolver: getValueFromCron,
|
|
250
|
+
// Export the parser for testing
|
|
251
|
+
_parseCronExpression: parseCronExpression
|
|
252
|
+
}
|