easy-starter 1.0.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Filip Paulů
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,72 @@
1
+ # easy-starter 🚀
2
+
3
+ A lightweight CLI tool to bootstrap modern fullstack React web projects.
4
+
5
+ ```bash
6
+ npx easy-starter [your-project-name]
7
+ ```
8
+
9
+ **Keywords:** React, Server-Side Rendering (SSR), Database (DB), Analytics, Type-Safe API, Fullstack, Minimalist
10
+
11
+ Tired of rigid frameworks forcing you into complex folder structures and steep learning curves? **easy-starter** is built on the philosophy of maximum freedom and modularity. It provides a solid foundation by combining a set of carefully curated, simple, and powerful tools without locking you into a monolithic architecture like Next.js or Remix.
12
+
13
+ ## Why easy-starter? (The Philosophy)
14
+
15
+ Modern full-stack frameworks are incredibly powerful, but they often come with significant trade-offs: strict file-system routing rules, immense complexity under the hood, and the modern paradigm of strictly dividing components into "client" and "server" boundaries.
16
+
17
+ **easy-starter solves this by giving you back control.**
18
+
19
+ * **No Server/Client Component Split:** Forget about adding `'use client'` everywhere or stressing about what runs where. With `easy-starter`, you just write standard React components. The integrated `ssr-hook` elegantly handles data fetching on the server and seamless hydration on the client for you.
20
+ * **No rigid structure:** You decide where your components, pages, and server logic live.
21
+ * **Clear Backend/Frontend separation:** Server code stays on the Express server, client code stays in React, connected by a seamlessly typed RPC API.
22
+ * **Minimalist dependencies:** We rely on simple, purpose-built libraries instead of heavy abstractions.
23
+ * **Instant Productivity:** You get SSR, a Database, Analytics, and a Type-Safe API out of the box in seconds.
24
+
25
+ ## What's included in the box?
26
+
27
+ The generated boilerplate comes pre-configured with a stack designed for developer happiness and raw performance:
28
+
29
+ * **Frontend/Backend:** React 19, TypeScript, and Express.
30
+ * **Bundler:** Parcel - zero-configuration, lightning-fast builds.
31
+ * **Server-Side Rendering (SSR):** [`ssr-hook`](https://www.npmjs.com/package/ssr-hook) - Dead-simple SSR for React. Fetches data on the server and hydrates on the client without the flash of loading content.
32
+ * **Type-Safe API:** [`typed-client-server-api`](https://www.npmjs.com/package/typed-client-server-api) - An RPC-like interface giving you full TypeScript autocomplete and safety between your Express server and React client.
33
+ * **Database:** [`easy-db-node`](https://www.npmjs.com/package/easy-db-node) - A simple, local JSON-based database perfect for small to medium projects where setting up Postgres/MongoDB is overkill.
34
+ * **Routing:** [`easy-page-router`](https://www.npmjs.com/package/easy-page-router) - A lightweight, programmatic routing solution.
35
+ * **Analytics:** [`easy-analytics`](https://www.npmjs.com/package/easy-analytics) - Privacy-friendly, local analytics tracking.
36
+ * **Deployment:** Integrated Github Actions workflow and PM2 deployment script for seamless, automated deployment straight to your own VPS.
37
+
38
+ ## Getting Started
39
+
40
+ Bootstrapping a new project is as simple as running one command:
41
+
42
+ ```bash
43
+ npx easy-starter [your-project-name]
44
+ ```
45
+
46
+ ### Interactive CLI
47
+
48
+ If you run `npx easy-starter` without a project name, the CLI will guide you interactively. It will also ask if you want to include a ready-to-use **Deployment Script** (`scripts/deploy.ts` and GitHub Actions workflow) utilizing `node-ssh` and PM2 for dead-simple automated deployments to your VPS.
49
+
50
+ ### Running your project
51
+
52
+ Once created, navigate to your new project folder and start the development server:
53
+
54
+ ```bash
55
+ cd your-project-name
56
+ npm run dev
57
+ ```
58
+
59
+ This command runs both the Express backend and the Parcel frontend bundler concurrently. Your app will be live at `http://localhost:1234`!
60
+
61
+ ## Available Scripts
62
+
63
+ Inside your newly generated project, you can use the following scripts:
64
+
65
+ * `npm run dev`: Starts the development server with hot-reloading for both backend and frontend.
66
+ * `npm run build`: Builds the project for production.
67
+ * `npm start`: Starts the production server (make sure to build first).
68
+ * `npm run deploy`: (Optional) Executes the `scripts/deploy.ts` script to deploy your built project via SSH and PM2.
69
+
70
+ ## License
71
+
72
+ MIT
package/bin/index.js ADDED
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const { execSync } = require('child_process');
7
+
8
+ const rl = readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout
11
+ });
12
+
13
+ const askQuestion = (query) => new Promise(resolve => rl.question(query, resolve));
14
+
15
+ async function main() {
16
+ console.log('🚀 Welcome to easy-starter!');
17
+
18
+ let projectName = process.argv[2];
19
+ if (!projectName) {
20
+ projectName = await askQuestion('Enter the project name (e.g., my-awesome-app): ');
21
+ }
22
+
23
+ if (!projectName) {
24
+ console.error('Project name cannot be empty!');
25
+ rl.close();
26
+ process.exit(1);
27
+ }
28
+
29
+ const useAdminDashboard = await askQuestion('Do you want to include an admin dashboard for analytics? (y/n) [y]: ');
30
+ const includeAdmin = useAdminDashboard.toLowerCase() !== 'n';
31
+
32
+ console.log('\n🚢 Deployment (GitHub Actions, VPS, SSH, PM2)');
33
+ console.log('You can automatically include a deployment script designed for GitHub Actions.');
34
+ console.log('It connects to your VPS via SSH, uploads your project, builds it, and runs it via PM2.');
35
+ const useDeployScript = await askQuestion('Do you want to include this GitHub Actions deploy script? (y/n) [y]: ');
36
+ const includeDeploy = useDeployScript.toLowerCase() !== 'n';
37
+
38
+ rl.close();
39
+
40
+ const targetPath = path.join(process.cwd(), projectName);
41
+
42
+ if (fs.existsSync(targetPath)) {
43
+ console.error(`Folder ${projectName} already exists!`);
44
+ process.exit(1);
45
+ }
46
+
47
+ console.log(`\n📁 Creating project in folder ${targetPath}...`);
48
+ fs.mkdirSync(targetPath, { recursive: true });
49
+
50
+ const templatePath = path.join(__dirname, '..', 'template');
51
+
52
+ function copyFolderSync(from, to, ignoreFiles = []) {
53
+ fs.mkdirSync(to, { recursive: true });
54
+ fs.readdirSync(from).forEach(element => {
55
+ if (ignoreFiles.includes(element)) return;
56
+ const stat = fs.lstatSync(path.join(from, element));
57
+ if (stat.isFile()) {
58
+ fs.copyFileSync(path.join(from, element), path.join(to, element));
59
+ } else if (stat.isSymbolicLink()) {
60
+ fs.symlinkSync(fs.readlinkSync(path.join(from, element)), path.join(to, element));
61
+ } else if (stat.isDirectory()) {
62
+ copyFolderSync(path.join(from, element), path.join(to, element), ignoreFiles);
63
+ }
64
+ });
65
+ }
66
+
67
+ const ignoreList = includeDeploy ? [] : ['scripts', '.github'];
68
+ copyFolderSync(templatePath, targetPath, ignoreList);
69
+
70
+ // Rename safe files back to dotfiles
71
+ if (fs.existsSync(path.join(targetPath, 'env'))) {
72
+ fs.renameSync(path.join(targetPath, 'env'), path.join(targetPath, '.env'));
73
+ }
74
+
75
+ // Update site.webmanifest
76
+ const manifestPath = path.join(targetPath, 'src', 'site.webmanifest');
77
+ if (fs.existsSync(manifestPath)) {
78
+ let manifestContent = fs.readFileSync(manifestPath, 'utf-8');
79
+ manifestContent = manifestContent.replace(/__NAME__/g, projectName);
80
+ fs.writeFileSync(manifestPath, manifestContent);
81
+ }
82
+
83
+ // Handle Admin section removal if the user doesn't want it
84
+ if (!includeAdmin) {
85
+ const adminPath = path.join(targetPath, 'src', 'pages', 'admin');
86
+ if (fs.existsSync(adminPath)) {
87
+ fs.rmSync(adminPath, { recursive: true, force: true });
88
+ }
89
+
90
+ // Strip // --- ADMIN START --- ... // --- ADMIN END --- blocks
91
+ const stripAdminBlocks = (filePath) => {
92
+ if (fs.existsSync(filePath)) {
93
+ let content = fs.readFileSync(filePath, 'utf-8');
94
+ content = content.replace(/\/\/ --- ADMIN START ---[\s\S]*?\/\/ --- ADMIN END ---\n?/g, '');
95
+ fs.writeFileSync(filePath, content);
96
+ }
97
+ };
98
+
99
+ stripAdminBlocks(path.join(targetPath, 'src', 'Router.tsx'));
100
+ stripAdminBlocks(path.join(targetPath, 'server', 'index.tsx'));
101
+ }
102
+
103
+ // Update package.json from template
104
+ const packageJsonPath = path.join(targetPath, 'package.json');
105
+ if (fs.existsSync(packageJsonPath)) {
106
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
107
+ pkg.name = projectName;
108
+
109
+ if (includeDeploy) {
110
+ pkg.devDependencies = pkg.devDependencies || {};
111
+ pkg.devDependencies['node-ssh'] = '^13.2.1';
112
+ pkg.scripts = pkg.scripts || {};
113
+ pkg.scripts['deploy'] = 'tsx scripts/deploy.ts';
114
+ } else if (pkg.scripts && pkg.scripts.deploy) {
115
+ delete pkg.scripts.deploy;
116
+ }
117
+
118
+ fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2));
119
+ }
120
+
121
+ console.log('📦 Installing dependencies (this may take a while)...');
122
+ try {
123
+ execSync('npm install', { cwd: targetPath, stdio: 'inherit' });
124
+ } catch (error) {
125
+ console.error('❌ Failed to install dependencies.', error);
126
+ process.exit(1);
127
+ }
128
+
129
+ console.log(`\n🎉 Project ${projectName} has been successfully created!`);
130
+ console.log('\nGetting started:');
131
+ console.log(` cd ${projectName}`);
132
+ console.log(' npm run dev -> http://localhost:1234');
133
+ if (includeDeploy) {
134
+ console.log(' npm run deploy');
135
+ }
136
+ }
137
+
138
+ main().catch(err => {
139
+ console.error(err);
140
+ process.exit(1);
141
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "easy-starter",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight CLI tool to bootstrap modern fullstack React web projects.",
5
+ "bin": {
6
+ "easy-starter": "./bin/index.js"
7
+ },
8
+ "keywords": [
9
+ "cli",
10
+ "bootstrap",
11
+ "starter",
12
+ "react",
13
+ "easy",
14
+ "ssr",
15
+ "typescript",
16
+ "fullstack",
17
+ "easy-db",
18
+ "parcel"
19
+ ],
20
+ "author": "Filip Paulů <ing.fenix@seznam.cz>",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/ingSlonik/easy-starter.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/ingSlonik/easy-starter/issues"
28
+ }
29
+ }
@@ -0,0 +1,29 @@
1
+ name: Deploy to VPS
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ - main
8
+
9
+ jobs:
10
+ deploy:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout Code
14
+ uses: actions/checkout@v3
15
+
16
+ - name: Setup Node.js
17
+ uses: actions/setup-node@v3
18
+ with:
19
+ node-version: 24
20
+
21
+ - name: Install Dependencies
22
+ run: npm install tsx node-ssh
23
+
24
+ - name: Run Deploy Script
25
+ env:
26
+ SSH_HOST: ${{ secrets.SSH_HOST }}
27
+ SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
28
+ SSH_PASSWORD: ${{ secrets.SSH_PASSWORD }}
29
+ run: npm run deploy
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@parcel/config-default",
3
+ "optimizers": {
4
+ "*.html": []
5
+ }
6
+ }
@@ -0,0 +1,40 @@
1
+ # Welcome to your new project! 🚀
2
+
3
+ This project was bootstrapped with `easy-starter`, giving you maximum freedom with a minimalist, yet powerful setup.
4
+
5
+ ## Features Included
6
+
7
+ * **React 19 & TypeScript**: For a robust frontend experience.
8
+ * **Express Backend**: A simple Node.js server.
9
+ * **Parcel Bundler**: Lightning-fast builds with zero configuration.
10
+ * **SSR Ready**: Powered by [`ssr-hook`](https://www.npmjs.com/package/ssr-hook).
11
+ * **Type-Safe API**: Connected via [`typed-client-server-api`](https://www.npmjs.com/package/typed-client-server-api).
12
+ * **Local Database**: Ready to use with [`easy-db-node`](https://www.npmjs.com/package/easy-db-node).
13
+ * **Routing**: Managed by [`easy-page-router`](https://www.npmjs.com/package/easy-page-router).
14
+
15
+ ## Getting Started
16
+
17
+ To start developing, run:
18
+
19
+ ```bash
20
+ npm run dev
21
+ ```
22
+
23
+ This will concurrently start your Express server and the Parcel bundler. Open [http://localhost:1234](http://localhost:1234) in your browser. The page will automatically reload when you make changes.
24
+
25
+ ## Scripts
26
+
27
+ * `npm run dev` - Starts the development server.
28
+ * `npm run build` - Builds the application for production.
29
+ * `npm start` - Starts the production server (run build first).
30
+ * `npm run deploy` - (If included) Deploys your app to a remote server via SSH.
31
+
32
+ ## Project Structure
33
+
34
+ * `/src` - Your React frontend code.
35
+ * `/server` - Your Express backend code.
36
+ * `/types.d.ts` - Shared TypeScript definitions for your API and Database.
37
+ * `/public` - Static assets (images, fonts, etc.).
38
+ * `/easy-db` - Where your local database JSON files are stored.
39
+
40
+ Enjoy building with freedom!
package/template/env ADDED
@@ -0,0 +1,7 @@
1
+ NODE_ENV=development
2
+ SERVER_PORT=1111
3
+ ADMIN_PASSWORD=admin
4
+
5
+ SSH_HOST=***
6
+ SSH_USERNAME=root
7
+ SSH_USERNAME=***
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "template",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "scripts": {
6
+ "start": "tsx --env-file=.env server/index.tsx",
7
+ "dev": "concurrently --kill-others \"npm run server:dev\" \"npm run fe:dev\"",
8
+ "server:dev": "tsx --env-file=.env --watch server/index.tsx",
9
+ "fe:dev": "parcel src/index.html --no-cache",
10
+ "build": "parcel build src/index.html",
11
+ "deploy": "tsx scripts/deploy.ts"
12
+ },
13
+ "dependencies": {
14
+ "cors": "^2.8.6",
15
+ "easy-analytics": "^1.0.1",
16
+ "easy-db-node": "^3.0.1",
17
+ "easy-page-router": "^0.2.1",
18
+ "express": "^5.2.1",
19
+ "react": "^19.2.6",
20
+ "react-dom": "^19.2.6",
21
+ "ssr-hook": "^0.2.0",
22
+ "typed-client-server-api": "^1.0.3"
23
+ },
24
+ "devDependencies": {
25
+ "@parcel/packager-raw-url": "^2.16.4",
26
+ "@parcel/transformer-sass": "^2.16.4",
27
+ "@parcel/transformer-webmanifest": "^2.16.4",
28
+ "@types/cors": "^2.8.19",
29
+ "@types/express": "^5.0.6",
30
+ "@types/node": "^24.0.0",
31
+ "@types/react": "^19.2.14",
32
+ "@types/react-dom": "^19.2.3",
33
+ "concurrently": "^9.2.1",
34
+ "parcel": "^2.16.4",
35
+ "sharp": "^0.33.5",
36
+ "tsx": "^4.21.0",
37
+ "typescript": "^6.0.3"
38
+ }
39
+ }
@@ -0,0 +1,148 @@
1
+ import { NodeSSH, SSHExecCommandResponse } from 'node-ssh';
2
+
3
+ import { lstat, readdir } from 'fs/promises';
4
+ import { relative, resolve } from 'path';
5
+
6
+ // Fill in your credentials here, or load them from .env or process.env (e.g. GitHub Secrets).
7
+ const host = process.env.SSH_HOST;
8
+ const username = process.env.SSH_USERNAME;
9
+ const password = process.env.SSH_PASSWORD;
10
+
11
+ // Configuration for your target server
12
+ const server = "https://your-production-url.com/";
13
+ const pm2AppName = "easy-starter-app"; // The name of the process in PM2
14
+ const sshPath = resolve("/", "root", "your-production-url.com"); // Path on the remote VPS
15
+
16
+ const path = resolve(process.cwd()); // Local project path
17
+ const ignoreFiles = [
18
+ ".git",
19
+ "node_modules",
20
+ "scripts",
21
+ ".github"
22
+ ];
23
+
24
+ // Ensure credentials are provided
25
+ if (host === undefined) throw new Error("SSH_HOST environment variable is not defined.");
26
+ if (username === undefined) throw new Error("SSH_USERNAME environment variable is not defined.");
27
+ if (password === undefined) throw new Error("SSH_PASSWORD environment variable is not defined.");
28
+
29
+ deploy();
30
+
31
+ // --------------------------------- functions ---------------------------------
32
+ async function deploy() {
33
+ const ssh = new NodeSSH();
34
+
35
+ console.log(`Connecting to ${host}...`);
36
+ await ssh.connect({ host, username, password });
37
+ console.log(`✔ Connected to SSH server.\n`);
38
+
39
+ const files = (await getFiles(path)).map(file => {
40
+ const relativePath = relative(path, file);
41
+ const pathTo = resolve(sshPath, relativePath);
42
+ return { file, pathTo, relativePath };
43
+ });
44
+ console.log(`✔ Found ${files.length} files to copy from ${path} to ${sshPath}.`);
45
+
46
+ if (files.length < 50) {
47
+ // Copy file by file
48
+ for (const [i, { file, pathTo, relativePath }] of Object.entries(files)) {
49
+ console.log(` ${parseInt(i) + 1} / ${files.length} Copying ${relativePath}`);
50
+ await ssh.putFile(file, pathTo);
51
+ }
52
+ } else {
53
+ // Copy all files at once
54
+ files.forEach((file) => console.log(` ${file.relativePath}`));
55
+ console.log(` Copying ${files.length} files at once...`)
56
+ await ssh.putFiles(files.map(f => ({ local: f.file, remote: f.pathTo })));
57
+ }
58
+ console.log(`✔ All files are copied.\n`);
59
+
60
+ console.log(`Installing dependencies...`);
61
+ await sshExec(ssh, "npm install", "Failed to install dependencies.");
62
+ console.log(`✔ Dependencies are installed.\n`);
63
+
64
+ const timeServerOff = new Date().getTime();
65
+
66
+ console.log(`Removing old build...`);
67
+ await sshExec(ssh, "rm -rf .parcel-cache", "Failed to remove .parcel-cache folder.");
68
+ await sshExec(ssh, "rm -rf dist", "Failed to remove dist folder.");
69
+ console.log(`✔ Old build removed.\n`);
70
+
71
+ console.log(`Building project...`);
72
+ await sshExec(ssh, "npm run build", "Failed to build project.");
73
+ console.log(`✔ Build project.\n`);
74
+
75
+ // PM2
76
+ try {
77
+ await sshExec(ssh, `pm2 reload ${pm2AppName}`);
78
+ } catch (e) {
79
+ console.log(`✘ ${pm2AppName} is not running in PM2!`);
80
+ await sshExec(ssh, `pm2 start npm --name "${pm2AppName}" -- start`, `${pm2AppName} could not be registered in PM2!`);
81
+ }
82
+ await sshExec(ssh, `pm2 list`);
83
+
84
+ ssh.dispose();
85
+ console.log(`✔ SSH connection is closed.\n`);
86
+
87
+ if (!await serverCheck(server))
88
+ throw new Error(`Server ${server} is not available!`);
89
+ console.log(`✔ Server ${server} is running.\n`);
90
+
91
+ console.log(`i Server was ${(new Date().getTime() - timeServerOff) / 1000}s off!\n`);
92
+
93
+ console.log(`------------------------\n✔ Successfully deployed.`);
94
+ }
95
+
96
+ async function serverCheck(server: string, tries = 3, delay = 1000) {
97
+ console.log(`Checking ${server} ...`);
98
+ const res = await fetch(server).catch(() => ({ status: 500 }));
99
+
100
+ if (res.status === 200) {
101
+ console.log(`i Response status: ${res.status}`);
102
+ return true;
103
+ }
104
+
105
+ console.log(`✘ Response status: ${res.status}.`);
106
+
107
+ if (tries < 1) return false;
108
+
109
+ console.log(`i I'm giving it ${tries} more tries to recover, the next try will be in ${delay / 1000} second...`);
110
+ await timeout(delay);
111
+ return await serverCheck(server, tries - 1, delay);
112
+ }
113
+
114
+ async function sshExec(ssh: NodeSSH, command: string, error?: string): Promise<SSHExecCommandResponse> {
115
+ console.log(`$ ${command}`);
116
+ const response = await ssh.execCommand(command, { cwd: sshPath });
117
+ if (response.code !== 0) {
118
+ console.log(`Code: ${response.code}\n${response.stderr.split("\n").join("\n | ")}`);
119
+ throw new Error(error);
120
+ }
121
+
122
+ console.log(response.stdout.split("\n").join("\n | "));
123
+ return response;
124
+ }
125
+
126
+ async function getFiles(path: string) {
127
+ const paths: string[] = [];
128
+ const files = await readdir(path);
129
+
130
+ for (const file of files) {
131
+ if (ignoreFiles.find(f => file.includes(f)))
132
+ continue;
133
+
134
+ const filePath = resolve(path, file);
135
+ const stat = await lstat(filePath);
136
+
137
+ if (stat.isDirectory()) {
138
+ paths.push(...await getFiles(filePath));
139
+ } else {
140
+ paths.push(filePath)
141
+ }
142
+ }
143
+ return paths;
144
+ }
145
+
146
+ async function timeout(ms: number) {
147
+ return new Promise(resolve => setTimeout(resolve, ms));
148
+ }