mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-18 01:29:09 +00:00
Compare commits
21 Commits
v2.2.0
...
release-pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f698f5ec7 | ||
|
|
f49b9acf63 | ||
| 9145078471 | |||
| 89dc43c73d | |||
| 2c43dc4daf | |||
| f4c907e6c9 | |||
| a275e688d4 | |||
| ff4ebf0a5b | |||
| ab9322e1ab | |||
|
|
35f0d014d9 | ||
| 8ad8cb4be1 | |||
| daaefaf54e | |||
| aefba4b773 | |||
|
|
8457f0996a | ||
|
|
adc95809ba | ||
| 98b326c843 | |||
| ddc115a037 | |||
| 19e7b0f0c3 | |||
|
|
f883571daa | ||
|
|
be3068a533 | ||
|
|
8acc660dea |
33
.github/workflows/develop.yml
vendored
Normal file
33
.github/workflows/develop.yml
vendored
Normal 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
|
||||
26
.github/workflows/docs.yml
vendored
26
.github/workflows/docs.yml
vendored
@@ -2,7 +2,12 @@ name: Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, pre, develop]
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
@@ -10,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:
|
||||
|
||||
26
.github/workflows/pull_requests.yml
vendored
26
.github/workflows/pull_requests.yml
vendored
@@ -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
|
||||
79
.github/workflows/release.yml
vendored
79
.github/workflows/release.yml
vendored
@@ -2,37 +2,62 @@ 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
|
||||
|
||||
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 build && npm publish
|
||||
env:
|
||||
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,5 +1,44 @@
|
||||
# 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 ([ab9322e](https://github.com/chenasraf/simple-scaffold/commit/ab9322e1ab9c0a07cdab7275f3398286dee67a64))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* exclude globs ([89dc43c](https://github.com/chenasraf/simple-scaffold/commit/89dc43c73d9a8640f45ae77e5c89e4f08f7f99ad))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* homepage url ([daaefaf](https://github.com/chenasraf/simple-scaffold/commit/daaefaf54e8c8887e6f210d02fd5f96c6ff4aa21))
|
||||
|
||||
## [2.2.1](https://github.com/chenasraf/simple-scaffold/compare/v2.2.0...v2.2.1) (2024-04-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* beforeWrite from config files ([98b326c](https://github.com/chenasraf/simple-scaffold/commit/98b326c84346162f379af46bc5aefb69df8be515))
|
||||
* use console.info for handlebars parse warning ([19e7b0f](https://github.com/chenasraf/simple-scaffold/commit/19e7b0f0c35c1b79a98781bdec9f54354123d8e0))
|
||||
|
||||
# [2.2.0](https://github.com/chenasraf/simple-scaffold/compare/v2.1.0...v2.2.0) (2024-02-23)
|
||||
|
||||
|
||||
|
||||
@@ -11,25 +11,26 @@ Usage: simple-scaffold [options]
|
||||
To see this and more information anytime, add the `-h` or `--help` flag to your call, e.g.
|
||||
`npx simple-scaffold@latest -h`.
|
||||
|
||||
| Command \| alias | |
|
||||
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--name` \| `-n` | Name to be passed to the generated files. `{{name}}` and other data parameters inside contents and file names will be replaced accordingly. You may omit the `--name` or `-n` for this specific option. |
|
||||
| `--config`\|`-c` | Filename or directory to load config from |
|
||||
| `--git`\|`-g` | Git URL or GitHub path to load a template from. |
|
||||
| `--key` \| `-k` | Key to load inside the config file. This overwrites the config key provided after the colon in `--config` (e.g. `--config scaffold.cmd.js:component`) |
|
||||
| `--output` \| `-o` | Path to output to. If `--create-sub-folder` is enabled, the subfolder will be created inside this path. Default is current working directory. |
|
||||
| `--templates` \| `-t` | 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. |
|
||||
| `--overwrite` \| `-w` | Enable to override output files, even if they already exist. |
|
||||
| `--data` \| `-d` | Add custom data to the templates. By default, only your app name is included. |
|
||||
| `--append-data` \| `-D` | Append additional custom data to the templates, which will overwrite `--data`, using an alternate syntax, which is easier to use with CLI: `-D key1=string -D key2:=raw` |
|
||||
| `--create-sub-folder` \| `-s` | Create subfolder with the input name |
|
||||
| `--sub-folder-name-helper` \| `-sh` | Default helper to apply to subfolder name when using `--create-sub-folder true`. |
|
||||
| `--quiet` \| `-q` | Suppress output logs (Same as `--log-level none`) |
|
||||
| `--log-level` \| `-l` | Determine amount of logs to display. The values are: `none \| debug \| info \| warn \| error`. The provided level will display messages of the same level or higher. |
|
||||
| `--before-write` \| `-B` | Run a script before writing the files. This can be a command or a path to a file. A temporary file path will be passed to the given command and the command should return a string for the final output. |
|
||||
| `--dry-run` \| `-dr` | Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write actual file contents or create directories. |
|
||||
| `--help` \| `-h` | Show this help message |
|
||||
| `--version` \| `-v` | Display version. |
|
||||
Options:
|
||||
|
||||
| Option/flag \| Alias | Description |
|
||||
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--name` \| `-n` | Name to be passed to the generated files. `{{name}}` and other data parameters inside contents and file names will be replaced accordingly. You may omit the `--name` or `-n` for this specific option. |
|
||||
| `--config` \| `-c` | Filename or directory to load config from |
|
||||
| `--git` \| `-g` | Git URL or GitHub path to load a template from. |
|
||||
| `--key` \| `-k` | Key to load inside the config file. This overwrites the config key provided after the colon in `--config` (e.g. `--config scaffold.cmd.js:component)` |
|
||||
| `--output` \| `-o` | Path to output to. If `--subdir` is enabled, the subdir will be created inside this path. Default is current working directory. |
|
||||
| `--templates` \| `-t` | 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. |
|
||||
| `--overwrite` \| `-w` \| `--no-overwrite` \| `-W` | Enable to override output files, even if they already exist. (default: false) |
|
||||
| `--data` \| `-d` | Add custom data to the templates. By default, only your app name is included. |
|
||||
| `--append-data` \| `-D` | Append additional custom data to the templates, which will overwrite `--data`, using an alternate syntax, which is easier to use with CLI: `-D key1=string -D key2:=raw` |
|
||||
| `--subdir` \| `-s` \| `--no-subdir` \| `-S` | Create a parent directory with the input name (and possibly `--subdir-helper` (default: false) |
|
||||
| `--subdir-helper` \| `-H` | Default helper to apply to subdir name when using `--subdir`. |
|
||||
| `--quiet` \| `-q` | Suppress output logs (Same as `--log-level none`)(default: false) |
|
||||
| `--log-level` \| `-l` | Determine amount of logs to display. The values are: `none, debug, info, warn, error`. The provided level will display messages of the same level or higher. (default: info) |
|
||||
| `--before-write` \| `-B` | Run a script before writing the files. This can be a command or a path to a file. A temporary file path will be passed to the given command and the command should return a string for the final output. |
|
||||
| `--dry-run` \| `-dr` | Don't emit files. This is good for testing your scaffolds and making sure they don't fail, without having to write actual file contents or create directories. (default: false) |
|
||||
| `--version` \| `-v` | Display version. |
|
||||
|
||||
### Before Write option
|
||||
|
||||
@@ -63,6 +64,12 @@ See
|
||||
Node.js API for more details. Instead of returning `undefined` to keep the default behavior, you can
|
||||
output `''` for the same effect.
|
||||
|
||||
## Available Commands:
|
||||
|
||||
| Command \| Alias | Description |
|
||||
| ---------------- | ------------------------------------------------------------------------------------ |
|
||||
| `list` \| `ls` | List all available templates for a given config. See `list -h` for more information. |
|
||||
|
||||
## Examples:
|
||||
|
||||
> See
|
||||
|
||||
@@ -19,14 +19,14 @@ interface ScaffoldConfig {
|
||||
name: string
|
||||
templates: string[]
|
||||
output: FileResponse<string>
|
||||
createSubFolder?: boolean
|
||||
subdir?: boolean
|
||||
data?: Record<string, any>
|
||||
overwrite?: FileResponse<boolean>
|
||||
quiet?: boolean
|
||||
verbose?: LogLevel
|
||||
dryRun?: boolean
|
||||
helpers?: Record<string, Helper>
|
||||
subFolderNameHelper?: DefaultHelpers | string
|
||||
subdirHelper?: DefaultHelpers | string
|
||||
beforeWrite?(
|
||||
content: Buffer,
|
||||
rawContent: Buffer,
|
||||
@@ -57,17 +57,17 @@ const config = {
|
||||
name: "component",
|
||||
templates: [path.join(__dirname, "scaffolds", "component")],
|
||||
output: path.join(__dirname, "src", "components"),
|
||||
createSubFolder: true,
|
||||
subFolderNameHelper: "upperCase"
|
||||
subdir: true,
|
||||
subdirHelper: "upperCase",
|
||||
data: {
|
||||
property: "value",
|
||||
},
|
||||
helpers: {
|
||||
twice: (text) => [text, text].join(" ")
|
||||
twice: (text) => [text, text].join(" "),
|
||||
},
|
||||
// return a string to replace the final file contents after pre-processing, or `undefined`
|
||||
// to keep it as-is
|
||||
beforeWrite: (content, rawContent, outputPath) => content.toString().toUpperCase()
|
||||
beforeWrite: (content, rawContent, outputPath) => content.toString().toUpperCase(),
|
||||
}
|
||||
|
||||
const scaffold = Scaffold(config)
|
||||
|
||||
10289
docs/pnpm-lock.yaml
generated
10289
docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
// },
|
||||
|
||||
36
package.json
36
package.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "simple-scaffold",
|
||||
"version": "2.2.0",
|
||||
"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",
|
||||
"homepage": "https://chenasraf.github.io/simple-scaffold",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/chenasraf/simple-scaffold.git"
|
||||
@@ -13,7 +13,7 @@
|
||||
"bin": {
|
||||
"simple-scaffold": "cmd.js"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.1",
|
||||
"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.3.1",
|
||||
"glob": "^10.3.10",
|
||||
"date-fns": "^4.0.0",
|
||||
"glob": "^11.0.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"massarg": "2.0.0"
|
||||
"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": "^12.1.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/mock-fs": "^4.13.4",
|
||||
"@types/node": "^20.11.17",
|
||||
"@types/semantic-release": "^20.0.6",
|
||||
"conventional-changelog": "^5.1.0",
|
||||
"conventional-changelog-cli": "^4.1.0",
|
||||
"@types/node": "^22.5.5",
|
||||
"jest": "^29.7.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"semantic-release": "^23.0.2",
|
||||
"semantic-release-conventional-commits": "^3.0.0",
|
||||
"ts-jest": "^29.1.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
5584
pnpm-lock.yaml
generated
5584
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||
}
|
||||
20
src/cmd.ts
20
src/cmd.ts
@@ -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))
|
||||
@@ -74,7 +74,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
name: "output",
|
||||
aliases: ["o"],
|
||||
description:
|
||||
"Path to output to. If `--subdir` is enabled, the subfolder will be created inside " +
|
||||
"Path to output to. If `--subdir` is enabled, the subdir will be created inside " +
|
||||
"this path. Default is current working directory.",
|
||||
required: !isConfigProvided,
|
||||
})
|
||||
@@ -120,7 +120,7 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
.option({
|
||||
name: "subdir-helper",
|
||||
aliases: ["H"],
|
||||
description: "Default helper to apply to subfolder name when using `--subdir`.",
|
||||
description: "Default helper to apply to subdir name when using `--subdir`.",
|
||||
})
|
||||
.flag({
|
||||
name: "quiet",
|
||||
@@ -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)
|
||||
|
||||
@@ -111,7 +111,8 @@ export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string
|
||||
}
|
||||
|
||||
output.data = { ...output.data, ...config.appendData }
|
||||
output.beforeWrite = config.beforeWrite ? wrapBeforeWrite(config, config.beforeWrite) : undefined
|
||||
const cmdBeforeWrite = config.beforeWrite ? wrapBeforeWrite(config, config.beforeWrite) : undefined
|
||||
output.beforeWrite = cmdBeforeWrite ?? output.beforeWrite
|
||||
|
||||
if (!output.name) {
|
||||
throw new Error("simple-scaffold: Missing required option: name")
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export function handlebarsParse(
|
||||
return Buffer.from(outputContents)
|
||||
} catch (e) {
|
||||
log(config, LogLevel.debug, e)
|
||||
log(config, LogLevel.warning, "Couldn't parse file with handlebars, returning original content")
|
||||
log(config, LogLevel.info, "Couldn't parse file with handlebars, returning original content")
|
||||
return Buffer.from(templateBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
21
src/types.ts
21
src/types.ts
@@ -22,12 +22,17 @@ 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[]
|
||||
|
||||
/**
|
||||
* Path to output to. If `subdir` is `true`, the subfolder will be created inside this path.
|
||||
* Path to output to. If `subdir` is `true`, the subdir will be created inside this path.
|
||||
*
|
||||
* May also be a {@link FileResponseHandler} which returns a new output path to override the default one.
|
||||
*
|
||||
@@ -37,7 +42,7 @@ export interface ScaffoldConfig {
|
||||
output: FileResponse<string>
|
||||
|
||||
/**
|
||||
* Whether to create subfolder with the input name.
|
||||
* Whether to create subdir with the input name.
|
||||
*
|
||||
* When `true`, you may also use {@link subdirHelper} to determine a pre-process helper on
|
||||
* the directory name.
|
||||
@@ -131,7 +136,7 @@ export interface ScaffoldConfig {
|
||||
helpers?: Record<string, Helper>
|
||||
|
||||
/**
|
||||
* Default transformer to apply to subfolder name when using `subdir: true`. Can be one of the default
|
||||
* Default transformer to apply to subdir name when using `subdir: true`. Can be one of the default
|
||||
* capitalization helpers, or a custom one you provide to `helpers`. Defaults to `undefined`, which means no
|
||||
* transformation is done.
|
||||
*
|
||||
@@ -165,7 +170,7 @@ export interface ScaffoldConfig {
|
||||
/**
|
||||
* The names of the available helper functions that relate to text capitalization.
|
||||
*
|
||||
* These are available for `subfolderNameHelper`.
|
||||
* These are available for `subdirHelper`.
|
||||
*
|
||||
* | Helper name | Example code | Example output |
|
||||
* | ------------ | ----------------------- | -------------- |
|
||||
@@ -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. */
|
||||
@@ -340,9 +347,9 @@ export type ScaffoldCmdConfig = {
|
||||
templates: string[]
|
||||
/** The output path to write to */
|
||||
output: string
|
||||
/** Whether to create subfolder with the input name */
|
||||
/** Whether to create subdir with the input name */
|
||||
subdir: boolean
|
||||
/** Default transformer to apply to subfolder name when using `subdir: true` */
|
||||
/** Default transformer to apply to subdir name when using `subdir: true` */
|
||||
subdirHelper?: string
|
||||
/** Add custom data to the templates */
|
||||
data?: Record<string, string>
|
||||
|
||||
62
src/utils.ts
62
src/utils.ts
@@ -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>,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -92,7 +100,7 @@ function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunct
|
||||
|
||||
describe("Scaffold", () => {
|
||||
describe(
|
||||
"create subfolder",
|
||||
"create subdir",
|
||||
withMock(fileStructNormal, () => {
|
||||
test("should not create by default", async () => {
|
||||
await Scaffold({
|
||||
@@ -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",
|
||||
@@ -395,7 +417,7 @@ describe("Scaffold", () => {
|
||||
}),
|
||||
)
|
||||
describe(
|
||||
"transform subfolder",
|
||||
"transform subdir",
|
||||
withMock(fileStructSubdirTransformer, () => {
|
||||
test("should work with no helper", async () => {
|
||||
await Scaffold({
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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/*"
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user