feat(test): Automate smoke tests for JS Client [DXJ-293] (#282)
45
package.json
@ -1,24 +1,25 @@
|
||||
{
|
||||
"name": "common-dev-deps",
|
||||
"version": "0.1.0",
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"pnpm": ">=3"
|
||||
},
|
||||
"scripts": {
|
||||
"simulate-cdn": "http-server -p 8765 ./packages/client/js-client.web.standalone/dist"
|
||||
},
|
||||
"author": "Fluence Labs",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"http-server": "14.1.1",
|
||||
"@types/node": "18.13.0",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.7",
|
||||
"@fluencelabs/aqua-lib": "0.6.0",
|
||||
"@fluencelabs/aqua": "0.9.1-374"
|
||||
}
|
||||
"name": "common-dev-deps",
|
||||
"version": "0.1.0",
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"pnpm": ">=3"
|
||||
},
|
||||
"scripts": {
|
||||
"simulate-cdn": "http-server -p 8765 ./packages/client/js-client.web.standalone/dist"
|
||||
},
|
||||
"author": "Fluence Labs",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"http-server": "14.1.1",
|
||||
"puppeteer": "19.7.2",
|
||||
"@types/node": "18.13.0",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.7",
|
||||
"@fluencelabs/aqua-lib": "0.6.0",
|
||||
"@fluencelabs/aqua": "0.9.1-374"
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,13 @@ import { Fluence } from '@fluencelabs/js-client.api';
|
||||
import { kras, randomKras } from '@fluencelabs/fluence-network-environment';
|
||||
import { registerHelloWorld, smokeTest } from './_aqua/smoke_test.js';
|
||||
|
||||
// Relay running on local machine
|
||||
// const relay = {
|
||||
// multiaddr: '/ip4/127.0.0.1/tcp/4310/ws/p2p/12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3',
|
||||
// peerId: '12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3',
|
||||
// };
|
||||
|
||||
// Currently the tests executes some calls to registry. And they fail for a single local node setup. So we use kras instead.
|
||||
const relay = randomKras();
|
||||
|
||||
function generateRandomUint8Array() {
|
||||
@ -27,7 +29,9 @@ const optsWithRandomKeyPair = () => {
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const main = async () => {
|
||||
export type TestResult = { res: string | null; errors: string[]; hello: string };
|
||||
|
||||
export const runTest = async (): Promise<TestResult> => {
|
||||
try {
|
||||
Fluence.onConnectionStateChange((state) => console.info('connection state changed: ', state));
|
||||
|
||||
@ -58,6 +62,8 @@ export const main = async () => {
|
||||
} else {
|
||||
console.log('aqua finished, result', res);
|
||||
}
|
||||
|
||||
return { res, errors, hello };
|
||||
} finally {
|
||||
console.log('disconnecting from Fluence Network...');
|
||||
await Fluence.disconnect();
|
||||
@ -66,7 +72,7 @@ export const main = async () => {
|
||||
};
|
||||
|
||||
export const runMain = () => {
|
||||
main()
|
||||
runTest()
|
||||
.then(() => console.log('done!'))
|
||||
.catch((err) => console.error('error: ', err));
|
||||
};
|
||||
|
@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"_test": "node --loader ts-node/esm ./src/index.ts"
|
||||
"test": "node --loader ts-node/esm ./src/index.ts"
|
||||
},
|
||||
"repository": "https://github.com/fluencelabs/fluence-js",
|
||||
"author": "Fluence Labs",
|
4
packages/@tests/smoke/node/src/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import '@fluencelabs/js-client.node';
|
||||
import { runTest } from '@test/aqua_for_test';
|
||||
|
||||
runTest().then(() => console.log('Smoke tests succeed!'));
|
7
packages/@tests/smoke/node/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
"name": "cra-ts",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@fluencelabs/js-client.api": "workspace:^",
|
||||
"@test/aqua_for_test": "workspace:^",
|
||||
@ -18,8 +19,11 @@
|
||||
"typescript": "4.9.5",
|
||||
"web-vitals": "2.1.4"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"devDependencies": {
|
||||
"@test/test-utils": "workspace:^"
|
||||
},
|
||||
"scripts": {
|
||||
"commented_out_test": "node --loader ts-node/esm ./test/index.ts",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"_test": "react-scripts test",
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
@ -7,7 +7,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using create-react-app" />
|
||||
<script src='http://localhost:8765/js-client.min.js' async></script>
|
||||
<script src='http://localhost:8766/js-client.min.js' async></script>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
48
packages/@tests/smoke/web-cra-ts/src/App.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { runTest } from '@test/aqua_for_test';
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const [result, setResult] = React.useState<string | null>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
const onButtonClick = () => {
|
||||
runTest()
|
||||
.then((res) => {
|
||||
if (res.errors.length === 0) {
|
||||
setResult(JSON.stringify(res));
|
||||
setError(null);
|
||||
} else {
|
||||
setResult(null);
|
||||
setError(res.errors.toString());
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setResult('');
|
||||
setError(err.toString());
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload.
|
||||
</p>
|
||||
<button id="btn" onClick={onButtonClick}>
|
||||
Click to run test
|
||||
</button>
|
||||
|
||||
{result && <div id="res">{result}</div>}
|
||||
{error && <div id="error">{error}</div>}
|
||||
<a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { runMain } from '@test/aqua_for_test';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
@ -16,5 +15,3 @@ root.render(
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
|
||||
runMain();
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
45
packages/@tests/smoke/web-cra-ts/test/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { startCdn, startContentServer, stopServer } from '@test/test-utils';
|
||||
|
||||
const port = 3001;
|
||||
const uri = `http://localhost:${port}/`;
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const publicPath = join(__dirname, '../build/');
|
||||
|
||||
const test = async () => {
|
||||
const cdn = await startCdn(8766);
|
||||
const localServer = await startContentServer(port, publicPath);
|
||||
|
||||
console.log('starting puppeteer...');
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
// uncomment to debug what's happening inside the browser
|
||||
// page.on('console', (msg) => console.log('// from console: ', msg.text()));
|
||||
|
||||
console.log('going to the page in browser...');
|
||||
await page.goto(uri);
|
||||
|
||||
console.log('clicking button...');
|
||||
await page.click('#btn');
|
||||
|
||||
console.log('waiting for result to appear...');
|
||||
const elem = await page.waitForSelector('#res');
|
||||
|
||||
console.log('getting the content of result div...');
|
||||
const content = await elem?.evaluate((x) => x.textContent);
|
||||
console.log('raw result: ', content);
|
||||
|
||||
await browser.close();
|
||||
await stopServer(cdn);
|
||||
await stopServer(localServer);
|
||||
|
||||
if (!content) {
|
||||
throw new Error('smoke test failed!');
|
||||
}
|
||||
};
|
||||
|
||||
test().then(() => console.log('smoke tests succeed!'));
|
20
packages/@tests/smoke/web-cra-ts/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
@ -8,17 +8,18 @@
|
||||
"node": ">=10",
|
||||
"pnpm": ">=3"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm copy-script",
|
||||
"serve": "http-server public",
|
||||
"copy-script": "cp ../../client/js-client.web.standalone/dist/js-client.min.js ./public"
|
||||
"build": "tsc",
|
||||
"commented_out_test": "node --loader ts-node/esm ./src/index.ts",
|
||||
"serve": "http-server public"
|
||||
},
|
||||
"repository": "https://github.com/fluencelabs/fluence-js",
|
||||
"author": "Fluence Labs",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"dependencies": {
|
||||
"@fluencelabs/js-client.web.standalone": "workspace:^",
|
||||
"http-server": "14.1.1"
|
||||
}
|
||||
"@test/test-utils": "workspace:^"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
@ -6,14 +6,18 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Smoke test for web</title>
|
||||
<script src="./js-client.min.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
<h1>Open console f12</h1>
|
||||
|
||||
<button id="btn">Click to run test</button>
|
||||
|
||||
<div id="res-placeholder"></div>
|
||||
|
||||
<script src='http://localhost:8765/js-client.min.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
</main>
|
||||
</body>
|
||||
|
@ -1,11 +1,19 @@
|
||||
const peer = globalThis.defaultPeer;
|
||||
const fluence = globalThis.fluence;
|
||||
|
||||
// Relay from kras network
|
||||
// Currently the tests executes some calls to registry. And they fail for a single local node setup. So we use kras instead.
|
||||
const relay = {
|
||||
multiaddr: '/ip4/127.0.0.1/tcp/4310/ws/p2p/12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3',
|
||||
peerId: '12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3',
|
||||
multiaddr: '/dns4/kras-01.fluence.dev/tcp/19001/wss/p2p/12D3KooWKnEqMfYo9zvfHmqTLpLdiHXPe4SVqUWcWHDJdFGrSmcA',
|
||||
peerId: '12D3KooWKnEqMfYo9zvfHmqTLpLdiHXPe4SVqUWcWHDJdFGrSmcA',
|
||||
};
|
||||
|
||||
const getRelayTime = (relayPeerId) => {
|
||||
// Relay running on local machine
|
||||
//const relay = {
|
||||
// multiaddr: '/ip4/127.0.0.1/tcp/4310/ws/p2p/12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3',
|
||||
// peerId: '12D3KooWKEprYXUXqoV5xSBeyqrWLpQLLH4PXfvVkDJtmcqmh5V3',
|
||||
//};
|
||||
|
||||
const getRelayTime = () => {
|
||||
const script = `
|
||||
(xor
|
||||
(seq
|
||||
@ -34,8 +42,7 @@ const getRelayTime = (relayPeerId) => {
|
||||
)
|
||||
)
|
||||
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 3])
|
||||
)
|
||||
`;
|
||||
)`;
|
||||
|
||||
const def = {
|
||||
functionName: 'getRelayTime',
|
||||
@ -73,31 +80,39 @@ const getRelayTime = (relayPeerId) => {
|
||||
|
||||
const config = {};
|
||||
|
||||
const args = {};
|
||||
return peer.compilerSupport.callFunction({
|
||||
const args = { relayPeerId: relay.peerId };
|
||||
return fluence.callAquaFunction({
|
||||
args,
|
||||
def,
|
||||
config,
|
||||
script,
|
||||
config,
|
||||
peer: fluence.defaultClient,
|
||||
});
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
console.log('starting fluence...');
|
||||
await peer.start({
|
||||
connectTo: relay,
|
||||
});
|
||||
await fluence.defaultClient.connect(relay);
|
||||
console.log('started fluence');
|
||||
|
||||
console.log('getting relay time...');
|
||||
const res = await getRelayTime(relay.peerId);
|
||||
console.log('got relay time, ', res);
|
||||
const relayTime = await getRelayTime();
|
||||
console.log('got relay time, ', relayTime);
|
||||
|
||||
console.log('stopping fluence...');
|
||||
await peer.stop();
|
||||
await fluence.defaultClient.stop();
|
||||
console.log('stopped fluence...');
|
||||
|
||||
return relayTime;
|
||||
};
|
||||
|
||||
main()
|
||||
.then(() => console.log('done!'))
|
||||
.catch((err) => console.error('error: ', err));
|
||||
const btn = document.getElementById('btn');
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
main().then((res) => {
|
||||
const inner = document.createElement('div');
|
||||
inner.id = 'res';
|
||||
inner.innerText = 'res';
|
||||
document.getElementById('res-placeholder').appendChild(inner);
|
||||
});
|
||||
});
|
45
packages/@tests/smoke/web/src/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import puppeteer from 'puppeteer';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { startCdn, startContentServer, stopServer } from '@test/test-utils';
|
||||
|
||||
const port = 3000;
|
||||
const uri = `http://localhost:${port}/`;
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const publicPath = join(__dirname, '../public/');
|
||||
|
||||
const test = async () => {
|
||||
const cdn = await startCdn(8765);
|
||||
const localServer = await startContentServer(port, publicPath);
|
||||
|
||||
console.log('starting puppeteer...');
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
// uncomment to debug what's happening inside the browser
|
||||
// page.on('console', (msg) => console.log('// from console: ', msg.text()));
|
||||
|
||||
console.log('going to the page in browser...');
|
||||
await page.goto(uri);
|
||||
|
||||
console.log('clicking button...');
|
||||
await page.click('#btn');
|
||||
|
||||
console.log('waiting for result to appear...');
|
||||
const elem = await page.waitForSelector('#res');
|
||||
|
||||
console.log('getting the content of result div...');
|
||||
const content = await elem?.evaluate((x) => x.textContent);
|
||||
console.log('raw result: ', content);
|
||||
|
||||
await browser.close();
|
||||
await stopServer(cdn);
|
||||
await stopServer(localServer);
|
||||
|
||||
if (!content) {
|
||||
throw new Error('smoke test failed!');
|
||||
}
|
||||
};
|
||||
|
||||
test().then(() => console.log('smoke tests succeed!'));
|
7
packages/@tests/smoke/web/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
import '@fluencelabs/js-client.node';
|
||||
import { runMain } from '@test/aqua_for_test';
|
||||
|
||||
runMain();
|
26
packages/@tests/test-utils/package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@test/test-utils",
|
||||
"version": "0.1.0",
|
||||
"description": "Test utils",
|
||||
"main": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"pnpm": ">=3"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"repository": "https://github.com/fluencelabs/fluence-js",
|
||||
"author": "Fluence Labs",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fluencelabs/js-client.api": "workspace:^",
|
||||
"@fluencelabs/js-client.node": "workspace:^",
|
||||
"serve-handler": "6.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/serve-handler": "6.1.1"
|
||||
}
|
||||
}
|
37
packages/@tests/test-utils/src/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import handler from 'serve-handler';
|
||||
import { createServer } from 'http';
|
||||
import type { Server } from 'http';
|
||||
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const CDN_PUBLIC_PATH = join(__dirname, '../../../client/js-client.web.standalone/dist/');
|
||||
|
||||
export const startCdn = (port: number) => startContentServer(port, CDN_PUBLIC_PATH);
|
||||
|
||||
export const startContentServer = (port: number, publicDir: string): Promise<Server> => {
|
||||
const server = createServer((request, response) => {
|
||||
return handler(request, response, {
|
||||
public: publicDir,
|
||||
});
|
||||
});
|
||||
|
||||
return new Promise<Server>((resolve) => {
|
||||
const result = server.listen(port, () => {
|
||||
console.log(`server started on port ${port}`);
|
||||
console.log(`public dir ${publicDir}`);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const stopServer = (app: Server): Promise<void> => {
|
||||
return new Promise<void>((resolve) => {
|
||||
app.close(() => {
|
||||
console.log('server stopped');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
@ -1,8 +1,7 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"moduleResolution": "nodenext"
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
|