feat!: separate git/github/config flags

feat: separate git/github/config

t

test

cleanup
This commit is contained in:
2024-01-29 01:37:40 +02:00
committed by Chen Asraf
parent 5373495f80
commit 9ce2845ace
8 changed files with 87 additions and 114 deletions

View File

@@ -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:

View File

@@ -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
View File

@@ -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

View File

@@ -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",

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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">

View File

@@ -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)