Compare commits

...

35 Commits

Author SHA1 Message Date
Chen Asraf
48631325c1 fixed release tarball file location 2021-11-18 14:16:58 +02:00
Chen Asraf
045ad0118a fix build output files 2021-11-18 14:09:21 +02:00
Chen Asraf
b4dca7a954 add build step 2021-11-18 13:57:57 +02:00
Chen Asraf
7c42808f63 try to fix workflow 2021-11-18 13:56:24 +02:00
Chen Asraf
fd42013e8b publish: debug mode off, try to fix workflow 2021-11-18 13:55:30 +02:00
Chen Asraf
961a72fcdc publish: debug mode on 2021-11-18 13:51:29 +02:00
Chen Asraf
d6d99cfdf2 try fix workflow 2021-11-18 13:45:37 +02:00
Chen Asraf
ea4ecabe02 Merge pull request #12 from chenasraf/v1.0
v1.0
2021-11-18 13:44:09 +02:00
Chen Asraf
c7749a8d33 update workflows 2021-11-18 13:42:28 +02:00
Chen Asraf
a59f29d71d fix build/publish cmd 2021-11-18 13:38:16 +02:00
Chen Asraf
bc224d93e1 support node 12 for fs package 2021-11-18 13:16:10 +02:00
Chen Asraf
cf923d8889 support node 12 for fs package 2021-11-18 13:14:53 +02:00
Chen Asraf
01e458ee0c update jest config 2021-11-18 13:12:37 +02:00
Chen Asraf
93853712f5 use node 12 2021-11-18 13:08:54 +02:00
Chen Asraf
474a3dcc1f try fix workflow 2021-11-18 13:04:19 +02:00
Chen Asraf
27e84d1093 update workflow 2021-11-18 12:55:07 +02:00
Chen Asraf
a6f25facc0 update workflows 2021-11-18 12:51:16 +02:00
Chen Asraf
3ee66b24f5 build: update workflow 2021-11-18 12:45:37 +02:00
Chen Asraf
0ce19a7e1a build: add workflow 2021-11-18 12:45:37 +02:00
Chen Asraf
7273538d79 fix main field in package.json 2021-11-18 12:45:37 +02:00
Chen Asraf
43b64965e3 v0.7.4 2021-11-18 12:45:37 +02:00
Chen Asraf
4f81654e05 added --quiet flag 2021-11-18 12:45:37 +02:00
Chen Asraf
8fcc7a6dc6 chore: cleanup 2021-11-18 12:45:31 +02:00
Chen Asraf
b4b0de6f2f docs: update README 2021-11-18 12:45:31 +02:00
Chen Asraf
2d5626ca70 improve tests 2021-11-18 12:45:26 +02:00
Chen Asraf
564e821419 maintain directory structure 2021-11-18 12:45:26 +02:00
Chen Asraf
a52f9a0b84 migrate cmd to massarg + update tests 2021-11-18 12:45:26 +02:00
Chen Asraf
aeddd44778 add dry run option 2021-11-18 12:45:26 +02:00
Chen Asraf
7cdf5e461b add cmd args 2021-11-18 12:45:26 +02:00
Chen Asraf
c42a58c2f2 add tests 2021-11-18 12:45:26 +02:00
Chen Asraf
d0c0152717 remove excess files 2021-11-18 12:45:26 +02:00
Chen Asraf
208ee307c8 code splitting 2021-11-18 12:45:16 +02:00
Chen Asraf
54834909b9 major refactor 2021-11-18 12:45:16 +02:00
Chen Asraf
40b592072e Create FUNDING.yml 2021-10-11 18:20:18 +03:00
Chen Asraf
3cb9a6fcd8 fix main field in package.json 2021-09-26 11:42:52 +03:00
41 changed files with 2233 additions and 4740 deletions

5
.editorconfig Normal file
View File

@@ -0,0 +1,5 @@
[*]
tab_width = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: casraf
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

48
.github/workflows/alpha.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Alpha Releases
on:
push:
branches: [alpha]
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: "12.x"
- run: yarn install --frozen-lockfile
- run: yarn build
- run: cd ./dist && yarn pack dist/ --filename=../release.tgz
- uses: Klemensas/action-autotag@stable
id: update_tag
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_prefix: "v"
- name: Publish on NPM
uses: JS-DevTools/npm-publish@v1
if: "!contains(github.event.head_commit.message, '[skip publish]')"
with:
package: ./dist/package.json
token: "${{ secrets.NPM_TOKEN }}"
- name: Create Release
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.update_tag.outputs.tagname }}
release_name: Release ${{ steps.update_tag.outputs.tagname }}
- name: Upload Release Asset
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./package.tgz
asset_name: package.tgz
asset_content_type: application/tgz

47
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Releases
on:
push:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: "12.x"
- run: yarn install --frozen-lockfile
- run: yarn build
- run: cd ./dist && yarn pack dist/ --filename=../release.tgz
- uses: Klemensas/action-autotag@stable
id: update_tag
with:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
tag_prefix: "v"
- name: Publish on NPM
uses: JS-DevTools/npm-publish@v1
with:
package: ./dist/package.json
token: "${{ secrets.NPM_TOKEN }}"
- name: Create Release
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.update_tag.outputs.tagname }}
release_name: Release ${{ steps.update_tag.outputs.tagname }}
- name: Upload Release Asset
if: steps.update_tag.outputs.tagname && !contains(github.event.head_commit.message, '[skip publish]')
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./package.tgz
asset_name: package.tgz
asset_content_type: application/tgz

17
.github/workflows/pull_requests.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Pull Requests
on:
pull_request:
branches: [master, alpha, beta]
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: "12.x"
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test

3
.gitignore vendored
View File

@@ -58,4 +58,5 @@ typings/
.env
examples/test-output/**/*
!examples/test-output/.gitkeep
dist/
.DS_Store

View File

@@ -1,3 +1,5 @@
{
"semi": false
}
"semi": false,
"printWidth": 120,
"tabWidth": 2
}

View File

@@ -1,4 +1,7 @@
{
"typescript.tsdk": "./node_modules/typescript/lib",
"npm.packageManager": "yarn"
}
"npm.packageManager": "yarn",
"cSpell.words": [
"massarg"
]
}

8
.vscode/tasks.json vendored
View File

