Compare commits

...

12 Commits

Author SHA1 Message Date
semantic-release-bot
d62eeeb8e4 chore(release): 1.7.2 [skip ci]
## [1.7.2](https://github.com/chenasraf/simple-scaffold/compare/v1.7.1...v1.7.2) (2023-08-20)

### Bug Fixes

* windows path resolution ([98ee000](98ee00031f))
2023-08-20 10:09:48 +00:00
Chen Asraf
565d1f33aa Merge pull request #61 from chenasraf/pre
v1.7.2
2023-08-20 13:08:25 +03:00
semantic-release-bot
9f58fff2cf chore(release): 1.7.2-pre.1 [skip ci]
## [1.7.2-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.1...v1.7.2-pre.1) (2023-08-15)

### Bug Fixes

* windows path resolution ([98ee000](98ee00031f))
2023-08-15 19:34:02 +00:00
dbba81d053 ci: trigger on pre branch 2023-08-15 22:32:25 +03:00
9f5716e1b9 chore: bump version number 2023-08-15 22:31:38 +03:00
Chen Asraf
4cbb79bb3b Merge pull request #60 from chenasraf/fix/windows
[BUGFIX] Windows path resolution
2023-08-15 22:28:04 +03:00
98ee00031f fix: windows path resolution 2023-08-14 00:19:50 +03:00
8c3369a00d chore: formatting 2023-08-14 00:19:36 +03:00
20ef0cea9b chore: update logs 2023-08-14 00:08:16 +03:00
Chen Asraf
3413151358 test: add tests
chore: use node:* modules for builtins
2023-06-12 23:26:25 +03:00
Chen Asraf
d2a2fda1b1 chore: update dependencies 2023-06-12 23:18:50 +03:00
Chen Asraf
486c07a55b Merge branch 'master' into develop 2023-06-08 08:13:52 +03:00
18 changed files with 2181 additions and 1543 deletions

View File

@@ -2,7 +2,7 @@ name: Test & Build
on:
push:
branches: [ master, develop, beta, feat/*, fix/* ]
branches: [ master, develop, pre, feat/*, fix/* ]
jobs:
test:
runs-on: ubuntu-latest

View File

@@ -14,9 +14,6 @@
"variabletoken"
],
"[markdown]": {
"editor.rulers": [
87,
100
],
},
"editor.rulers": [87, 100]
}
}

6
.vscode/tasks.json vendored
View File

@@ -14,7 +14,7 @@
"problemMatcher": []
},
{
"command": "yarn typedoc --watch",
"command": "pnpm typedoc --watch",
"label": "typedoc --watch",
"type": "shell",
"problemMatcher": []
@@ -32,8 +32,8 @@
"problemMatcher": []
},
{
"command": "yarn test --watchAll",
"label": "yarn test --watchAll",
"command": "pnpm test --watchAll",
"label": "pnpm test --watchAll",
"type": "shell",
"problemMatcher": []
},

View File

@@ -1,11 +1,25 @@
# Change Log
## [1.7.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0...v1.7.1) (2023-06-07)
## [1.7.2](https://github.com/chenasraf/simple-scaffold/compare/v1.7.1...v1.7.2) (2023-08-20)
### Bug Fixes
* local config file load error ([2b74239](https://github.com/chenasraf/simple-scaffold/commit/2b7423993be06b2375631642455c801ae2acf75f))
* windows path resolution ([98ee000](https://github.com/chenasraf/simple-scaffold/commit/98ee00031fc1ad67a53797a9e28e5c4759bc8bce))
## [1.7.2-pre.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.1...v1.7.2-pre.1) (2023-08-15)
### Bug Fixes
* windows path resolution ([98ee000](https://github.com/chenasraf/simple-scaffold/commit/98ee00031fc1ad67a53797a9e28e5c4759bc8bce))
## [1.7.1](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0...v1.7.1) (2023-06-07)
### Bug Fixes
- local config file load error
([2b74239](https://github.com/chenasraf/simple-scaffold/commit/2b7423993be06b2375631642455c801ae2acf75f))
## [1.7.0](https://github.com/chenasraf/simple-scaffold/compare/v1.7.0-develop.5...v1.7.0) (2023-05-17)

5
examples/.dotdir/README.md Executable file
View File

@@ -0,0 +1,5 @@
# {{ name }} Readme
TO DO:
- [ ] ...

View File

@@ -1,13 +1,6 @@
{
"ignore": [
"**/*.test.ts",
"**/*.spec.ts",
".git",
"node_modules"
],
"watch": [
"src"
],
"ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
"watch": ["src"],
"exec": "node -r tsconfig-paths/register -r ts-node/register ./src/index.ts",
"ext": "ts, js"
}

