add status for redis
This commit is contained in:
parent
0b47b38060
commit
1662fd4dfa
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "page-proxy",
|
"name": "page-proxy",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2-beta.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@ -16,12 +16,15 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"watch": "rollup -c --watch",
|
||||||
"dev": "cross-env NODE_ENV=development nodemon --ignore upload --exec tsx src/index.ts",
|
"dev": "cross-env NODE_ENV=development nodemon --ignore upload --exec tsx src/index.ts",
|
||||||
|
"dev:watch": "cross-env NODE_ENV=development concurrently -n \"Watch,Dev\" -c \"green,blue\" \"npm run watch\" \"sleep 1 && npm run dev\" ",
|
||||||
"build": "rimraf dist && rollup -c",
|
"build": "rimraf dist && rollup -c",
|
||||||
"start": "pm2 start dist/app.mjs --name page-proxy",
|
"start": "pm2 start dist/app.mjs --name page-proxy",
|
||||||
"release": "node ./scripts/release/index.mjs",
|
"release": "node ./scripts/release/index.mjs",
|
||||||
"deploy": "envision switch root && envision pack -p -u",
|
"deploy": "envision switch root && envision pack -p -u",
|
||||||
"pub": "npm run build && npm run deploy"
|
"pub": "npm run build && npm run deploy",
|
||||||
|
"ssl": "ssh -L 6379:localhost:6379 light"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@ -33,6 +36,7 @@
|
|||||||
"@rollup/plugin-typescript": "^12.1.2",
|
"@rollup/plugin-typescript": "^12.1.2",
|
||||||
"@types/http-proxy": "^1.17.16",
|
"@types/http-proxy": "^1.17.16",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
|
"concurrently": "^9.1.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
"rollup": "^4.34.8",
|
"rollup": "^4.34.8",
|
||||||
@ -42,6 +46,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kevisual/router": "0.0.7",
|
"@kevisual/router": "0.0.7",
|
||||||
"@kevisual/use-config": "^1.0.8",
|
"@kevisual/use-config": "^1.0.8",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
"ioredis": "^5.5.0",
|
"ioredis": "^5.5.0",
|
||||||
"nanoid": "^5.1.2"
|
"nanoid": "^5.1.2"
|
||||||
},
|
},
|
||||||
|
634
pnpm-lock.yaml
generated
634
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -7,17 +7,19 @@ import { nanoid } from 'nanoid';
|
|||||||
import { pipeline } from 'stream';
|
import { pipeline } from 'stream';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { fetchApp, fetchDomain, fetchTest } from './query/get-router.ts';
|
import { fetchApp, fetchDomain, fetchTest } from './query/get-router.ts';
|
||||||
|
import { getAppLoadStatus, setAppLoadStatus, AppLoadStatus } from './redis/get-app-status.ts';
|
||||||
|
|
||||||
const pipelineAsync = promisify(pipeline);
|
const pipelineAsync = promisify(pipeline);
|
||||||
|
|
||||||
const { resources } = config?.proxy || { resources: 'https://minio.xiongxiao.me/resources' };
|
const { resources } = config?.proxy || { resources: 'https://minio.xiongxiao.me/resources' };
|
||||||
const status: { [key: string]: boolean } = {};
|
|
||||||
const demoData = {
|
const demoData = {
|
||||||
user: 'root',
|
user: 'root',
|
||||||
key: 'codeflow',
|
key: 'codeflow',
|
||||||
appType: 'web-single', //
|
appType: 'web-single', //
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
domain: null,
|
domain: null,
|
||||||
type: 'local',
|
type: 'local', // local, oss, 默认是oss
|
||||||
data: {
|
data: {
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
@ -51,6 +53,10 @@ export class UserApp {
|
|||||||
this.isTest = true;
|
this.isTest = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 是否已经加载到本地了
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
async getExist() {
|
async getExist() {
|
||||||
const app = this.app;
|
const app = this.app;
|
||||||
const user = this.user;
|
const user = this.user;
|
||||||
@ -106,82 +112,106 @@ export class UserApp {
|
|||||||
user: fetchData.user,
|
user: fetchData.user,
|
||||||
app: fetchData.key,
|
app: fetchData.key,
|
||||||
};
|
};
|
||||||
redis.set(key, data.user + ':' + data.app, 'EX', 60 * 60 * 24 * 7); // 24小时
|
redis.set(key, data.user + ':' + data.app, 'EX', 60 * 60 * 24 * 7); // 7天
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
async setLoaded() {
|
/**
|
||||||
|
* 加载结束
|
||||||
|
* @param msg
|
||||||
|
*/
|
||||||
|
async setLoaded(status: 'running' | 'error' | 'loading', msg?: string) {
|
||||||
const app = this.app;
|
const app = this.app;
|
||||||
const user = this.user;
|
const user = this.user;
|
||||||
const key = 'user:app:' + app + ':' + user;
|
await setAppLoadStatus(user, app, {
|
||||||
if (status[key]) {
|
status,
|
||||||
status[key] = false;
|
message: msg,
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
async getLoaded() {
|
async getLoaded() {
|
||||||
const app = this.app;
|
const app = this.app;
|
||||||
const user = this.user;
|
const user = this.user;
|
||||||
const key = 'user:app:' + app + ':' + user;
|
const value = await getAppLoadStatus(user, app);
|
||||||
return status[key];
|
return value;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 设置缓存数据,当出问题了,就重新加载。
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
async setCacheData() {
|
async setCacheData() {
|
||||||
const app = this.app;
|
const app = this.app;
|
||||||
const user = this.user;
|
const user = this.user;
|
||||||
const isTest = this.isTest;
|
const isTest = this.isTest;
|
||||||
const key = 'user:app:' + app + ':' + user;
|
const key = 'user:app:' + app + ':' + user;
|
||||||
if (status[key]) {
|
const fetchRes = isTest ? await fetchTest(app) : await fetchApp({ user, app });
|
||||||
|
if (fetchRes?.code !== 200) {
|
||||||
|
console.log('fetchRes is error', fetchRes, 'user', user, 'app', app);
|
||||||
|
return { code: 500, message: 'fetchRes is error' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadStatus = await getAppLoadStatus(user, app);
|
||||||
|
if (loadStatus.status === 'loading') {
|
||||||
|
// 其他情况,error或者running都可以重新加载
|
||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
status[key] = true;
|
|
||||||
|
|
||||||
const fetchRes = isTest ? await fetchTest(app) : await fetchApp({ user, app });
|
|
||||||
if (fetchRes?.code !== 200) {
|
|
||||||
console.log('fetchRes is error', fetchRes);
|
|
||||||
this.setLoaded();
|
|
||||||
return { code: 500, message: 'fetchRes is error' };
|
|
||||||
}
|
|
||||||
const fetchData = fetchRes.data;
|
const fetchData = fetchRes.data;
|
||||||
if (!fetchData.type) {
|
if (!fetchData.type) {
|
||||||
// console.error('fetchData type is error', fetchData);
|
// console.error('fetchData type is error', fetchData);
|
||||||
// return false;
|
// return false;
|
||||||
fetchData.type = 'oss';
|
fetchData.type = 'oss';
|
||||||
}
|
}
|
||||||
if (fetchData.status !== 'running') {
|
this.setLoaded('loading', 'loading');
|
||||||
console.error('fetchData status is not running', fetchData.user, fetchData.key);
|
const loadFilesFn = async () => {
|
||||||
this.setLoaded();
|
const value = await downloadUserAppFiles(user, app, fetchData);
|
||||||
return {
|
if (value.data.files.length === 0) {
|
||||||
code: 500,
|
console.error('root files length is zero', user, app);
|
||||||
message: 'fetchData status is not running',
|
this.setLoaded('running', 'root files length is zero');
|
||||||
};
|
const mockPath = path.join(fileStore, user, app, 'index.html');
|
||||||
}
|
value.data.files = [
|
||||||
const value = await downloadUserAppFiles(user, app, fetchData);
|
{
|
||||||
if (value.data.files.length === 0) {
|
name: 'index.html', // 映射
|
||||||
console.error('root files length is zero', user, app);
|
path: mockPath.replace(fileStore, ''), // 实际
|
||||||
this.setLoaded();
|
},
|
||||||
return { code: 404 };
|
];
|
||||||
}
|
if (!checkFileExistsSync(path.join(fileStore, user, app))) {
|
||||||
let valueIndexHtml = value.data.files.find((file) => file.name === 'index.html');
|
fs.mkdirSync(path.join(fileStore, user, app), { recursive: true });
|
||||||
if (!valueIndexHtml) {
|
}
|
||||||
valueIndexHtml = value.data.files.find((file) => file.name === 'index.js');
|
// 自己创建一个index.html
|
||||||
if (!valueIndexHtml) {
|
fs.writeFileSync(path.join(fileStore, user, app, 'index.html'), 'not has any app info', {
|
||||||
valueIndexHtml = value.data.files[0];
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
let valueIndexHtml = value.data.files.find((file) => file.name === 'index.html');
|
||||||
|
if (!valueIndexHtml) {
|
||||||
|
valueIndexHtml = value.data.files.find((file) => file.name === 'index.js');
|
||||||
|
if (!valueIndexHtml) {
|
||||||
|
valueIndexHtml = value.data.files[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await redis.set(key, JSON.stringify(value));
|
||||||
|
await redis.set('user:app:exist:' + app + ':' + user, valueIndexHtml.path, 'EX', 60 * 60 * 24 * 7); // 7天
|
||||||
|
const files = value.data.files;
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
// 将文件名和路径添加到 `data` 对象中
|
||||||
|
files.forEach((file) => {
|
||||||
|
data[file.name] = file.path;
|
||||||
|
});
|
||||||
|
await redis.hset('user:app:set:' + app + ':' + user, data);
|
||||||
|
this.setLoaded('running', 'loaded');
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
loadFilesFn();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('loadFilesFn error', e);
|
||||||
|
this.setLoaded('error', 'loadFilesFn error');
|
||||||
}
|
}
|
||||||
await redis.set(key, JSON.stringify(value));
|
return {
|
||||||
await redis.set('user:app:exist:' + app + ':' + user, valueIndexHtml.path, 'EX', 60 * 60 * 24 * 7); // 24小时
|
code: 20000,
|
||||||
const files = value.data.files;
|
data: 'loading',
|
||||||
// await redis.hset(key, 'files', JSON.stringify(files));
|
};
|
||||||
const data = {};
|
|
||||||
|
|
||||||
// 将文件名和路径添加到 `data` 对象中
|
|
||||||
files.forEach((file) => {
|
|
||||||
data[file.name] = file.path;
|
|
||||||
});
|
|
||||||
await redis.hset('user:app:set:' + app + ':' + user, data);
|
|
||||||
this.setLoaded();
|
|
||||||
|
|
||||||
return { code: 200, data: valueIndexHtml.path };
|
|
||||||
}
|
}
|
||||||
async getAllCacheData() {
|
async getAllCacheData() {
|
||||||
const app = this.app;
|
const app = this.app;
|
||||||
@ -201,6 +231,7 @@ export class UserApp {
|
|||||||
await redis.del(key);
|
await redis.del(key);
|
||||||
await redis.del('user:app:exist:' + app + ':' + user);
|
await redis.del('user:app:exist:' + app + ':' + user);
|
||||||
await redis.del('user:app:set:' + app + ':' + user);
|
await redis.del('user:app:set:' + app + ':' + user);
|
||||||
|
await redis.del('user:app:status:' + app + ':' + user);
|
||||||
// 删除所有文件
|
// 删除所有文件
|
||||||
deleteUserAppFiles(user, app);
|
deleteUserAppFiles(user, app);
|
||||||
}
|
}
|
||||||
|
42
src/module/html/create-refresh-html.ts
Normal file
42
src/module/html/create-refresh-html.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 创建一个刷新页面,定时
|
||||||
|
* fetch('/api/proxy/refresh?user=user&app=app'), 如果返回200,则刷新页面
|
||||||
|
* @param user
|
||||||
|
* @param app
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createRefreshHtml = (user: string, app: string) => {
|
||||||
|
return `
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN" >
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>App: ${user}/${app}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>App: ${user}/${app}</h1>
|
||||||
|
<p>Loading...</p>
|
||||||
|
<p>如果长时间没有加载出来,请手动 <a href="javascript:void(0)" onclick="window.location.reload()">刷新页面</a></p>
|
||||||
|
<p>loadCount: <span id="loadCount">0</span></p>
|
||||||
|
<script type="module">
|
||||||
|
let count = 0;
|
||||||
|
const refresh = () => {
|
||||||
|
const origin = window.location.origin;
|
||||||
|
const loadCount = document.getElementById('loadCount');
|
||||||
|
count++;
|
||||||
|
loadCount.innerHTML = count.toString();
|
||||||
|
fetch(origin + '/api/proxy?user=${user}&app=${app}&path=app&key=status').then((res) => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
setTimeout(refresh, 3000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
setTimeout(refresh, 2000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
};
|
BIN
src/module/html/favicon.ico
Normal file
BIN
src/module/html/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
@ -5,17 +5,16 @@ import { config, fileStore } from '../module/config.ts';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { getContentType } from './get-content-type.ts';
|
import { getContentType } from './get-content-type.ts';
|
||||||
import { sleep } from '@/utils/sleep.ts';
|
import { createRefreshHtml } from './html/create-refresh-html.ts';
|
||||||
|
|
||||||
const api = config?.api || { host: 'kevisual.xiongxiao.me', path: '/api/router' };
|
const api = config?.api || { host: 'kevisual.xiongxiao.me', path: '/api/router' };
|
||||||
const domain = config?.proxy?.domain || 'kevisual.xiongxiao.me';
|
const domain = config?.proxy?.domain || 'kevisual.xiongxiao.me';
|
||||||
const allowedOrigins = config?.proxy?.allowOrigin || [];
|
const allowedOrigins = config?.proxy?.allowOrigin || [];
|
||||||
|
|
||||||
|
|
||||||
const noProxyUrl = ['/', '/favicon.ico'];
|
const noProxyUrl = ['/', '/favicon.ico'];
|
||||||
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
export const handleRequest = async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
if (req.url === '/favicon.ico') {
|
if (req.url === '/favicon.ico') {
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
res.writeHead(200, { 'Content-Type': 'image/x-icon' });
|
||||||
res.write('proxy no favicon.ico\n');
|
res.write('proxy no favicon.ico\n');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -144,33 +143,24 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
let isExist = await userApp.getExist();
|
let isExist = await userApp.getExist();
|
||||||
if (!isExist) {
|
if (!isExist) {
|
||||||
try {
|
try {
|
||||||
const { code, loading, data } = await userApp.setCacheData();
|
const { code, loading } = await userApp.setCacheData();
|
||||||
if (loading) {
|
if (loading || code === 20000) {
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
res.write('Loading App\n');
|
|
||||||
res.end();
|
res.end(createRefreshHtml(user, app));
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (code !== 200) {
|
|
||||||
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
||||||
res.write('Not Found App\n');
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await sleep(1000);
|
|
||||||
isExist = data; // 设置缓存后再次获取
|
|
||||||
if (!isExist) {
|
|
||||||
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
||||||
res.write('Not Found App Index Page\n');
|
|
||||||
res.end();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
|
res.write('Not Found App\n');
|
||||||
|
res.end();
|
||||||
|
// 不存在就一定先返回loading状态。
|
||||||
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('setCacheData error', error);
|
console.error('setCacheData error', error);
|
||||||
res.writeHead(500, { 'Content-Type': 'text/html' });
|
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
res.write('Server Error\n');
|
res.write('Server Error\n');
|
||||||
res.end();
|
res.end();
|
||||||
userApp.setLoaded();
|
userApp.setLoaded('error', 'setCacheData error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -189,7 +179,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
const isHTML = contentType.includes('html');
|
const isHTML = contentType.includes('html');
|
||||||
const filePath = path.join(fileStore, indexFilePath);
|
const filePath = path.join(fileStore, indexFilePath);
|
||||||
if (!userApp.fileCheck(filePath)) {
|
if (!userApp.fileCheck(filePath)) {
|
||||||
res.writeHead(500, { 'Content-Type': 'text/html' });
|
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
res.write('File expired, Not Found\n');
|
res.write('File expired, Not Found\n');
|
||||||
res.end();
|
res.end();
|
||||||
await userApp.clearCacheData();
|
await userApp.clearCacheData();
|
||||||
@ -232,7 +222,7 @@ export const handleRequest = async (req: http.IncomingMessage, res: http.ServerR
|
|||||||
});
|
});
|
||||||
if (!userApp.fileCheck(filePath)) {
|
if (!userApp.fileCheck(filePath)) {
|
||||||
console.error('File expired', filePath);
|
console.error('File expired', filePath);
|
||||||
res.writeHead(500, { 'Content-Type': 'text/html' });
|
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
res.write('File expired\n');
|
res.write('File expired\n');
|
||||||
res.end();
|
res.end();
|
||||||
await userApp.clearCacheData();
|
await userApp.clearCacheData();
|
||||||
|
30
src/module/redis/get-app-status.ts
Normal file
30
src/module/redis/get-app-status.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { redis } from './redis.ts';
|
||||||
|
|
||||||
|
export type AppLoadStatus = {
|
||||||
|
status: 'running' | 'loading' | 'error' | 'not-exist';
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAppLoadStatus = async (user: string, app: string): Promise<AppLoadStatus> => {
|
||||||
|
const key = 'user:app:status:' + app + ':' + user;
|
||||||
|
const value = await redis.get(key);
|
||||||
|
if (!value) {
|
||||||
|
return {
|
||||||
|
status: 'not-exist',
|
||||||
|
message: 'not-exist',
|
||||||
|
}; // 没有加载过
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
status: 'error',
|
||||||
|
message: 'error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const setAppLoadStatus = async (user: string, app: string, status: AppLoadStatus) => {
|
||||||
|
const key = 'user:app:status:' + app + ':' + user;
|
||||||
|
const value = JSON.stringify(status);
|
||||||
|
await redis.set(key, value);
|
||||||
|
};
|
@ -78,7 +78,7 @@ app
|
|||||||
key: 'get',
|
key: 'get',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { user, app } = ctx.query.data || {};
|
const { user, app } = ctx.query;
|
||||||
if (!user || !app) {
|
if (!user || !app) {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
ctx.throw('user is required');
|
ctx.throw('user is required');
|
||||||
@ -95,3 +95,16 @@ app
|
|||||||
ctx.body = cache;
|
ctx.body = cache;
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|
||||||
|
app
|
||||||
|
.route({
|
||||||
|
path: 'app',
|
||||||
|
key: 'status',
|
||||||
|
})
|
||||||
|
.define(async (ctx) => {
|
||||||
|
const { user, app } = ctx.query;
|
||||||
|
const userApp = new UserApp({ user, app });
|
||||||
|
const status = await userApp.getLoaded();
|
||||||
|
ctx.body = status;
|
||||||
|
})
|
||||||
|
.addTo(app);
|
||||||
|
7
src/scripts/clear.ts
Normal file
7
src/scripts/clear.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { UserApp, clearAllUserApp } from '../module/get-user-app.ts';
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
await clearAllUserApp();
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
Loading…
x
Reference in New Issue
Block a user