Compare commits

...

8 Commits

Author SHA1 Message Date
Chen Asraf
db6177c200 chore(develop): release 2.3.0 2024-09-18 00:12:00 +03:00
ae64db846f chore: add coverage script 2024-09-18 00:12:00 +03:00
89dc43c73d fix: exclude globs
refactor: split main file iteration to 2 steps
2024-09-17 23:57:48 +03:00
2c43dc4daf build: update target 2024-09-17 23:57:48 +03:00
f4c907e6c9 ci: fix build 2024-09-17 23:57:48 +03:00
a275e688d4 ci: use release-please 2024-09-17 23:57:48 +03:00
ff4ebf0a5b test: add color tests 2024-09-17 23:57:48 +03:00
ab9322e1ab feat: remove chalk dependency 2024-09-17 23:57:48 +03:00
18 changed files with 494 additions and 2715 deletions

33
.github/workflows/develop.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Test
on:
pull_request:
branches:
- master
- develop
permissions:
contents: write
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm build

View File

@@ -1,11 +1,13 @@
name: Documentation
permissions:
contents: write
on:
push:
branches: [master, pre, develop]
branches:
- master
- develop
permissions:
contents: write
jobs:
docs:
@@ -13,22 +15,15 @@ jobs:
runs-on: ubuntu-latest
# if: "contains(github.event.head_commit.message, 'chore(release)')"
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20.x"
- name: Install PNPM
run: npm i -g pnpm
- name: Install dependencies
run: |
node-version: 20
- run: npm i -g pnpm
- run: |
pnpm install --frozen-lockfile
cd docs && pnpm install --frozen-lockfile
- name: Build Docs
run: pnpm docs:build
- run: pnpm docs:build
- name: Deploy on GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:

View File

@@ -1,26 +0,0 @@
name: Pull Requests
on:
pull_request:
branches: [master, pre, develop]
jobs:
build:
name: Test & Build PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20.x"
- name: Install PNPM
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Tests
run: pnpm test
- name: Build Package
run: pnpm build

View File

