diff --git a/docs/docs/usage/03-cli.md b/docs/docs/usage/03-cli.md index 1c92f5e..fd24b81 100644 --- a/docs/docs/usage/03-cli.md +++ b/docs/docs/usage/03-cli.md @@ -11,24 +11,24 @@ 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 to load config from instead of passing arguments to CLI or using a Node.js script. See examples for syntax. This can also work in conjunction with `--git` or `--github` to point to remote files, and with `--key` to denote which key to select from the file., | -| `--git`\|`-g` | Git URL to load config from instead of passing arguments to CLI or using a Node.js script. See examples for syntax. | -| `--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. | -| `--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. | +| 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. | +| `--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. | ## Examples: diff --git a/release.config.js b/release.config.js index 051c9f5..c1cf0f1 100644 --- a/release.config.js +++ b/release.config.js @@ -13,20 +13,14 @@ module.exports = { "@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/npm', - // { - // // only update the pkg version on doc, don't publish - // npmPublish: false, - // pkgRoot: 'doc', - // }, - // ] [ "@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"', }, ], @@ -34,10 +28,12 @@ module.exports = { "@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"], @@ -45,6 +41,7 @@ module.exports = { ], branch === "master" ? [ + // Update CHANGELOG.md only on master "@semantic-release/changelog", { changelogFile: "CHANGELOG.md", @@ -53,6 +50,7 @@ module.exports = { ] : undefined, [ + // Commit the package.json and CHANGELOG.md files to git (if modified) "@semantic-release/git", { assets: ["package.json", "CHANGELOG.md"].filter(Boolean), diff --git a/src/cmd.ts b/src/cmd.ts index 926afa5..8e20610 100644 --- a/src/cmd.ts +++ b/src/cmd.ts @@ -3,20 +3,22 @@ import os from "node:os" import { massarg } from "massarg" import chalk from "chalk" -import { LogLevel, ScaffoldCmdConfig } from "./types" +import { ListCommandCliOptions, LogLevel, ScaffoldCmdConfig } from "./types" import { Scaffold } from "./scaffold" import path from "node:path" import fs from "node:fs/promises" -import { parseAppendData, parseConfigFile } from "./config" +import { getConfigFile, parseAppendData, parseConfigFile } from "./config" import { log } from "./logger" +import { MassargCommand } from "massarg/command" export async function parseCliArgs(args = process.argv.slice(2)) { const isProjectRoot = Boolean(await fs.stat(path.join(__dirname, "package.json")).catch(() => false)) const pkgFile = await fs.readFile(path.resolve(__dirname, isProjectRoot ? "." : "..", "package.json")) const pkg = JSON.parse(pkgFile.toString()) const isVersionFlag = args.includes("--version") || args.includes("-v") - const isConfigProvided = - args.includes("--config") || args.includes("-c") || args.includes("--git") || args.includes("-g") || isVersionFlag + const isConfigFileProvided = args.includes("--config") || args.includes("-c") + const isGitProvided = args.includes("--git") || args.includes("-g") + const isConfigProvided = isConfigFileProvided || isGitProvided || isVersionFlag return massarg({ name: pkg.name, @@ -46,24 +48,20 @@ export async function parseCliArgs(args = process.argv.slice(2)) { aliases: ["n"], description: "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.", + "contents and file names will be replaced accordingly. You may omit the `--name` or `-n` " + + "for this specific option.", isDefault: true, required: !isConfigProvided, }) .option({ name: "config", aliases: ["c"], - description: - "Filename to load config from instead of passing arguments to CLI or using a Node.js " + - "script. See examples for syntax. This can also work in conjunction with `--git` or `--github` to point " + - "to remote files, and with `--key` to denote which key to select from the file.", + description: "Filename or directory to load config from", }) .option({ name: "git", aliases: ["g"], - description: - "Git URL to load config from instead of passing arguments to CLI or using a Node.js script. See " + - "examples for syntax.", + description: "Git URL or GitHub path to load a template from.", }) .option({ name: "key", @@ -159,6 +157,67 @@ export async function parseCliArgs(args = process.argv.slice(2)) { aliases: ["v"], description: "Display version.", }) + .command( + new MassargCommand({ + name: "list", + aliases: ["ls"], + description: "List all available templates for a given config. See `list -h` for more information.", + run: async (_config) => { + const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`) + const config = { + templates: [], + name: "", + version: false, + output: "", + subdir: false, + overwrite: false, + dryRun: false, + ..._config, + config: _config.config ?? (!_config.git ? process.cwd() : undefined), + } + try { + const file = await getConfigFile(config, tmpPath) + console.log(chalk.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() + log(config, LogLevel.error, message) + } finally { + log(config, LogLevel.debug, "Cleaning up temporary files...", tmpPath) + await fs.rm(tmpPath, { recursive: true, force: true }) + } + }, + }) + .option({ + name: "config", + aliases: ["c"], + description: "Filename or directory to load config from. Defaults to current working directory.", + }) + .option({ + name: "git", + aliases: ["g"], + description: "Git URL or GitHub path to load a template from.", + }) + .option({ + name: "log-level", + aliases: ["l"], + defaultValue: LogLevel.none, + description: + "Determine amount of logs to display. The values are: " + + `${chalk.bold`\`none | debug | info | warn | error\``}. ` + + "The provided level will display messages of the same level or higher.", + parse: (v) => { + const val = v.toLowerCase() + if (!(val in LogLevel)) { + throw new Error(`Invalid log level: ${val}, must be one of: ${Object.keys(LogLevel).join(", ")}`) + } + return val + }, + }) + .help({ + bindOption: true, + }), + ) .example({ description: "Usage with config file", input: "simple-scaffold -c scaffold.cmd.js --key component", diff --git a/src/config.ts b/src/config.ts index 0f0d6c0..6e86158 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,6 +10,7 @@ import { ScaffoldCmdConfig, ScaffoldConfig, ScaffoldConfigFile, + ScaffoldConfigMap, } from "./types" import { handlebarsParse } from "./parser" import { log } from "./logger" @@ -49,6 +50,34 @@ function isWrappedWithQuotes(string: string): boolean { return (string.startsWith('"') && string.endsWith('"')) || (string.startsWith("'") && string.endsWith("'")) } +/** @internal */ +export async function getConfigFile(config: ScaffoldCmdConfig, tmpPath: string): Promise { + if (config.git && !config.git.includes("://")) { + log(config, LogLevel.info, `Loading config from GitHub ${config.git}`) + config.git = githubPartToUrl(config.git) + } + + const isGit = Boolean(config.git) + const configFilename = config.config + const configPath = isGit ? config.git : configFilename + + log(config, LogLevel.info, `Loading config from file ${configFilename}`) + + const configPromise = await (isGit + ? getRemoteConfig({ git: configPath, config: configFilename, logLevel: config.logLevel, tmpPath }) + : getLocalConfig({ config: configFilename, logLevel: config.logLevel })) + + // resolve the config + let configImport = await resolve(configPromise, config) + + // If the config is a function or promise, return the output + if (typeof configImport.default === "function" || configImport.default instanceof Promise) { + log(config, LogLevel.debug, "Config is a function or promise, resolving...") + configImport = await resolve(configImport.default, config) + } + return configImport +} + /** @internal */ export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string): Promise { let output: ScaffoldConfig = config @@ -57,36 +86,14 @@ export async function parseConfigFile(config: ScaffoldCmdConfig, tmpPath: string config.logLevel = LogLevel.none } - if (config.git && !config.git.includes("://")) { - log(config, LogLevel.info, `Loading config from GitHub ${config.git}`) - config.git = githubPartToUrl(config.git) - } - - const shouldLoadConfig = config.config || config.git + const shouldLoadConfig = Boolean(config.config || config.git) if (shouldLoadConfig) { - const isGit = Boolean(config.git) const key = config.key ?? "default" - const configFilename = config.config - const configPath = isGit ? config.git : configFilename - - log(config, LogLevel.info, `Loading config from file ${configFilename} with key ${key}`) - - const configPromise = await (isGit - ? getRemoteConfig({ git: configPath, config: configFilename, logLevel: config.logLevel, tmpPath }) - : getLocalConfig({ config: configFilename, logLevel: config.logLevel })) - - // resolve the config - let configImport = await resolve(configPromise, config) - - // If the config is a function or promise, return the output - if (typeof configImport.default === "function" || configImport.default instanceof Promise) { - log(config, LogLevel.debug, "Config is a function or promise, resolving...") - configImport = await resolve(configImport.default, config) - } + const configImport = await getConfigFile(config, tmpPath) if (!configImport[key]) { - throw new Error(`Template "${key}" not found in ${configFilename}`) + throw new Error(`Template "${key}" not found in ${config.config}`) } const imported = configImport[key] diff --git a/src/types.ts b/src/types.ts index 450991b..1692e14 100644 --- a/src/types.ts +++ b/src/types.ts @@ -413,3 +413,5 @@ export type RemoteConfigLoadConfig = LogConfig & Pick + +export type ListCommandCliOptions = Pick