mirror of
https://github.com/chenasraf/simple-scaffold.git
synced 2026-05-17 17:28:09 +00:00
feat: function config file
This commit is contained in:
@@ -5,7 +5,7 @@ import { LogLevel, ScaffoldCmdConfig } from "./types"
|
||||
import { Scaffold } from "./scaffold"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { parseAppendData, parseConfig } from "./utils"
|
||||
import { parseAppendData, parseConfig } from "./config"
|
||||
|
||||
export async function parseCliArgs(args = process.argv.slice(2)) {
|
||||
const pkg = JSON.parse((await fs.readFile(path.join(__dirname, "package.json"))).toString())
|
||||
|
||||
168
src/config.ts
Normal file
168
src/config.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import path from "path"
|
||||
import {
|
||||
AsyncResolver,
|
||||
FileResponse,
|
||||
FileResponseHandler,
|
||||
LogLevel,
|
||||
Resolver,
|
||||
ScaffoldCmdConfig,
|
||||
ScaffoldConfig,
|
||||
ScaffoldConfigFile,
|
||||
ScaffoldConfigMap,
|
||||
} from "./types"
|
||||
import { OptionsBase } from "massarg/types"
|
||||
import { spawn } from "node:child_process"
|
||||
import os from "node:os"
|
||||
import { handlebarsParse } from "./parser"
|
||||
import { log } from "./logger"
|
||||
import { resolve } from "./utils"
|
||||
|
||||
export function getOptionValueForFile<T>(
|
||||
config: ScaffoldConfig,
|
||||
filePath: string,
|
||||
fn: FileResponse<T>,
|
||||
defaultValue?: T,
|
||||
): T {
|
||||
if (typeof fn !== "function") {
|
||||
return defaultValue ?? (fn as T)
|
||||
}
|
||||
return (fn as FileResponseHandler<T>)(
|
||||
filePath,
|
||||
path.dirname(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||
path.basename(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||
)
|
||||
}
|
||||
|
||||
export function parseAppendData(value: string, options: ScaffoldCmdConfig & OptionsBase): unknown {
|
||||
const data = options.data ?? {}
|
||||
const [key, val] = value.split(/\:?=/)
|
||||
// raw
|
||||
if (value.includes(":=") && !val.includes(":=")) {
|
||||
return { ...data, [key]: JSON.parse(val) }
|
||||
}
|
||||
return { ...data, [key]: isWrappedWithQuotes(val) ? val.substring(1, val.length - 1) : val }
|
||||
}
|
||||
|
||||
function isWrappedWithQuotes(string: string): boolean {
|
||||
return (string.startsWith('"') && string.endsWith('"')) || (string.startsWith("'") && string.endsWith("'"))
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function parseConfig(config: ScaffoldCmdConfig & OptionsBase): Promise<ScaffoldConfig> {
|
||||
let c: ScaffoldConfig = config
|
||||
if (config.github) {
|
||||
log(config, LogLevel.Info, `Loading config from github ${config.github}`)
|
||||
const gitUrl = new URL(`https://github.com/${config.github}`)
|
||||
if (!gitUrl.pathname.endsWith(".git")) {
|
||||
gitUrl.pathname += ".git"
|
||||
}
|
||||
config.config = gitUrl.toString()
|
||||
}
|
||||
|
||||
if (config.config) {
|
||||
const isUrl = config.config.includes("://")
|
||||
|
||||
const hasColonToken = (!isUrl && config.config.includes(":")) || (isUrl && count(config.config, ":") > 1)
|
||||
const colonIndex = config.config.lastIndexOf(":")
|
||||
const [configFile, templateKey = "default"] = hasColonToken
|
||||
? [config.config.substring(0, colonIndex), config.config.substring(colonIndex + 1)]
|
||||
: [config.config, undefined]
|
||||
const key = (config.key ?? templateKey) || "default"
|
||||
log(config, LogLevel.Info, `Loading config from ${configFile} with key ${key}`)
|
||||
const configPromise = await getConfig({ config: configFile, quiet: config.quiet, verbose: config.verbose })
|
||||
const configImport = await resolve(configPromise, config)
|
||||
|
||||
if (!configImport[key]) {
|
||||
throw new Error(`Template "${key}" not found in ${configFile}`)
|
||||
}
|
||||
c = {
|
||||
...config,
|
||||
...configImport[key],
|
||||
data: {
|
||||
...configImport[key].data,
|
||||
...config.data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
c.data = { ...c.data, ...config.appendData }
|
||||
delete config.appendData
|
||||
return c
|
||||
}
|
||||
|
||||
function wrapNoopResolver<T, R = T>(value: Resolver<T, R>): Resolver<T, R> {
|
||||
if (typeof value === "function") {
|
||||
return value
|
||||
}
|
||||
|
||||
return (_) => value
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getConfig(
|
||||
config: Pick<ScaffoldCmdConfig, "quiet" | "verbose" | "config">,
|
||||
): Promise<ScaffoldConfigFile> {
|
||||
const { config: configFile, ...logConfig } = config as Required<typeof config>
|
||||
const url = new URL(configFile)
|
||||
|
||||
if (url.protocol === "file:") {
|
||||
log(logConfig, LogLevel.Info, `Loading config from file ${configFile}`)
|
||||
const absolutePath = path.resolve(process.cwd(), configFile)
|
||||
return wrapNoopResolver(import(absolutePath))
|
||||
}
|
||||
|
||||
const isHttp = url.protocol === "http:" || url.protocol === "https:"
|
||||
const isGit = url.protocol === "git:" || (isHttp && url.pathname.endsWith(".git"))
|
||||
|
||||
if (isHttp || isGit) {
|
||||
if (isGit) {
|
||||
return getGitConfig(url, logConfig)
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported protocol ${url.protocol}`)
|
||||
}
|
||||
|
||||
return wrapNoopResolver(import(path.resolve(process.cwd(), configFile)))
|
||||
}
|
||||
|
||||
async function getGitConfig(
|
||||
url: URL,
|
||||
logConfig: Pick<ScaffoldCmdConfig, "verbose" | "quiet">,
|
||||
): Promise<AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>> {
|
||||
const repoUrl = `${url.protocol}//${url.host}${url.pathname}`
|
||||
|
||||
log(logConfig, LogLevel.Info, `Cloning git repo ${repoUrl}`)
|
||||
|
||||
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const clone = spawn("git", ["clone", "--depth", "1", repoUrl, tmpPath])
|
||||
|
||||
clone.on("error", reject)
|
||||
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)
|
||||
const loadedConfig = (await import(absolutePath)).default as ScaffoldConfigMap
|
||||
log(logConfig, LogLevel.Info, `Loaded config from git`)
|
||||
log(logConfig, LogLevel.Debug, `Raw config:`, loadedConfig)
|
||||
const fixedConfig: ScaffoldConfigMap = Object.fromEntries(
|
||||
Object.entries(loadedConfig).map(([k, v]) => [
|
||||
k,
|
||||
// use absolute paths for template as config is necessarily in another directory
|
||||
{ ...v, templates: v.templates.map((t) => path.resolve(tmpPath, t)) },
|
||||
]),
|
||||
)
|
||||
|
||||
resolve(wrapNoopResolver(fixedConfig))
|
||||
} else {
|
||||
reject(new Error(`Git clone failed with code ${code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function count(string: string, substring: string): number {
|
||||
return string.split(substring).length - 1
|
||||
}
|
||||
170
src/file.ts
Normal file
170
src/file.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import path from "path"
|
||||
import { F_OK } from "constants"
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
import { promises as fsPromises } from "fs"
|
||||
const { stat, access, mkdir } = fsPromises
|
||||
import { glob, hasMagic } from "glob"
|
||||
import { log } from "./logger"
|
||||
import { getOptionValueForFile } from "./config"
|
||||
import { handlebarsParse } from "./parser"
|
||||
|
||||
const { readFile, writeFile } = fsPromises
|
||||
|
||||
export async function createDirIfNotExists(dir: string, config: ScaffoldConfig): Promise<void> {
|
||||
const parentDir = path.dirname(dir)
|
||||
|
||||
if (!(await pathExists(parentDir))) {
|
||||
await createDirIfNotExists(parentDir, config)
|
||||
}
|
||||
|
||||
if (!(await pathExists(dir))) {
|
||||
try {
|
||||
log(config, LogLevel.Debug, `Creating dir ${dir}`)
|
||||
await mkdir(dir)
|
||||
return
|
||||
} catch (e: any) {
|
||||
if (e.code !== "EEXIST") {
|
||||
throw e
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function pathExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await access(filePath, F_OK)
|
||||
return true
|
||||
} catch (e: any) {
|
||||
if (e.code === "ENOENT") {
|
||||
return false
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export async function isDir(path: string): Promise<boolean> {
|
||||
const tplStat = await stat(path)
|
||||
return tplStat.isDirectory()
|
||||
}
|
||||
|
||||
export function removeGlob(template: string): string {
|
||||
return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep)
|
||||
}
|
||||
|
||||
export function makeRelativePath(str: string): string {
|
||||
return str.startsWith(path.sep) ? str.slice(1) : str
|
||||
}
|
||||
|
||||
export function getBasePath(relPath: string): string {
|
||||
return path
|
||||
.resolve(process.cwd(), relPath)
|
||||
.replace(process.cwd() + path.sep, "")
|
||||
.replace(process.cwd(), "")
|
||||
}
|
||||
|
||||
export async function getFileList(_config: ScaffoldConfig, template: string): Promise<string[]> {
|
||||
return (
|
||||
await glob(template, {
|
||||
dot: true,
|
||||
nodir: true,
|
||||
// debug: config.verbose === LogLevel.Debug,
|
||||
})
|
||||
).map(path.normalize)
|
||||
}
|
||||
|
||||
export interface GlobInfo {
|
||||
nonGlobTemplate: string
|
||||
origTemplate: string
|
||||
isDirOrGlob: boolean
|
||||
isGlob: boolean
|
||||
template: string
|
||||
}
|
||||
|
||||
export async function getTemplateGlobInfo(config: ScaffoldConfig, template: string): Promise<GlobInfo> {
|
||||
const isGlob = hasMagic(template)
|
||||
log(config, LogLevel.Debug, "before isDir", "isGlob:", isGlob, template)
|
||||
let _template = template
|
||||
let nonGlobTemplate = isGlob ? removeGlob(template) : template
|
||||
nonGlobTemplate = path.normalize(nonGlobTemplate)
|
||||
const isDirOrGlob = isGlob ? true : await isDir(template)
|
||||
log(config, LogLevel.Debug, "after isDir", isDirOrGlob)
|
||||
const _shouldAddGlob = !isGlob && isDirOrGlob
|
||||
const origTemplate = template
|
||||
if (_shouldAddGlob) {
|
||||
_template = path.join(template, "**", "*")
|
||||
}
|
||||
return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template }
|
||||
}
|
||||
|
||||
export interface OutputFileInfo {
|
||||
inputPath: string
|
||||
outputPathOpt: string
|
||||
outputDir: string
|
||||
outputPath: string
|
||||
exists: boolean
|
||||
}
|
||||
|
||||
export async function getTemplateFileInfo(
|
||||
config: ScaffoldConfig,
|
||||
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
||||
): Promise<OutputFileInfo> {
|
||||
const inputPath = path.resolve(process.cwd(), templatePath)
|
||||
const outputPathOpt = getOptionValueForFile(config, inputPath, config.output)
|
||||
const outputDir = getOutputDir(config, outputPathOpt, basePath)
|
||||
const outputPath = handlebarsParse(config, path.join(outputDir, path.basename(inputPath)), {
|
||||
isPath: true,
|
||||
}).toString()
|
||||
const exists = await pathExists(outputPath)
|
||||
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
|
||||
}
|
||||
|
||||
export async function copyFileTransformed(
|
||||
config: ScaffoldConfig,
|
||||
{
|
||||
exists,
|
||||
overwrite,
|
||||
outputPath,
|
||||
inputPath,
|
||||
}: {
|
||||
exists: boolean
|
||||
overwrite: boolean
|
||||
outputPath: string
|
||||
inputPath: string
|
||||
},
|
||||
): Promise<void> {
|
||||
if (!exists || overwrite) {
|
||||
if (exists && overwrite) {
|
||||
log(config, LogLevel.Info, `File ${outputPath} exists, overwriting`)
|
||||
}
|
||||
const templateBuffer = await readFile(inputPath)
|
||||
const unprocessedOutputContents = handlebarsParse(config, templateBuffer)
|
||||
const finalOutputContents =
|
||||
(await config.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath)) ?? unprocessedOutputContents
|
||||
|
||||
if (!config.dryRun) {
|
||||
await writeFile(outputPath, finalOutputContents)
|
||||
log(config, LogLevel.Info, "Done.")
|
||||
} else {
|
||||
log(config, LogLevel.Info, "Dry Run. Output should be:")
|
||||
log(config, LogLevel.Info, finalOutputContents)
|
||||
}
|
||||
} else if (exists) {
|
||||
log(config, LogLevel.Info, `File ${outputPath} already exists, skipping`)
|
||||
}
|
||||
}
|
||||
|
||||
export function getOutputDir(config: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
|
||||
return path.resolve(
|
||||
process.cwd(),
|
||||
...([
|
||||
outputPathOpt,
|
||||
basePath,
|
||||
config.createSubFolder
|
||||
? config.subFolderNameHelper
|
||||
? handlebarsParse(config, `{{ ${config.subFolderNameHelper} name }}`).toString()
|
||||
: config.name
|
||||
: undefined,
|
||||
].filter(Boolean) as string[]),
|
||||
)
|
||||
}
|
||||
90
src/logger.ts
Normal file
90
src/logger.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { LogLevel, ScaffoldConfig } from "./types"
|
||||
import chalk from "chalk"
|
||||
|
||||
/** @internal */
|
||||
export type LogConfig = Pick<ScaffoldConfig, "quiet" | "verbose">
|
||||
|
||||
export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
|
||||
if (config.quiet || config.verbose === LogLevel.None || level < (config.verbose ?? LogLevel.Info)) {
|
||||
return
|
||||
}
|
||||
|
||||
const levelColor: Record<LogLevel, keyof typeof chalk> = {
|
||||
[LogLevel.None]: "reset",
|
||||
[LogLevel.Debug]: "blue",
|
||||
[LogLevel.Info]: "dim",
|
||||
[LogLevel.Warning]: "yellow",
|
||||
[LogLevel.Error]: "red",
|
||||
}
|
||||
|
||||
const chalkFn: any = chalk[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)
|
||||
: typeof i === "object"
|
||||
? chalkFn(JSON.stringify(i, undefined, 1))
|
||||
: chalkFn(i),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export function logInputFile(
|
||||
config: ScaffoldConfig,
|
||||
{
|
||||
origTemplate,
|
||||
relPath,
|
||||
template,
|
||||
inputFilePath,
|
||||
nonGlobTemplate,
|
||||
basePath,
|
||||
isDirOrGlob,
|
||||
isGlob,
|
||||
}: {
|
||||
origTemplate: string
|
||||
relPath: string
|
||||
template: string
|
||||
inputFilePath: string
|
||||
nonGlobTemplate: string
|
||||
basePath: string
|
||||
isDirOrGlob: boolean
|
||||
isGlob: boolean
|
||||
},
|
||||
): void {
|
||||
log(
|
||||
config,
|
||||
LogLevel.Debug,
|
||||
`\nprocess.cwd(): ${process.cwd()}`,
|
||||
`\norigTemplate: ${origTemplate}`,
|
||||
`\nrelPath: ${relPath}`,
|
||||
`\ntemplate: ${template}`,
|
||||
`\ninputFilePath: ${inputFilePath}`,
|
||||
`\nnonGlobTemplate: ${nonGlobTemplate}`,
|
||||
`\nbasePath: ${basePath}`,
|
||||
`\nisDirOrGlob: ${isDirOrGlob}`,
|
||||
`\nisGlob: ${isGlob}`,
|
||||
`\n`,
|
||||
)
|
||||
}
|
||||
|
||||
export function logInitStep(config: ScaffoldConfig): void {
|
||||
log(config, LogLevel.Debug, "Full config:", {
|
||||
name: config.name,
|
||||
templates: config.templates,
|
||||
output: config.output,
|
||||
createSubFolder: config.createSubFolder,
|
||||
data: config.data,
|
||||
overwrite: config.overwrite,
|
||||
quiet: config.quiet,
|
||||
subFolderNameHelper: config.subFolderNameHelper,
|
||||
helpers: Object.keys(config.helpers ?? {}),
|
||||
verbose: `${config.verbose} (${Object.keys(LogLevel).find(
|
||||
(k) => (LogLevel[k as any] as unknown as number) === config.verbose!,
|
||||
)})`,
|
||||
dryRun: config.dryRun,
|
||||
beforeWrite: config.beforeWrite,
|
||||
} as Record<keyof ScaffoldConfig, unknown>)
|
||||
log(config, LogLevel.Info, "Data:", config.data)
|
||||
}
|
||||
108
src/parser.ts
Normal file
108
src/parser.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import path from "path"
|
||||
import { DefaultHelpers, Helper, LogLevel, ScaffoldConfig } from "./types"
|
||||
import camelCase from "lodash/camelCase"
|
||||
import snakeCase from "lodash/snakeCase"
|
||||
import kebabCase from "lodash/kebabCase"
|
||||
import startCase from "lodash/startCase"
|
||||
import Handlebars from "handlebars"
|
||||
import dtAdd from "date-fns/add"
|
||||
import dtFormat from "date-fns/format"
|
||||
import dtParseISO from "date-fns/parseISO"
|
||||
import { log } from "./logger"
|
||||
|
||||
const dateFns = {
|
||||
add: dtAdd,
|
||||
format: dtFormat,
|
||||
parseISO: dtParseISO,
|
||||
}
|
||||
|
||||
export const defaultHelpers: Record<DefaultHelpers, Helper> = {
|
||||
camelCase,
|
||||
snakeCase,
|
||||
startCase,
|
||||
kebabCase,
|
||||
hyphenCase: kebabCase,
|
||||
pascalCase,
|
||||
lowerCase: (text) => text.toLowerCase(),
|
||||
upperCase: (text) => text.toUpperCase(),
|
||||
now: nowHelper,
|
||||
date: dateHelper,
|
||||
}
|
||||
|
||||
export function _dateHelper(date: Date, formatString: string): string
|
||||
export function _dateHelper(
|
||||
date: Date,
|
||||
formatString: string,
|
||||
durationDifference: number,
|
||||
durationType: keyof Duration,
|
||||
): string
|
||||
export function _dateHelper(
|
||||
date: Date,
|
||||
formatString: string,
|
||||
durationDifference?: number,
|
||||
durationType?: keyof Duration,
|
||||
): string {
|
||||
if (durationType && durationDifference !== undefined) {
|
||||
return dateFns.format(dateFns.add(date, { [durationType]: durationDifference }), formatString)
|
||||
}
|
||||
return dateFns.format(date, formatString)
|
||||
}
|
||||
|
||||
export function nowHelper(formatString: string): string
|
||||
export function nowHelper(formatString: string, durationDifference: number, durationType: keyof Duration): string
|
||||
export function nowHelper(formatString: string, durationDifference?: number, durationType?: keyof Duration): string {
|
||||
return _dateHelper(new Date(), formatString, durationDifference!, durationType!)
|
||||
}
|
||||
|
||||
export function dateHelper(date: string, formatString: string): string
|
||||
export function dateHelper(
|
||||
date: string,
|
||||
formatString: string,
|
||||
durationDifference: number,
|
||||
durationType: keyof Duration,
|
||||
): string
|
||||
|
||||
export function dateHelper(
|
||||
date: string,
|
||||
formatString: string,
|
||||
durationDifference?: number,
|
||||
durationType?: keyof Duration,
|
||||
): string {
|
||||
return _dateHelper(dateFns.parseISO(date), formatString, durationDifference!, durationType!)
|
||||
}
|
||||
|
||||
export function pascalCase(s: string): string {
|
||||
return startCase(s).replace(/\s+/g, "")
|
||||
}
|
||||
|
||||
export function registerHelpers(config: ScaffoldConfig): void {
|
||||
const _helpers = { ...defaultHelpers, ...config.helpers }
|
||||
for (const helperName in _helpers) {
|
||||
log(config, LogLevel.Debug, `Registering helper: ${helperName}`)
|
||||
Handlebars.registerHelper(helperName, _helpers[helperName as keyof typeof _helpers])
|
||||
}
|
||||
}
|
||||
|
||||
export function handlebarsParse(
|
||||
config: ScaffoldConfig,
|
||||
templateBuffer: Buffer | string,
|
||||
{ isPath = false }: { isPath?: boolean } = {},
|
||||
): Buffer {
|
||||
const { data } = config
|
||||
try {
|
||||
let str = templateBuffer.toString()
|
||||
if (isPath) {
|
||||
str = str.replace(/\\/g, "/")
|
||||
}
|
||||
const parser = Handlebars.compile(str, { noEscape: true })
|
||||
let outputContents = parser(data)
|
||||
if (isPath && path.sep !== "/") {
|
||||
outputContents = outputContents.replace(/\//g, "\\")
|
||||
}
|
||||
return Buffer.from(outputContents)
|
||||
} catch (e) {
|
||||
log(config, LogLevel.Debug, e)
|
||||
log(config, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
|
||||
return Buffer.from(templateBuffer)
|
||||
}
|
||||
}
|
||||
@@ -5,28 +5,23 @@
|
||||
* See [readme](README.md)
|
||||
*/
|
||||
import path from "path"
|
||||
|
||||
import { handleErr, resolve } from "./utils"
|
||||
import {
|
||||
createDirIfNotExists,
|
||||
getOptionValueForFile,
|
||||
handleErr,
|
||||
log,
|
||||
pascalCase,
|
||||
isDir,
|
||||
removeGlob,
|
||||
makeRelativePath,
|
||||
registerHelpers,
|
||||
getTemplateGlobInfo,
|
||||
getFileList,
|
||||
getBasePath,
|
||||
copyFileTransformed,
|
||||
getTemplateFileInfo,
|
||||
logInitStep,
|
||||
logInputFile,
|
||||
parseConfig,
|
||||
} from "./utils"
|
||||
import { LogLevel, ScaffoldCmdConfig, ScaffoldConfig } from "./types"
|
||||
} from "./file"
|
||||
import { LogLevel, Resolver, ScaffoldCmdConfig, ScaffoldConfig } from "./types"
|
||||
import { OptionsBase } from "massarg/types"
|
||||
import { pascalCase, registerHelpers } from "./parser"
|
||||
import { log, logInitStep, logInputFile } from "./logger"
|
||||
import { getOptionValueForFile, parseConfig } from "./config"
|
||||
|
||||
/**
|
||||
* Create a scaffold using given `options`.
|
||||
@@ -119,7 +114,7 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
|
||||
Scaffold.fromConfig = async function (
|
||||
pathOrUrl: string,
|
||||
config: Pick<ScaffoldCmdConfig, "name" | "key">,
|
||||
overrides?: Partial<Omit<ScaffoldConfig, "name">>,
|
||||
overrides?: Resolver<ScaffoldCmdConfig, Partial<Omit<ScaffoldConfig, "name">>>,
|
||||
): Promise<void> {
|
||||
const _cmdConfig: ScaffoldCmdConfig & OptionsBase = {
|
||||
dryRun: false,
|
||||
@@ -134,8 +129,9 @@ Scaffold.fromConfig = async function (
|
||||
config: pathOrUrl,
|
||||
...config,
|
||||
}
|
||||
const _overrides = resolve(overrides, _cmdConfig)
|
||||
const _config = await parseConfig(_cmdConfig)
|
||||
return Scaffold({ ..._config, ...overrides })
|
||||
return Scaffold({ ..._config, ..._overrides })
|
||||
}
|
||||
|
||||
async function handleTemplateFile(
|
||||
|
||||
16
src/types.ts
Executable file → Normal file
16
src/types.ts
Executable file → Normal file
@@ -356,4 +356,18 @@ export interface ScaffoldCmdConfig {
|
||||
*
|
||||
* @see {@link ScaffoldConfig}
|
||||
*/
|
||||
export type ScaffoldConfigFile = Record<string, ScaffoldConfig>
|
||||
export type ScaffoldConfigMap = Record<string, ScaffoldConfig>
|
||||
|
||||
/** The scaffold config file is either:
|
||||
* - A {@link ScaffoldConfigMap} object
|
||||
* - A function that returns a {@link ScaffoldConfigMap} object
|
||||
* - A promise that resolves to a {@link ScaffoldConfigMap} object
|
||||
* - A function that returns a promise that resolves to a {@link ScaffoldConfigMap} object
|
||||
*/
|
||||
export type ScaffoldConfigFile = AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>
|
||||
|
||||
/** @internal */
|
||||
export type Resolver<T, R = T> = R | ((value: T) => R)
|
||||
|
||||
/** @internal */
|
||||
export type AsyncResolver<T, R = T> = Resolver<T, Promise<R> | R>
|
||||
|
||||
505
src/utils.ts
505
src/utils.ts
@@ -1,508 +1,9 @@
|
||||
import path from "path"
|
||||
import { F_OK } from "constants"
|
||||
import {
|
||||
DefaultHelpers,
|
||||
FileResponse,
|
||||
FileResponseHandler,
|
||||
Helper,
|
||||
LogLevel,
|
||||
ScaffoldCmdConfig,
|
||||
ScaffoldConfig,
|
||||
ScaffoldConfigFile,
|
||||
} from "./types"
|
||||
import camelCase from "lodash/camelCase"
|
||||
import snakeCase from "lodash/snakeCase"
|
||||
import kebabCase from "lodash/kebabCase"
|
||||
import startCase from "lodash/startCase"
|
||||
import Handlebars from "handlebars"
|
||||
import { promises as fsPromises } from "fs"
|
||||
import chalk from "chalk"
|
||||
const { stat, access, mkdir } = fsPromises
|
||||
import dtAdd from "date-fns/add"
|
||||
import dtFormat from "date-fns/format"
|
||||
import dtParseISO from "date-fns/parseISO"
|
||||
import { glob, hasMagic } from "glob"
|
||||
import { OptionsBase } from "massarg/types"
|
||||
import { spawn } from "node:child_process"
|
||||
import os from "node:os"
|
||||
|
||||
const dateFns = {
|
||||
add: dtAdd,
|
||||
format: dtFormat,
|
||||
parseISO: dtParseISO,
|
||||
}
|
||||
|
||||
const { readFile, writeFile } = fsPromises
|
||||
|
||||
export const defaultHelpers: Record<DefaultHelpers, Helper> = {
|
||||
camelCase,
|
||||
snakeCase,
|
||||
startCase,
|
||||
kebabCase,
|
||||
hyphenCase: kebabCase,
|
||||
pascalCase,
|
||||
lowerCase: (text) => text.toLowerCase(),
|
||||
upperCase: (text) => text.toUpperCase(),
|
||||
now: nowHelper,
|
||||
date: dateHelper,
|
||||
}
|
||||
|
||||
export function _dateHelper(date: Date, formatString: string): string
|
||||
export function _dateHelper(
|
||||
date: Date,
|
||||
formatString: string,
|
||||
durationDifference: number,
|
||||
durationType: keyof Duration,
|
||||
): string
|
||||
export function _dateHelper(
|
||||
date: Date,
|
||||
formatString: string,
|
||||
durationDifference?: number,
|
||||
durationType?: keyof Duration,
|
||||
): string {
|
||||
if (durationType && durationDifference !== undefined) {
|
||||
return dateFns.format(dateFns.add(date, { [durationType]: durationDifference }), formatString)
|
||||
}
|
||||
return dateFns.format(date, formatString)
|
||||
}
|
||||
|
||||
export function nowHelper(formatString: string): string
|
||||
export function nowHelper(formatString: string, durationDifference: number, durationType: keyof Duration): string
|
||||
export function nowHelper(formatString: string, durationDifference?: number, durationType?: keyof Duration): string {
|
||||
return _dateHelper(new Date(), formatString, durationDifference!, durationType!)
|
||||
}
|
||||
|
||||
export function dateHelper(date: string, formatString: string): string
|
||||
export function dateHelper(
|
||||
date: string,
|
||||
formatString: string,
|
||||
durationDifference: number,
|
||||
durationType: keyof Duration,
|
||||
): string
|
||||
|
||||
export function dateHelper(
|
||||
date: string,
|
||||
formatString: string,
|
||||
durationDifference?: number,
|
||||
durationType?: keyof Duration,
|
||||
): string {
|
||||
return _dateHelper(dateFns.parseISO(date), formatString, durationDifference!, durationType!)
|
||||
}
|
||||
|
||||
export function registerHelpers(config: ScaffoldConfig): void {
|
||||
const _helpers = { ...defaultHelpers, ...config.helpers }
|
||||
for (const helperName in _helpers) {
|
||||
log(config, LogLevel.Debug, `Registering helper: ${helperName}`)
|
||||
Handlebars.registerHelper(helperName, _helpers[helperName as keyof typeof _helpers])
|
||||
}
|
||||
}
|
||||
import { Resolver } from "./types"
|
||||
|
||||
export function handleErr(err: NodeJS.ErrnoException | null): void {
|
||||
if (err) throw err
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export type LogConfig = Pick<ScaffoldConfig, "quiet" | "verbose">
|
||||
|
||||
export function log(config: LogConfig, level: LogLevel, ...obj: any[]): void {
|
||||
if (config.quiet || config.verbose === LogLevel.None || level < (config.verbose ?? LogLevel.Info)) {
|
||||
return
|
||||
}
|
||||
|
||||
const levelColor: Record<LogLevel, keyof typeof chalk> = {
|
||||
[LogLevel.None]: "reset",
|
||||
[LogLevel.Debug]: "blue",
|
||||
[LogLevel.Info]: "dim",
|
||||
[LogLevel.Warning]: "yellow",
|
||||
[LogLevel.Error]: "red",
|
||||
}
|
||||
|
||||
const chalkFn: any = chalk[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)
|
||||
: typeof i === "object"
|
||||
? chalkFn(JSON.stringify(i, undefined, 1))
|
||||
: chalkFn(i),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export async function createDirIfNotExists(dir: string, config: ScaffoldConfig): Promise<void> {
|
||||
const parentDir = path.dirname(dir)
|
||||
|
||||
if (!(await pathExists(parentDir))) {
|
||||
await createDirIfNotExists(parentDir, config)
|
||||
}
|
||||
|
||||
if (!(await pathExists(dir))) {
|
||||
try {
|
||||
log(config, LogLevel.Debug, `Creating dir ${dir}`)
|
||||
await mkdir(dir)
|
||||
return
|
||||
} catch (e: any) {
|
||||
if (e.code !== "EEXIST") {
|
||||
throw e
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getOptionValueForFile<T>(
|
||||
config: ScaffoldConfig,
|
||||
filePath: string,
|
||||
fn: FileResponse<T>,
|
||||
defaultValue?: T,
|
||||
): T {
|
||||
if (typeof fn !== "function") {
|
||||
return defaultValue ?? (fn as T)
|
||||
}
|
||||
return (fn as FileResponseHandler<T>)(
|
||||
filePath,
|
||||
path.dirname(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||
path.basename(handlebarsParse(config, filePath, { isPath: true }).toString()),
|
||||
)
|
||||
}
|
||||
|
||||
export function handlebarsParse(
|
||||
config: ScaffoldConfig,
|
||||
templateBuffer: Buffer | string,
|
||||
{ isPath = false }: { isPath?: boolean } = {},
|
||||
): Buffer {
|
||||
const { data } = config
|
||||
try {
|
||||
let str = templateBuffer.toString()
|
||||
if (isPath) {
|
||||
str = str.replace(/\\/g, "/")
|
||||
}
|
||||
const parser = Handlebars.compile(str, { noEscape: true })
|
||||
let outputContents = parser(data)
|
||||
if (isPath && path.sep !== "/") {
|
||||
outputContents = outputContents.replace(/\//g, "\\")
|
||||
}
|
||||
return Buffer.from(outputContents)
|
||||
} catch (e) {
|
||||
log(config, LogLevel.Debug, e)
|
||||
log(config, LogLevel.Warning, "Couldn't parse file with handlebars, returning original content")
|
||||
return Buffer.from(templateBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
export async function pathExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await access(filePath, F_OK)
|
||||
return true
|
||||
} catch (e: any) {
|
||||
if (e.code === "ENOENT") {
|
||||
return false
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function pascalCase(s: string): string {
|
||||
return startCase(s).replace(/\s+/g, "")
|
||||
}
|
||||
|
||||
export async function isDir(path: string): Promise<boolean> {
|
||||
const tplStat = await stat(path)
|
||||
return tplStat.isDirectory()
|
||||
}
|
||||
|
||||
export function removeGlob(template: string): string {
|
||||
return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep)
|
||||
}
|
||||
|
||||
export function makeRelativePath(str: string): string {
|
||||
return str.startsWith(path.sep) ? str.slice(1) : str
|
||||
}
|
||||
|
||||
export function getBasePath(relPath: string): string {
|
||||
return path
|
||||
.resolve(process.cwd(), relPath)
|
||||
.replace(process.cwd() + path.sep, "")
|
||||
.replace(process.cwd(), "")
|
||||
}
|
||||
|
||||
export async function getFileList(_config: ScaffoldConfig, template: string): Promise<string[]> {
|
||||
return (
|
||||
await glob(template, {
|
||||
dot: true,
|
||||
nodir: true,
|
||||
// debug: config.verbose === LogLevel.Debug,
|
||||
})
|
||||
).map(path.normalize)
|
||||
}
|
||||
|
||||
export interface GlobInfo {
|
||||
nonGlobTemplate: string
|
||||
origTemplate: string
|
||||
isDirOrGlob: boolean
|
||||
isGlob: boolean
|
||||
template: string
|
||||
}
|
||||
|
||||
export async function getTemplateGlobInfo(config: ScaffoldConfig, template: string): Promise<GlobInfo> {
|
||||
const isGlob = hasMagic(template)
|
||||
log(config, LogLevel.Debug, "before isDir", "isGlob:", isGlob, template)
|
||||
let _template = template
|
||||
let nonGlobTemplate = isGlob ? removeGlob(template) : template
|
||||
nonGlobTemplate = path.normalize(nonGlobTemplate)
|
||||
const isDirOrGlob = isGlob ? true : await isDir(template)
|
||||
log(config, LogLevel.Debug, "after isDir", isDirOrGlob)
|
||||
const _shouldAddGlob = !isGlob && isDirOrGlob
|
||||
const origTemplate = template
|
||||
if (_shouldAddGlob) {
|
||||
_template = path.join(template, "**", "*")
|
||||
}
|
||||
return { nonGlobTemplate, origTemplate, isDirOrGlob, isGlob, template: _template }
|
||||
}
|
||||
|
||||
export interface OutputFileInfo {
|
||||
inputPath: string
|
||||
outputPathOpt: string
|
||||
outputDir: string
|
||||
outputPath: string
|
||||
exists: boolean
|
||||
}
|
||||
|
||||
export async function getTemplateFileInfo(
|
||||
config: ScaffoldConfig,
|
||||
{ templatePath, basePath }: { templatePath: string; basePath: string },
|
||||
): Promise<OutputFileInfo> {
|
||||
const inputPath = path.resolve(process.cwd(), templatePath)
|
||||
const outputPathOpt = getOptionValueForFile(config, inputPath, config.output)
|
||||
const outputDir = getOutputDir(config, outputPathOpt, basePath)
|
||||
const outputPath = handlebarsParse(config, path.join(outputDir, path.basename(inputPath)), {
|
||||
isPath: true,
|
||||
}).toString()
|
||||
const exists = await pathExists(outputPath)
|
||||
return { inputPath, outputPathOpt, outputDir, outputPath, exists }
|
||||
}
|
||||
|
||||
export async function copyFileTransformed(
|
||||
config: ScaffoldConfig,
|
||||
{
|
||||
exists,
|
||||
overwrite,
|
||||
outputPath,
|
||||
inputPath,
|
||||
}: {
|
||||
exists: boolean
|
||||
overwrite: boolean
|
||||
outputPath: string
|
||||
inputPath: string
|
||||
},
|
||||
): Promise<void> {
|
||||
if (!exists || overwrite) {
|
||||
if (exists && overwrite) {
|
||||
log(config, LogLevel.Info, `File ${outputPath} exists, overwriting`)
|
||||
}
|
||||
const templateBuffer = await readFile(inputPath)
|
||||
const unprocessedOutputContents = handlebarsParse(config, templateBuffer)
|
||||
const finalOutputContents =
|
||||
(await config.beforeWrite?.(unprocessedOutputContents, templateBuffer, outputPath)) ?? unprocessedOutputContents
|
||||
|
||||
if (!config.dryRun) {
|
||||
await writeFile(outputPath, finalOutputContents)
|
||||
log(config, LogLevel.Info, "Done.")
|
||||
} else {
|
||||
log(config, LogLevel.Info, "Content output:")
|
||||
log(config, LogLevel.Info, finalOutputContents)
|
||||
}
|
||||
} else if (exists) {
|
||||
log(config, LogLevel.Info, `File ${outputPath} already exists, skipping`)
|
||||
}
|
||||
}
|
||||
|
||||
export function getOutputDir(config: ScaffoldConfig, outputPathOpt: string, basePath: string): string {
|
||||
return path.resolve(
|
||||
process.cwd(),
|
||||
...([
|
||||
outputPathOpt,
|
||||
basePath,
|
||||
config.createSubFolder
|
||||
? config.subFolderNameHelper
|
||||
? handlebarsParse(config, `{{ ${config.subFolderNameHelper} name }}`).toString()
|
||||
: config.name
|
||||
: undefined,
|
||||
].filter(Boolean) as string[]),
|
||||
)
|
||||
}
|
||||
|
||||
export function logInputFile(
|
||||
config: ScaffoldConfig,
|
||||
{
|
||||
origTemplate,
|
||||
relPath,
|
||||
template,
|
||||
inputFilePath,
|
||||
nonGlobTemplate,
|
||||
basePath,
|
||||
isDirOrGlob,
|
||||
isGlob,
|
||||
}: {
|
||||
origTemplate: string
|
||||
relPath: string
|
||||
template: string
|
||||
inputFilePath: string
|
||||
nonGlobTemplate: string
|
||||
basePath: string
|
||||
isDirOrGlob: boolean
|
||||
isGlob: boolean
|
||||
},
|
||||
): void {
|
||||
log(
|
||||
config,
|
||||
LogLevel.Debug,
|
||||
`\nprocess.cwd(): ${process.cwd()}`,
|
||||
`\norigTemplate: ${origTemplate}`,
|
||||
`\nrelPath: ${relPath}`,
|
||||
`\ntemplate: ${template}`,
|
||||
`\ninputFilePath: ${inputFilePath}`,
|
||||
`\nnonGlobTemplate: ${nonGlobTemplate}`,
|
||||
`\nbasePath: ${basePath}`,
|
||||
`\nisDirOrGlob: ${isDirOrGlob}`,
|
||||
`\nisGlob: ${isGlob}`,
|
||||
`\n`,
|
||||
)
|
||||
}
|
||||
|
||||
export function logInitStep(config: ScaffoldConfig): void {
|
||||
log(config, LogLevel.Debug, "Full config:", {
|
||||
name: config.name,
|
||||
templates: config.templates,
|
||||
output: config.output,
|
||||
createSubFolder: config.createSubFolder,
|
||||
data: config.data,
|
||||
overwrite: config.overwrite,
|
||||
quiet: config.quiet,
|
||||
subFolderNameHelper: config.subFolderNameHelper,
|
||||
helpers: Object.keys(config.helpers ?? {}),
|
||||
verbose: `${config.verbose} (${Object.keys(LogLevel).find(
|
||||
(k) => (LogLevel[k as any] as unknown as number) === config.verbose!,
|
||||
)})`,
|
||||
dryRun: config.dryRun,
|
||||
beforeWrite: config.beforeWrite,
|
||||
} as Record<keyof ScaffoldConfig, unknown>)
|
||||
log(config, LogLevel.Info, "Data:", config.data)
|
||||
}
|
||||
|
||||
export function parseAppendData(value: string, options: ScaffoldCmdConfig & OptionsBase): unknown {
|
||||
const data = options.data ?? {}
|
||||
const [key, val] = value.split(/\:?=/)
|
||||
// raw
|
||||
if (value.includes(":=") && !val.includes(":=")) {
|
||||
return { ...data, [key]: JSON.parse(val) }
|
||||
}
|
||||
return { ...data, [key]: isWrappedWithQuotes(val) ? val.substring(1, val.length - 1) : val }
|
||||
}
|
||||
|
||||
function isWrappedWithQuotes(string: string): boolean {
|
||||
return (string.startsWith('"') && string.endsWith('"')) || (string.startsWith("'") && string.endsWith("'"))
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function parseConfig(config: ScaffoldCmdConfig & OptionsBase): Promise<ScaffoldConfig> {
|
||||
let c: ScaffoldConfig = config
|
||||
if (config.github) {
|
||||
log(config, LogLevel.Info, `Loading config from github ${config.github}`)
|
||||
const gitUrl = new URL(`https://github.com/${config.github}`)
|
||||
if (!gitUrl.pathname.endsWith(".git")) {
|
||||
gitUrl.pathname += ".git"
|
||||
}
|
||||
config.config = gitUrl.toString()
|
||||
}
|
||||
|
||||
if (config.config) {
|
||||
const isUrl = config.config.includes("://")
|
||||
|
||||
const hasColonToken = (!isUrl && config.config.includes(":")) || (isUrl && count(config.config, ":") > 1)
|
||||
const colonIndex = config.config.lastIndexOf(":")
|
||||
const [configFile, templateKey = "default"] = hasColonToken
|
||||
? [config.config.substring(0, colonIndex), config.config.substring(colonIndex + 1)]
|
||||
: [config.config, undefined]
|
||||
const key = (config.key ?? templateKey) || "default"
|
||||
log(config, LogLevel.Info, `Loading config from ${configFile} with key ${key}`)
|
||||
const configImport = await getConfig({ config: configFile, quiet: config.quiet, verbose: config.verbose })
|
||||
if (!configImport[key]) {
|
||||
throw new Error(`Template "${key}" not found in ${configFile}`)
|
||||
}
|
||||
c = {
|
||||
...config,
|
||||
...configImport[key],
|
||||
data: {
|
||||
...configImport[key].data,
|
||||
...config.data,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
c.data = { ...c.data, ...config.appendData }
|
||||
delete config.appendData
|
||||
return c
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export async function getConfig(
|
||||
config: Pick<ScaffoldCmdConfig, "quiet" | "verbose" | "config">,
|
||||
): Promise<ScaffoldConfigFile> {
|
||||
const { config: configFile, ...logConfig } = config as Required<typeof config>
|
||||
const url = new URL(configFile)
|
||||
|
||||
if (url.protocol === "file:") {
|
||||
log(logConfig, LogLevel.Info, `Loading config from file ${configFile}`)
|
||||
const absolutePath = path.resolve(process.cwd(), configFile)
|
||||
return import(absolutePath)
|
||||
}
|
||||
|
||||
const isHttp = url.protocol === "http:" || url.protocol === "https:"
|
||||
const isGit = url.protocol === "git:" || (isHttp && url.pathname.endsWith(".git"))
|
||||
|
||||
if (isHttp || isGit) {
|
||||
if (isGit) {
|
||||
const repoUrl = `${url.protocol}//${url.host}${url.pathname}`
|
||||
log(logConfig, LogLevel.Info, `Cloning git repo ${repoUrl}`)
|
||||
const tmpPath = path.resolve(os.tmpdir(), `scaffold-config-${Date.now()}`)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const clone = spawn("git", ["clone", "--depth", "1", repoUrl, tmpPath])
|
||||
|
||||
clone.on("error", reject)
|
||||
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)
|
||||
const loadedConfig = (await import(absolutePath)).default as ScaffoldConfigFile
|
||||
log(logConfig, LogLevel.Info, `Loaded config from git`)
|
||||
log(logConfig, LogLevel.Debug, `Raw config:`, loadedConfig)
|
||||
const fixedConfig: ScaffoldConfigFile = Object.fromEntries(
|
||||
Object.entries(loadedConfig).map(([k, v]) => [
|
||||
k,
|
||||
// use absolute paths for template as config is necessarily in another directory
|
||||
{ ...v, templates: v.templates.map((t) => path.resolve(tmpPath, t)) },
|
||||
]),
|
||||
)
|
||||
|
||||
resolve(fixedConfig)
|
||||
} else {
|
||||
reject(new Error(`Git clone failed with code ${code}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported protocol ${url.protocol}`)
|
||||
}
|
||||
|
||||
return import(path.resolve(process.cwd(), configFile))
|
||||
}
|
||||
|
||||
function count(string: string, substring: string): number {
|
||||
return string.split(substring).length - 1
|
||||
export function resolve<T, R = T>(resolver: Resolver<T, R>, arg: T): R {
|
||||
return typeof resolver === "function" ? (resolver as (value: T) => R)(arg) : (resolver as R)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2019",
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ES2019"],
|
||||
"lib": ["ES2022"],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
|
||||
Reference in New Issue
Block a user