@@ -2,37 +2,63 @@ name: Release
on:
push:
branches: [master, pre, develop]
branches:
- master
permissions:
contents: read # for checkout
contents: write
pull-requests: write
jobs:
release:
name: Release
test:
runs-on: ubuntu-latest
permissions:
contents: write # to be able to publish a GitHub release
issues: write # to be able to comment on released issues
pull-requests: write # to be able to comment on released pull requests
id-token: write # to enable use of OIDC for npm provenance
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install PNPM
run: npm i -g pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Tests
run: pnpm test
- name: Build Package
run: pnpm build
- name: Semantic Release
run: npx semantic-release
node-version: 20
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm build
release:
needs:
- build
- test
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.release.outputs.release_created }}
steps:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
release-type: node
target-branch: master
publish:
needs: release
runs-on: ubuntu-latest
if: ${{ needs.release.outputs.release_created }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- run: npm i -g pnpm
- run: pnpm run ci
- run: pnpm build
- run: cd dist && npm publish
env:
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,5 +1,17 @@
# Change Log
## [2.3.0](https://github.com/chenasraf/simple-scaffold/compare/v2.2.2...v2.3.0) (2024-09-17)
### Features
* remove chalk dependency ([ba96ca6](https://github.com/chenasraf/simple-scaffold/commit/ba96ca64d1e02beb16bd127fc889da3ef016b7d5))
### Bug Fixes
* exclude globs ([a2788e7](https://github.com/chenasraf/simple-scaffold/commit/a2788e7c7d27f46d55cf4810e1a8193b5d403568))
## [2.2.2](https://github.com/chenasraf/simple-scaffold/compare/v2.2.1...v2.2.2) (2024-08-27)

View File

@@ -88,7 +88,6 @@ export default {
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// moduleNameMapper: {
// chalk: "<rootDir>/node_modules/chalk/source/index.js",
// "#ansi-styles": "<rootDir>/node_modules/chalk/source/vendor/ansi-styles/index.js",
// "#supports-color": "<rootDir>/node_modules/chalk/source/vendor/supports-color/index.js",
// },

View File

@@ -1,6 +1,6 @@
{
"name": "simple-scaffold",
"version": "2.2.2",
"version": "2.3.0",
"description": "Generate any file structure - from single components to entire app boilerplates, with a single command.",
"homepage": "https://chenasraf.github.io/simple-scaffold",
"repository": {
@@ -13,7 +13,7 @@
"bin": {
"simple-scaffold": "cmd.js"
},
"packageManager": "pnpm@9.0.4",
"packageManager": "pnpm@9.9.0",
"keywords": [
"javascript",
"cli",
@@ -26,41 +26,33 @@
"scaffolding"
],
"scripts": {
"clean": "rm -rf dist/",
"clean": "rimraf dist",
"build": "pnpm clean && tsc && chmod -R +x ./dist && cp ./package.json ./README.md ./dist/",
"dev": "tsc --watch",
"start": "ts-node src/scaffold.ts",
"test": "jest",
"coverage": "open coverage/lcov-report/index.html",
"cmd": "ts-node src/cmd.ts",
"docs:build": "cd docs && pnpm build",
"docs:watch": "cd docs && pnpm start",
"audit-fix": "pnpm audit --fix",
"changelog": "conventional-changelog -i CHANGELOG.md -s -r 0; echo \"# Change Log\n\n$(cat CHANGELOG.md)\" > CHANGELOG.md"
"ci": "pnpm install --frozen-lockfile"
},
"dependencies": {
"chalk": "^4.1.2",
"date-fns": "^3.6.0",
"date-fns": "^4.0.0",
"glob": "^11.0.0",
"handlebars": "^4.7.8",
"massarg": "2.0.1"
},
"devDependencies": {
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/release-notes-generator": "^14.0.1",
"@types/jest": "^29.5.12",
"@types/jest": "^29.5.13",
"@types/mock-fs": "^4.13.4",
"@types/node": "^22.5.0",
"@types/semantic-release": "^20.0.6",
"conventional-changelog": "^6.0.0",
"conventional-changelog-cli": "^5.0.0",
"@types/node": "^22.5.5",
"jest": "^29.7.0",
"mock-fs": "^5.2.0",
"semantic-release": "^24.1.0",
"semantic-release-conventional-commits": "^3.0.0",
"rimraf": "^6.0.1",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
"typescript": "^5.6.2"
}
}

2672
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,67 +0,0 @@
const ref = process.env.GITHUB_REF || ""
const branch = ref.split("/").pop()
/**
* @type {import('semantic-release').GlobalConfig}
*/
module.exports = {
branches: ["master", { name: "pre", prerelease: true }],
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/npm",
{
// only update the pkg version on root, don't publish
// this is to keep package.json version in sync with the release
npmPublish: false,
},
],
[
"@semantic-release/exec",
{
// pack the dist folder, during publish step (after version was bumped)
publishCmd: 'echo "Packing..."; cd ./dist && pnpm pack --pack-destination=../; echo "Done"',
},
],
[
"@semantic-release/npm",
{
// publish from dist dir instead of root
// this is the actual uild output
pkgRoot: "dist",
},
],
[
// Release to GitHub
"@semantic-release/github",
{
assets: ["*.tgz"],
},
],
branch === "master"
? [
// Update CHANGELOG.md only on master
"@semantic-release/changelog",
{
changelogFile: "CHANGELOG.md",
changelogTitle: "# Change Log",
},
]
: undefined,
[
// Commit the package.json and CHANGELOG.md files to git (if modified)
"@semantic-release/git",
{
assets: ["package.json", "CHANGELOG.md"].filter(Boolean),
},
],
//
// [
// '@semantic-release/exec',
// {
// verifyReleaseCmd: 'echo ${nextRelease.version} > .VERSION',
// },
// ],
].filter(Boolean),
}

View File

@@ -3,13 +3,13 @@
import path from "node:path"
import fs from "node:fs/promises"
import { massarg } from "massarg"
import chalk from "chalk"
import { ListCommandCliOptions, LogLevel, ScaffoldCmdConfig } from "./types"
import { Scaffold } from "./scaffold"
import { getConfigFile, parseAppendData, parseConfigFile } from "./config"
import { log } from "./logger"
import { MassargCommand } from "massarg/command"
import { getUniqueTmpPath as generateUniqueTmpPath } from "./file"
import { colorize } from "./utils"
export async function parseCliArgs(args = process.argv.slice(2)) {
const isProjectRoot = Boolean(await fs.stat(path.join(__dirname, "package.json")).catch(() => false))
@@ -134,7 +134,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
defaultValue: LogLevel.info,
description:
"Determine amount of logs to display. The values are: " +
`${chalk.bold`\`none | debug | info | warn | error\``}. ` +
`${colorize.bold`\`none | debug | info | warn | error\``}. ` +
"The provided level will display messages of the same level or higher.",
parse: (v) => {
const val = v.toLowerCase()
@@ -185,7 +185,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
}
try {
const file = await getConfigFile(config, tmpPath)
console.log(chalk.underline`Available templates:\n`)
console.log(colorize.underline`Available templates:\n`)
console.log(Object.keys(file).join("\n"))
} catch (e) {
const message = "message" in (e as any) ? (e as any).message : e?.toString()
@@ -212,7 +212,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
defaultValue: LogLevel.none,
description:
"Determine amount of logs to display. The values are: " +
`${chalk.bold`\`none | debug | info | warn | error\``}. ` +
`${colorize.bold`\`none | debug | info | warn | error\``}. ` +
"The provided level will display messages of the same level or higher.",
parse: (v) => {
const val = v.toLowerCase()
@@ -251,7 +251,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
bindOption: true,
lineLength: 100,
useGlobalTableColumns: true,
usageText: [chalk.yellow`simple-scaffold`, chalk.gray`[options]`, chalk.cyan`<name>`].join(" "),
usageText: [colorize.yellow`simple-scaffold`, colorize.gray`[options]`, colorize.cyan`<name>`].join(" "),
optionOptions: {
displayNegations: true,
},
@@ -259,9 +259,9 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
`Version: ${pkg.version}`,
`Copyright © Chen Asraf 2017-${new Date().getFullYear()}`,
``,
`Documentation: ${chalk.underline`https://chenasraf.github.io/simple-scaffold`}`,
`NPM: ${chalk.underline`https://npmjs.com/package/simple-scaffold`}`,
`GitHub: ${chalk.underline`https://github.com/chenasraf/simple-scaffold`}`,
`Documentation: ${colorize.underline`https://chenasraf.github.io/simple-scaffold`}`,
`NPM: ${colorize.underline`https://npmjs.com/package/simple-scaffold`}`,
`GitHub: ${colorize.underline`https://github.com/chenasraf/simple-scaffold`}`,
].join("\n"),
})
.parse(args)

View File

@@ -71,11 +71,10 @@ export function getBasePath(relPath: string): string {
.replace(process.cwd(), "")
}
export async function getFileList(_config: ScaffoldConfig, template: string): Promise<string[]> {
template = template.replaceAll(/[\\]+/g, "/")
log(_config, LogLevel.debug, `Getting file list for ${template}`)
export async function getFileList(config: ScaffoldConfig, templates: string[]): Promise<string[]> {
log(config, LogLevel.debug, `Getting file list for glob list: ${templates}`)
return (
await glob(template, {
await glob(templates, {
dot: true,
nodir: true,
})

View File

@@ -1,6 +1,6 @@
import util from "util"
import { LogConfig, LogLevel, ScaffoldConfig } from "./types"
import chalk from "chalk"
import { colorize, TermColor } from "./utils"
export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
const priority: Record<LogLevel, number> = {
@@ -15,7 +15,7 @@ export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
return
}
const levelColor: Record<keyof typeof LogLevel, keyof typeof chalk> = {
const levelColor: Record<keyof typeof LogLevel, TermColor> = {
[LogLevel.none]: "reset",
[LogLevel.debug]: "blue",
[LogLevel.info]: "dim",
@@ -23,16 +23,16 @@ export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
[LogLevel.error]: "red",
}
const chalkFn: any = chalk[levelColor[level]]
const colorFn = colorize[levelColor[level]]
const key: "log" | "warn" | "error" = level === LogLevel.error ? "error" : level === LogLevel.warning ? "warn" : "log"
const logFn: any = console[key]
logFn(
...obj.map((i) =>
i instanceof Error
? chalkFn(i, JSON.stringify(i, undefined, 1), i.stack)
? colorFn(i, JSON.stringify(i, undefined, 1), i.stack)
: typeof i === "object"
? util.inspect(i, { depth: null, colors: true })
: chalkFn(i),
: colorFn(i),
),
)
}

View File

@@ -16,6 +16,7 @@ import {
getFileList,
getBasePath,
handleTemplateFile,
GlobInfo,
} from "./file"
import { LogLevel, MinimalConfig, Resolver, ScaffoldCmdConfig, ScaffoldConfig } from "./types"
import { registerHelpers } from "./parser"
@@ -61,39 +62,45 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
try {
config.data = { name: config.name, ...config.data }
logInitStep(config)
for (let _template of config.templates) {
const excludes = config.templates.filter((t) => t.startsWith("!"))
const includes = config.templates.filter((t) => !t.startsWith("!"))
const templates: GlobInfo[] = []
for (let _template of includes) {
try {
const { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template } = await getTemplateGlobInfo(
config,
_template,
)
const files = await getFileList(config, template)
log(config, LogLevel.debug, "Iterating files", { files, template })
for (const inputFilePath of files) {
if (await isDir(inputFilePath)) {
continue
}
const relPath = makeRelativePath(path.dirname(removeGlob(inputFilePath).replace(nonGlobTemplate, "")))
const basePath = getBasePath(relPath)
logInputFile(config, {
originalTemplate: origTemplate,
relativePath: relPath,
parsedTemplate: template,
inputFilePath,
nonGlobTemplate,
basePath,
isDirOrGlob,
isGlob,
})
await handleTemplateFile(config, {
templatePath: inputFilePath,
basePath,
})
}
templates.push({ nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template })
} catch (e: any) {
handleErr(e)
}
}
for (const tpl of templates) {
const files = await getFileList(config, [tpl.template, ...excludes])
for (const file of files) {
if (await isDir(file)) {
continue
}
log(config, LogLevel.debug, "Iterating files", { files, file })
const relPath = makeRelativePath(path.dirname(removeGlob(file).replace(tpl.nonGlobTemplate, "")))
const basePath = getBasePath(relPath)
logInputFile(config, {
originalTemplate: tpl.origTemplate,
relativePath: relPath,
parsedTemplate: tpl.template,
inputFilePath: file,
nonGlobTemplate: tpl.nonGlobTemplate,
basePath,
isDirOrGlob: tpl.isDirOrGlob,
isGlob: tpl.isGlob,
})
await handleTemplateFile(config, {
templatePath: file,
basePath,
})
}
}
} catch (e: any) {
log(config, LogLevel.error, e)
throw e
@@ -111,7 +118,7 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
* @category Main
* @return {Promise<void>} A promise that resolves when the scaffold is complete
*/
Scaffold.fromConfig = async function (
Scaffold.fromConfig = async function(
/** The path or URL to the config file */
pathOrUrl: string,
/** Information needed before loading the config */

View File

@@ -22,6 +22,11 @@ export interface ScaffoldConfig {
* Template files to use as input. You may provide multiple files, each of which can be a relative or absolute path,
* or a glob pattern for multiple file matching easily.
*
* You may omit files from output by prepending a `!` to their glob pattern.
*
* For example, `["components/**", "!components/README.md"]` will include everything in the directory `components`
* except the `README.md` file inside.
*
* @default Current working directory
*/
templates: string[]
@@ -331,7 +336,9 @@ export type FileResponse<T> = T | FileResponseHandler<T>
/**
* The Scaffold config for CLI
* Contains less and more specific options than {@link ScaffoldConfig}
* Contains less and more specific options than {@link ScaffoldConfig}.
*
* For more information about each option, see {@link ScaffoldConfig}.
*/
export type ScaffoldCmdConfig = {
/** The name of the scaffold template to use. */

View File

@@ -15,3 +15,65 @@ export function wrapNoopResolver<T, R = T>(value: Resolver<T, R>): Resolver<T, R
return (_) => value
}
const colorMap = {
reset: 0,
dim: 2,
bold: 1,
italic: 3,
underline: 4,
red: 31,
green: 32,
yellow: 33,
blue: 34,
magenta: 35,
cyan: 36,
white: 37,
gray: 90,
} as const
export type TermColor = keyof typeof colorMap
function _colorize(text: string, color: TermColor): string {
const c = colorMap[color]!
let r = 0
if (c > 1 && c < 30) {
r = c + 20
} else if (c === 1) {
r = 23
} else {
r = 0
}
return `\x1b[${c}m${text}\x1b[${r}m`
}
function isTemplateStringArray(template: TemplateStringsArray | unknown): template is TemplateStringsArray {
return Array.isArray(template) && typeof template[0] === "string"
}
const createColorize =
(color: TermColor) =>
(template: TemplateStringsArray | unknown, ...params: unknown[]): string => {
return isTemplateStringArray(template)
? _colorize(
(template as TemplateStringsArray).reduce((acc, str, i) => acc + str + (params[i] ?? ""), ""),
color,
)
: _colorize(String(template), color)
}
type TemplateStringsFn = ReturnType<typeof createColorize> & ((text: string) => string)
type TemplateStringsFns = { [key in TermColor]: TemplateStringsFn }
export const colorize: typeof _colorize & TemplateStringsFns = Object.assign(
_colorize,
Object.entries(colorMap).reduce(
(acc, [key]) => {
acc[key as TermColor] = createColorize(key as TermColor)
return acc
},
{} as Record<TermColor, TemplateStringsFn>,
),
)

View File

@@ -71,6 +71,14 @@ const fileStructDates = {
output: {},
}
const fileStructExcludes = {
input: {
"include.txt": "This file should be included",
"exclude.txt": "This file should be excluded",
},
output: {},
}
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
return () => {
beforeEach(() => {
@@ -268,8 +276,7 @@ describe("Scaffold", () => {
}),
)
describe(
"output structure",
describe("output structure", () => {
withMock(fileStructNested, () => {
test("should maintain input structure on output", async () => {
await Scaffold({
@@ -294,8 +301,23 @@ describe("Scaffold", () => {
expect(oneDeepFile.toString()).toEqual("Hello, my value is 1")
expect(twoDeepFile.toString()).toEqual("Hi! My value is actually NOT 1!")
})
}),
)
})
withMock(fileStructExcludes, () => {
test("should exclude files", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input", "!exclude.txt"],
data: { value: "1" },
logLevel: "none",
})
const includeFile = readFileSync(join(process.cwd(), "output", "app_name.txt"))
expect(includeFile.toString()).toEqual("This file should be included")
expect(() => readFileSync(join(process.cwd(), "output", "exclude.txt"))).toThrow()
})
})
})
describe(
"capitalization helpers",

View File

@@ -1,5 +1,4 @@
import { handleErr, resolve } from "../src/utils"
import { handleErr, resolve, colorize, TermColor } from "../src/utils"
describe("utils", () => {
describe("resolve", () => {
test("should resolve function", () => {
@@ -19,3 +18,51 @@ describe("utils", () => {
})
})
})
describe("colorize", () => {
it("should colorize text with red color", () => {
const result = colorize("Hello", "red")
expect(result).toBe("\x1b[31mHello\x1b[0m")
})
it("should colorize text with bold", () => {
const result = colorize("Hello", "bold")
expect(result).toBe("\x1b[1mHello\x1b[23m")
})
it("should reset color", () => {
const result = colorize("Hello", "reset")
expect(result).toBe("\x1b[0mHello\x1b[0m")
})
it("should have all color functions", () => {
const colors: TermColor[] = [
"reset",
"dim",
"bold",
"italic",
"underline",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
"gray",
]
colors.forEach((color) => {
expect(typeof colorize[color]).toBe("function")
})
})
it("should colorize text using colorize.red", () => {
const result = colorize.red("Hello")
expect(result).toBe("\x1b[31mHello\x1b[0m")
})
it("should colorize text using template strings with colorize.blue", () => {
const result = colorize.blue`Hello ${"World"}`
expect(result).toBe("\x1b[34mHello World\x1b[0m")
})
})

View File

@@ -1,20 +1,29 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"target": "ESNext",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"lib": ["ES2022"],
"lib": [
"ESNext"
],
"declaration": true,
"outDir": "dist",
"strict": true,
"sourceMap": true,
"removeComments": false,
"paths": {
"@/*": ["./src/*"],
"@/*": [
"./src/*"
],
},
},
"include": ["src/index.ts", "src/cmd.ts"],
"exclude": ["tests/*"],
"include": [
"src/index.ts",
"src/cmd.ts"
],
"exclude": [
"tests/*"
],
}