View File

@@ -1,6 +1,6 @@
{
"name": "simple-scaffold",
"version": "1.7.1",
"version": "1.7.2",
"description": "Generate any file structure - from single components to entire app boilerplates, with a single command.",
"homepage": "https://chenasraf.github.io/simple-scaffold",
"repository": "https://github.com/chenasraf/simple-scaffold.git",
@@ -8,7 +8,7 @@
"license": "MIT",
"main": "index.js",
"bin": "cmd.js",
"packageManager": "pnpm@8.5.0",
"packageManager": "pnpm@8.6.2",
"keywords": [
"javascript",
"cli",
@@ -37,7 +37,7 @@
"dependencies": {
"chalk": "^4.1.2",
"date-fns": "^2.30.0",
"glob": "^10.2.3",
"glob": "^10.3.3",
"handlebars": "^4.7.7",
"massarg": "^1.0.7-pre.1"
},
@@ -60,7 +60,7 @@
"semantic-release-conventional-commits": "^3.0.0",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"typedoc": "^0.24.6",
"typedoc": "^0.24.7",
"typescript": "^5.0.4"
}
}

3428
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@ import massarg from "massarg"
import chalk from "chalk"
import { LogLevel, ScaffoldCmdConfig } from "./types"
import { Scaffold } from "./scaffold"
import path from "path"
import fs from "fs/promises"
import path from "node:path"
import fs from "node:fs/promises"
import { parseAppendData, parseConfig } from "./config"
export async function parseCliArgs(args = process.argv.slice(2)) {

View File

@@ -1,23 +1,18 @@
import path from "path"
import path from "node:path"
import {
AsyncResolver,
ConfigLoadConfig,
FileResponse,
FileResponseHandler,
LogConfig,
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"
import { resolve, wrapNoopResolver } from "./utils"
import { getGitConfig } from "./git"
export function getOptionValueForFile<T>(
config: ScaffoldConfig,
@@ -109,14 +104,6 @@ export function githubPartToUrl(part: string): string {
return gitUrl.toString()
}
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: ConfigLoadConfig): Promise<ScaffoldConfigFile> {
const { config: configFile, isRemote, ...logConfig } = config as Required<typeof config>
@@ -132,7 +119,6 @@ export async function getConfig(config: ConfigLoadConfig): Promise<ScaffoldConfi
const isGit = url.protocol === "git:" || (isHttp && url.pathname.endsWith(".git"))
if (isGit) {
console.log("Calling getGitConfig", getGitConfig)
return getGitConfig(url, logConfig)
}
@@ -143,46 +129,6 @@ export async function getConfig(config: ConfigLoadConfig): Promise<ScaffoldConfi
return wrapNoopResolver(import(path.resolve(process.cwd(), configFile)))
}
export async function getGitConfig(
url: URL,
logConfig: LogConfig,
): Promise<AsyncResolver<ScaffoldCmdConfig, ScaffoldConfigMap>> {
console.log("Calling real getGitConfig")
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))
return
}
reject(new Error(`Git clone failed with code ${code}`))
})
})
}
function count(string: string, substring: string): number {
return string.split(substring).length - 1
}

View File

