mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-17 17:28:09 +00:00
feat!: separate git/github/config flags
feat: separate git/github/config t test cleanup
This commit is contained in:
@@ -11,23 +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 or https git URL to load config from instead of passing arguments to CLI or using a Node.js script. See examples for syntax. |
|
||||
| `--github` \| `-gh` | GitHub path 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 |
|
||||
| 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. |
|
||||
| `--github`\|`-gh` | GitHub path to load config from instead of passing arguments to CLI or using a Node.js script. |
|
||||
| `--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 |
|
||||
|
||||
## Examples:
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"date-fns": "^3.3.1",
|
||||
"glob": "^10.3.10",
|
||||
"handlebars": "^4.7.8",
|
||||
"massarg": "2.0.0-pre.10"
|
||||
"massarg": "2.0.0-pre.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@knodes/typedoc-plugin-pages": "^0.23.4",
|
||||
|
||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -18,8 +18,8 @@ dependencies:
|
||||
specifier: ^4.7.8
|
||||
version: 4.7.8
|
||||
massarg:
|
||||
specifier: 2.0.0-pre.10
|
||||
version: 2.0.0-pre.10
|
||||
specifier: 2.0.0-pre.11
|
||||
version: 2.0.0-pre.11
|
||||
|
||||
devDependencies:
|
||||
'@knodes/typedoc-plugin-pages':
|
||||
@@ -3231,8 +3231,8 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/massarg@2.0.0-pre.10:
|
||||
resolution: {integrity: sha512-0ngO00xzP9qxB4K0SsGoUzsc0X+/XTN+ctwflK+JNM1zuAPwq8Znucl5PfmTSFpXw764IrhNH5A1Xv+DINAweQ==}
|
||||
/massarg@2.0.0-pre.11:
|
||||
resolution: {integrity: sha512-MrN5ZllZyGI8DPSA8o164WgeEhfpDYnFvtA0xRoWrGJ2eCDUeyyCItUk3EB55tV3kwysqi6AAOei49xUQ5BG4w==}
|
||||
dependencies:
|
||||
zod: 3.22.4
|
||||
dev: false
|
||||
|
||||
12
src/cmd.ts
12
src/cmd.ts
@@ -35,8 +35,16 @@ export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
name: "config",
|
||||
aliases: ["c"],
|
||||
description:
|
||||
"Filename or https git URL to load config from instead of passing arguments to CLI or using a Node.js " +
|
||||
"script. See examples for syntax.",
|
||||
"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.",
|
||||
})
|
||||
.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.",
|
||||
})
|
||||
.option({
|
||||
name: "github",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
FileResponseHandler,
|
||||
LogConfig,
|
||||
LogLevel,
|
||||
RemoteConfigLoadConfig,
|
||||
ScaffoldCmdConfig,
|
||||
ScaffoldConfig,
|
||||
ScaffoldConfigFile,
|
||||
@@ -14,6 +15,7 @@ import { log } from "./logger"
|
||||
import { resolve, wrapNoopResolver } from "./utils"
|
||||
import { getGitConfig } from "./git"
|
||||
|
||||
/** @internal */
|
||||
export function getOptionValueForFile<T>(
|
||||
config: ScaffoldConfig,
|
||||
filePath: string,
|
||||
@@ -30,6 +32,7 @@ export function getOptionValueForFile<T>(
|
||||
)
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function parseAppendData(value: string, options: ScaffoldCmdConfig): unknown {
|
||||
const data = options.data ?? {}
|
||||
const [key, val] = value.split(/\:?=/)
|
||||
@@ -46,7 +49,7 @@ function isWrappedWithQuotes(string: string): boolean {
|
||||
|
||||
/** @internal */
|
||||
export async function parseConfigFile(config: ScaffoldCmdConfig): Promise<ScaffoldConfig> {
|
||||
let c: ScaffoldConfig = config
|
||||
let output: ScaffoldConfig = config
|
||||
|
||||
if (config.quiet) {
|
||||
config.logLevel = LogLevel.none
|
||||
@@ -54,26 +57,34 @@ export async function parseConfigFile(config: ScaffoldCmdConfig): Promise<Scaffo
|
||||
|
||||
if (config.github) {
|
||||
log(config, LogLevel.info, `Loading config from github ${config.github}`)
|
||||
config.config = githubPartToUrl(config.github)
|
||||
config.git = githubPartToUrl(config.github)
|
||||
}
|
||||
|
||||
if (config.config) {
|
||||
const { configFile, key, isRemote } = parseConfigSelection(config.config, config.key)
|
||||
if (config.config || config.git) {
|
||||
const isGit = Boolean(config.git)
|
||||
const key = config.key ?? "default"
|
||||
const configFile = config.config
|
||||
const loadPath = isGit ? config.git : configFile
|
||||
|
||||
log(config, LogLevel.info, `Loading config from ${configFile} with key ${key}`)
|
||||
const configPromise = await getConfig({
|
||||
config: configFile,
|
||||
isRemote,
|
||||
logLevel: config.logLevel,
|
||||
})
|
||||
const configPromise = await (config.git
|
||||
? getRemoteConfig({ git: loadPath, config: configFile, logLevel: config.logLevel })
|
||||
: getLocalConfig({ config: configFile, 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) {
|
||||
configImport = await resolve(configImport.default, config)
|
||||
}
|
||||
|
||||
if (!configImport[key]) {
|
||||
throw new Error(`Template "${key}" not found in ${configFile}`)
|
||||
}
|
||||
|
||||
const importedKey = configImport[key]
|
||||
c = {
|
||||
output = {
|
||||
...config,
|
||||
...importedKey,
|
||||
data: {
|
||||
@@ -83,20 +94,12 @@ export async function parseConfigFile(config: ScaffoldCmdConfig): Promise<Scaffo
|
||||
}
|
||||
}
|
||||
|
||||
c.data = { ...c.data, ...config.appendData }
|
||||
output.data = { ...output.data, ...config.appendData }
|
||||
delete config.appendData
|
||||
return c
|
||||
}
|
||||
|
||||
export function parseConfigSelection(
|
||||
config: string,
|
||||
key?: string,
|
||||
): { configFile: string; key: string; isRemote: boolean } {
|
||||
const isUrl = config.includes("://")
|
||||
const _key = key || "default"
|
||||
return { configFile: config, key: _key, isRemote: isUrl }
|
||||
return output
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function githubPartToUrl(part: string): string {
|
||||
const gitUrl = new URL(`https://github.com/${part}`)
|
||||
if (!gitUrl.pathname.endsWith(".git")) {
|
||||
@@ -106,26 +109,29 @@ export function githubPartToUrl(part: string): string {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getConfig(config: ConfigLoadConfig & Partial<LogConfig>): Promise<ScaffoldConfigFile> {
|
||||
const { config: configFile, isRemote, ...logConfig } = config as Required<typeof config>
|
||||
export async function getLocalConfig(config: ConfigLoadConfig & Partial<LogConfig>): Promise<ScaffoldConfigFile> {
|
||||
const { config: configFile, ...logConfig } = config as Required<typeof config>
|
||||
|
||||
if (!isRemote) {
|
||||
log(logConfig, LogLevel.info, `Loading config from file ${configFile}`)
|
||||
const absolutePath = path.resolve(process.cwd(), configFile)
|
||||
return wrapNoopResolver(import(absolutePath))
|
||||
}
|
||||
log(logConfig, LogLevel.info, `Loading config from file ${configFile}`)
|
||||
const absolutePath = path.resolve(process.cwd(), configFile)
|
||||
return wrapNoopResolver(import(absolutePath))
|
||||
}
|
||||
|
||||
const url = new URL(configFile)
|
||||
/** @internal */
|
||||
export async function getRemoteConfig(
|
||||
config: RemoteConfigLoadConfig & Partial<LogConfig>,
|
||||
): Promise<ScaffoldConfigFile> {
|
||||
const { config: configFile, git, ...logConfig } = config as Required<typeof config>
|
||||
|
||||
log(logConfig, LogLevel.info, `Loading config from remote ${git}, file ${configFile}`)
|
||||
|
||||
const url = new URL(git!)
|
||||
const isHttp = url.protocol === "http:" || url.protocol === "https:"
|
||||
const isGit = url.protocol === "git:" || (isHttp && url.pathname.endsWith(".git"))
|
||||
|
||||
if (isGit) {
|
||||
return getGitConfig(url, logConfig)
|
||||
}
|
||||
|
||||
if (!isHttp) {
|
||||
if (!isGit) {
|
||||
throw new Error(`Unsupported protocol ${url.protocol}`)
|
||||
}
|
||||
|
||||
return wrapNoopResolver(import(path.resolve(process.cwd(), configFile)))
|
||||
return getGitConfig(url, configFile, logConfig)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { resolve, wrapNoopResolver } from "./utils"
|
||||
|
||||
export async function getGitConfig(
|
||||
url: URL,
|
||||
file: string,
|
||||
logConfig: LogConfig,
|
||||
): Promise<AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>> {
|
||||
const repoUrl = `${url.protocol}//${url.host}${url.pathname}`
|
||||
@@ -22,8 +23,9 @@ export async function getGitConfig(
|
||||
clone.on("close", async (code) => {
|
||||
if (code === 0) {
|
||||
log(logConfig, LogLevel.info, `Loading config from git repo: ${repoUrl}`)
|
||||
const hashPath = url.hash?.replace("#", "") || "scaffold.config.js"
|
||||
const absolutePath = path.resolve(tmpPath, hashPath)
|
||||
// TODO search for dynamic config file in repo if not provided
|
||||
const filename = file || "scaffold.config.js"
|
||||
const absolutePath = path.resolve(tmpPath, filename)
|
||||
const loadedConfig = await resolve(
|
||||
async () => (await import(absolutePath)).default as ScaffoldConfigMap,
|
||||
logConfig,
|
||||
|
||||
12
src/types.ts
12
src/types.ts
@@ -322,10 +322,6 @@ export type FileResponse<T> = T | FileResponseHandler<T>
|
||||
|
||||
/** @internal */
|
||||
export interface ScaffoldCmdConfig {
|
||||
/**
|
||||
* Name to be passed to the generated files. `{{name}}` and `{{Name}}` inside contents and file names will be replaced
|
||||
* accordingly.
|
||||
*/
|
||||
name: string
|
||||
templates: string[]
|
||||
output: string
|
||||
@@ -337,8 +333,8 @@ export interface ScaffoldCmdConfig {
|
||||
logLevel: LogLevel
|
||||
dryRun: boolean
|
||||
config?: string
|
||||
/** The key to use for the file which contains the template configurations. */
|
||||
key?: string
|
||||
git?: string
|
||||
github?: string
|
||||
}
|
||||
|
||||
@@ -372,9 +368,11 @@ export type AsyncResolver<T, R = T> = Resolver<T, Promise<R> | R>
|
||||
/** @internal */
|
||||
export type LogConfig = Pick<ScaffoldConfig, "logLevel">
|
||||
|
||||
// TODO deprecat isRemote
|
||||
/** @internal */
|
||||
export type ConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config"> & { isRemote: boolean }
|
||||
export type ConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config">
|
||||
|
||||
/** @internal */
|
||||
export type RemoteConfigLoadConfig = LogConfig & Pick<ScaffoldCmdConfig, "config" | "git">
|
||||
|
||||
/** @internal */
|
||||
export type MinimalConfig = Pick<ScaffoldCmdConfig, "name" | "key">
|
||||
|
||||
@@ -14,7 +14,7 @@ jest.mock("../src/git", () => {
|
||||
}
|
||||
})
|
||||
|
||||
const { githubPartToUrl, parseAppendData, parseConfigFile, parseConfigSelection } = config
|
||||
const { githubPartToUrl, parseAppendData, parseConfigFile } = config
|
||||
|
||||
const blankCliConf: ScaffoldCmdConfig = {
|
||||
logLevel: LogLevel.none,
|
||||
@@ -56,46 +56,6 @@ describe("config", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseConfigSelection", () => {
|
||||
test("no key", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "default",
|
||||
isRemote: false,
|
||||
})
|
||||
})
|
||||
test("separate key", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js", "component")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "component",
|
||||
isRemote: false,
|
||||
})
|
||||
})
|
||||
test("key override", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js", "main")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "main",
|
||||
isRemote: false,
|
||||
})
|
||||
})
|
||||
test("isRemote: false", () => {
|
||||
expect(parseConfigSelection("scaffold.config.js", "main")).toEqual({
|
||||
configFile: "scaffold.config.js",
|
||||
key: "main",
|
||||
isRemote: false,
|
||||
})
|
||||
})
|
||||
test("isRemote: true", () => {
|
||||
expect(
|
||||
parseConfigSelection("https://github.com/chenasraf/simple-scaffold.git#scaffold.config.js", "main"),
|
||||
).toEqual({
|
||||
configFile: "https://github.com/chenasraf/simple-scaffold.git#scaffold.config.js",
|
||||
key: "main",
|
||||
isRemote: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseConfigFile", () => {
|
||||
test("normal config does not change", async () => {
|
||||
expect(
|
||||
@@ -125,9 +85,8 @@ describe("config", () => {
|
||||
|
||||
describe("getConfig", () => {
|
||||
test("gets git config", async () => {
|
||||
const resultFn = await config.getConfig({
|
||||
config: "https://github.com/chenasraf/simple-scaffold.git",
|
||||
isRemote: true,
|
||||
const resultFn = await config.getRemoteConfig({
|
||||
git: "https://github.com/chenasraf/simple-scaffold.git",
|
||||
logLevel: LogLevel.none,
|
||||
})
|
||||
const result = await resolve(resultFn, blankCliConf)
|
||||
@@ -135,9 +94,8 @@ describe("config", () => {
|
||||
})
|
||||
|
||||
test("gets local file config", async () => {
|
||||
const resultFn = await config.getConfig({
|
||||
const resultFn = await config.getLocalConfig({
|
||||
config: "scaffold.config.js",
|
||||
isRemote: false,
|
||||
logLevel: LogLevel.none,
|
||||
})
|
||||
const result = await resolve(resultFn, {} as any)
|
||||
|
||||
Reference in New Issue
Block a user