@outloud/adonis-scheduler 1.0.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/LICENSE +21 -0
- package/README.md +190 -0
- package/build/chunk-7P6J5U6O.js +25 -0
- package/build/chunk-BBI2I5B5.js +183 -0
- package/build/chunk-SHUYVCID.js +4 -0
- package/build/command.task-GUNK3QFY.js +22 -0
- package/build/commands/commands.json +1 -0
- package/build/commands/main.d.ts +4 -0
- package/build/commands/main.js +36 -0
- package/build/commands/make_task.d.ts +18 -0
- package/build/commands/make_task.js +42 -0
- package/build/commands/scheduler_run.d.ts +18 -0
- package/build/commands/scheduler_run.js +46 -0
- package/build/index.d.ts +13 -0
- package/build/index.js +29 -0
- package/build/providers/scheduler.provider.d.ts +23 -0
- package/build/providers/scheduler.provider.js +40 -0
- package/build/scheduler-BWT0Iqko.d.ts +91 -0
- package/build/services/main.d.ts +9 -0
- package/build/services/main.js +9 -0
- package/build/stubs/command/task.stub +16 -0
- package/build/stubs/config/scheduler.stub +14 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present, Outloud
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h2><b>Adonis Scheduler</b></h2>
|
|
3
|
+
|
|
4
|
+
<p>
|
|
5
|
+
|
|
6
|
+
`@outloud/adonis-scheduler` is a cron job scheduler for [AdonisJS](https://adonisjs.com/).
|
|
7
|
+
|
|
8
|
+
</p>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
<div align="center">
|
|
13
|
+
|
|
14
|
+
[![npm-image]][npm-url] [![license-image]][license-url]
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
## Features
|
|
20
|
+
- Define tasks with cron-like scheduling.
|
|
21
|
+
- Run tasks as standalone processes or as part of the HTTP server.
|
|
22
|
+
- Locking mechanism to prevent concurrent task execution.
|
|
23
|
+
- Cancellation support for long-running tasks.
|
|
24
|
+
- Graceful shutdown.
|
|
25
|
+
- Global and task-level error handling.
|
|
26
|
+
|
|
27
|
+
## Getting Started
|
|
28
|
+
|
|
29
|
+
Install the package from the npm registry and configure it.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node ace add @outloud/adonis-scheduler
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
See `config/scheduler.ts` for available configuration options.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
To make scheduler work, you must define and register tasks.
|
|
40
|
+
|
|
41
|
+
### Define a task
|
|
42
|
+
|
|
43
|
+
You can create a task using `node ace make:task <task-name>` command. This will create a new task file in the `app/tasks` directory.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Task, type TaskOptions } from '@outloud/adonis-scheduler'
|
|
47
|
+
|
|
48
|
+
export default class TestTask extends Task {
|
|
49
|
+
static options: TaskOptions = {
|
|
50
|
+
schedule: '* * * * *'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async run(): Promise<void> {
|
|
54
|
+
// Your task logic here
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Register a task
|
|
60
|
+
|
|
61
|
+
For task to run it must be registered in the scheduler. You can register tasks in two ways: using the `start/scheduler.ts` preloaded file or in a provider's `start` method.
|
|
62
|
+
|
|
63
|
+
Using `start/scheduler.ts` file.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import scheduler from '@outloud/adonis-scheduler/services/main'
|
|
67
|
+
|
|
68
|
+
scheduler.register(() => import('../app/tasks/test.task.js'))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Or using a provider.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import type { ApplicationService } from '@adonisjs/core/types'
|
|
75
|
+
import scheduler from '@outloud/adonis-scheduler/services/main'
|
|
76
|
+
|
|
77
|
+
export default class AppProvider {
|
|
78
|
+
constructor(protected app: ApplicationService) {}
|
|
79
|
+
|
|
80
|
+
start() {
|
|
81
|
+
scheduler.register(() => import('../app/tasks/test.task.js'))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
You can also run other commands using scheduler without defining custom Task class.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import scheduler from '@outloud/adonis-scheduler/services/main'
|
|
91
|
+
|
|
92
|
+
scheduler.register({
|
|
93
|
+
command: '<command-name>',
|
|
94
|
+
schedule: '* * * * *',
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Running the scheduler
|
|
99
|
+
|
|
100
|
+
The scheduler can be run as standalone process or as part of the HTTP server.
|
|
101
|
+
|
|
102
|
+
To run it as a **standalone process**, you can use the following command:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
node ace scheduler:run
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
To run it as part of the **HTTP server**, set following env variable:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
SCHEDULER_HTTP_SERVER=true
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Locking
|
|
115
|
+
|
|
116
|
+
> [!NOTE]
|
|
117
|
+
> This requires [@adonisjs/lock](https://docs.adonisjs.com/guides/digging-deeper/locks) package to be installed and configured.
|
|
118
|
+
|
|
119
|
+
The scheduler supports locking to prevent multiple instances of the same task from running concurrently. You can enable locking by setting the `lock` option in the task options.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
import { Task, type TaskOptions } from '@outloud/adonis-scheduler'
|
|
123
|
+
|
|
124
|
+
export default class TestTask extends Task {
|
|
125
|
+
static options: TaskOptions = {
|
|
126
|
+
schedule: '* * * * *',
|
|
127
|
+
lock: true // or value for lock ttl
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async run(): Promise<void> {
|
|
131
|
+
// Your task logic here
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Cancellation
|
|
137
|
+
|
|
138
|
+
The package supports cancellation and graceful shutdown. You can add `onCancel` handler in your task or watch for `isCanceled` property.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { Task } from '@outloud/adonis-scheduler'
|
|
142
|
+
|
|
143
|
+
export default class TestTask extends Task {
|
|
144
|
+
async run(): Promise<void> {
|
|
145
|
+
while (!this.isCanceled) {
|
|
146
|
+
// Your task logic here
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async onCancel(): Promise<void> {
|
|
151
|
+
// teardown running logic
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Error handling
|
|
157
|
+
|
|
158
|
+
It's possible to globally handle errors for all your tasks or define custom error handler for each task.
|
|
159
|
+
|
|
160
|
+
To register global error handler, you can use the `onError` method of the scheduler service. You can define it in `start/scheduler.ts` preloaded file.
|
|
161
|
+
This handler will run only if custom error handler is not defined in the task itself.
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import logger from '@adonisjs/core/services/logger'
|
|
165
|
+
import scheduler from '@outloud/adonis-scheduler/services/main'
|
|
166
|
+
import { Sentry } from '@rlanz/sentry'
|
|
167
|
+
|
|
168
|
+
scheduler.onError((error, task) => {
|
|
169
|
+
logger.error(error)
|
|
170
|
+
Sentry.captureException(error)
|
|
171
|
+
})
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Custom error handler can be defined in the task itself by implementing `onError` method.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { Task } from '@outloud/adonis-scheduler'
|
|
178
|
+
|
|
179
|
+
export default class TestTask extends Task {
|
|
180
|
+
async onError(error: Error): Promise<void> {
|
|
181
|
+
// handle error
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
[npm-image]: https://img.shields.io/npm/v/@outloud/adonis-scheduler.svg?style=for-the-badge&logo=**npm**
|
|
187
|
+
[npm-url]: https://npmjs.org/package/@outloud/adonis-scheduler "npm"
|
|
188
|
+
|
|
189
|
+
[license-image]: https://img.shields.io/npm/l/@outloud/adonis-scheduler?color=blueviolet&style=for-the-badge
|
|
190
|
+
[license-url]: LICENSE "license"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { __name } from './chunk-SHUYVCID.js';
|
|
2
|
+
|
|
3
|
+
// src/task.ts
|
|
4
|
+
var Task = class {
|
|
5
|
+
static {
|
|
6
|
+
__name(this, "Task");
|
|
7
|
+
}
|
|
8
|
+
isCanceled = false;
|
|
9
|
+
promise;
|
|
10
|
+
static options;
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
|
12
|
+
constructor(..._) {
|
|
13
|
+
}
|
|
14
|
+
get name() {
|
|
15
|
+
const Ctor = this.constructor;
|
|
16
|
+
return Ctor.options.name ?? Ctor.name;
|
|
17
|
+
}
|
|
18
|
+
async $cancel() {
|
|
19
|
+
this.isCanceled = true;
|
|
20
|
+
await this.onCancel?.();
|
|
21
|
+
await this.promise;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export { Task };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { __name } from './chunk-SHUYVCID.js';
|
|
2
|
+
import { Cron } from 'croner';
|
|
3
|
+
import timers from 'timers/promises';
|
|
4
|
+
|
|
5
|
+
async function waitUntil(callback, interval = 50) {
|
|
6
|
+
while (!callback()) {
|
|
7
|
+
await timers.setTimeout(interval);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
__name(waitUntil, "waitUntil");
|
|
11
|
+
|
|
12
|
+
// src/scheduler.ts
|
|
13
|
+
var Scheduler = class {
|
|
14
|
+
static {
|
|
15
|
+
__name(this, "Scheduler");
|
|
16
|
+
}
|
|
17
|
+
config;
|
|
18
|
+
resolver;
|
|
19
|
+
logger;
|
|
20
|
+
locks;
|
|
21
|
+
definitions = [];
|
|
22
|
+
state = "created";
|
|
23
|
+
errorHandler;
|
|
24
|
+
constructor(config, resolver, logger, locks) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.resolver = resolver;
|
|
27
|
+
this.logger = logger;
|
|
28
|
+
this.locks = locks;
|
|
29
|
+
}
|
|
30
|
+
register(options) {
|
|
31
|
+
const definition = {
|
|
32
|
+
schedule: "* * * * *",
|
|
33
|
+
state: "created",
|
|
34
|
+
jobs: []
|
|
35
|
+
};
|
|
36
|
+
if (typeof options === "object") {
|
|
37
|
+
const command = Array.isArray(options.command) ? options.command : [
|
|
38
|
+
options.command
|
|
39
|
+
];
|
|
40
|
+
Object.assign(definition, options);
|
|
41
|
+
definition.loader = () => import('./command.task-GUNK3QFY.js').then((module) => {
|
|
42
|
+
return class extends module.CommandTask {
|
|
43
|
+
static command = command;
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
definition.loader = options;
|
|
48
|
+
}
|
|
49
|
+
if (!definition.loader) {
|
|
50
|
+
throw new Error("Task definition must have either a command or a task defined.");
|
|
51
|
+
}
|
|
52
|
+
this.definitions.push(definition);
|
|
53
|
+
if (this.hasState([
|
|
54
|
+
"starting",
|
|
55
|
+
"running"
|
|
56
|
+
])) {
|
|
57
|
+
this.schedule(definition);
|
|
58
|
+
}
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
async start(wait = false) {
|
|
62
|
+
if (!this.hasState([
|
|
63
|
+
"created",
|
|
64
|
+
"stopped"
|
|
65
|
+
])) {
|
|
66
|
+
this.logger.warn("Scheduler is already running");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.setState("starting");
|
|
70
|
+
await Promise.all(this.definitions.map((definition) => this.schedule(definition)));
|
|
71
|
+
if (!this.definitions.length) {
|
|
72
|
+
this.logger.warn("No tasks registered, scheduler will not run any jobs.");
|
|
73
|
+
}
|
|
74
|
+
this.setState("running");
|
|
75
|
+
if (wait) {
|
|
76
|
+
return waitUntil(() => this.state === "stopped");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async stop() {
|
|
80
|
+
if (this.state === "starting") {
|
|
81
|
+
await waitUntil(() => this.state !== "starting");
|
|
82
|
+
}
|
|
83
|
+
if (this.state !== "running") {
|
|
84
|
+
this.logger.warn("Scheduler is not running");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
this.setState("stopping");
|
|
88
|
+
await Promise.all(this.definitions.map((definition) => this.terminate(definition)));
|
|
89
|
+
this.setState("stopped");
|
|
90
|
+
}
|
|
91
|
+
onError(callback) {
|
|
92
|
+
this.errorHandler = callback;
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
async load(definition) {
|
|
96
|
+
if (definition.loader) {
|
|
97
|
+
const module = await definition.loader();
|
|
98
|
+
definition.task = "default" in module ? module.default : module;
|
|
99
|
+
}
|
|
100
|
+
if (!definition.task) {
|
|
101
|
+
throw new Error("Failed to load task, no loader or task provided.");
|
|
102
|
+
}
|
|
103
|
+
Object.assign(definition, definition.task.options ?? {});
|
|
104
|
+
}
|
|
105
|
+
async make(definition) {
|
|
106
|
+
return await this.resolver.make(definition.task);
|
|
107
|
+
}
|
|
108
|
+
async run(definition) {
|
|
109
|
+
const task = await this.make(definition);
|
|
110
|
+
const lockDuration = definition.lock && typeof definition.lock !== "boolean" ? definition.lock : this.config.lockDuration;
|
|
111
|
+
const lock = definition.lock ? this.locks?.createLock(`scheduler:${task.name}`, lockDuration) : void 0;
|
|
112
|
+
if (definition.lock && !this.locks) {
|
|
113
|
+
this.logger.warn("Lock is not available, install @adonisjs/lock to use task locking.");
|
|
114
|
+
}
|
|
115
|
+
if (lock) {
|
|
116
|
+
const acquired = await lock.acquireImmediately();
|
|
117
|
+
if (!acquired) {
|
|
118
|
+
this.config.warnWhenLocked && this.logger.warn(`Task "${definition.task?.name}" is locked and cannot be run.`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
definition.jobs.push(task);
|
|
124
|
+
const promise = this.resolver.call(task, "run").finally(() => lock?.release());
|
|
125
|
+
task.promise = promise.catch(() => {
|
|
126
|
+
});
|
|
127
|
+
await promise;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
await this.handleError(error, definition, task);
|
|
130
|
+
} finally {
|
|
131
|
+
definition.jobs.splice(definition.jobs.indexOf(task), 1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
hasState(state) {
|
|
135
|
+
const states = Array.isArray(state) ? state : [
|
|
136
|
+
state
|
|
137
|
+
];
|
|
138
|
+
return states.includes(this.state);
|
|
139
|
+
}
|
|
140
|
+
setState(state) {
|
|
141
|
+
if (this.state === state) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.state = state;
|
|
145
|
+
this.logger.info(`Scheduler: ${state}`);
|
|
146
|
+
}
|
|
147
|
+
async handleError(error, _, task) {
|
|
148
|
+
if (task && task.onError) {
|
|
149
|
+
await task.onError(error);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (this.errorHandler) {
|
|
153
|
+
await this.errorHandler(error, task);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
async cancel(job) {
|
|
159
|
+
await job.$cancel();
|
|
160
|
+
}
|
|
161
|
+
async terminate(definition) {
|
|
162
|
+
definition.cron?.stop();
|
|
163
|
+
definition.cron = void 0;
|
|
164
|
+
await Promise.all(definition.jobs.map((job) => this.cancel(job)));
|
|
165
|
+
definition.state = "created";
|
|
166
|
+
}
|
|
167
|
+
async schedule(definition) {
|
|
168
|
+
if (definition.state !== "created") {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
definition.state = "preparing";
|
|
172
|
+
if (!definition.task) {
|
|
173
|
+
await this.load(definition);
|
|
174
|
+
}
|
|
175
|
+
definition.cron = new Cron(definition.schedule, {
|
|
176
|
+
timezone: definition.timeZone
|
|
177
|
+
}, () => this.run(definition));
|
|
178
|
+
definition.state = "ready";
|
|
179
|
+
this.logger.debug(`Scheduler: Task "${definition.task?.name}" scheduled with "${definition.schedule}"`);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export { Scheduler };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Task } from './chunk-7P6J5U6O.js';
|
|
2
|
+
import { __name } from './chunk-SHUYVCID.js';
|
|
3
|
+
import ace from '@adonisjs/core/services/ace';
|
|
4
|
+
|
|
5
|
+
var CommandTask = class extends Task {
|
|
6
|
+
static {
|
|
7
|
+
__name(this, "CommandTask");
|
|
8
|
+
}
|
|
9
|
+
static command = [];
|
|
10
|
+
get name() {
|
|
11
|
+
return this.constructor.command.join(",");
|
|
12
|
+
}
|
|
13
|
+
async run() {
|
|
14
|
+
const [name, ...args] = this.constructor.command;
|
|
15
|
+
if (!name) {
|
|
16
|
+
throw new Error("No command name provided.");
|
|
17
|
+
}
|
|
18
|
+
await ace.exec(name, args);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export { CommandTask };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"commands":[{"commandName":"make:task","description":"Make a new task class","help":"","namespace":"make","aliases":[],"flags":[],"args":[{"name":"name","argumentName":"name","required":true,"description":"Name of the task","type":"string"}],"options":{"allowUnknownFlags":true},"filePath":"make_task.js"},{"commandName":"scheduler:run","description":"Run a scheduler","help":"","namespace":"scheduler","aliases":[],"flags":[],"args":[],"options":{"startApp":true,"staysAlive":true},"filePath":"scheduler_run.js"}],"version":1}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory cache of commands after they have been loaded
|
|
5
|
+
*/
|
|
6
|
+
let commandsMetaData
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reads the commands from the "./commands.json" file. Since, the commands.json
|
|
10
|
+
* file is generated automatically, we do not have to validate its contents
|
|
11
|
+
*/
|
|
12
|
+
export async function getMetaData() {
|
|
13
|
+
if (commandsMetaData) {
|
|
14
|
+
return commandsMetaData
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const commandsIndex = await readFile(new URL('./commands.json', import.meta.url), 'utf-8')
|
|
18
|
+
commandsMetaData = JSON.parse(commandsIndex).commands
|
|
19
|
+
|
|
20
|
+
return commandsMetaData
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Imports the command by lookingup its path from the commands
|
|
25
|
+
* metadata
|
|
26
|
+
*/
|
|
27
|
+
export async function getCommand(metaData) {
|
|
28
|
+
const commands = await getMetaData()
|
|
29
|
+
const command = commands.find(({ commandName }) => metaData.commandName === commandName)
|
|
30
|
+
if (!command) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { default: commandConstructor } = await import(new URL(command.filePath, import.meta.url).href)
|
|
35
|
+
return commandConstructor
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
|
|
4
|
+
declare class MakeTask extends BaseCommand {
|
|
5
|
+
static commandName: string;
|
|
6
|
+
static description: string;
|
|
7
|
+
static options: CommandOptions;
|
|
8
|
+
/**
|
|
9
|
+
* The name of the job file.
|
|
10
|
+
*/
|
|
11
|
+
name: string;
|
|
12
|
+
/**
|
|
13
|
+
* Execute command
|
|
14
|
+
*/
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { MakeTask as default };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { __name } from '../chunk-SHUYVCID.js';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
import { BaseCommand, args } from '@adonisjs/core/ace';
|
|
4
|
+
|
|
5
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
6
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
8
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
}
|
|
11
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
12
|
+
function _ts_metadata(k, v) {
|
|
13
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
14
|
+
}
|
|
15
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
16
|
+
var MakeTask = class extends BaseCommand {
|
|
17
|
+
static {
|
|
18
|
+
__name(this, "MakeTask");
|
|
19
|
+
}
|
|
20
|
+
static commandName = "make:task";
|
|
21
|
+
static description = "Make a new task class";
|
|
22
|
+
static options = {
|
|
23
|
+
allowUnknownFlags: true
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Execute command
|
|
27
|
+
*/
|
|
28
|
+
async run() {
|
|
29
|
+
const codemods = await this.createCodemods();
|
|
30
|
+
await codemods.makeUsingStub(import.meta.dirname + "/../stubs", "command/task.stub", {
|
|
31
|
+
entity: this.app.generators.createEntity(this.name)
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
_ts_decorate([
|
|
36
|
+
args.string({
|
|
37
|
+
description: "Name of the task"
|
|
38
|
+
}),
|
|
39
|
+
_ts_metadata("design:type", String)
|
|
40
|
+
], MakeTask.prototype, "name", void 0);
|
|
41
|
+
|
|
42
|
+
export { MakeTask as default };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
2
|
+
import { CommandOptions } from '@adonisjs/core/types/ace';
|
|
3
|
+
import { a as Scheduler } from '../scheduler-BWT0Iqko.js';
|
|
4
|
+
import '@adonisjs/core/container';
|
|
5
|
+
import '@adonisjs/core/types';
|
|
6
|
+
import '@adonisjs/lock/types';
|
|
7
|
+
import '@adonisjs/core/logger';
|
|
8
|
+
|
|
9
|
+
declare class SchedulerRun extends BaseCommand {
|
|
10
|
+
static commandName: string;
|
|
11
|
+
static description: string;
|
|
12
|
+
static options: CommandOptions;
|
|
13
|
+
private scheduler?;
|
|
14
|
+
prepare(): void;
|
|
15
|
+
run(scheduler: Scheduler): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { SchedulerRun as default };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Scheduler } from '../chunk-BBI2I5B5.js';
|
|
2
|
+
import { __name } from '../chunk-SHUYVCID.js';
|
|
3
|
+
import 'reflect-metadata';
|
|
4
|
+
import { inject } from '@adonisjs/core';
|
|
5
|
+
import { BaseCommand } from '@adonisjs/core/ace';
|
|
6
|
+
|
|
7
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
8
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
10
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
11
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
12
|
+
}
|
|
13
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
14
|
+
function _ts_metadata(k, v) {
|
|
15
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
16
|
+
}
|
|
17
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
18
|
+
var SchedulerRun = class extends BaseCommand {
|
|
19
|
+
static {
|
|
20
|
+
__name(this, "SchedulerRun");
|
|
21
|
+
}
|
|
22
|
+
static commandName = "scheduler:run";
|
|
23
|
+
static description = "Run a scheduler";
|
|
24
|
+
static options = {
|
|
25
|
+
startApp: true,
|
|
26
|
+
staysAlive: true
|
|
27
|
+
};
|
|
28
|
+
scheduler;
|
|
29
|
+
prepare() {
|
|
30
|
+
this.app.terminating(() => this.scheduler?.stop());
|
|
31
|
+
}
|
|
32
|
+
async run(scheduler) {
|
|
33
|
+
this.scheduler = scheduler;
|
|
34
|
+
await this.scheduler.start(true);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
_ts_decorate([
|
|
38
|
+
inject(),
|
|
39
|
+
_ts_metadata("design:type", Function),
|
|
40
|
+
_ts_metadata("design:paramtypes", [
|
|
41
|
+
typeof Scheduler === "undefined" ? Object : Scheduler
|
|
42
|
+
]),
|
|
43
|
+
_ts_metadata("design:returntype", Promise)
|
|
44
|
+
], SchedulerRun.prototype, "run", null);
|
|
45
|
+
|
|
46
|
+
export { SchedulerRun as default };
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { S as SchedulerConfig } from './scheduler-BWT0Iqko.js';
|
|
2
|
+
export { a as Scheduler, T as Task, b as TaskOptions } from './scheduler-BWT0Iqko.js';
|
|
3
|
+
import Configure from '@adonisjs/core/commands/configure';
|
|
4
|
+
import '@adonisjs/core/container';
|
|
5
|
+
import '@adonisjs/core/types';
|
|
6
|
+
import '@adonisjs/lock/types';
|
|
7
|
+
import '@adonisjs/core/logger';
|
|
8
|
+
|
|
9
|
+
declare function configure(command: Configure): Promise<void>;
|
|
10
|
+
|
|
11
|
+
declare function defineConfig<T extends SchedulerConfig>(config: T): T;
|
|
12
|
+
|
|
13
|
+
export { SchedulerConfig, configure, defineConfig };
|
package/build/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export { Scheduler } from './chunk-BBI2I5B5.js';
|
|
2
|
+
export { Task } from './chunk-7P6J5U6O.js';
|
|
3
|
+
import { __name } from './chunk-SHUYVCID.js';
|
|
4
|
+
|
|
5
|
+
// configure.ts
|
|
6
|
+
async function configure(command) {
|
|
7
|
+
const codemods = await command.createCodemods();
|
|
8
|
+
await codemods.makeUsingStub(import.meta.dirname + "/stubs", "config/scheduler.stub", {});
|
|
9
|
+
await codemods.defineEnvVariables({
|
|
10
|
+
SCHEDULER_HTTP_SERVER: false
|
|
11
|
+
});
|
|
12
|
+
await codemods.defineEnvValidations({
|
|
13
|
+
variables: {
|
|
14
|
+
SCHEDULER_HTTP_SERVER: `Env.schema.boolean.optional()`
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
await codemods.updateRcFile((rcFile) => {
|
|
18
|
+
rcFile.addProvider("@outloud/adonis-scheduler/provider").addCommand("@outloud/adonis-scheduler/commands");
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
__name(configure, "configure");
|
|
22
|
+
|
|
23
|
+
// src/config.ts
|
|
24
|
+
function defineConfig(config) {
|
|
25
|
+
return config;
|
|
26
|
+
}
|
|
27
|
+
__name(defineConfig, "defineConfig");
|
|
28
|
+
|
|
29
|
+
export { configure, defineConfig };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ApplicationService } from '@adonisjs/core/types';
|
|
2
|
+
import { a as Scheduler } from '../scheduler-BWT0Iqko.js';
|
|
3
|
+
import '@adonisjs/core/container';
|
|
4
|
+
import '@adonisjs/lock/types';
|
|
5
|
+
import '@adonisjs/core/logger';
|
|
6
|
+
|
|
7
|
+
declare class SchedulerProvider {
|
|
8
|
+
protected app: ApplicationService;
|
|
9
|
+
private scheduler?;
|
|
10
|
+
constructor(app: ApplicationService);
|
|
11
|
+
private getConfig;
|
|
12
|
+
private getLocks;
|
|
13
|
+
register(): void;
|
|
14
|
+
ready(): Promise<void>;
|
|
15
|
+
shutdown(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
declare module '@adonisjs/core/types' {
|
|
18
|
+
interface ContainerBindings {
|
|
19
|
+
scheduler: Scheduler;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { SchedulerProvider as default };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Scheduler } from '../chunk-BBI2I5B5.js';
|
|
2
|
+
import { __name } from '../chunk-SHUYVCID.js';
|
|
3
|
+
|
|
4
|
+
// providers/scheduler.provider.ts
|
|
5
|
+
var SchedulerProvider = class {
|
|
6
|
+
static {
|
|
7
|
+
__name(this, "SchedulerProvider");
|
|
8
|
+
}
|
|
9
|
+
app;
|
|
10
|
+
scheduler;
|
|
11
|
+
constructor(app) {
|
|
12
|
+
this.app = app;
|
|
13
|
+
}
|
|
14
|
+
getConfig() {
|
|
15
|
+
return this.app.config.get("scheduler", {});
|
|
16
|
+
}
|
|
17
|
+
async getLocks() {
|
|
18
|
+
if (this.app.container.hasBinding("lock.manager")) {
|
|
19
|
+
return await this.app.container.make("lock.manager");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
register() {
|
|
23
|
+
this.app.container.singleton(Scheduler, async () => {
|
|
24
|
+
return new Scheduler(this.getConfig(), this.app.container.createResolver(), await this.app.container.make("logger"), await this.getLocks());
|
|
25
|
+
});
|
|
26
|
+
this.app.container.alias("scheduler", Scheduler);
|
|
27
|
+
}
|
|
28
|
+
async ready() {
|
|
29
|
+
const config = this.getConfig();
|
|
30
|
+
if (this.app.getEnvironment() === "web" && config.httpServer) {
|
|
31
|
+
this.scheduler = await this.app.container.make(Scheduler);
|
|
32
|
+
await this.scheduler.start();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async shutdown() {
|
|
36
|
+
await this.scheduler?.stop();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { SchedulerProvider as default };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { ContainerResolver } from '@adonisjs/core/container';
|
|
2
|
+
import { ContainerBindings } from '@adonisjs/core/types';
|
|
3
|
+
import { LockService } from '@adonisjs/lock/types';
|
|
4
|
+
import { Logger } from '@adonisjs/core/logger';
|
|
5
|
+
|
|
6
|
+
interface SchedulerConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Should the scheduler start with HTTP server?
|
|
9
|
+
*/
|
|
10
|
+
httpServer: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Warn when a task is locked and cannot be run
|
|
13
|
+
*/
|
|
14
|
+
warnWhenLocked: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* The default ttl for the lock.
|
|
17
|
+
*/
|
|
18
|
+
lockDuration: number | string;
|
|
19
|
+
}
|
|
20
|
+
interface TaskOptions {
|
|
21
|
+
/**
|
|
22
|
+
* A unique name for the task.
|
|
23
|
+
*/
|
|
24
|
+
name?: string;
|
|
25
|
+
/**
|
|
26
|
+
* The pattern to when the task should run.
|
|
27
|
+
*
|
|
28
|
+
* See https://croner.56k.guru/usage/pattern/ for more information.
|
|
29
|
+
*/
|
|
30
|
+
schedule: string;
|
|
31
|
+
/**
|
|
32
|
+
* Time zone to use for the task.
|
|
33
|
+
*/
|
|
34
|
+
timeZone?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Lock the task to prevent it from running concurrently.
|
|
37
|
+
*
|
|
38
|
+
* If a string or number is provided, it will be as ttl.
|
|
39
|
+
*
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
lock?: boolean | number | string;
|
|
43
|
+
}
|
|
44
|
+
interface TaskRegisterOptions extends TaskOptions {
|
|
45
|
+
command: string | string[];
|
|
46
|
+
}
|
|
47
|
+
type ErrorHandler = (error: Error, task: Task) => (void | Promise<void>);
|
|
48
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
49
|
+
type Factory<T> = () => MaybePromise<{
|
|
50
|
+
default: T;
|
|
51
|
+
} | T>;
|
|
52
|
+
|
|
53
|
+
declare abstract class Task {
|
|
54
|
+
isCanceled: boolean;
|
|
55
|
+
promise?: Promise<any>;
|
|
56
|
+
static options: TaskOptions;
|
|
57
|
+
constructor(..._: any[]);
|
|
58
|
+
get name(): string;
|
|
59
|
+
abstract run(...args: any[]): Promise<void>;
|
|
60
|
+
$cancel(): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
interface Task {
|
|
63
|
+
onCancel?(): Promise<void>;
|
|
64
|
+
onError?(error: Error): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
declare class Scheduler {
|
|
68
|
+
private config;
|
|
69
|
+
private resolver;
|
|
70
|
+
private logger;
|
|
71
|
+
private locks?;
|
|
72
|
+
private definitions;
|
|
73
|
+
private state;
|
|
74
|
+
private errorHandler?;
|
|
75
|
+
constructor(config: SchedulerConfig, resolver: ContainerResolver<ContainerBindings>, logger: Logger, locks?: LockService | undefined);
|
|
76
|
+
register(options: TaskRegisterOptions | Factory<typeof Task>): this;
|
|
77
|
+
start(wait?: boolean): Promise<void>;
|
|
78
|
+
stop(): Promise<void>;
|
|
79
|
+
onError(callback: ErrorHandler): this;
|
|
80
|
+
private load;
|
|
81
|
+
private make;
|
|
82
|
+
private run;
|
|
83
|
+
private hasState;
|
|
84
|
+
private setState;
|
|
85
|
+
private handleError;
|
|
86
|
+
private cancel;
|
|
87
|
+
private terminate;
|
|
88
|
+
private schedule;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { type SchedulerConfig as S, Task as T, Scheduler as a, type TaskOptions as b };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { a as Scheduler } from '../scheduler-BWT0Iqko.js';
|
|
2
|
+
import '@adonisjs/core/container';
|
|
3
|
+
import '@adonisjs/core/types';
|
|
4
|
+
import '@adonisjs/lock/types';
|
|
5
|
+
import '@adonisjs/core/logger';
|
|
6
|
+
|
|
7
|
+
declare let scheduler: Scheduler;
|
|
8
|
+
|
|
9
|
+
export { scheduler as default };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{#var name = string(entity.name).removeSuffix('task').suffix('Task').pascalCase().toString()}}
|
|
2
|
+
{{#var fileName = string(entity.name).removeSuffix('task').suffix('.task').snakeCase().removeExtension().ext('.ts').toString()}}
|
|
3
|
+
{{{
|
|
4
|
+
exports({ to: app.makePath('app/tasks', entity.path, fileName) })
|
|
5
|
+
}}}
|
|
6
|
+
import { Task, type TaskOptions } from '@outloud/adonis-scheduler'
|
|
7
|
+
|
|
8
|
+
export default class {{ name }} extends Task {
|
|
9
|
+
static options: TaskOptions = {
|
|
10
|
+
schedule: '* * * * *'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async run(): Promise<void> {
|
|
14
|
+
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports({ to: app.configPath('scheduler.ts') })
|
|
3
|
+
}}}
|
|
4
|
+
|
|
5
|
+
import { defineConfig } from '@outloud/adonis-scheduler'
|
|
6
|
+
import env from '#start/env'
|
|
7
|
+
|
|
8
|
+
const schedulerConfig = defineConfig({
|
|
9
|
+
httpServer: env.get('SCHEDULER_HTTP_SERVER', false),
|
|
10
|
+
warnWhenLocked: false,
|
|
11
|
+
lockDuration: '10m',
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export default schedulerConfig
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@outloud/adonis-scheduler",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.5",
|
|
5
|
+
"description": "Schedule cron jobs in AdonisJS.",
|
|
6
|
+
"author": "Outloud <hello@outloud.co>",
|
|
7
|
+
"contributors": [
|
|
8
|
+
"Andrej Adamcik"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"homepage": "https://github.com/madebyoutloud/adonis-scheduler#readme",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/madebyoutloud/adonis-scheduler.git"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"tag": "latest",
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"build"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./build/index.js",
|
|
25
|
+
"./provider": "./build/providers/scheduler.provider.js",
|
|
26
|
+
"./commands": "./build/commands/main.js",
|
|
27
|
+
"./services/*": "./build/services/*.js"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@adonisjs/core": "^6",
|
|
31
|
+
"@adonisjs/lock": "^1"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"@adonisjs/lock": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@adonisjs/assembler": "^7.8.2",
|
|
40
|
+
"@adonisjs/core": "~6.19.0",
|
|
41
|
+
"@adonisjs/lock": "^1.1.1",
|
|
42
|
+
"@adonisjs/tsconfig": "^1.4.1",
|
|
43
|
+
"@outloud/eslint-config": "^2.0.5",
|
|
44
|
+
"@swc/core": "^1.13.3",
|
|
45
|
+
"@types/node": "^24.2.1",
|
|
46
|
+
"copyfiles": "^2.4.1",
|
|
47
|
+
"eslint": "^9.33.0",
|
|
48
|
+
"reflect-metadata": "^0.2.2",
|
|
49
|
+
"release-it": "^19.0.4",
|
|
50
|
+
"tsup": "~8.5.0",
|
|
51
|
+
"typescript": "~5.9.2"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"croner": "^9"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"copy:templates": "copyfiles \"stubs/**/*.stub\" build",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"lint": "eslint . ",
|
|
60
|
+
"lint:fix": "eslint --fix .",
|
|
61
|
+
"quick:test": "node --import=./tsnode.esm.js --enable-source-maps bin/test.ts",
|
|
62
|
+
"pretest": "pnpm run lint",
|
|
63
|
+
"test": "c8 pnpm run quick:test",
|
|
64
|
+
"precompile": "pnpm run lint && rm -rf build",
|
|
65
|
+
"compile": "tsup-node",
|
|
66
|
+
"postcompile": "pnpm run copy:templates && pnpm run index:commands",
|
|
67
|
+
"build": "rm -rf build && pnpm run compile",
|
|
68
|
+
"release": "release-it",
|
|
69
|
+
"index:commands": "adonis-kit index build/commands",
|
|
70
|
+
"version": "pnpm run build"
|
|
71
|
+
}
|
|
72
|
+
}
|