zyket 1.0.15 → 1.0.17
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 +134 -23
- package/index.js +3 -0
- package/package.json +3 -1
- package/src/services/bullmq/Worker.js +8 -0
- package/src/services/bullmq/index.js +75 -0
- package/src/services/events/Event.js +7 -0
- package/src/services/events/index.js +60 -0
- package/src/services/index.js +19 -5
- package/src/services/scheduler/Schedule.js +7 -0
- package/src/services/scheduler/index.js +42 -0
- package/src/utils/EnvManager.js +2 -1
package/README.md
CHANGED
|
@@ -1,63 +1,174 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Zyket
|
|
2
2
|
|
|
3
|
-
Zyket is a framework
|
|
3
|
+
Zyket is a Node.js framework designed to simplify the development of real-time applications with Socket.IO and Express. Inspired by the structured approach of frameworks like Symfony, Zyket provides a robust, service-oriented architecture for building scalable and maintainable server-side applications.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Upon initial boot, Zyket automatically scaffolds a default project structure, including handlers, routes, and configuration files, allowing you to get started immediately.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Getting Started
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
To begin using Zyket, install it in your project:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
10
12
|
npm i zyket
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
Then
|
|
15
|
+
Then, create an `index.js` file and boot the Zyket Kernel:
|
|
14
16
|
|
|
15
17
|
```javascript
|
|
18
|
+
// index.js
|
|
16
19
|
const { Kernel } = require("zyket");
|
|
20
|
+
|
|
21
|
+
// Instantiate the kernel
|
|
17
22
|
const kernel = new Kernel();
|
|
18
23
|
|
|
19
|
-
kernel
|
|
24
|
+
// Boot the kernel to start all services
|
|
25
|
+
kernel.boot().then(() => {
|
|
26
|
+
console.log("Kernel booted successfully!");
|
|
27
|
+
});
|
|
20
28
|
```
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
When you run this for the first time, Zyket will create a default `.env` file and a `src` directory containing boilerplate for routes, handlers, guards, and middlewares.
|
|
31
|
+
|
|
32
|
+
## Core Concepts
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
Zyket is built around a few key architectural concepts:
|
|
35
|
+
|
|
36
|
+
* **Socket.IO Handlers & Guards**: For managing real-time WebSocket events and their authorization.
|
|
37
|
+
* **Express Routes & Middlewares**: For handling traditional RESTful API endpoints.
|
|
38
|
+
* **Services**: Reusable components that are managed by a dependency injection container.
|
|
39
|
+
* **CLI**: A command-line tool to scaffold features from templates.
|
|
40
|
+
|
|
41
|
+
### Socket.IO Development
|
|
42
|
+
|
|
43
|
+
#### Handlers
|
|
44
|
+
|
|
45
|
+
Handlers are classes that process incoming Socket.IO events. The name of the handler file (e.g., `message.js`) determines the event it listens to (`message`).
|
|
25
46
|
|
|
26
|
-
Handlers are the way to interact with user messages on the socket:
|
|
27
47
|
```javascript
|
|
48
|
+
// src/handlers/message.js
|
|
28
49
|
const { Handler } = require("zyket");
|
|
29
50
|
|
|
30
51
|
module.exports = class MessageHandler extends Handler {
|
|
31
|
-
|
|
52
|
+
// Array of guard names to execute before the handler
|
|
53
|
+
guards = ["default"];
|
|
54
|
+
|
|
55
|
+
async handle({ container, socket, data, io }) {
|
|
56
|
+
const logger = container.get("logger");
|
|
57
|
+
logger.info(`Received message: ${JSON.stringify(data)}`);
|
|
58
|
+
socket.emit("response", { received: data });
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
```
|
|
32
62
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
#### Guards
|
|
64
|
+
|
|
65
|
+
Guards are used to protect Socket.IO handlers or the initial connection. They run before the handler's `handle` method and are ideal for authorization logic.
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
// src/guards/default.js
|
|
69
|
+
const { Guard } = require("zyket");
|
|
70
|
+
|
|
71
|
+
module.exports = class DefaultGuard extends Guard {
|
|
72
|
+
async handle({ container, socket, io }) {
|
|
73
|
+
container.get("logger").info(`Executing default guard for socket ${socket.id}`);
|
|
74
|
+
|
|
75
|
+
// Example: Check for an auth token. If invalid, disconnect the user.
|
|
76
|
+
// if (!socket.token) {
|
|
77
|
+
// socket.disconnect();
|
|
78
|
+
// }
|
|
79
|
+
}
|
|
36
80
|
};
|
|
37
81
|
```
|
|
38
82
|
|
|
39
|
-
|
|
83
|
+
### Express API Development
|
|
40
84
|
|
|
41
|
-
|
|
85
|
+
#### Routes
|
|
86
|
+
|
|
87
|
+
Routes handle HTTP requests. The file path maps directly to the URL endpoint. For example, `src/routes/index.js` handles requests to `/`, and `src/routes/[test]/message.js` handles requests to `/:test/message`.
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
// src/routes/index.js
|
|
91
|
+
const { Route } = require("zyket");
|
|
92
|
+
const DefaultMiddleware = require("../middlewares/default");
|
|
93
|
+
|
|
94
|
+
module.exports = class DefaultRoute extends Route {
|
|
95
|
+
// Apply middlewares to specific HTTP methods
|
|
96
|
+
middlewares = {
|
|
97
|
+
get: [ new DefaultMiddleware() ],
|
|
98
|
+
post: [ new DefaultMiddleware() ]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async get({ container, request }) {
|
|
102
|
+
container.get("logger").info("Default route GET");
|
|
103
|
+
return { test: "Hello World GET!" };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async post({ container, request }) {
|
|
107
|
+
container.get("logger").info("Default route POST");
|
|
108
|
+
return { test: "Hello World POST!" };
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
```
|
|
42
112
|
|
|
43
|
-
|
|
113
|
+
#### Middlewares
|
|
44
114
|
|
|
45
|
-
Middlewares
|
|
115
|
+
Middlewares process the request before it reaches the route handler. They follow the standard Express middleware pattern.
|
|
46
116
|
|
|
47
117
|
```javascript
|
|
118
|
+
// src/middlewares/default.js
|
|
48
119
|
const { Middleware } = require("zyket");
|
|
49
120
|
|
|
50
121
|
module.exports = class DefaultMiddleware extends Middleware {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
122
|
+
async handle({ container, request, response, next }) {
|
|
123
|
+
container.get("logger").info("Default Express middleware executing");
|
|
124
|
+
next(); // Pass control to the next middleware or route handler
|
|
125
|
+
}
|
|
54
126
|
};
|
|
55
127
|
```
|
|
56
128
|
|
|
57
|
-
|
|
129
|
+
## Services
|
|
58
130
|
|
|
131
|
+
Services are the cornerstone of Zyket's architecture, providing reusable functionality across your application. Zyket includes several default services and allows you to register your own.
|
|
59
132
|
|
|
60
|
-
|
|
133
|
+
### Custom Services
|
|
134
|
+
|
|
135
|
+
You can create your own services by extending the `Service` class and registering it with the Kernel.
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
// src/services/MyCustomService.js
|
|
139
|
+
const { Service } = require("zyket");
|
|
140
|
+
|
|
141
|
+
module.exports = class MyCustomService extends Service {
|
|
142
|
+
constructor() {
|
|
143
|
+
super("my-custom-service");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async boot() {
|
|
147
|
+
console.log("MyCustomService has been booted!");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
doSomething() {
|
|
151
|
+
return "Something was done.";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Register the service in your main `index.js` file:
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
// index.js
|
|
160
|
+
const { Kernel } = require("zyket");
|
|
161
|
+
const MyCustomService = require("./src/services/MyCustomService");
|
|
162
|
+
|
|
163
|
+
const kernel = new Kernel({
|
|
164
|
+
services: [
|
|
165
|
+
// [name, class, [constructor_args]]
|
|
166
|
+
["my-service", MyCustomService, []]
|
|
167
|
+
]
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
kernel.boot();
|
|
171
|
+
```
|
|
61
172
|
Services are reusable components specified in the kernel configuration. Each service must include a boot() function that is executed when the kernel starts.
|
|
62
173
|
|
|
63
174
|
```javascript
|
package/index.js
CHANGED
|
@@ -4,11 +4,14 @@ const EnvManager = require("./src/utils/EnvManager");
|
|
|
4
4
|
|
|
5
5
|
const {Route, Middleware} = require("./src/services/express");
|
|
6
6
|
const { Handler, Guard } = require("./src/services/socketio");
|
|
7
|
+
const Schedule = require("./src/services/scheduler/Schedule");
|
|
8
|
+
const Event = require("./src/services/events/Event");
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
module.exports = {
|
|
10
12
|
Kernel, Service,
|
|
11
13
|
Route, Middleware,
|
|
12
14
|
Handler, Guard,
|
|
15
|
+
Schedule, Event,
|
|
13
16
|
EnvManager
|
|
14
17
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zyket",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"license": "ISC",
|
|
14
14
|
"description": "",
|
|
15
15
|
"dependencies": {
|
|
16
|
+
"bullmq": "^5.61.0",
|
|
16
17
|
"colors": "^1.4.0",
|
|
17
18
|
"cors": "^2.8.5",
|
|
18
19
|
"dotenv": "^16.4.7",
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
"fast-glob": "^3.3.3",
|
|
21
22
|
"mariadb": "^3.4.0",
|
|
22
23
|
"minio": "^8.0.5",
|
|
24
|
+
"node-cron": "^4.2.1",
|
|
23
25
|
"node-dependency-injection": "^3.2.2",
|
|
24
26
|
"prompts": "^2.4.2",
|
|
25
27
|
"redis": "^4.7.0",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const Service = require("../Service");
|
|
2
|
+
const { Queue, QueueEvents, Worker: BullWorker } = require("bullmq");
|
|
3
|
+
const Worker = require("./Worker");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const fg = require('fast-glob');
|
|
7
|
+
|
|
8
|
+
module.exports = class BullMQ extends Service {
|
|
9
|
+
#container
|
|
10
|
+
queues = {};
|
|
11
|
+
queuesEvents = {};
|
|
12
|
+
|
|
13
|
+
constructor(container) {
|
|
14
|
+
super("queues");
|
|
15
|
+
this.#container = container;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async boot() {
|
|
19
|
+
const queusConfig = this.#getActivatedQueues();
|
|
20
|
+
|
|
21
|
+
for (const queueName of queusConfig) {
|
|
22
|
+
this.queues[queueName] = new Queue(queueName, this.#connection());
|
|
23
|
+
this.queuesEvents[queueName] = new QueueEvents(queueName, this.#connection());
|
|
24
|
+
await this.#container.loggfer.info(`Queue ${queueName} initialized`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const workers = await this.#loadWorkersFromFolder(path.join(process.cwd(), "src", "workers"));
|
|
28
|
+
|
|
29
|
+
for (const wkr of workers) {
|
|
30
|
+
if(!wkr.queueName) {
|
|
31
|
+
this.#container.get('logger').warn(`Worker ${wkr.name} has no queueName defined, skipping...`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!this.queues[wkr.queueName]) {
|
|
35
|
+
this.#container.get('logger').warn(`Queue ${wkr.queueName} not found for worker ${wkr.name}, skipping...`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
new BullWorker(
|
|
39
|
+
wkr.queueName,
|
|
40
|
+
async (job) => wkr.handle({ container: this.#container, job }),
|
|
41
|
+
this.#connection()
|
|
42
|
+
);
|
|
43
|
+
this.#container.get('logger').info(`Worker ${wkr.name} for queue ${wkr.queueName} initialized`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async #loadWorkersFromFolder(workersFolder) {
|
|
48
|
+
this.#createWorkersFolder(workersFolder);
|
|
49
|
+
const workers = (await fg('**/*.js', { cwd: workersFolder })).map((wkr) => {
|
|
50
|
+
const worker = require(path.join(workersFolder, wkr));
|
|
51
|
+
if(!(worker.prototype instanceof Worker)) throw new Error(`${wkr} is not a valid handler`);
|
|
52
|
+
return new worker(wkr.replace('.js', ''));
|
|
53
|
+
});
|
|
54
|
+
return workers;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#createWorkersFolder(workersFolder, overwrite = false) {
|
|
58
|
+
if (fs.existsSync(workersFolder) && !overwrite) return;
|
|
59
|
+
this.#container.get('logger').info(`Creating workers folder at ${workersFolder}`);
|
|
60
|
+
fs.mkdirSync(workersFolder);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#getActivatedQueues() {
|
|
64
|
+
const list = process.env.QUEUES || "";
|
|
65
|
+
return list.split(",").map(q => q.trim()).filter(q => q.length);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#connection() {
|
|
69
|
+
return {
|
|
70
|
+
connection: {
|
|
71
|
+
url: process.env.REDIS_URL || "redis://localhost:6379"
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const Service = require("../Service");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const fg = require('fast-glob');
|
|
5
|
+
const Event = require("./Event");
|
|
6
|
+
|
|
7
|
+
module.exports = class EventService extends Service {
|
|
8
|
+
#container
|
|
9
|
+
events = {};
|
|
10
|
+
|
|
11
|
+
constructor(container) {
|
|
12
|
+
super("events");
|
|
13
|
+
this.#container = container;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
emit(eventName, payload) {
|
|
18
|
+
const event = this.events[eventName];
|
|
19
|
+
if (!event) throw new Error(`Event ${eventName} not found`);
|
|
20
|
+
this.#container.get('logger').debug(`Emitting event ${eventName}`);
|
|
21
|
+
return event.handle({ container: this.#container, payload });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
emitAsync(eventName, payload, timeout = 30000) {
|
|
25
|
+
const event = this.events[eventName];
|
|
26
|
+
if (!event) throw new Error(`Event ${eventName} not found`);
|
|
27
|
+
this.#container.get('logger').debug(`Emitting event ${eventName} asynchronously`);
|
|
28
|
+
return Promise.race([
|
|
29
|
+
event.handle({ container: this.#container, payload }),
|
|
30
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Event ${eventName} timed out after ${timeout}ms`)), timeout))
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async boot() {
|
|
35
|
+
const events = await this.#loadEventsFromFolder(path.join(process.cwd(), "src", "events"));
|
|
36
|
+
this.#container.get('logger').info(`Loaded ${events.length} events`);
|
|
37
|
+
for (const evt of events) {
|
|
38
|
+
this.events[evt.name] = evt;
|
|
39
|
+
this.#container.get('logger').debug(`Event ${evt.name} initialized`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async #loadEventsFromFolder(eventsFolder) {
|
|
44
|
+
this.#createEventsFolder(eventsFolder);
|
|
45
|
+
const events = (await fg('**/*.js', { cwd: eventsFolder })).map((evt) => {
|
|
46
|
+
const event = require(path.join(eventsFolder, evt));
|
|
47
|
+
if(!(event.prototype instanceof Event)) throw new Error(`${evt} is not a valid handler`);
|
|
48
|
+
return new event(evt.replace('.js', ''));
|
|
49
|
+
});
|
|
50
|
+
return events;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#createEventsFolder(eventsFolder, overwrite = false) {
|
|
54
|
+
if (fs.existsSync(eventsFolder) && !overwrite) return;
|
|
55
|
+
this.#container.get('logger').info(`Creating schedules folder at ${eventsFolder}`);
|
|
56
|
+
fs.mkdirSync(eventsFolder);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
}
|
package/src/services/index.js
CHANGED
|
@@ -3,13 +3,27 @@ const Cache = require("./cache");
|
|
|
3
3
|
const S3 = require("./s3");
|
|
4
4
|
const { SocketIO } = require("./socketio");
|
|
5
5
|
const { Express } = require("./express");
|
|
6
|
+
const Scheduler = require("./scheduler");
|
|
7
|
+
const EventService = require("./events");
|
|
8
|
+
|
|
9
|
+
const eventsActivated = process.env.DISABLE_EVENTS !== 'true';
|
|
10
|
+
const databaseActivated = !!process.env.DATABASE_URL;
|
|
11
|
+
const cacheActivated = !!process.env.CACHE_URL;
|
|
12
|
+
const bullmqActivated = !!process.env.CACHE_URL && process.env.DISABLE_BULLMQ !== 'true';
|
|
13
|
+
const s3Activated = process.env.S3_ENDPOINT && process.env.S3_ACCESS_KEY && process.env.S3_SECRET_KEY;
|
|
14
|
+
const schedulerActivated = process.env.DISABLE_SCHEDULER !== 'true';
|
|
15
|
+
const socketActivated = process.env.DISABLE_SOCKET !== 'true';
|
|
16
|
+
const expressActivated = process.env.DISABLE_EXPRESS !== 'true';
|
|
6
17
|
|
|
7
18
|
module.exports = [
|
|
8
19
|
["logger", require("./logger"), ["@service_container", process.env.LOG_DIRECTORY || `${process.cwd()}/logs`, process.env.DEBUG === "true"]],
|
|
9
20
|
["template-manager", require("./template-manager"), []],
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
eventsActivated ? ["events", EventService, ["@service_container"]] : null,
|
|
22
|
+
databaseActivated ? ["database", Database, ["@service_container", process.env.DATABASE_URL]] : null,
|
|
23
|
+
cacheActivated ? ["cache", Cache, ["@service_container", process.env.CACHE_URL]] : null,
|
|
24
|
+
s3Activated ? ["s3", S3, ["@service_container", process.env.S3_ENDPOINT, process.env.S3_PORT, process.env.S3_USE_SSL === "true", process.env.S3_ACCESS_KEY, process.env.S3_SECRET_KEY]] : null,
|
|
25
|
+
schedulerActivated ? ["scheduler", Scheduler, ["@service_container"]] : null,
|
|
26
|
+
bullmqActivated ? ["bullmq", require("./bullmq"), ["@service_container"]] : null,
|
|
27
|
+
socketActivated ? ["socketio", SocketIO, ["@service_container"]] : null,
|
|
28
|
+
expressActivated ? ["express", Express, ["@service_container"]] : null
|
|
15
29
|
].filter(Boolean);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const Service = require("../Service");
|
|
2
|
+
const Schedule = require("./Schedule");
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fg = require('fast-glob');
|
|
6
|
+
const cron = require('node-cron');
|
|
7
|
+
|
|
8
|
+
module.exports = class Scheduler extends Service {
|
|
9
|
+
#container
|
|
10
|
+
|
|
11
|
+
constructor(container) {
|
|
12
|
+
super("scheduler");
|
|
13
|
+
this.#container = container;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async boot() {
|
|
17
|
+
const schedulers = await this.#loadSchedulersFromFolder(path.join(process.cwd(), "src", "schedulers"));
|
|
18
|
+
await this.#container.get('logger').info(`Loaded ${schedulers.length} schedulers`);
|
|
19
|
+
for (const schd of schedulers) {
|
|
20
|
+
cron.schedule(schd.time, () => schd.handle({ container: this.#container }));
|
|
21
|
+
this.#container.get('logger').info(`Scheduler ${schd.name}, ${schd.time} initialized`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async #loadSchedulersFromFolder(schedulersFolder) {
|
|
26
|
+
this.#createSchedulersFolder(schedulersFolder);
|
|
27
|
+
const schedulers = (await fg('**/*.js', { cwd: schedulersFolder })).map((schd) => {
|
|
28
|
+
const schedule = require(path.join(schedulersFolder, schd));
|
|
29
|
+
if(!(schedule.prototype instanceof Schedule)) throw new Error(`${schd} is not a valid handler`);
|
|
30
|
+
return new schedule(schd.replace('.js', ''));
|
|
31
|
+
});
|
|
32
|
+
return schedulers;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#createSchedulersFolder(schedulersFolder, overwrite = false) {
|
|
36
|
+
if (fs.existsSync(schedulersFolder) && !overwrite) return;
|
|
37
|
+
this.#container.get('logger').info(`Creating schedules folder at ${schedulersFolder}`);
|
|
38
|
+
fs.mkdirSync(schedulersFolder);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
}
|
package/src/utils/EnvManager.js
CHANGED