@@ -1,17 +1,20 @@
import path from "path"
import { F_OK } from "constants"
import path from "node:path"
import { F_OK } from "node:constants"
import { LogLevel, ScaffoldConfig } from "./types"
import { promises as fsPromises } from "fs"
const { stat, access, mkdir } = fsPromises
import fs from "node:fs/promises"
import { glob, hasMagic } from "glob"
import { log } from "./logger"
import { getOptionValueForFile } from "./config"
import { handlebarsParse } from "./parser"
import { handleErr } from "./utils"
const { readFile, writeFile } = fsPromises
const { stat, access, mkdir, readFile, writeFile } = fs
export async function createDirIfNotExists(dir: string, config: ScaffoldConfig): Promise<void> {
if (config.dryRun) {
log(config, LogLevel.Info, `Dry Run. Not creating dir ${dir}`)
return
}
const parentDir = path.dirname(dir)
if (!(await pathExists(parentDir))) {
@@ -50,7 +53,7 @@ export async function isDir(path: string): Promise<boolean> {
}
export function removeGlob(template: string): string {
return template.replace(/\*/g, "").replace(/(\/\/|\\\\)/g, path.sep)
return path.normalize(template.replace(/\*/g, ""))
}
export function makeRelativePath(str: string): string {
@@ -65,6 +68,8 @@ export function getBasePath(relPath: string): string {
}
export async function getFileList(_config: ScaffoldConfig, template: string): Promise<string[]> {
template = template.replaceAll(/[\\]+/g, "/")
log(_config, LogLevel.Debug, `Getting file list for ${template}`)
return (
await glob(template, {
dot: true,
@@ -89,8 +94,8 @@ export async function getTemplateGlobInfo(config: ScaffoldConfig, template: stri
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
log(config, LogLevel.Debug, "after", { isDirOrGlob, _shouldAddGlob })
const origTemplate = template
if (_shouldAddGlob) {
_template = path.join(template, "**", "*")
@@ -148,7 +153,7 @@ export async function copyFileTransformed(
log(config, LogLevel.Info, "Done.")
} else {
log(config, LogLevel.Info, "Dry Run. Output should be:")
log(config, LogLevel.Info, finalOutputContents)
log(config, LogLevel.Info, finalOutputContents.toString())
}
} else if (exists) {
log(config, LogLevel.Info, `File ${outputPath} already exists, skipping`)

45
src/git.ts Normal file
View File

@@ -0,0 +1,45 @@
import path from "node:path"
import os from "node:os"
import { log } from "./logger"
import { AsyncResolver, LogConfig, LogLevel, ScaffoldCmdConfig, ScaffoldConfigMap } from "./types"
import { spawn } from "node:child_process"
import { wrapNoopResolver } from "./utils"
export async function getGitConfig(
url: URL,
logConfig: LogConfig,
): 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))
return
}
reject(new Error(`Git clone failed with code ${code}`))
})
})
}

View File

@@ -1,4 +1,4 @@
import path from "path"
import path from "node:path"
import { DefaultHelpers, Helper, LogLevel, ScaffoldConfig } from "./types"
import Handlebars from "handlebars"
import dtAdd from "date-fns/add"

View File

@@ -4,7 +4,7 @@
*
* See [readme](README.md)
*/
import path from "path"
import path from "node:path"
import { handleErr, resolve } from "./utils"
import {
isDir,
@@ -67,6 +67,7 @@ export async function Scaffold(config: ScaffoldConfig): Promise<void> {
_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
@@ -109,7 +110,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 */

View File

@@ -7,3 +7,11 @@ export function handleErr(err: NodeJS.ErrnoException | null): void {
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)
}
export function wrapNoopResolver<T, R = T>(value: Resolver<T, R>): Resolver<T, R> {
if (typeof value === "function") {
return value
}
return (_) => value
}

View File

@@ -2,8 +2,20 @@ import { ScaffoldCmdConfig } from "../src/types"
import { OptionsBase } from "massarg/types"
import * as config from "../src/config"
import { resolve } from "../src/utils"
// @ts-ignore
import * as configFile from "../scaffold.config"
const { getConfig, githubPartToUrl, parseAppendData, parseConfig, parseConfigSelection } = config
jest.mock("../src/git", () => {
return {
__esModule: true,
...jest.requireActual("../src/git"),
getGitConfig: () => {
return Promise.resolve({ default: blankCliConf })
},
}
})
const { githubPartToUrl, parseAppendData, parseConfig, parseConfigSelection } = config
const blankCliConf: ScaffoldCmdConfig & OptionsBase = {
verbose: 0,
@@ -19,14 +31,6 @@ const blankCliConf: ScaffoldCmdConfig & OptionsBase = {
help: false,
}
jest.mock("../src/config", () => {
console.log("mocking config")
return {
...jest.requireActual("../src/config"),
getGitConfig: () => Promise.resolve({ default: blankCliConf }),
}
})
describe("config", () => {
describe("parseAppendData", () => {
test('works for "key=value"', () => {
@@ -56,6 +60,13 @@ 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",
@@ -113,40 +124,29 @@ describe("config", () => {
expect(result?.data?.num).toEqual("1234")
})
})
// describe("remote config", () => {
// test("works", async () => {
// // mock
// const result = await parseConfig({
// ...blankCliConf,
// config: "https://github.com/chenasraf/simple-scaffold",
// })
// })
// })
})
// TODO find how to mock getGitConfig properly
//
// describe("getConfig", () => {
// test("gets git config", async () => {
// // const original = config.githubPartToUrl
// // config.githubPartToUrl = jest.fn().mockReturnValue(Promise.resolve(blankCliConf))
// // const spy = jest.spyOn(config, "parseConfig").mockReturnValue(Promise.resolve(blankCliConf))
//
// // jest.spyOn(config, "getGitConfig").mockImplementation(() => Promise.resolve({ default: blankCliConf }))
// // jest.mock("../src/config", () => ({
// // ...jest.requireActual("../src/config"),
// // getGitConfig: () => Promise.resolve({ default: blankCliConf }),
// // }))
// const resultFn = await config.getConfig({
// config: "https://github.com/chenasraf/simple-scaffold.git",
// isRemote: true,
// quiet: true,
// verbose: 0,
// })
// const result = await resolve(resultFn, blankCliConf)
// expect(result).toEqual({ default: blankCliConf })
// // expect(spy.)
// // config.githubPartToUrl = original
// })
// })
describe("getConfig", () => {
test("gets git config", async () => {
const resultFn = await config.getConfig({
config: "https://github.com/chenasraf/simple-scaffold.git",
isRemote: true,
quiet: true,
verbose: 0,
})
const result = await resolve(resultFn, blankCliConf)
expect(result).toEqual({ default: blankCliConf })
})
test("gets local file config", async () => {
const resultFn = await config.getConfig({
config: "scaffold.config.js",
isRemote: false,
quiet: true,
verbose: 0,
})
const result = await resolve(resultFn, {} as any)
expect(result).toEqual(configFile)
})
})
})

View File

@@ -1,5 +1,5 @@
import { ScaffoldCmdConfig, ScaffoldConfig } from "../src/types"
import path from "path"
import path from "node:path"
import * as dateFns from "date-fns"
import { OptionsBase } from "massarg/types"
import { dateHelper, defaultHelpers, handlebarsParse, nowHelper } from "../src/parser"
@@ -38,8 +38,8 @@ describe("parser", () => {
Object.defineProperty(path, "sep", { value: origSep })
})
test("should work for windows paths", async () => {
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { isPath: true })).toEqual(
Buffer.from("C:\\exports\\test.txt"),
expect(handlebarsParse(blankConf, "C:\\exports\\{{name}}.txt", { isPath: true }).toString()).toEqual(
"C:\\exports\\test.txt",
)
})
})

View File

@@ -1,4 +1,4 @@
const path = require("path")
const path = require("node:path")
/** @type {import('typedoc').TypeDocOptions} */
module.exports = {