mushroom-cli 1.2.8 → 1.2.9
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.
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const extEvents = {
|
|
2
|
+
auth: {
|
|
3
|
+
'Events.Login.Before': {
|
|
4
|
+
type: 'Before',
|
|
5
|
+
},
|
|
6
|
+
'Events.Login.Validate.Before': {
|
|
7
|
+
type: 'Before',
|
|
8
|
+
},
|
|
9
|
+
'Events.Login.Validate.After': {
|
|
10
|
+
type: 'After',
|
|
11
|
+
},
|
|
12
|
+
'Events.Login.CheckLocking.Before': {
|
|
13
|
+
type: 'Before',
|
|
14
|
+
},
|
|
15
|
+
'Events.Login.CheckLocking.After': {
|
|
16
|
+
type: 'After',
|
|
17
|
+
data: 'Mushroom.Extensions.DefaultAuth.UserLock',
|
|
18
|
+
},
|
|
19
|
+
CustomLoginAsyncFunc: {
|
|
20
|
+
type: 'Func',
|
|
21
|
+
registerFunc: 'RegisterCustomLoginAsyncHandler',
|
|
22
|
+
},
|
|
23
|
+
'Events.Login.LogAuth.Before': {
|
|
24
|
+
type: 'Before',
|
|
25
|
+
},
|
|
26
|
+
GetAuthLogInfoAsyncFunc: {
|
|
27
|
+
type: 'Func',
|
|
28
|
+
registerFunc: 'RegisterGetAuthLogInfoAsyncHandler',
|
|
29
|
+
},
|
|
30
|
+
'Events.Login.LogAuth.After': {
|
|
31
|
+
type: 'After',
|
|
32
|
+
data: 'Mushroom.Extensions.DefaultAuth.UserAuthLogResult',
|
|
33
|
+
},
|
|
34
|
+
'Events.Login.LoginFail': {
|
|
35
|
+
type: 'After',
|
|
36
|
+
},
|
|
37
|
+
'Events.Login.LoginSuccess': {
|
|
38
|
+
type: 'Event',
|
|
39
|
+
registerFunc: 'RegisterLoginSuccessHandler',
|
|
40
|
+
},
|
|
41
|
+
'Events.Login.After': {
|
|
42
|
+
type: 'After',
|
|
43
|
+
data: 'string',
|
|
44
|
+
},
|
|
45
|
+
'Events.Logout.Before': {
|
|
46
|
+
type: 'Before',
|
|
47
|
+
},
|
|
48
|
+
'Events.Logout.After': {
|
|
49
|
+
type: 'After',
|
|
50
|
+
data: 'bool',
|
|
51
|
+
},
|
|
52
|
+
'Events.Status.Before': {
|
|
53
|
+
type: 'Before',
|
|
54
|
+
},
|
|
55
|
+
'Events.Status.After': {
|
|
56
|
+
type: 'After',
|
|
57
|
+
data: 'bool',
|
|
58
|
+
},
|
|
59
|
+
'Events.Me.Before': {
|
|
60
|
+
type: 'Before',
|
|
61
|
+
},
|
|
62
|
+
'Events.Me.After': {
|
|
63
|
+
type: 'After',
|
|
64
|
+
data: 'Mushroom.Extensions.DefaultAuth.User',
|
|
65
|
+
},
|
|
66
|
+
'Events.Register.Before': {
|
|
67
|
+
type: 'Before',
|
|
68
|
+
},
|
|
69
|
+
SendCodeItemFunc: {
|
|
70
|
+
type: 'Func',
|
|
71
|
+
registerFunc: 'RegisterSendCodeItemHandler',
|
|
72
|
+
},
|
|
73
|
+
'Events.Register.After': {
|
|
74
|
+
type: 'After',
|
|
75
|
+
data: 'Mushroom.Extensions.DefaultAuth.User',
|
|
76
|
+
},
|
|
77
|
+
'Events.Activate.Before': {
|
|
78
|
+
type: 'Before',
|
|
79
|
+
},
|
|
80
|
+
'Events.Activate.After': {
|
|
81
|
+
type: 'After',
|
|
82
|
+
data: 'Mushroom.Extensions.DefaultAuth.User',
|
|
83
|
+
},
|
|
84
|
+
'Events.RecoverPassword.Before': {
|
|
85
|
+
type: 'Before',
|
|
86
|
+
},
|
|
87
|
+
'Events.RecoverPassword.After': {
|
|
88
|
+
type: 'After',
|
|
89
|
+
data: 'Mushroom.Extensions.DefaultAuth.User',
|
|
90
|
+
},
|
|
91
|
+
'Events.ResetPassword.Before': {
|
|
92
|
+
type: 'Before',
|
|
93
|
+
},
|
|
94
|
+
'Events.ResetPassword.After': {
|
|
95
|
+
type: 'After',
|
|
96
|
+
data: 'Mushroom.Extensions.DefaultAuth.User',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
file: {
|
|
100
|
+
'Events.Upload.Before': {
|
|
101
|
+
type: 'Before',
|
|
102
|
+
},
|
|
103
|
+
'Events.Upload.After': {
|
|
104
|
+
type: 'After',
|
|
105
|
+
},
|
|
106
|
+
'Events.Thumb.Before': {
|
|
107
|
+
type: 'Before',
|
|
108
|
+
},
|
|
109
|
+
'Events.Thumb.After': {
|
|
110
|
+
type: 'After',
|
|
111
|
+
},
|
|
112
|
+
'Events.Download.Before': {
|
|
113
|
+
type: 'Before',
|
|
114
|
+
},
|
|
115
|
+
'Events.Download.After': {
|
|
116
|
+
type: 'After',
|
|
117
|
+
},
|
|
118
|
+
'Events.Zip.Before': {
|
|
119
|
+
type: 'Before',
|
|
120
|
+
},
|
|
121
|
+
'Events.Zip.After': {
|
|
122
|
+
type: 'After',
|
|
123
|
+
},
|
|
124
|
+
LimitFileUploadSizeFunc: {
|
|
125
|
+
type: 'Func',
|
|
126
|
+
data: 'System.Threading.Tasks.Task<long?>',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
mail: {
|
|
130
|
+
AuthorizationFunc: {
|
|
131
|
+
type: 'Func',
|
|
132
|
+
data: 'System.Threading.Tasks.Task<bool>, Mushroom.Extensions.Mailing.MailingAuthorizationArgument',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
export const getExtensionEventsCode = (source, newLine) => {
|
|
137
|
+
const eventCodes = [];
|
|
138
|
+
const addEventCodes = (key, code, source) => {
|
|
139
|
+
const eventCode = eventCodes.find((ec) => ec.key === key);
|
|
140
|
+
if (eventCode) {
|
|
141
|
+
eventCode.source.push(source);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
eventCodes.push({
|
|
145
|
+
key,
|
|
146
|
+
code,
|
|
147
|
+
source: [source],
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
Object.keys(extEvents).forEach((ext) => {
|
|
151
|
+
const extension = source.extensions?.[ext];
|
|
152
|
+
if (!extension?.config)
|
|
153
|
+
return;
|
|
154
|
+
const config = extension.config;
|
|
155
|
+
Object.keys(extEvents[ext]).forEach((configName) => {
|
|
156
|
+
const def = extEvents[ext][configName];
|
|
157
|
+
const configValue = config[configName];
|
|
158
|
+
if (!configValue || typeof configValue !== 'string' || !configValue.startsWith('lib:'))
|
|
159
|
+
return;
|
|
160
|
+
if (def.type == 'Func') {
|
|
161
|
+
const defaultFunc = `RegisterCustomMethod<${def.data || 'System.Threading.Tasks.Task'}>`;
|
|
162
|
+
addEventCodes(ext + '.' + configName, `${def.registerFunc || defaultFunc}(${configValue.substring(4)});`, `extension: ${ext} - config: ${configName}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (def.type == 'Event') {
|
|
166
|
+
configValue.split(',').forEach((lib) => {
|
|
167
|
+
if (!lib || !lib.startsWith('lib:'))
|
|
168
|
+
return;
|
|
169
|
+
addEventCodes(ext + '.' + configName, `${def.registerFunc || 'RegisterEvent'}(${lib.substring(4)});`, `extension: ${ext} - config: ${configName}`);
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (def.type == 'Before') {
|
|
174
|
+
configValue.split(',').forEach((lib) => {
|
|
175
|
+
if (!lib || !lib.startsWith('lib:'))
|
|
176
|
+
return;
|
|
177
|
+
addEventCodes(ext + '.' + configName, `${def.registerFunc || 'RegisterBeforeEvent'}(${lib.substring(4)});`, `extension: ${ext} - config: ${configName}`);
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (def.type == 'After') {
|
|
182
|
+
configValue.split(',').forEach((lib) => {
|
|
183
|
+
if (!lib || !lib.startsWith('lib:'))
|
|
184
|
+
return;
|
|
185
|
+
addEventCodes(ext + '.' + configName, `${def.registerFunc || (def.data ? `RegisterAfterEvent<${def.data}>` : 'RegisterAfterEvent')}(${lib.substring(4)});`, `extension: ${ext} - config: ${configName}`);
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
return '';
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
return eventCodes
|
|
193
|
+
.map((ec) => `${ec.source.map((s) => ' // ' + s).join(newLine)}${newLine} ${ec.code}`)
|
|
194
|
+
.join(newLine);
|
|
195
|
+
};
|
package/dist/gen/gen-csharp.js
CHANGED
|
@@ -435,7 +435,21 @@ namespace ${namespace}
|
|
|
435
435
|
public static class DbConnector
|
|
436
436
|
{
|
|
437
437
|
${collectionNames}
|
|
438
|
-
public static IMongoDatabase GetDatabase()
|
|
438
|
+
public static IMongoDatabase GetDatabase()
|
|
439
|
+
{
|
|
440
|
+
try
|
|
441
|
+
{
|
|
442
|
+
if (Mushroom.MushroomDaoFactory.GetDbAdapter() is IMongoDatabase db) return db;
|
|
443
|
+
}
|
|
444
|
+
catch
|
|
445
|
+
{
|
|
446
|
+
// Not in mushrom context
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Use DbAdapter of the first project in MushroomServer for default
|
|
450
|
+
var project = Mushroom.MushroomServer.GetAllApis().First();
|
|
451
|
+
return Mushroom.MushroomDaoFactory.GetDbAdapter(project.IdType, project.ConnectionString) as IMongoDatabase;
|
|
452
|
+
}
|
|
439
453
|
}
|
|
440
454
|
|
|
441
455
|
public class ResourceNames
|
|
@@ -790,7 +804,21 @@ namespace ${namespace}
|
|
|
790
804
|
public static class DbConnector
|
|
791
805
|
{
|
|
792
806
|
${collectionNames}
|
|
793
|
-
public static IMongoDatabase GetDatabase()
|
|
807
|
+
public static IMongoDatabase GetDatabase()
|
|
808
|
+
{
|
|
809
|
+
try
|
|
810
|
+
{
|
|
811
|
+
if (Mushroom.MushroomDaoFactory.GetDbAdapter() is IMongoDatabase db) return db;
|
|
812
|
+
}
|
|
813
|
+
catch
|
|
814
|
+
{
|
|
815
|
+
// Not in mushrom context
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Use DbAdapter of the first project in MushroomServer for default
|
|
819
|
+
var project = Mushroom.MushroomServer.GetAllApis().First();
|
|
820
|
+
return Mushroom.MushroomDaoFactory.GetDbAdapter(project.IdType, project.ConnectionString) as IMongoDatabase;
|
|
821
|
+
}
|
|
794
822
|
}
|
|
795
823
|
}
|
|
796
824
|
`;
|
package/dist/gen/gen-list-api.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as common from '../common.js';
|
|
2
2
|
export function listApi(source) {
|
|
3
|
-
const entities = common.getEntities(source, [
|
|
3
|
+
const entities = common.getEntities(source, ['*'], true);
|
|
4
4
|
const endpoints = new Set();
|
|
5
5
|
for (const entityName in entities) {
|
|
6
6
|
const actions = entities[entityName].actions || {};
|
|
7
7
|
for (const actionName in actions) {
|
|
8
8
|
const action = actions[actionName];
|
|
9
|
-
if (!Array.isArray(action) || !action.length || action.every(a => !a.enabled))
|
|
9
|
+
if (!Array.isArray(action) || !action.length || action.every((a) => !a.enabled))
|
|
10
10
|
continue;
|
|
11
11
|
let method;
|
|
12
12
|
let id = '';
|
|
@@ -43,10 +43,10 @@ export function listApi(source) {
|
|
|
43
43
|
const views = entities[entityName].views || {};
|
|
44
44
|
for (const viewName in views) {
|
|
45
45
|
const view = views[viewName];
|
|
46
|
-
if (!Array.isArray(view) || !view.length || view.every(v => !v.enabled))
|
|
46
|
+
if (!Array.isArray(view) || !view.length || view.every((v) => !v.enabled))
|
|
47
47
|
continue;
|
|
48
48
|
endpoints.add(`GET ${source.rootUrl}${entityName}s/views/${viewName}`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
endpoints.forEach(e => console.log(e));
|
|
51
|
+
endpoints.forEach((e) => console.log(e));
|
|
52
52
|
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import readlineSync from 'readline-sync';
|
|
4
|
+
import * as common from '../../common.js';
|
|
5
|
+
export function process(basePath, templatePath) {
|
|
6
|
+
const templateJsonContent = fs.readFileSync(path.join(basePath, templatePath)).toString();
|
|
7
|
+
const template = JSON.parse(templateJsonContent);
|
|
8
|
+
if (template && typeof template !== 'object') {
|
|
9
|
+
throw new TypeError(`Invalid template file ${templatePath}`);
|
|
10
|
+
}
|
|
11
|
+
const commands = template.steps;
|
|
12
|
+
if (!Array.isArray(commands)) {
|
|
13
|
+
throw new TypeError(`Invalid template file ${templatePath}`);
|
|
14
|
+
}
|
|
15
|
+
const variables = {};
|
|
16
|
+
commands.forEach((command) => processCommand(command, basePath, variables));
|
|
17
|
+
return variables;
|
|
18
|
+
}
|
|
19
|
+
function processCommand(command, basePath, variables) {
|
|
20
|
+
const commandName = command.command;
|
|
21
|
+
switch (commandName) {
|
|
22
|
+
case 'input':
|
|
23
|
+
processInputCommand(command, variables);
|
|
24
|
+
return;
|
|
25
|
+
case 'set':
|
|
26
|
+
processSetCommand(command, variables);
|
|
27
|
+
return;
|
|
28
|
+
case 'rename':
|
|
29
|
+
processRenameCommand(command, basePath, variables);
|
|
30
|
+
return;
|
|
31
|
+
case 'delete':
|
|
32
|
+
processDeleteCommand(command, basePath, variables);
|
|
33
|
+
return;
|
|
34
|
+
case 'replace':
|
|
35
|
+
processReplaceCommand(command, basePath, variables);
|
|
36
|
+
return;
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`Unknown command: ${commandName}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function processInputCommand(command, variables) {
|
|
42
|
+
const value = readlineSync.question(command.message ?? '');
|
|
43
|
+
if (command.validation) {
|
|
44
|
+
if (command.validation.required) {
|
|
45
|
+
if (value === '')
|
|
46
|
+
throw new TypeError(`${command.variable} is required`);
|
|
47
|
+
}
|
|
48
|
+
if (command.validation.pattern) {
|
|
49
|
+
if (!new RegExp(command.validation.pattern).test(value))
|
|
50
|
+
throw new Error(`${value} is not valid`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
variables[command.variable] = value;
|
|
54
|
+
}
|
|
55
|
+
function processSetCommand(command, variables) {
|
|
56
|
+
switch (command['function']) {
|
|
57
|
+
case 'New-GUID':
|
|
58
|
+
processSetNewGuidCommand(command.variable, variables);
|
|
59
|
+
break;
|
|
60
|
+
case 'New-RandomNumber':
|
|
61
|
+
processSetNewRandomNumberCommand(command.variable, variables, command.min, command.max);
|
|
62
|
+
break;
|
|
63
|
+
case 'New-RandomString':
|
|
64
|
+
processSetNewRandomStringCommand(command.variable, variables, command.length, command.characterPool);
|
|
65
|
+
break;
|
|
66
|
+
case 'ToLowerCase':
|
|
67
|
+
toLowerCommand(command.variable, variables, command.input);
|
|
68
|
+
break;
|
|
69
|
+
case 'ToUpperCase':
|
|
70
|
+
toUpperCaseCommand(command.variable, variables, command.input);
|
|
71
|
+
break;
|
|
72
|
+
case 'Trim':
|
|
73
|
+
trimCommand(command.variable, variables, command.input);
|
|
74
|
+
break;
|
|
75
|
+
case 'Replace':
|
|
76
|
+
replaceCommand(command.variable, variables, command.input, command.replacement, command.pattern, command.mode);
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
throw new Error(`Unknown set-command: ${command['function']}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function processSetNewGuidCommand(variable, variables) {
|
|
83
|
+
variables[variable] = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
|
|
84
|
+
.replace(/[xy]/g, (c) => {
|
|
85
|
+
const r = (Math.random() * 16) | 0;
|
|
86
|
+
const v = c == 'x' ? r : (r & 0x3) | 0x8;
|
|
87
|
+
return v.toString(16);
|
|
88
|
+
})
|
|
89
|
+
.toUpperCase();
|
|
90
|
+
}
|
|
91
|
+
function processSetNewRandomNumberCommand(variable, variables, min = 0, max = (1 << 30) * 4) {
|
|
92
|
+
variables[variable] = Math.floor(min + Math.random() * (max - min)).toString();
|
|
93
|
+
}
|
|
94
|
+
function processSetNewRandomStringCommand(variable, variables, length, characterPool = '1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM') {
|
|
95
|
+
let result = '';
|
|
96
|
+
for (let i = 0; i < length; i++) {
|
|
97
|
+
result += characterPool.charAt(Math.floor(Math.random() * characterPool.length));
|
|
98
|
+
}
|
|
99
|
+
variables[variable] = result;
|
|
100
|
+
}
|
|
101
|
+
function toLowerCommand(variable, variables, input) {
|
|
102
|
+
variables[variable] = variables[input].toLowerCase();
|
|
103
|
+
}
|
|
104
|
+
function toUpperCaseCommand(variable, variables, input) {
|
|
105
|
+
variables[variable] = variables[input].toUpperCase();
|
|
106
|
+
}
|
|
107
|
+
function trimCommand(variable, variables, input) {
|
|
108
|
+
variables[variable] = variables[input].trim();
|
|
109
|
+
}
|
|
110
|
+
function replaceCommand(variable, variables, input, replacement, pattern, mode) {
|
|
111
|
+
const regex = new RegExp(pattern, mode);
|
|
112
|
+
variables[variable] = variables[input].replace(regex, replacement);
|
|
113
|
+
}
|
|
114
|
+
function processRenameCommand(command, basePath, variables) {
|
|
115
|
+
const input = replaceVariables(command.input, variables);
|
|
116
|
+
const output = replaceVariables(command.output, variables);
|
|
117
|
+
fs.renameSync(path.join(basePath, input), path.join(basePath, output));
|
|
118
|
+
}
|
|
119
|
+
function processDeleteCommand(command, basePath, variables) {
|
|
120
|
+
const input = replaceVariables(command.input, variables);
|
|
121
|
+
const deletedPath = path.join(basePath, input);
|
|
122
|
+
if (fs.lstatSync(deletedPath).isDirectory()) {
|
|
123
|
+
fs.rmSync(deletedPath, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
fs.rmSync(deletedPath, { force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function processReplaceCommand(command, basePath, variables) {
|
|
130
|
+
const files = command.input;
|
|
131
|
+
const pattern = new RegExp(command.pattern ?? '', 'g');
|
|
132
|
+
const replacement = replaceVariables(command.replacement ?? '', variables);
|
|
133
|
+
files.forEach((file) => {
|
|
134
|
+
const resolvedInput = replaceVariables(file, variables);
|
|
135
|
+
const filePaths = resolveInputPaths(basePath, resolvedInput);
|
|
136
|
+
if (filePaths.length === 0) {
|
|
137
|
+
common.logWarning(`File ${file} does not exist, skipping...`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
filePaths.forEach((filePath) => {
|
|
141
|
+
let content = fs.readFileSync(filePath).toString();
|
|
142
|
+
content = content.replace(pattern, () => replacement);
|
|
143
|
+
fs.writeFileSync(filePath, content);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function resolveInputPaths(basePath, input) {
|
|
148
|
+
const fileName = path.basename(input);
|
|
149
|
+
if (/[*?]/.test(fileName)) {
|
|
150
|
+
const dir = path.join(basePath, path.dirname(input));
|
|
151
|
+
if (!fs.existsSync(dir)) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
return findFilesByGlob(dir, globToRegExp(fileName));
|
|
155
|
+
}
|
|
156
|
+
const filePath = path.join(basePath, input);
|
|
157
|
+
return fs.existsSync(filePath) ? [filePath] : [];
|
|
158
|
+
}
|
|
159
|
+
function globToRegExp(glob) {
|
|
160
|
+
const pattern = glob
|
|
161
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
162
|
+
.replace(/\*/g, '.*')
|
|
163
|
+
.replace(/\?/g, '.');
|
|
164
|
+
return new RegExp(`^${pattern}$`, 'i');
|
|
165
|
+
}
|
|
166
|
+
function findFilesByGlob(dir, fileNamePattern) {
|
|
167
|
+
const results = [];
|
|
168
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
169
|
+
entries.forEach((entry) => {
|
|
170
|
+
const entryPath = path.join(dir, entry.name);
|
|
171
|
+
if (entry.isDirectory()) {
|
|
172
|
+
results.push(...findFilesByGlob(entryPath, fileNamePattern));
|
|
173
|
+
}
|
|
174
|
+
else if (fileNamePattern.test(entry.name)) {
|
|
175
|
+
results.push(entryPath);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
return results;
|
|
179
|
+
}
|
|
180
|
+
function replaceVariables(str, variables) {
|
|
181
|
+
return str.replace(/\$\{(\w+)\}/g, (_, varName) => variables[varName] ?? '');
|
|
182
|
+
}
|