@@ -25,6 +25,12 @@
"type": "npm",
"problemMatcher": [],
},
{
"command": "yarn test --watchAll",
"label": "yarn test --watchAll",
"type": "shell",
"problemMatcher": [],
},
{
"script": "cmd",
"label": "cmd",
@@ -44,4 +50,4 @@
"problemMatcher": [],
},
],
}
}

168
README.md
View File

@@ -11,6 +11,8 @@ You can either use it as a command line tool or import into your own code and ru
npm install [-g] simple-scaffold
# yarn
yarn [global] add simple-scaffold
# run without installing
npx simple-scaffold <...args>
```
## Use as a command line tool
@@ -18,53 +20,51 @@ yarn [global] add simple-scaffold
### Command Line Options
```plaintext
Scaffold Generator
Usage: simple-scaffold [options]
Generate scaffolds for your project based on file templates.
Usage: simple-scaffold scaffold-name [options]
Options:
Options
--help|-h Display help information
-n, --name string Component output name
-t, --templates File[] A glob pattern of template files to load.
A template file may be of any type and extension, and supports Handlebars as
a parsing engine for the file names and contents, so you may customize both
with variables from your configuration.
-o, --output File The output directory to put the new files in. They will attempt to maintain
their regular structure as they are found, if possible.
-l, --locals JSON string A JSON string for the template to use in parsing.
-w, --overwrite Boolean Whether to overwrite files when they are found to already exist. Default=true
-q, --quiet Boolean When set to true, logs will not output (including warnings and errors).
Default=false
-S, --create-sub-folder Boolean Whether to create a subdirectory with {{Name}} in the output directory.
Default=true
-h, --help Display this help message
--name|-n Name to be passed to the generated files. {{name}} and
{{Name}} inside contents and file names will be replaced
accordingly.
--output|-o Path to output to. If --create-sub-folder is enabled, the
subfolder will be created inside this path.
--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.
(default: false)
--data|-d Add custom data to the templates. By default, only your app
name is included.
--create-sub-folder|-s Create subfolder with the input name (default:
false)
--quiet|-q Suppress output logs (default:
false)
--dry-run|-dr Don't emit actual files. This is good for testing your
scaffolds and making sure they don't fail, without having to write
actual files. (default: false)
```
You can add this as a script in your `package.json`:
You can also add this as a script in your `package.json`:
```json
{
"scripts": {
"scaffold": "yarn simple-scaffold --template scaffolds/component/**/* --output src/components --locals myProp=\"propname\",myVal=123"
"scaffold": "yarn simple-scaffold --templates scaffolds/component/**/* --output src/components --data '{\"myProp\": \"propName\", \"myVal\": \"123\"}'"
}
}
```
## Scaffolding
Scaffolding will replace {{vars}} in both the file name and its contents and put the transformed files
in `<output>/<{{Name}}>`, as per the Handlebars formatting rules.
Your context will be pre-populated with the following:
- `{{Name}}`: CapitalizedName of the component
- `{{name}}`: camelCasedName of the component
Any `locals` you add in the config will populate with their names wrapped in `{{` and `}}`.
They are all stringified, so be sure to parse them accordingly by creating a script, if necessary.
### Use in Node.js
## Use in Node.js
You can also build the scaffold yourself, if you want to create more complex arguments or scaffold groups.
Simply pass a config object to the constructor, and invoke `run()` when you are ready to start.
@@ -94,20 +94,73 @@ config.output = (fullPath, baseDir, baseName) => {
}
```
## Example Scaffold Input
## Preparing files
### Input directory structure
### Template files
Put your template files anywhere, and fill them with tokens for replacement.
### Variable/token replacement
Scaffolding will replace `{{ varName }}` in both the file name and its contents and put the
transformed files in the output directory.
The data available for the template parser is the data you pass to the `data` config option (or
`--data` argument in CLI).
Your `data` will be pre-populated with the following:
- `{{Name}}`: PascalCase of the component name
- `{{name}}`: raw name of the component
> Simple-Scaffold uses [Handlebars.js](https://handlebarsjs.com/) for outputting the file contents,
> see their documentation for more information on syntax.
> Any `data` you add in the config will be available for use with their names wrapped in
> `{{` and `}}`.
Simple-Scaffold provides some built-in text transformation filters usable by handleBars.
For example, you may use `{{ name | snakeCase }}` inside a template file or filename, and it will
replace `My Name` with `my_name` when producing the final value.
Here are the built-in helpers available for use:
```plaintext
{{ name | camelCase }} => myName
{{ name | snakeCase }} => my_name
{{ name | startCase }} => My Name
{{ name | kebabCase }} => my-name
{{ name | hyphenCase }} => my-name
{{ name | pascalCase }} => MyName
```
**Note:** These helpers are available for any data property, not exclusive to `name`.
## Examples
### Command Example
```bash
simple-scaffold MyComponent \
-t project/scaffold/**/* \
-o src/components \
-d '{"className":"myClassName"}'
```
### Example Scaffold Input
#### Input Directory structure
```plaintext
- project
- scaffold
- {{Name}}.js
- src
- components
- ...
- scaffold
- {{Name}}.js
- src
- components
- ...
```
#### project/scaffold/{{Name}}.js
#### Contents of `project/scaffold/{{Name}}.js`
```js
const React = require('react')
@@ -119,39 +172,30 @@ module.exports = class {{Name}} extends React.Component {
}
```
### Run Example
```bash
simple-scaffold MyComponent \
-t project/scaffold/**/* \
-o src/components \
-l className=my-component
```
## Example Scaffold Output
### Example Scaffold Output
### Output directory structure
```plaintext
- project
- src
- components
- MyComponent
- MyComponent.js
- ...
- src
- components
- MyComponent
- MyComponent.js
- ...
```
With `createSubfolder = false`:
```plaintext
- project
- src
- components
- MyComponent.js
- ...
- src
- components
- MyComponent.js
- ...
```
#### project/scaffold/MyComponent/MyComponent.js
#### Contents of `project/scaffold/MyComponent/MyComponent.js`
```js
const React = require("react")

3
babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
plugins: ["@babel/plugin-proposal-nullish-coalescing-operator"],
}

135
cmd.ts
View File

@@ -1,135 +0,0 @@
import SimpleScaffold from "./scaffold"
import * as fs from "fs"
import { IScaffold } from "./index"
import * as cliArgs from "command-line-args"
import * as cliUsage from "command-line-usage"
import * as path from "path"
type Def = cliArgs.OptionDefinition & {
description?: string
typeLabel?: string
}
function localsParser(content: string) {
return JSON.parse(content)
}
function filePathParser(content: string) {
if (content.startsWith("/")) {
return content
}
return [process.cwd(), content].join(path.sep)
}
function booleanParser(text: string) {
return text && text.trim().length
? ["true", "1", "on"].includes(text.trim())
: true
}
const defs: Def[] = [
{
name: "name",
alias: "n",
type: String,
description: "Component output name",
defaultOption: true,
},
{
name: "templates",
alias: "t",
type: filePathParser,
typeLabel: "{underline File}[]",
description: `A glob pattern of template files to load.\nA template file may be of any type and extension, and supports Handlebars as a parsing engine for the file names and contents, so you may customize both with variables from your configuration.`,
multiple: true,
},
{
name: "output",
alias: "o",
type: filePathParser,
typeLabel: "{underline File}",
description: `The output directory to put the new files in. They will attempt to maintain their regular structure as they are found, if possible.`,
},
{
name: "locals",
alias: "l",
description: `A JSON string for the template to use in parsing.`,
typeLabel: "{underline JSON string}",
type: localsParser,
},
{
name: "overwrite",
alias: "w",
description: `Whether to overwrite files when they are found to already exist. {bold Default=true}`,
type: booleanParser,
typeLabel: "{underline Boolean}",
defaultValue: true,
},
{
name: "quiet",
alias: "q",
description:
"When set to {bold true}, logs will not output (including warnings and errors). {bold Default=false}",
type: booleanParser,
typeLabel: "{underline Boolean}",
defaultValue: false,
},
{
name: "create-sub-folder",
alias: "S",
typeLabel: "{underline Boolean}",
description:
"Whether to create a subdirectory with \\{\\{Name\\}\\} in the {underline output} directory. {bold Default=true}",
type: booleanParser,
defaultValue: true,
},
{
name: "help",
alias: "h",
type: Boolean,
description: "Display this help message",
},
]
const args = cliArgs(defs, { camelCase: true }) as Omit<
IScaffold.Config,
"createSubFolder"
> & {
help: boolean
createSubFolder: boolean
}
const help = [
{
header: "Scaffold Generator",
content: `Generate scaffolds for your project based on file templates.\nUsage: {bold simple-scaffold} {underline scaffold-name} {underline [options]}`,
},
{ header: "Options", optionList: defs },
]
if (args.createSubFolder === null) {
args.createSubFolder = true
}
if (args.quiet === null) {
args.quiet = true
}
if (args.help || !args.name) {
console.log(cliUsage(help))
process.exit(0)
}
if (!args.quiet) {
console.info("Config:", args)
}
new SimpleScaffold({
name: args.name,
templates: args.templates,
output: args.output,
locals: args.locals,
createSubfolder: args.createSubFolder,
overwrite: args.overwrite,
quiet: args.quiet,
}).run()

1
dist/cmd.d.ts vendored
View File

@@ -1 +0,0 @@
export {};

3
dist/cmd.js vendored

File diff suppressed because one or more lines are too long

1
dist/cmd.js.map vendored

File diff suppressed because one or more lines are too long

30
dist/index.d.ts vendored
View File

@@ -1,30 +0,0 @@
declare namespace IScaffold {
class SimpleScaffold {
constructor(config: Config)
run(): void
}
export interface Config {
name?: string
templates: string[]
output:
| string
| ((fullPath: string, basedir: string, basename: string) => string)
locals?: Locals
createSubfolder?: boolean
overwrite?: boolean | ((path: string) => boolean)
quiet?: boolean
}
export interface Locals {
[k: string]: string
}
export interface FileRepr {
base: string
file: string
}
}
export default IScaffold.SimpleScaffold
export { IScaffold }

2
dist/index.js vendored
View File

@@ -1,2 +0,0 @@
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.library=e():t.library=e()}(global,(function(){return(()=>{"use strict";var t={493:function(t,e,o){var r=this&&this.__assign||function(){return(r=Object.assign||function(t){for(var e,o=1,r=arguments.length;o<r;o++)for(var i in e=arguments[o])Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i]);return t}).apply(this,arguments)},i=this&&this.__spreadArrays||function(){for(var t=0,e=0,o=arguments.length;e<o;e++)t+=arguments[e].length;var r=Array(t),i=0;for(e=0;e<o;e++)for(var n=arguments[e],s=0,a=n.length;s<a;s++,i++)r[i]=n[s];return r};Object.defineProperty(e,"__esModule",{value:!0});var n=o(747),s=o(622),a=o(878),l=o(778),f=function(){function t(t){this.locals={};var e={name:"scaffold",templates:[],output:process.cwd(),createSubfolder:!0,overwrite:!0,quiet:!1};this.config=r(r({},e),t);var o={Name:this.config.name[0].toUpperCase()+this.config.name.slice(1),name:this.config.name[0].toLowerCase()+this.config.name.slice(1)};this.locals=r(r({},o),t.locals)}return t.prototype.parseLocals=function(t){try{return l.compile(t,{noEscape:!0})(this.locals)}catch(e){return this.warn("Problem using Handlebars, returning unmodified content"),t}},t.prototype.fileList=function(t){for(var e=[],o=0,r=t;o<r.length;o++){var i=r[o],n=a.sync(i,{dot:!0}).map((function(t){return"/"==t[0]?t:s.join(process.cwd(),t)})),l=i.indexOf("*"),f=i;l>=0&&(f=i.slice(0,l-1));for(var c=0,p=n;c<p.length;c++){var u=p[c];e.push({base:f,file:u})}}return e},t.prototype.getFileContents=function(t){return this.log(n.readFileSync(t)),n.readFileSync(t).toString()},t.prototype.getOutputPath=function(t,e){var o;if("function"==typeof this.config.output)o=this.config.output(t,e,s.basename(t));else{var r=this.config.output+(this.config.createSubfolder?"/"+this.config.name+"/":"/"),i=t.indexOf(e),n=t;i>=0&&(n=t!==e?t.slice(i+e.length+1):s.basename(t)),o=r+n}return this.parseLocals(o)},t.prototype.writeFile=function(t,e){var o=s.dirname(t);this.writeDirectory(o,t),n.writeFile(t,e,{encoding:"utf-8"},(function(t){if(t)throw t}))},t.prototype.shouldWriteFile=function(t){var e,o,r="boolean"==typeof this.config.overwrite?this.config.overwrite:null===(o=(e=this.config).overwrite)||void 0===o?void 0:o.call(e,t);return!n.existsSync(t)||!1!==r},t.prototype.run=function(){this.log("Generating scaffold: "+this.config.name+"...");var t,e=this.fileList(this.config.templates),o=0;this.log("Template files:",e);for(var r=0,i=e;r<i.length;r++){t=i[r];var s=void 0,a=void 0,l=void 0,f=void 0,c=void 0;try{if(o++,f=t.file,c=t.base,s=this.getOutputPath(f,c),n.lstatSync(f).isDirectory()){this.writeDirectory(s,f);continue}a=this.getFileContents(f),l=this.parseLocals(a),this.shouldWriteFile(s)?(this.info("Writing:",{file:f,base:c,outputPath:s,outputContents:l.replace("\n","\\n")}),this.writeFile(s,l)):this.log("Skipping file "+s)}catch(t){throw this.error("Error while processing file:",{file:f,base:c,contents:a,outputPath:s,outputContents:l}),t}}if(!o)throw new Error("No files to scaffold!");this.log("Done")},t.prototype.writeDirectory=function(t,e){var o=s.dirname(t);n.existsSync(o)||this.writeDirectory(o,t),n.existsSync(t)||(this.info("Creating directory:",{file:e,outputPath:t}),n.mkdirSync(t))},t.prototype._log=function(t){for(var e=[],o=1;o<arguments.length;o++)e[o-1]=arguments[o];if(!this.config.quiet){var r=console[t];r.apply(void 0,e)}},t.prototype.log=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];this._log.apply(this,i(["log"],t))},t.prototype.info=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];this._log.apply(this,i(["info"],t))},t.prototype.warn=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];this._log.apply(this,i(["warn"],t))},t.prototype.error=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];this._log.apply(this,i(["error"],t))},t}();e.default=f},747:t=>{t.exports=require("fs")},878:t=>{t.exports=require("glob")},778:t=>{t.exports=require("handlebars")},622:t=>{t.exports=require("path")}},e={};return function o(r){if(e[r])return e[r].exports;var i=e[r]={exports:{}};return t[r].call(i.exports,i,i.exports,o),i.exports}(493)})()}));
//# sourceMappingURL=index.js.map

1
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

21
dist/scaffold.d.ts vendored
View File

@@ -1,21 +0,0 @@
/// <reference types="node" />
import { IScaffold } from "./index.d";
declare class SimpleScaffold {
config: IScaffold.Config;
locals: IScaffold.Config["locals"];
constructor(config: IScaffold.Config);
private parseLocals;
private fileList;
private getFileContents;
private getOutputPath;
private writeFile;
private shouldWriteFile;
run(): void;
private writeDirectory;
_log(method: keyof typeof console, ...args: any[]): void;
log(...args: any[]): void;
info(...args: any[]): void;
warn(...args: any[]): void;
error(...args: any[]): void;
}
export default SimpleScaffold;

1
dist/test.d.ts vendored
View File

@@ -1 +0,0 @@
export {};

2
dist/test.js vendored
View File

@@ -1,2 +0,0 @@
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.library=e():t.library=e()}(global,(function(){return(()=>{"use strict";var t={493:function(t,e,o){var r=this&&this.__assign||function(){return(r=Object.assign||function(t){for(var e,o=1,r=arguments.length;o<r;o++)for(var i in e=arguments[o])Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i]);return t}).apply(this,arguments)},i=this&&this.__spreadArrays||function(){for(var t=0,e=0,o=arguments.length;e<o;e++)t+=arguments[e].length;var r=Array(t),i=0;for(e=0;e<o;e++)for(var n=arguments[e],s=0,a=n.length;s<a;s++,i++)r[i]=n[s];return r};Object.defineProperty(e,"__esModule",{value:!0});var n=o(747),s=o(622),a=o(878),l=o(778),p=function(){function t(t){this.locals={};var e={name:"scaffold",templates:[],output:process.cwd(),createSubfolder:!0,overwrite:!0,quiet:!1};this.config=r(r({},e),t);var o={Name:this.config.name[0].toUpperCase()+this.config.name.slice(1),name:this.config.name[0].toLowerCase()+this.config.name.slice(1)};this.locals=r(r({},o),t.locals)}return t.prototype.parseLocals=function(t){try{return l.compile(t,{noEscape:!0})(this.locals)}catch(e){return this.warn("Problem using Handlebars, returning unmodified content"),t}},t.prototype.fileList=function(t){for(var e=[],o=0,r=t;o<r.length;o++){var i=r[o],n=a.sync(i,{dot:!0}).map((function(t){return"/"==t[0]?t:s.join(process.cwd(),t)})),l=i.indexOf("*"),p=i;l>=0&&(p=i.slice(0,l-1));for(var u=0,c=n;u<c.length;u++){var f=c[u];e.push({base:p,file:f})}}return e},t.prototype.getFileContents=function(t){return this.log(n.readFileSync(t)),n.readFileSync(t).toString()},t.prototype.getOutputPath=function(t,e){var o;if("function"==typeof this.config.output)o=this.config.output(t,e,s.basename(t));else{var r=this.config.output+(this.config.createSubfolder?"/"+this.config.name+"/":"/"),i=t.indexOf(e),n=t;i>=0&&(n=t!==e?t.slice(i+e.length+1):s.basename(t)),o=r+n}return this.parseLocals(o)},t.prototype.writeFile=function(t,e){var o=s.dirname(t);this.writeDirectory(o,t),n.writeFile(t,e,{encoding:"utf-8"},(function(t){if(t)throw t}))},t.prototype.shouldWriteFile=function(t){var e,o,r="boolean"==typeof this.config.overwrite?this.config.overwrite:null===(o=(e=this.config).overwrite)||void 0===o?void 0:o.call(e,t);return!n.existsSync(t)||!1!==r},t.prototype.run=function(){this.log("Generating scaffold: "+this.config.name+"...");var t,e=this.fileList(this.config.templates),o=0;this.log("Template files:",e);for(var r=0,i=e;r<i.length;r++){t=i[r];var s=void 0,a=void 0,l=void 0,p=void 0,u=void 0;try{if(o++,p=t.file,u=t.base,s=this.getOutputPath(p,u),n.lstatSync(p).isDirectory()){this.writeDirectory(s,p);continue}a=this.getFileContents(p),l=this.parseLocals(a),this.shouldWriteFile(s)?(this.info("Writing:",{file:p,base:u,outputPath:s,outputContents:l.replace("\n","\\n")}),this.writeFile(s,l)):this.log("Skipping file "+s)}catch(t){throw this.error("Error while processing file:",{file:p,base:u,contents:a,outputPath:s,outputContents:l}),t}}if(!o)throw new Error("No files to scaffold!");this.log("Done")},t.prototype.writeDirectory=function(t,e){var o=s.dirname(t);n.existsSync(o)||this.writeDirectory(o,t),n.existsSync(t)||(this.info("Creating directory:",{file:e,outputPath:t}),n.mkdirSync(t))},t.prototype._log=function(t){for(var e=[],o=1;o<arguments.length;o++)e[o-1]=arguments[o];if(!this.config.quiet){var r=console[t];r.apply(void 0,e)}},t.prototype.log=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];this._log.apply(this,i(["log"],t))},t.prototype.info=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];this._log.apply(this,i(["info"],t))},t.prototype.warn=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];this._log.apply(this,i(["warn"],t))},t.prototype.error=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];this._log.apply(this,i(["error"],t))},t}();e.default=p},743:(t,e,o)=>{Object.defineProperty(e,"__esModule",{value:!0});var r=o(493),i=o(622).join(process.cwd(),"examples");new r.default({templates:[i+"/test-input/Component/**/*"],output:i+"/test-output/no-create-subpath",createSubfolder:!1,locals:{property:"myProp",value:'"value"'}}).run(),new r.default({templates:[i+"/test-input/Component/**/*"],output:i+"/test-output",locals:{property:"myProp",value:'"value"'}}).run(),new r.default({templates:[i+"/test-input/Component/**/*"],output:function(t,e,o){return console.log({file:t,basedir:e,basename:o}),t},locals:{property:"myProp",value:'"value"'}}).run()},747:t=>{t.exports=require("fs")},878:t=>{t.exports=require("glob")},778:t=>{t.exports=require("handlebars")},622:t=>{t.exports=require("path")}},e={};return function o(r){if(e[r])return e[r].exports;var i=e[r]={exports:{}};return t[r].call(i.exports,i,i.exports,o),i.exports}(743)})()}));
//# sourceMappingURL=test.js.map

1
dist/test.js.map vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,19 +0,0 @@
import * as React from 'react'
import * as css from './Scaffold.css'
class Scaffold extends React.Component<any> {
private myProp
constructor(props: any) {
super(props)
this.myProp = "value"
}
public render() {
return (
<div className={ css.Scaffold } />
)
}
}
export default Scaffold

View File

@@ -0,0 +1 @@
{{name}}

View File

@@ -1,18 +1,16 @@
import * as React from 'react'
import * as css from './{{Name}}.css'
import * as React from "react"
import * as css from "./{{Name}}.css"
class {{Name}} extends React.Component<any> {
private {{property}}
private {{ property }}
constructor(props: any) {
super(props)
this.{{property}} = {{value}}
this.{{ property }} = {{ value }}
}
public render() {
return (
<div className={ css.{{Name}} } />
)
return <div className={ css.{{Name}} } />
}
}

30
index.d.ts vendored
View File

@@ -1,30 +0,0 @@
declare namespace IScaffold {
class SimpleScaffold {
constructor(config: Config)
run(): void
}
export interface Config {
name?: string
templates: string[]
output:
| string
| ((fullPath: string, basedir: string, basename: string) => string)
locals?: Locals
createSubfolder?: boolean
overwrite?: boolean | ((path: string) => boolean)
quiet?: boolean
}
export interface Locals {
[k: string]: string
}
export interface FileRepr {
base: string
file: string
}
}
export default IScaffold.SimpleScaffold
export { IScaffold }

View File

@@ -1,2 +0,0 @@
const SimpleScaffold = require('./dist')
module.exports = SimpleScaffold

196
jest.config.ts Normal file
View File

@@ -0,0 +1,196 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
*/
export default {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/q9/0mns8fgd00b4t5j5lq2wh2yh0000gn/T/jest_dx",
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
modulePathIgnorePatterns: ["<rootDir>/dist"],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
preset: "ts-jest",
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: {},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
// extensionsToTreatAsEsm: [".ts"],
}

View File

@@ -1,47 +1,42 @@
{
"name": "simple-scaffold",
"version": "0.7.4",
"version": "1.0.0-alpha.3",
"description": "Create files based on templates",
"repository": "https://github.com/chenasraf/simple-scaffold.git",
"author": "Chen Asraf <inbox@casraf.com>",
"license": "MIT",
"main": "./dist/scaffold.js",
"bin": "./dist/cmd.js",
"types": "index.d.ts",
"main": "index.js",
"bin": "cmd.js",
"types": "types.d.ts",
"scripts": {
"build": "NODE_ENV=${NODE_ENV:-production} webpack && chmod -R +x ./dist",
"prepublishOnly": "yarn build",
"dev": "webpack --watch",
"clean": "rm -rf dist/",
"build": "yarn clean && tsc && chmod -R +x ./dist && cp ./package.json ./dist/ && cp ./README.md ./dist/",
"dev": "tsc --watch",
"start": "node dist/scaffold.js",
"test": "node dist/test.js",
"cmd": "dist/cmd.js",
"test": "jest --verbose",
"cmd": "node --trace-warnings dist/cmd.js",
"build-test": "yarn build && yarn test",
"build-cmd": "yarn build && yarn cmd"
},
"dependencies": {
"command-line-args": "^5.0.2",
"command-line-usage": "^6.1.1",
"args": "^5.0.1",
"glob": "^7.1.3",
"handlebars": "^4.1.0"
"handlebars": "^4.7.7",
"lodash": "^4.17.21",
"massarg": "^0.1.2",
"util.promisify": "^1.1.1"
},
"devDependencies": {
"@types/command-line-args": "^5.0.0",
"@types/command-line-usage": "^5.0.1",
"@types/args": "^3.0.1",
"@types/glob": "^7.1.1",
"@types/jest": "^26.0.24",
"@types/lodash": "^4.14.171",
"@types/mock-fs": "^4.13.1",
"@types/node": "^14.14.22",
"copy-webpack-plugin": "^7.0.0",
"jest": "^26.6.3",
"ts-loader": "^8.0.14",
"typescript": "^4.1.3",
"webpack": "^5.19.0",
"webpack-cli": "^4.4.0",
"webpack-dev-server": "^3.2.1",
"webpack-node-externals": "^2.5.2"
},
"jest": {
"testPathIgnorePatterns": [
"node_modules",
"dist"
]
"jest": "^27.0.6",
"mock-fs": "^5.0.0",
"ts-jest": "^27.0.3",
"ts-node": "^10.1.0",
"typescript": "^4.3.5"
}
}

View File

@@ -1,198 +0,0 @@
import * as fs from "fs"
import * as path from "path"
import { IScaffold } from "./index.d"
import * as glob from "glob"
import * as handlebars from "handlebars"
class SimpleScaffold {
public config: IScaffold.Config
public locals: IScaffold.Config["locals"] = {} as any
constructor(config: IScaffold.Config) {
const DefaultConfig: IScaffold.Config = {
name: "scaffold",
templates: [],
output: process.cwd(),
createSubfolder: true,
overwrite: true,
quiet: false,
}
this.config = { ...DefaultConfig, ...config }
const DefaultLocals = {
// TODO improve
Name: this.config.name![0].toUpperCase() + this.config.name!.slice(1),
name: this.config.name![0].toLowerCase() + this.config.name!.slice(1),
}
this.locals = { ...DefaultLocals, ...config.locals }
}
private parseLocals(text: string): string {
try {
const template = handlebars.compile(text, {
noEscape: true,
})
return template(this.locals)
} catch (e) {
this.warn("Problem using Handlebars, returning unmodified content")
return text
}
}
private fileList(input: string[]): IScaffold.FileRepr[] {
const output: IScaffold.FileRepr[] = []
for (const checkPath of input) {
const files = glob
.sync(checkPath, { dot: true })
.map((g) => (g[0] == "/" ? g : path.join(process.cwd(), g)))
const idx = checkPath.indexOf("*")
let cleanCheckPath = checkPath
if (idx >= 0) {
cleanCheckPath = checkPath.slice(0, idx - 1)
}
for (const file of files) {
output.push({ base: cleanCheckPath, file })
}
}
return output
}
private getFileContents(filePath: string): string {
this.log(fs.readFileSync(filePath))
return fs.readFileSync(filePath).toString()
}
private getOutputPath(file: string, basePath: string): string {
let out: string
if (typeof this.config.output === "function") {
out = this.config.output(file, basePath, path.basename(file))
} else {
const outputDir =
this.config.output +
(this.config.createSubfolder ? `/${this.config.name}/` : "/")
const idx = file.indexOf(basePath)
let relativeFilePath = file
if (idx >= 0) {
if (file !== basePath) {
relativeFilePath = file.slice(idx + basePath.length + 1)
} else {
relativeFilePath = path.basename(file)
}
}
out = outputDir + relativeFilePath
}
return this.parseLocals(out)
}
private writeFile(filePath: string, fileContents: string): void {
const baseDir = path.dirname(filePath)
this.writeDirectory(baseDir, filePath)
fs.writeFile(filePath, fileContents, { encoding: "utf-8" }, (err) => {
if (err) {
throw err
}
})
}
private shouldWriteFile(filePath: string) {
const overwrite =
typeof this.config.overwrite === "boolean"
? this.config.overwrite
: this.config.overwrite?.(filePath)
const exists = fs.existsSync(filePath)
return !exists || overwrite !== false
}
public run(): void {
this.log(`Generating scaffold: ${this.config.name}...`)
const templates = this.fileList(this.config.templates)
let fileConf,
count = 0
this.log("Template files:", templates)
for (fileConf of templates) {
let outputPath, contents, outputContents, file, base
try {
count++
file = fileConf.file
base = fileConf.base
outputPath = this.getOutputPath(file, base)
if (fs.lstatSync(file).isDirectory()) {
this.writeDirectory(outputPath, file)
continue
}
contents = this.getFileContents(file)
outputContents = this.parseLocals(contents)
if (this.shouldWriteFile(outputPath)) {
this.info("Writing:", {
file,
base,
outputPath,
outputContents: outputContents.replace("\n", "\\n"),
})
this.writeFile(outputPath, outputContents)
} else {
this.log(`Skipping file ${outputPath}`)
}
} catch (e) {
this.error("Error while processing file:", {
file,
base,
contents,
outputPath,
outputContents,
})
throw e
}
}
if (!count) {
throw new Error("No files to scaffold!")
}
this.log("Done")
}
private writeDirectory(outputPath: string, file: any): void {
const parent = path.dirname(outputPath)
if (!fs.existsSync(parent)) {
this.writeDirectory(parent, outputPath)
}
if (!fs.existsSync(outputPath)) {
this.info("Creating directory:", {
file,
outputPath,
})
fs.mkdirSync(outputPath)
}
}
_log(method: keyof typeof console, ...args: any[]): void {
if (this.config.quiet) {
return
}
const fn = console[method] as (...a: any[]) => void
fn(...args)
}
log(...args: any[]): void {
this._log("log", ...args)
}
info(...args: any[]): void {
this._log("info", ...args)
}
warn(...args: any[]): void {
this._log("warn", ...args)
}
error(...args: any[]): void {
this._log("error", ...args)
}
}
export default SimpleScaffold

69
src/cmd.ts Normal file
View File

@@ -0,0 +1,69 @@
import Scaffold from "./scaffold"
import massarg from "massarg"
import { ScaffoldCmdConfig } from "./types"
massarg<ScaffoldCmdConfig & { help: boolean; extras: string[] }>()
.main(Scaffold)
.option({
name: "name",
aliases: ["n"],
isDefault: true,
description:
"Name to be passed to the generated files. {{name}} and {{Name}} inside contents and file names will be replaced accordingly.",
})
.option({
name: "output",
aliases: ["o"],
description:
"Path to output to. If --create-sub-folder is enabled, the subfolder will be created inside this path.",
})
.option({
name: "templates",
aliases: ["t"],
description:
"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.",
defaultValue: [],
array: true,
})
.option({
aliases: ["w"],
name: "overwrite",
description: "Enable to override output files, even if they already exist.",
defaultValue: false,
boolean: true,
})
.option({
aliases: ["d"],
name: "data",
description: "Add custom data to the templates. By default, only your app name is included.",
parse: (v) => JSON.parse(v),
})
.option({
aliases: ["s"],
name: "create-sub-folder",
description: "Create subfolder with the input name",
defaultValue: false,
boolean: true,
})
.option({ aliases: ["q"], name: "quiet", description: "Suppress output logs", defaultValue: false, boolean: true })
.option({
aliases: ["dr"],
name: "dry-run",
description:
"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.",
defaultValue: false,
boolean: true,
})
// .example({
// input: `yarn cmd -t examples/test-input/Component -o examples/test-output -d '{"property":"myProp","value":"10"}'`,
// description: "Usage",
// output: "",
// })
.help({
binName: "simple-scaffold",
useGlobalColumns: true,
usageExample: "[options]",
})
.parse()

3
src/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from "./scaffold"
import Scaffold from "./scaffold"
export default Scaffold

104
src/scaffold.ts Normal file
View File

@@ -0,0 +1,104 @@
import { glob } from "glob"
import path from "path"
import { promisify } from "util"
import { promises as fsPromises } from "fs"
const { readFile, writeFile } = fsPromises
import {
createDirIfNotExists,
getOptionValueForFile,
handleErr,
handlebarsParse,
log,
pathExists,
pascalCase,
isDir,
} from "./utils"
import { ScaffoldConfig } from "./types"
export async function Scaffold(config: ScaffoldConfig) {
try {
const options = { ...config }
const data = { name: options.name, Name: pascalCase(options.name), ...options.data }
log(options, "Config:", {
name: options.name,
templates: options.templates,
output: options.output,
createSubfolder: options.createSubFolder,
data: options.data,
overwrite: options.overwrite,
quiet: options.quiet,
})
log(options, "Data:", data)
for (let template of config.templates) {
try {
const _isDir = await isDir(template)
const basePath = path
.resolve(process.cwd(), _isDir ? template : path.dirname(template.replace("*", "").replace("//", "/")))
.replace(process.cwd(), ".")
if (_isDir) {
template = template + "/**/*"
}
const files = await promisify(glob)(template, { dot: true, debug: false })
for (const templatePath of files) {
if (!(await isDir(templatePath))) {
await handleTemplateFile(templatePath, basePath, options, data)
}
}
} catch (e: any) {
handleErr(e)
}
}
} catch (e: any) {
console.error(e)
throw e
}
}
async function handleTemplateFile(
templatePath: string,
basePath: string,
options: ScaffoldConfig,
data: Record<string, string>
): Promise<void> {
return new Promise(async (resolve, reject) => {
try {
log(options, `Parsing ${templatePath}`)
const inputPath = path.join(process.cwd(), templatePath)
const outputPathOpt = getOptionValueForFile(inputPath, data, options.output)
const outputDir = path.resolve(
process.cwd(),
...([outputPathOpt, options.createSubFolder ? options.name : undefined].filter(Boolean) as string[])
)
const outputPath = path.join(outputDir, handlebarsParse(path.basename(inputPath), data))
const overwrite = getOptionValueForFile(inputPath, data, options.overwrite ?? false)
const exists = await pathExists(outputPath)
await createDirIfNotExists(outputDir, options)
log(options, `Writing to ${outputPath}`)
if (!exists || overwrite) {
if (exists && overwrite) {
log(options, `File ${outputPath} exists, overwriting`)
}
const templateBuffer = await readFile(inputPath)
const outputContents = handlebarsParse(templateBuffer, data)
if (!options.dryRun) {
await writeFile(outputPath, outputContents)
} else {
log(options, "Content output:")
log(options, outputContents)
}
} else if (exists) {
log(options, `File ${outputPath} already exists, skipping`)
}
resolve()
} catch (e: any) {
handleErr(e)
reject(e)
}
})
}
export default Scaffold

25
src/types.ts Normal file
View File

@@ -0,0 +1,25 @@
export type FileResponseFn<T> = (fullPath: string, basedir: string, basename: string) => T
export type FileResponse<T> = T | FileResponseFn<T>
export interface ScaffoldConfig {
/** The name supplied for the output templates */
name: string
templates: string[]
output: FileResponse<string>
createSubFolder?: boolean
data?: Record<string, string>
overwrite?: FileResponse<boolean>
quiet?: boolean
dryRun?: boolean
}
export interface ScaffoldCmdConfig {
name: string
templates: string[]
output: string
createSubFolder: boolean
data?: Record<string, string>
overwrite: boolean
quiet: boolean
dryRun: boolean
}

97
src/utils.ts Normal file
View File

@@ -0,0 +1,97 @@
import path from "path"
import { F_OK } from "constants"
import { FileResponse, FileResponseFn, 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 { promises as fsPromises } from "fs"
const { stat, access, mkdir } = fsPromises
const helpers = {
camelCase,
snakeCase,
startCase,
kebabCase,
hyphenCase: kebabCase,
pascalCase,
}
for (const helperName in helpers) {
Handlebars.registerHelper(helperName, helpers[helperName as keyof typeof helpers])
}
export function handleErr(err: NodeJS.ErrnoException | null) {
if (err) throw err
}
export function log(options: ScaffoldConfig, ...obj: any[]) {
if (options.quiet) {
return
}
console["log"](...obj)
}
export async function createDirIfNotExists(dir: string, options: ScaffoldConfig): Promise<void> {
const parentDir = path.dirname(dir)
if (!(await pathExists(parentDir))) {
await createDirIfNotExists(parentDir, options)
}
if (!(await pathExists(dir))) {
try {
await mkdir(dir)
return
} catch (e: any) {
if (e.code !== "EEXIST") {
throw e
}
return
}
}
}
export function getOptionValueForFile<T>(
filePath: string,
data: Record<string, string>,
fn: FileResponse<T>,
defaultValue?: T
): T {
if (typeof fn !== "function") {
return defaultValue ?? (fn as T)
}
return (fn as FileResponseFn<T>)(
filePath,
path.dirname(handlebarsParse(filePath, data)),
path.basename(handlebarsParse(filePath, data))
)
}
export function handlebarsParse(templateBuffer: Buffer | string, data: Record<string, string>) {
const parser = Handlebars.compile(templateBuffer.toString(), { noEscape: true })
const outputContents = parser(data)
return outputContents
}
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()
}

35
test.ts
View File

@@ -1,35 +0,0 @@
import SimpleScaffold from "./scaffold"
import * as path from "path"
const templateDir = path.join(process.cwd(), "examples")
new SimpleScaffold({
templates: [templateDir + "/test-input/Component/**/*"],
output: templateDir + "/test-output/no-create-subpath",
createSubfolder: false,
locals: {
property: "myProp",
value: '"value"',
},
}).run()
new SimpleScaffold({
templates: [templateDir + "/test-input/Component/**/*"],
output: templateDir + "/test-output",
locals: {
property: "myProp",
value: '"value"',
},
}).run()
new SimpleScaffold({
templates: [templateDir + "/test-input/Component/**/*"],
output: (file, basedir, basename) => {
console.log({ file, basedir, basename })
return file
},
locals: {
property: "myProp",
value: '"value"',
},
}).run()

183
tests/scaffold.test.ts Normal file
View File

@@ -0,0 +1,183 @@
import mockFs from "mock-fs"
import FileSystem from "mock-fs/lib/filesystem"
import Scaffold from "../src/scaffold"
import { readdirSync, readFileSync } from "fs"
const fileStructNormal = {
input: {
"{{name}}.txt": "Hello, my app is {{name}}",
},
output: {},
}
const fileStructWithData = {
input: {
"{{name}}.txt": "Hello, my value is {{value}}",
},
output: {},
}
const fileStructNested = {
input: {
"{{name}}-1.text": "This should be in root",
"{{Name}}": {
"{{name}}-2.txt": "Hello, my value is {{value}}",
},
},
output: {},
}
function withMock(fileStruct: FileSystem.DirectoryItems, testFn: jest.EmptyFunction): jest.EmptyFunction {
return () => {
beforeEach(() => {
mockFs(fileStruct)
})
testFn()
afterEach(() => {
mockFs.restore()
})
}
}
describe("Scaffold", () => {
describe(
"create subfolder",
withMock(fileStructNormal, () => {
test("should not create by default", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
quiet: true,
})
const data = readFileSync(process.cwd() + "/output/app_name.txt")
expect(data.toString()).toBe("Hello, my app is app_name")
})
test("should create with config", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
createSubFolder: true,
quiet: true,
})
const data = readFileSync(process.cwd() + "/output/app_name/app_name.txt")
expect(data.toString()).toBe("Hello, my app is app_name")
})
})
)
describe(
"overwrite",
withMock(fileStructWithData, () => {
test("should not overwrite by default", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
data: { value: "1" },
quiet: true,
})
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
data: { value: "2" },
quiet: true,
})
const data = readFileSync(process.cwd() + "/output/app_name.txt")
expect(data.toString()).toBe("Hello, my value is 1")
})
test("should overwrite with config", async () => {
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
data: { value: "1" },
quiet: true,
})
await Scaffold({
name: "app_name",
output: "output",
templates: ["input"],
data: { value: "2" },
overwrite: true,
quiet: true,
})
const data = readFileSync(process.cwd() + "/output/app_name.txt")
expect(data.toString()).toBe("Hello, my value is 2")
})
})
)
describe(
"errors",
withMock(fileStructNormal, () => {
let consoleMock: jest.SpyInstance
beforeAll(() => {
consoleMock = jest.spyOn(console, "error").mockImplementation(() => void 0)
})
test("should throw for bad input", async () => {
await expect(
Scaffold({
name: "app_name",
output: "output",
templates: ["non-existing-input"],
data: { value: "1" },
quiet: true,
})
).rejects.toThrow()
expect(() => readFileSync(process.cwd() + "/output/app_name.txt")).toThrow()
})
afterAll(() => {
consoleMock.mockRestore()
})
})
)
describe(
"outputPath override",
withMock(fileStructNormal, () => {
test("should allow override function", async () => {
await Scaffold({
name: "app_name",
output: (fullPath, basedir, basename) => `custom-output/${basename.split(".")[0]}`,
templates: ["input"],
data: { value: "1" },
quiet: true,
})
const data = readFileSync(process.cwd() + "/custom-output/app_name/app_name.txt")
expect(data.toString()).toBe("Hello, my app is app_name")
})
})
)
describe(
"output structure",
withMock(fileStructNested, () => {
test("should maintain input structure on output", async () => {
await Scaffold({
name: "app_name",
output: "./",
templates: ["input"],
data: { value: "1" },
quiet: true,
})
const dir = readdirSync(process.cwd())
expect(dir).toHaveProperty("length")
})
})
)
})

View File

@@ -1,14 +1,23 @@
{
"compilerOptions": {
"target": "es5",
"target": "ES2019",
"module": "commonjs",
"lib": ["es2017", "es6"],
"moduleResolution": "node",
"esModuleInterop": true,
"lib": [
"ES2019",
],
"declaration": true,
"outDir": "dist",
"strict": true,
"sourceMap": true
"sourceMap": true,
"removeComments": false,
},
"include": [
"src/index.ts",
"src/cmd.ts",
],
"exclude": [
"examples"
"tests/*"
]
}

View File

@@ -1,46 +0,0 @@
const path = require("path")
const webpack = require("webpack")
const nodeExternals = require("webpack-node-externals")
const CopyPlugin = require("copy-webpack-plugin")
module.exports = {
devtool:
process.env.NODE_ENV === "develop" ? "inline-source-map" : "source-map",
target: "node",
entry: {
index: "./scaffold.ts",
test: "./test.ts",
cmd: "./cmd.ts",
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist"),
devtoolModuleFilenameTemplate: "[absolute-resource-path]",
library: "library",
libraryTarget: "umd",
},
resolve: {
extensions: [".ts"],
},
externals: [nodeExternals()],
module: {
rules: [
{
test: [/\.tsx?$/],
loader: "ts-loader",
exclude: [/\/examples\//, /\/node_modules\//],
},
],
},
plugins: [
new webpack.DefinePlugin({
__dirname: "__dirname",
}),
new webpack.BannerPlugin({
banner: "#!/usr/bin/env node",
raw: true,
include: [/cmd\.js/],
}),
new CopyPlugin({ patterns: ["index.d.ts"] }),
],
}

5363
yarn.lock

File diff suppressed because it is too large Load Diff