@budibase/cli 0.0.1
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/.eslintrc +12 -0
- package/LICENSE.md +674 -0
- package/dist/analytics/Client.js +32 -0
- package/dist/analytics/index.js +55 -0
- package/dist/backups/index.js +119 -0
- package/dist/backups/objectStore.js +82 -0
- package/dist/backups/utils.js +110 -0
- package/dist/constants.js +9 -0
- package/dist/core/db.js +55 -0
- package/dist/environment.js +4 -0
- package/dist/events.js +13 -0
- package/dist/exec.js +49 -0
- package/dist/hosting/genUser.js +32 -0
- package/dist/hosting/index.js +21 -0
- package/dist/hosting/init.js +117 -0
- package/dist/hosting/makeFiles.js +144 -0
- package/dist/hosting/start.js +63 -0
- package/dist/hosting/status.js +30 -0
- package/dist/hosting/stop.js +30 -0
- package/dist/hosting/types.js +2 -0
- package/dist/hosting/update.js +62 -0
- package/dist/hosting/utils.js +159 -0
- package/dist/hosting/watch.js +47 -0
- package/dist/index.js +38 -0
- package/dist/options.js +14 -0
- package/dist/plugins/index.js +199 -0
- package/dist/plugins/skeleton.js +75 -0
- package/dist/prebuilds.js +46 -0
- package/dist/questions.js +58 -0
- package/dist/structures/Command.js +75 -0
- package/dist/structures/ConfigManager.js +39 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/utils.js +126 -0
- package/package.json +62 -0
- package/src/analytics/Client.ts +33 -0
- package/src/analytics/index.ts +60 -0
- package/src/backups/index.ts +130 -0
- package/src/backups/objectStore.ts +69 -0
- package/src/backups/utils.ts +99 -0
- package/src/constants.ts +4 -0
- package/src/core/db.ts +39 -0
- package/src/environment.ts +3 -0
- package/src/events.ts +11 -0
- package/src/exec.ts +27 -0
- package/src/hosting/genUser.ts +23 -0
- package/src/hosting/index.ts +48 -0
- package/src/hosting/init.ts +76 -0
- package/src/hosting/makeFiles.ts +140 -0
- package/src/hosting/start.ts +30 -0
- package/src/hosting/status.ts +13 -0
- package/src/hosting/stop.ts +13 -0
- package/src/hosting/types.ts +4 -0
- package/src/hosting/update.ts +55 -0
- package/src/hosting/utils.ts +127 -0
- package/src/hosting/watch.ts +37 -0
- package/src/index.ts +27 -0
- package/src/options.ts +8 -0
- package/src/plugins/index.ts +198 -0
- package/src/plugins/skeleton.ts +65 -0
- package/src/prebuilds.ts +50 -0
- package/src/questions.ts +40 -0
- package/src/structures/Command.ts +94 -0
- package/src/structures/ConfigManager.ts +49 -0
- package/src/utils.ts +112 -0
- package/start.sh +3 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fetch from "node-fetch"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import os from "os"
|
|
4
|
+
import { join } from "path"
|
|
5
|
+
import { processStringSync } from "@budibase/string-templates"
|
|
6
|
+
const download = require("download")
|
|
7
|
+
const tar = require("tar")
|
|
8
|
+
|
|
9
|
+
const HBS_FILES = ["package.json.hbs", "schema.json.hbs", "README.md.hbs"]
|
|
10
|
+
|
|
11
|
+
async function getSkeletonUrl(type: string) {
|
|
12
|
+
const resp = await fetch(
|
|
13
|
+
"https://api.github.com/repos/budibase/budibase-skeleton/releases/latest"
|
|
14
|
+
)
|
|
15
|
+
if (resp.status >= 300) {
|
|
16
|
+
throw new Error("Failed to retrieve skeleton metadata")
|
|
17
|
+
}
|
|
18
|
+
const json = (await resp.json()) as { assets: any[] }
|
|
19
|
+
for (let asset of json["assets"]) {
|
|
20
|
+
if (asset.name && asset.name.includes(type)) {
|
|
21
|
+
return asset["browser_download_url"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
throw new Error("No skeleton found in latest release.")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function getSkeleton(type: string, name: string) {
|
|
28
|
+
const url = await getSkeletonUrl(type)
|
|
29
|
+
const tarballFile = join(os.tmpdir(), "skeleton.tar.gz")
|
|
30
|
+
|
|
31
|
+
// download the full skeleton tarball
|
|
32
|
+
fs.writeFileSync(tarballFile, await download(url))
|
|
33
|
+
fs.mkdirSync(name)
|
|
34
|
+
// extract it and get what we need
|
|
35
|
+
await tar.extract({
|
|
36
|
+
file: tarballFile,
|
|
37
|
+
C: name,
|
|
38
|
+
})
|
|
39
|
+
// clear up
|
|
40
|
+
fs.rmSync(tarballFile)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function fleshOutSkeleton(
|
|
44
|
+
type: string,
|
|
45
|
+
name: string,
|
|
46
|
+
description: string,
|
|
47
|
+
version: string
|
|
48
|
+
) {
|
|
49
|
+
for (let file of HBS_FILES) {
|
|
50
|
+
const oldFile = join(name, file),
|
|
51
|
+
newFile = join(name, file.substring(0, file.length - 4))
|
|
52
|
+
const hbsContents = fs.readFileSync(oldFile, "utf8")
|
|
53
|
+
if (!hbsContents) {
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
const output = processStringSync(hbsContents, {
|
|
57
|
+
name,
|
|
58
|
+
description,
|
|
59
|
+
version,
|
|
60
|
+
})
|
|
61
|
+
// write the updated file and remove the HBS file
|
|
62
|
+
fs.writeFileSync(newFile, output)
|
|
63
|
+
fs.rmSync(oldFile)
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/prebuilds.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os from "os"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import fs from "fs"
|
|
4
|
+
import { error } from "./utils"
|
|
5
|
+
|
|
6
|
+
const PREBUILDS = "prebuilds"
|
|
7
|
+
const ARCH = `${os.platform()}-${os.arch()}`
|
|
8
|
+
const PREBUILD_DIR = join(process.execPath, "..", PREBUILDS, ARCH)
|
|
9
|
+
|
|
10
|
+
// running as built CLI pkg bundle
|
|
11
|
+
if (!process.argv[0].includes("node")) {
|
|
12
|
+
checkForBinaries()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function checkForBinaries() {
|
|
16
|
+
const readDir = join(__filename, "..", "..", PREBUILDS, ARCH)
|
|
17
|
+
if (fs.existsSync(PREBUILD_DIR) || !fs.existsSync(readDir)) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
const natives = fs.readdirSync(readDir)
|
|
21
|
+
if (fs.existsSync(readDir)) {
|
|
22
|
+
fs.mkdirSync(PREBUILD_DIR, { recursive: true })
|
|
23
|
+
for (let native of natives) {
|
|
24
|
+
const filename = `${native.split(".fake")[0]}.node`
|
|
25
|
+
fs.cpSync(join(readDir, native), join(PREBUILD_DIR, filename))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanup(evt?: number) {
|
|
31
|
+
if (evt && !isNaN(evt)) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
if (evt) {
|
|
35
|
+
console.error(
|
|
36
|
+
error(
|
|
37
|
+
"Failed to run CLI command - please report with the following message:"
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
console.error(error(evt))
|
|
41
|
+
}
|
|
42
|
+
if (fs.existsSync(PREBUILD_DIR)) {
|
|
43
|
+
fs.rmSync(PREBUILD_DIR, { recursive: true })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const events = ["exit", "SIGINT", "SIGUSR1", "SIGUSR2", "uncaughtException"]
|
|
48
|
+
events.forEach(event => {
|
|
49
|
+
process.on(event, cleanup)
|
|
50
|
+
})
|
package/src/questions.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const inquirer = require("inquirer")
|
|
2
|
+
|
|
3
|
+
export async function confirmation(question: string) {
|
|
4
|
+
const config = {
|
|
5
|
+
type: "confirm",
|
|
6
|
+
message: question,
|
|
7
|
+
default: true,
|
|
8
|
+
name: "confirmation",
|
|
9
|
+
}
|
|
10
|
+
return (await inquirer.prompt(config)).confirmation
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function string(question: string, defaultString?: string) {
|
|
14
|
+
const config: any = {
|
|
15
|
+
type: "input",
|
|
16
|
+
name: "string",
|
|
17
|
+
message: question,
|
|
18
|
+
}
|
|
19
|
+
if (defaultString) {
|
|
20
|
+
config.default = defaultString
|
|
21
|
+
}
|
|
22
|
+
return (await inquirer.prompt(config)).string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function number(question: string, defaultNumber?: number) {
|
|
26
|
+
const config: any = {
|
|
27
|
+
type: "input",
|
|
28
|
+
name: "number",
|
|
29
|
+
message: question,
|
|
30
|
+
validate: (value: string) => {
|
|
31
|
+
let valid = !isNaN(parseFloat(value))
|
|
32
|
+
return valid || "Please enter a number"
|
|
33
|
+
},
|
|
34
|
+
filter: Number,
|
|
35
|
+
}
|
|
36
|
+
if (defaultNumber) {
|
|
37
|
+
config.default = defaultNumber
|
|
38
|
+
}
|
|
39
|
+
return (await inquirer.prompt(config)).number
|
|
40
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSubHelpDescription,
|
|
3
|
+
getHelpDescription,
|
|
4
|
+
error,
|
|
5
|
+
capitaliseFirstLetter,
|
|
6
|
+
} from "../utils"
|
|
7
|
+
|
|
8
|
+
type CommandOpt = {
|
|
9
|
+
command: string
|
|
10
|
+
help: string
|
|
11
|
+
func?: Function
|
|
12
|
+
extras: any[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class Command {
|
|
16
|
+
command: string
|
|
17
|
+
opts: CommandOpt[]
|
|
18
|
+
func?: Function
|
|
19
|
+
help?: string
|
|
20
|
+
|
|
21
|
+
constructor(command: string, func?: Function) {
|
|
22
|
+
// if there are options, need to just get the command name
|
|
23
|
+
this.command = command
|
|
24
|
+
this.opts = []
|
|
25
|
+
this.func = func
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
convertToCommander(lookup: string) {
|
|
29
|
+
const parts = lookup.toLowerCase().split("-")
|
|
30
|
+
// camel case, separate out first
|
|
31
|
+
const first = parts.shift()
|
|
32
|
+
return [first]
|
|
33
|
+
.concat(parts.map(part => capitaliseFirstLetter(part)))
|
|
34
|
+
.join("")
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
addHelp(help: string) {
|
|
38
|
+
this.help = help
|
|
39
|
+
return this
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
addSubOption(
|
|
43
|
+
command: string,
|
|
44
|
+
help: string,
|
|
45
|
+
func?: Function,
|
|
46
|
+
extras: any[] = []
|
|
47
|
+
) {
|
|
48
|
+
this.opts.push({ command, help, func, extras })
|
|
49
|
+
return this
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
configure(program: any) {
|
|
53
|
+
const thisCmd = this
|
|
54
|
+
let command = program.command(thisCmd.command)
|
|
55
|
+
if (this.help) {
|
|
56
|
+
command = command.description(getHelpDescription(thisCmd.help!))
|
|
57
|
+
}
|
|
58
|
+
for (let opt of thisCmd.opts) {
|
|
59
|
+
command = command.option(opt.command, getSubHelpDescription(opt.help))
|
|
60
|
+
}
|
|
61
|
+
command.helpOption(
|
|
62
|
+
"--help",
|
|
63
|
+
getSubHelpDescription(`Get help with ${this.command} options`)
|
|
64
|
+
)
|
|
65
|
+
command.action(async (options: Record<string, string>) => {
|
|
66
|
+
try {
|
|
67
|
+
let executed = false,
|
|
68
|
+
found = false
|
|
69
|
+
for (let opt of thisCmd.opts) {
|
|
70
|
+
let lookup = opt.command.split(" ")[0].replace("--", "")
|
|
71
|
+
// need to handle how commander converts watch-plugin-dir to watchPluginDir
|
|
72
|
+
lookup = this.convertToCommander(lookup)
|
|
73
|
+
found = !executed && !!options[lookup]
|
|
74
|
+
if (found && opt.func) {
|
|
75
|
+
const input =
|
|
76
|
+
Object.keys(options).length > 1 ? options : options[lookup]
|
|
77
|
+
await opt.func(input)
|
|
78
|
+
executed = true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (found && !executed) {
|
|
82
|
+
console.log(
|
|
83
|
+
error(`${Object.keys(options)[0]} is an option, not an operation.`)
|
|
84
|
+
)
|
|
85
|
+
} else if (!executed) {
|
|
86
|
+
console.log(error(`Unknown ${this.command} option.`))
|
|
87
|
+
command.help()
|
|
88
|
+
}
|
|
89
|
+
} catch (err: any) {
|
|
90
|
+
console.log(error(err))
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
const os = require("os")
|
|
4
|
+
const { error } = require("../utils")
|
|
5
|
+
|
|
6
|
+
export class ConfigManager {
|
|
7
|
+
path: string
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.path = path.join(os.homedir(), ".budibase.json")
|
|
11
|
+
if (!fs.existsSync(this.path)) {
|
|
12
|
+
fs.writeFileSync(this.path, "{}")
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get config() {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(fs.readFileSync(this.path, "utf8"))
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.log(
|
|
21
|
+
error(
|
|
22
|
+
"Error parsing configuration file. Please check your .budibase.json is valid."
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
return {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set config(json: any) {
|
|
30
|
+
fs.writeFileSync(this.path, JSON.stringify(json))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getValue(key: string) {
|
|
34
|
+
return this.config[key]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setValue(key: string, value: any) {
|
|
38
|
+
this.config = {
|
|
39
|
+
...this.config,
|
|
40
|
+
[key]: value,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
removeKey(key: string) {
|
|
45
|
+
const updated = { ...this.config }
|
|
46
|
+
delete updated[key]
|
|
47
|
+
this.config = updated
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import fs from "fs"
|
|
3
|
+
import path from "path"
|
|
4
|
+
import { join } from "path"
|
|
5
|
+
import fetch from "node-fetch"
|
|
6
|
+
const progress = require("cli-progress")
|
|
7
|
+
|
|
8
|
+
export function downloadFile(url: string, filePath: string) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
filePath = path.resolve(filePath)
|
|
11
|
+
fetch(url, {
|
|
12
|
+
method: "GET",
|
|
13
|
+
})
|
|
14
|
+
.then(response => {
|
|
15
|
+
const writer = fs.createWriteStream(filePath)
|
|
16
|
+
if (response.body) {
|
|
17
|
+
response.body.pipe(writer)
|
|
18
|
+
response.body.on("end", resolve)
|
|
19
|
+
response.body.on("error", reject)
|
|
20
|
+
} else {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Unable to retrieve docker-compose file - ${response.status}`
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
.catch(err => {
|
|
27
|
+
throw err
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function httpCall(url: string, method: string) {
|
|
33
|
+
const response = await fetch(url, {
|
|
34
|
+
method,
|
|
35
|
+
})
|
|
36
|
+
return response.body
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getHelpDescription(str: string) {
|
|
40
|
+
return chalk.cyan(str)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getSubHelpDescription(str: string) {
|
|
44
|
+
return chalk.green(str)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function error(err: string | number) {
|
|
48
|
+
process.exitCode = -1
|
|
49
|
+
return chalk.red(`Error - ${err}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function success(str: string) {
|
|
53
|
+
return chalk.green(str)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function info(str: string) {
|
|
57
|
+
return chalk.cyan(str)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function logErrorToFile(file: string, error: string) {
|
|
61
|
+
fs.writeFileSync(path.resolve(`./${file}`), `Budibase Error\n${error}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseEnv(env: string) {
|
|
65
|
+
const lines = env.toString().split("\n")
|
|
66
|
+
let result: Record<string, string> = {}
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const match = line.match(/^([^=:#]+?)[=:](.*)/)
|
|
69
|
+
if (match) {
|
|
70
|
+
result[match[1].trim()] = match[2].trim()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function progressBar(total: number) {
|
|
77
|
+
const bar = new progress.SingleBar({}, progress.Presets.shades_classic)
|
|
78
|
+
bar.start(total, 0)
|
|
79
|
+
return bar
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function checkSlashesInUrl(url: string) {
|
|
83
|
+
return url.replace(/(https?:\/\/)|(\/)+/g, "$1$2")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function moveDirectory(oldPath: string, newPath: string) {
|
|
87
|
+
const files = fs.readdirSync(oldPath)
|
|
88
|
+
// check any file exists already
|
|
89
|
+
for (let file of files) {
|
|
90
|
+
if (fs.existsSync(join(newPath, file))) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"Unable to remove top level directory - some skeleton files already exist."
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (let file of files) {
|
|
97
|
+
fs.renameSync(join(oldPath, file), join(newPath, file))
|
|
98
|
+
}
|
|
99
|
+
fs.rmdirSync(oldPath)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function capitaliseFirstLetter(str: string) {
|
|
103
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function stringifyToDotEnv(json: Record<string, string | number>) {
|
|
107
|
+
let str = ""
|
|
108
|
+
for (let [key, value] of Object.entries(json)) {
|
|
109
|
+
str += `${key}=${value}\n`
|
|
110
|
+
}
|
|
111
|
+
return str
|
|
112
|
+
}
|
package/start.sh
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es6",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["es2020"],
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noImplicitAny": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"incremental": true,
|
|
11
|
+
"types": [ "node", "jest" ],
|
|
12
|
+
"outDir": "dist",
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"src/**/*"
|
|
17
|
+
],
|
|
18
|
+
"exclude": [
|
|
19
|
+
"node_modules",
|
|
20
|
+
"dist",
|
|
21
|
+
"**/*.spec.ts",
|
|
22
|
+
"**/*.spec.js"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.build.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"sourceMap": true,
|
|
7
|
+
"baseUrl": ".",
|
|
8
|
+
"paths": {
|
|
9
|
+
"@budibase/types": ["../types/src"],
|
|
10
|
+
"@budibase/backend-core": ["../backend-core/src"],
|
|
11
|
+
"@budibase/backend-core/*": ["../backend-core/*"]
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"ts-node": {
|
|
15
|
+
"require": ["tsconfig-paths/register"],
|
|
16
|
+
"swc": true
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
"include": ["src/**/*", "package.json"],
|
|
20
|
+
"exclude": ["node_modules", "dist"]
|
|
21
|
+
}
|