@codeandmoney/jargal 0.0.2-RC.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/launch.json +26 -0
- package/TODO.md +2 -0
- package/actions/ai.ts +7 -0
- package/actions/answer.ts +7 -0
- package/actions/blank.ts +62 -0
- package/actions/config.ts +7 -0
- package/actions/context.ts +10 -0
- package/actions/echo.ts +7 -0
- package/actions/exports.ts +8 -0
- package/actions/load_templates.ts +33 -0
- package/actions/modify.ts +7 -0
- package/actions/parallel.ts +10 -0
- package/actions/prompt.ts +144 -0
- package/actions/render_template.ts +21 -0
- package/actions/select_generator.ts +23 -0
- package/actions/use.ts +9 -0
- package/actions/validate_answers.ts +14 -0
- package/actions/write.ts +84 -0
- package/deno.json +30 -0
- package/deno.lock +115 -0
- package/lib.ts +147 -0
- package/mod.ts +8 -0
- package/package.json +36 -0
- package/renderer.ts +105 -0
- package/runner.ts +100 -0
- package/runner_test.ts +27 -0
- package/types.ts +97 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2.0",
|
|
3
|
+
"configurations": [
|
|
4
|
+
{
|
|
5
|
+
"type": "node",
|
|
6
|
+
"request": "attach",
|
|
7
|
+
"name": "Attach",
|
|
8
|
+
"port": 9229,
|
|
9
|
+
"skipFiles": ["<node_internals>/**"]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "node",
|
|
13
|
+
"request": "launch",
|
|
14
|
+
"name": "Execute Command",
|
|
15
|
+
"skipFiles": ["<node_internals>/**"],
|
|
16
|
+
"runtimeExecutable": "node",
|
|
17
|
+
"runtimeArgs": [
|
|
18
|
+
"--loader",
|
|
19
|
+
"ts-node/esm",
|
|
20
|
+
"--no-warnings=ExperimentalWarning"
|
|
21
|
+
],
|
|
22
|
+
"program": "${workspaceFolder}/bin/dev.js",
|
|
23
|
+
"args": ["hello", "world"]
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
package/TODO.md
ADDED
package/actions/ai.ts
ADDED
package/actions/blank.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Action } from "../types.ts"
|
|
2
|
+
|
|
3
|
+
export const blank1: Action = () => {
|
|
4
|
+
console.log( "blank1:", "1" )
|
|
5
|
+
|
|
6
|
+
return function execute( _params ) {
|
|
7
|
+
console.log( "blank1:", "2" )
|
|
8
|
+
|
|
9
|
+
return function action() {
|
|
10
|
+
console.log( "blank1:", "3" )
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const blank2: Action = () => {
|
|
16
|
+
console.log( "blank2:", 1 )
|
|
17
|
+
|
|
18
|
+
return function execute( _params ) {
|
|
19
|
+
console.log( "blank2:", 2 )
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
function action() {
|
|
23
|
+
console.log( "blank2:", 3 )
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
function action() {
|
|
27
|
+
console.log( "blank2:", 4 )
|
|
28
|
+
return [
|
|
29
|
+
() => console.log( "blank2:", 4, 1 ),
|
|
30
|
+
() => console.log( "blank2:", 4, 2 ),
|
|
31
|
+
() => console.log( "blank2:", 4, 3 ),
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const blank3: Action = () => {
|
|
39
|
+
console.log( "blank3:", 1 )
|
|
40
|
+
|
|
41
|
+
return function execute( _params ) {
|
|
42
|
+
console.log( "blank3:", 2 )
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function blankRecursive( depth = 10 ): Action {
|
|
47
|
+
return () =>
|
|
48
|
+
Math.random() > 0.5 ? recursive( depth ) : [
|
|
49
|
+
recursive( depth ),
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function recursive( depth: number ): Action {
|
|
54
|
+
if ( depth > 0 ) {
|
|
55
|
+
return () => {
|
|
56
|
+
console.log( depth )
|
|
57
|
+
return [ blank1, blank2, blank3, recursive( depth - 1 ) ]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return () => console.log( 0 )
|
|
62
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { merge } from "es-toolkit"
|
|
2
|
+
import { readonly } from "../lib.ts"
|
|
3
|
+
import type { Action, Context, ContextAction, DeepReadonly } from "../types.ts"
|
|
4
|
+
|
|
5
|
+
export function context( callback: ContextAction ): Action {
|
|
6
|
+
return async function execute( params ) {
|
|
7
|
+
const newContext = await callback( readonly( params.context ) )
|
|
8
|
+
merge( params.context, newContext )
|
|
9
|
+
}
|
|
10
|
+
}
|
package/actions/echo.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { context } from "./context.ts"
|
|
2
|
+
export { prompt } from "./prompt.ts"
|
|
3
|
+
export { parallel } from "./parallel.ts"
|
|
4
|
+
export { write } from "./write.ts"
|
|
5
|
+
export { echo } from "./echo.ts"
|
|
6
|
+
export { validateAnswers } from "./validate_answers.ts"
|
|
7
|
+
export { loadTemplates } from "./load_templates.ts"
|
|
8
|
+
export { renderTemplate } from "./render_template.ts"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ContextAction } from "../types.ts"
|
|
2
|
+
import { promises as fs } from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
export function loadTemplates( templatesPath: string ): ContextAction {
|
|
6
|
+
return async function execute() {
|
|
7
|
+
const templates: Map<string, { fullPath: string; realativePath: string; content: string }> = new Map()
|
|
8
|
+
|
|
9
|
+
for await ( const fullPath of walkDir( templatesPath ) ) {
|
|
10
|
+
const realativePath = path.relative( templatesPath, fullPath )
|
|
11
|
+
|
|
12
|
+
const contentRaw = await fs.readFile( path.resolve( templatesPath, fullPath ) )
|
|
13
|
+
|
|
14
|
+
templates.set( realativePath, { content: new TextDecoder().decode( contentRaw ), fullPath, realativePath } )
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { templates }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function* walkDir( dir: string ): AsyncGenerator<string, void, void> {
|
|
22
|
+
const entries = await fs.readdir( dir, { withFileTypes: true } )
|
|
23
|
+
|
|
24
|
+
for ( const entry of entries ) {
|
|
25
|
+
const fullPath = path.join( dir, entry.name )
|
|
26
|
+
|
|
27
|
+
if ( entry.isDirectory() ) {
|
|
28
|
+
yield* walkDir( fullPath )
|
|
29
|
+
} else if ( entry.isFile() ) {
|
|
30
|
+
yield fullPath
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { executeAction } from "../runner.ts"
|
|
2
|
+
import type { Action } from "../types.ts"
|
|
3
|
+
|
|
4
|
+
export function parallel( ...actions: Action[] ): Action {
|
|
5
|
+
return async function execute( params ) {
|
|
6
|
+
await Promise.all(
|
|
7
|
+
actions.map( ( action ) => executeAction( { action, context: params.context, renderer: params.renderer } ) ),
|
|
8
|
+
)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// deno-lint-ignore-file no-explicit-any
|
|
2
|
+
|
|
3
|
+
import enquirer from "enquirer"
|
|
4
|
+
import type { Action } from "../types.ts"
|
|
5
|
+
import { EventEmitter } from "node:events"
|
|
6
|
+
|
|
7
|
+
export function prompt( questions: PromptOptions | PromptOptions[] ): Action {
|
|
8
|
+
return async function executePrompts( params ) {
|
|
9
|
+
const answers = await enquirer.prompt( questions )
|
|
10
|
+
|
|
11
|
+
if ( !params.context.answers ) {
|
|
12
|
+
Object.assign( params.context, { answers: {} } )
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for ( const [ key, value ] of Object.entries( answers ) ) {
|
|
16
|
+
Object.assign( params.context.answers, { [key]: value } )
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// NOTE: Stupid enquirer doesn't export types at all!!!
|
|
22
|
+
// So, I, being stupid dev, just copied them to get better inference
|
|
23
|
+
|
|
24
|
+
type PromptTypes =
|
|
25
|
+
| "autocomplete"
|
|
26
|
+
| "editable"
|
|
27
|
+
| "form"
|
|
28
|
+
| "multiselect"
|
|
29
|
+
| "select"
|
|
30
|
+
| "survey"
|
|
31
|
+
| "list"
|
|
32
|
+
| "scale"
|
|
33
|
+
| "confirm"
|
|
34
|
+
| "input"
|
|
35
|
+
| "invisible"
|
|
36
|
+
| "list"
|
|
37
|
+
| "password"
|
|
38
|
+
| "text"
|
|
39
|
+
| "numeral"
|
|
40
|
+
| "snippet"
|
|
41
|
+
| "sort"
|
|
42
|
+
|
|
43
|
+
interface BasePromptOptions {
|
|
44
|
+
name: string | (() => string)
|
|
45
|
+
type: PromptTypes | (() => PromptTypes)
|
|
46
|
+
message: string | (() => string) | (() => Promise<string>)
|
|
47
|
+
prefix?: string
|
|
48
|
+
initial?: any
|
|
49
|
+
required?: boolean
|
|
50
|
+
enabled?: boolean | string
|
|
51
|
+
disabled?: boolean | string
|
|
52
|
+
format?( value: string ): string | Promise<string>
|
|
53
|
+
result?( value: string ): string | Promise<string>
|
|
54
|
+
skip?: (( state: object ) => boolean | Promise<boolean>) | boolean
|
|
55
|
+
validate?( value: string ): boolean | string | Promise<boolean | string>
|
|
56
|
+
onSubmit?( name: string, value: any, prompt: BasePrompt ): boolean | Promise<boolean>
|
|
57
|
+
onCancel?( name: string, value: any, prompt: BasePrompt ): boolean | Promise<boolean>
|
|
58
|
+
stdin?: NodeJS.ReadStream
|
|
59
|
+
stdout?: NodeJS.WriteStream
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface Choice {
|
|
63
|
+
name: string
|
|
64
|
+
message?: string
|
|
65
|
+
value?: unknown
|
|
66
|
+
hint?: string
|
|
67
|
+
role?: string
|
|
68
|
+
enabled?: boolean
|
|
69
|
+
disabled?: boolean | string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ArrayPromptOptions extends BasePromptOptions {
|
|
73
|
+
type:
|
|
74
|
+
| "autocomplete"
|
|
75
|
+
| "editable"
|
|
76
|
+
| "form"
|
|
77
|
+
| "multiselect"
|
|
78
|
+
| "select"
|
|
79
|
+
| "survey"
|
|
80
|
+
| "list"
|
|
81
|
+
| "scale"
|
|
82
|
+
choices: (string | Choice)[]
|
|
83
|
+
maxChoices?: number
|
|
84
|
+
multiple?: boolean
|
|
85
|
+
initial?: number
|
|
86
|
+
delay?: number
|
|
87
|
+
separator?: boolean
|
|
88
|
+
sort?: boolean
|
|
89
|
+
linebreak?: boolean
|
|
90
|
+
edgeLength?: number
|
|
91
|
+
align?: "left" | "right"
|
|
92
|
+
scroll?: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface BooleanPromptOptions extends BasePromptOptions {
|
|
96
|
+
type: "confirm"
|
|
97
|
+
initial?: boolean
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface StringPromptOptions extends BasePromptOptions {
|
|
101
|
+
type: "input" | "invisible" | "list" | "password" | "text"
|
|
102
|
+
initial?: string
|
|
103
|
+
multiline?: boolean
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface NumberPromptOptions extends BasePromptOptions {
|
|
107
|
+
type: "numeral"
|
|
108
|
+
min?: number
|
|
109
|
+
max?: number
|
|
110
|
+
delay?: number
|
|
111
|
+
float?: boolean
|
|
112
|
+
round?: boolean
|
|
113
|
+
major?: number
|
|
114
|
+
minor?: number
|
|
115
|
+
initial?: number
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface SnippetPromptOptions extends BasePromptOptions {
|
|
119
|
+
type: "snippet"
|
|
120
|
+
newline?: string
|
|
121
|
+
template?: string
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface SortPromptOptions extends BasePromptOptions {
|
|
125
|
+
type: "sort"
|
|
126
|
+
hint?: string
|
|
127
|
+
drag?: boolean
|
|
128
|
+
numbered?: boolean
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type PromptOptions =
|
|
132
|
+
| BasePromptOptions
|
|
133
|
+
| ArrayPromptOptions
|
|
134
|
+
| BooleanPromptOptions
|
|
135
|
+
| StringPromptOptions
|
|
136
|
+
| NumberPromptOptions
|
|
137
|
+
| SnippetPromptOptions
|
|
138
|
+
| SortPromptOptions
|
|
139
|
+
|
|
140
|
+
declare class BasePrompt extends EventEmitter {
|
|
141
|
+
constructor( options?: PromptOptions )
|
|
142
|
+
render(): void
|
|
143
|
+
run(): Promise<any>
|
|
144
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Action, Context } from "../types.ts"
|
|
2
|
+
import type { WriteActionConfig } from "./write.ts"
|
|
3
|
+
|
|
4
|
+
export function renderTemplate( { fullpath, template, getData, write }: {
|
|
5
|
+
template: string
|
|
6
|
+
fullpath: string
|
|
7
|
+
getData?: ( ctx: Context ) => Record<string, unknown>
|
|
8
|
+
write?: ( config: WriteActionConfig ) => Action
|
|
9
|
+
} ): Action {
|
|
10
|
+
return function execute( params ) {
|
|
11
|
+
const data = getData?.( params.context ) ?? params.context
|
|
12
|
+
|
|
13
|
+
const renderedTemplate = params.renderer.renderString( { template, data } )
|
|
14
|
+
|
|
15
|
+
const renderedPath = params.renderer.renderString( { template: fullpath, data } )
|
|
16
|
+
|
|
17
|
+
if ( write ) {
|
|
18
|
+
return write( { content: renderedTemplate, destination: renderedPath } )
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { assert } from "@std/assert"
|
|
2
|
+
import type { Action, Config } from "../types.ts"
|
|
3
|
+
import { prompt } from "./prompt.ts"
|
|
4
|
+
import { Renderer } from "../renderer.ts"
|
|
5
|
+
import { runGenerator } from "../runner.ts"
|
|
6
|
+
|
|
7
|
+
export function selectGenerator( config: Config ): Action {
|
|
8
|
+
const choices = config.generators.map( ( { name, description } ) => ( { name, hint: description } ) )
|
|
9
|
+
|
|
10
|
+
return () => [
|
|
11
|
+
prompt( [ { type: "select", choices, message: "select", name: "generator" } ] ),
|
|
12
|
+
|
|
13
|
+
function runSelectedGenerator( params ) {
|
|
14
|
+
const generator = config.generators.find( ( generator ) =>
|
|
15
|
+
generator.name === params?.context?.answers?.generator
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
assert( generator )
|
|
19
|
+
|
|
20
|
+
return runGenerator( { context: { errors: [], answers: {} }, renderer: new Renderer(), generator } )
|
|
21
|
+
},
|
|
22
|
+
]
|
|
23
|
+
}
|
package/actions/use.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Action } from "../types.ts"
|
|
2
|
+
|
|
3
|
+
// TODO: implement use in a way, that it can use actions from some repository... Maybe...
|
|
4
|
+
|
|
5
|
+
export function use(): Action {
|
|
6
|
+
return function execute( _params ) {
|
|
7
|
+
throw new Error( "Action is not implemented" )
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Action } from "../types.ts"
|
|
2
|
+
import * as v from "valibot"
|
|
3
|
+
|
|
4
|
+
const primitiveSchema = v.union( [ v.pipe( v.string(), v.minLength( 1 ) ), v.boolean() ] )
|
|
5
|
+
|
|
6
|
+
const defaultSchema = v.record( v.string(), v.union( [ primitiveSchema, v.array( primitiveSchema ) ] ) )
|
|
7
|
+
|
|
8
|
+
export function validateAnswers<Schema extends v.BaseSchema<unknown, unknown, v.BaseIssue<unknown>>>(
|
|
9
|
+
schema?: Schema,
|
|
10
|
+
): Action {
|
|
11
|
+
return function execute( params ) {
|
|
12
|
+
v.assert( schema ?? defaultSchema, params.context.answers )
|
|
13
|
+
}
|
|
14
|
+
}
|
package/actions/write.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// TODO: get rid of `node:` imports and "del", "mkdirp" libraries
|
|
2
|
+
import { access } from "node:fs/promises"
|
|
3
|
+
|
|
4
|
+
import { dirname } from "@std/path"
|
|
5
|
+
|
|
6
|
+
import type { Action } from "../types.ts"
|
|
7
|
+
|
|
8
|
+
// type WriteActionConfig = {
|
|
9
|
+
|
|
10
|
+
// data?: Record<string, unknown>
|
|
11
|
+
// destination: string
|
|
12
|
+
// writeMode?: "skip-if-exists" | "force"
|
|
13
|
+
// } & (
|
|
14
|
+
// // |{templatePath: string}
|
|
15
|
+
// | { template: string }
|
|
16
|
+
// |
|
|
17
|
+
// )
|
|
18
|
+
|
|
19
|
+
export type WriteActionConfig = {
|
|
20
|
+
content: string
|
|
21
|
+
destination: string
|
|
22
|
+
mode?: "force" | "skip-if-exists"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function write( config: WriteActionConfig ): Action {
|
|
26
|
+
return async function execute() {
|
|
27
|
+
await Deno.mkdir( dirname( config.destination ), { recursive: true } )
|
|
28
|
+
|
|
29
|
+
let doesExist = await fileExists( config.destination )
|
|
30
|
+
|
|
31
|
+
if ( doesExist && config.mode === "force" ) {
|
|
32
|
+
await Deno.remove( config.destination, { recursive: true } )
|
|
33
|
+
doesExist = false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if ( doesExist && config.mode !== "skip-if-exists" ) {
|
|
37
|
+
throw `File already exists\n -> ${config.destination}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if ( doesExist && config.mode === "skip-if-exists" ) {
|
|
41
|
+
console.info( `[SKIPPED] ${config.destination} (exists)` )
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await Deno.writeFile( config.destination, new TextEncoder().encode( config.content ) )
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// if(eager) {
|
|
49
|
+
// return execute()
|
|
50
|
+
// } else {
|
|
51
|
+
// return execute
|
|
52
|
+
// }
|
|
53
|
+
// return async ( params ) => {
|
|
54
|
+
// await Deno.mkdir( dirname( config.destination ), { recursive: true } )
|
|
55
|
+
|
|
56
|
+
// // const template = ( await readFile( config.template ) ).toString()
|
|
57
|
+
|
|
58
|
+
// const rendered = params.renderer.renderString( { data: config.data ?? {}, template: template } )
|
|
59
|
+
|
|
60
|
+
// const doesExist = await fileExists( config.destination )
|
|
61
|
+
|
|
62
|
+
// if ( doesExist && config.writeMode === "force" ) {
|
|
63
|
+
// await Deno.remove( config.destination, { recursive: true } )
|
|
64
|
+
// }
|
|
65
|
+
|
|
66
|
+
// if ( doesExist ) {
|
|
67
|
+
// if ( config.writeMode === "skip-if-exists" ) {
|
|
68
|
+
// console.info( `[SKIPPED] ${config.destination} (exists)` )
|
|
69
|
+
// return
|
|
70
|
+
// }
|
|
71
|
+
|
|
72
|
+
// throw `File already exists\n -> ${config.destination}`
|
|
73
|
+
// }
|
|
74
|
+
|
|
75
|
+
// await Deno.writeFile( config.destination, new TextEncoder().encode( rendered ) )
|
|
76
|
+
// }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fileExists( destination: string ) {
|
|
80
|
+
return access( destination ).then(
|
|
81
|
+
() => true,
|
|
82
|
+
() => false,
|
|
83
|
+
)
|
|
84
|
+
}
|
package/deno.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeandmoney/jargal",
|
|
3
|
+
"version": "0.0.2-RC.3",
|
|
4
|
+
"description": "Renderer",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Code & Money Team",
|
|
7
|
+
"imports": {
|
|
8
|
+
"es-toolkit": "jsr:@es-toolkit/es-toolkit@^1.39.10",
|
|
9
|
+
"@std/assert": "jsr:@std/assert@^1.0.14",
|
|
10
|
+
"@std/path": "jsr:@std/path@^1.1.2",
|
|
11
|
+
"valibot": "jsr:@valibot/valibot@^1.1.0",
|
|
12
|
+
"change-case": "npm:change-case@^5.4.4",
|
|
13
|
+
"enquirer": "npm:enquirer@^2.4.1",
|
|
14
|
+
"handlebars": "npm:handlebars@^4.7.8",
|
|
15
|
+
"title-case": "npm:title-case@^4.3.2"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": "./mod.ts",
|
|
19
|
+
"./types": "./types.ts",
|
|
20
|
+
"./actions": "./actions/exports.ts"
|
|
21
|
+
},
|
|
22
|
+
"fmt": {
|
|
23
|
+
"semiColons": false,
|
|
24
|
+
"trailingCommas": "onlyMultiLine",
|
|
25
|
+
"quoteProps": "asNeeded",
|
|
26
|
+
"indentWidth": 2,
|
|
27
|
+
"lineWidth": 120,
|
|
28
|
+
"spaceAround": true
|
|
29
|
+
}
|
|
30
|
+
}
|
package/deno.lock
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "5",
|
|
3
|
+
"specifiers": {
|
|
4
|
+
"jsr:@es-toolkit/es-toolkit@^1.39.10": "1.39.10",
|
|
5
|
+
"jsr:@std/assert@^1.0.14": "1.0.14",
|
|
6
|
+
"jsr:@std/internal@^1.0.10": "1.0.10",
|
|
7
|
+
"jsr:@std/path@^1.1.2": "1.1.2",
|
|
8
|
+
"jsr:@valibot/valibot@^1.1.0": "1.1.0",
|
|
9
|
+
"npm:@types/node@*": "24.2.0",
|
|
10
|
+
"npm:change-case@^5.4.4": "5.4.4",
|
|
11
|
+
"npm:enquirer@^2.4.1": "2.4.1",
|
|
12
|
+
"npm:handlebars@^4.7.8": "4.7.8",
|
|
13
|
+
"npm:title-case@^4.3.2": "4.3.2"
|
|
14
|
+
},
|
|
15
|
+
"jsr": {
|
|
16
|
+
"@es-toolkit/es-toolkit@1.39.10": {
|
|
17
|
+
"integrity": "8757072a13aa64b3b349ba2b9d7d22fbe7ea6f138506c6cd2222d767cd79918f"
|
|
18
|
+
},
|
|
19
|
+
"@std/assert@1.0.14": {
|
|
20
|
+
"integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4",
|
|
21
|
+
"dependencies": [
|
|
22
|
+
"jsr:@std/internal"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"@std/internal@1.0.10": {
|
|
26
|
+
"integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7"
|
|
27
|
+
},
|
|
28
|
+
"@std/path@1.1.2": {
|
|
29
|
+
"integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038",
|
|
30
|
+
"dependencies": [
|
|
31
|
+
"jsr:@std/internal"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"@valibot/valibot@1.1.0": {
|
|
35
|
+
"integrity": "2617f02b532011b8140926899d420a3e1bbb0fcb7cdf8e7b669df89e7edd7f5f"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"npm": {
|
|
39
|
+
"@types/node@24.2.0": {
|
|
40
|
+
"integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
|
|
41
|
+
"dependencies": [
|
|
42
|
+
"undici-types"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"ansi-colors@4.1.3": {
|
|
46
|
+
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="
|
|
47
|
+
},
|
|
48
|
+
"ansi-regex@5.0.1": {
|
|
49
|
+
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
|
50
|
+
},
|
|
51
|
+
"change-case@5.4.4": {
|
|
52
|
+
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w=="
|
|
53
|
+
},
|
|
54
|
+
"enquirer@2.4.1": {
|
|
55
|
+
"integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
|
|
56
|
+
"dependencies": [
|
|
57
|
+
"ansi-colors",
|
|
58
|
+
"strip-ansi"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"handlebars@4.7.8": {
|
|
62
|
+
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
|
|
63
|
+
"dependencies": [
|
|
64
|
+
"minimist",
|
|
65
|
+
"neo-async",
|
|
66
|
+
"source-map",
|
|
67
|
+
"wordwrap"
|
|
68
|
+
],
|
|
69
|
+
"optionalDependencies": [
|
|
70
|
+
"uglify-js"
|
|
71
|
+
],
|
|
72
|
+
"bin": true
|
|
73
|
+
},
|
|
74
|
+
"minimist@1.2.8": {
|
|
75
|
+
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
|
|
76
|
+
},
|
|
77
|
+
"neo-async@2.6.2": {
|
|
78
|
+
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
|
|
79
|
+
},
|
|
80
|
+
"source-map@0.6.1": {
|
|
81
|
+
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
|
82
|
+
},
|
|
83
|
+
"strip-ansi@6.0.1": {
|
|
84
|
+
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
|
85
|
+
"dependencies": [
|
|
86
|
+
"ansi-regex"
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"title-case@4.3.2": {
|
|
90
|
+
"integrity": "sha512-I/nkcBo73mO42Idfv08jhInV61IMb61OdIFxk+B4Gu1oBjWBPOLmhZdsli+oJCVaD+86pYQA93cJfFt224ZFAA=="
|
|
91
|
+
},
|
|
92
|
+
"uglify-js@3.19.3": {
|
|
93
|
+
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
|
|
94
|
+
"bin": true
|
|
95
|
+
},
|
|
96
|
+
"undici-types@7.10.0": {
|
|
97
|
+
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="
|
|
98
|
+
},
|
|
99
|
+
"wordwrap@1.0.0": {
|
|
100
|
+
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"workspace": {
|
|
104
|
+
"dependencies": [
|
|
105
|
+
"jsr:@es-toolkit/es-toolkit@^1.39.10",
|
|
106
|
+
"jsr:@std/assert@^1.0.14",
|
|
107
|
+
"jsr:@std/path@^1.1.2",
|
|
108
|
+
"jsr:@valibot/valibot@^1.1.0",
|
|
109
|
+
"npm:change-case@^5.4.4",
|
|
110
|
+
"npm:enquirer@^2.4.1",
|
|
111
|
+
"npm:handlebars@^4.7.8",
|
|
112
|
+
"npm:title-case@^4.3.2"
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
}
|
package/lib.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// TODO: clean up
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
camelCase,
|
|
5
|
+
capitalCase,
|
|
6
|
+
constantCase,
|
|
7
|
+
dotCase,
|
|
8
|
+
kebabCase,
|
|
9
|
+
noCase,
|
|
10
|
+
pascalCase,
|
|
11
|
+
pascalSnakeCase,
|
|
12
|
+
pathCase,
|
|
13
|
+
sentenceCase,
|
|
14
|
+
snakeCase,
|
|
15
|
+
trainCase,
|
|
16
|
+
} from "change-case"
|
|
17
|
+
import { titleCase } from "title-case"
|
|
18
|
+
import { deburr, lowerFirst, startCase, trim } from "es-toolkit"
|
|
19
|
+
|
|
20
|
+
import type { ActionHooks, ActionHooksChanges, ActionHooksFailures, TextHelpers } from "./types.ts"
|
|
21
|
+
|
|
22
|
+
//
|
|
23
|
+
// -----
|
|
24
|
+
//
|
|
25
|
+
|
|
26
|
+
export const textHelpers: TextHelpers = {
|
|
27
|
+
upperCase: ( str ) => str.toUpperCase(),
|
|
28
|
+
lowerCase: ( str ) => str.toLowerCase(),
|
|
29
|
+
camelCase: ( str ) => camelCase( str ),
|
|
30
|
+
snakeCase: ( str ) => snakeCase( str ),
|
|
31
|
+
dotCase: ( str ) => dotCase( str ),
|
|
32
|
+
pathCase: ( str ) => pathCase( str ),
|
|
33
|
+
sentenceCase: ( str ) => sentenceCase( str ),
|
|
34
|
+
constantCase: ( str ) => constantCase( str ),
|
|
35
|
+
titleCase: ( str ) => titleCase( str ),
|
|
36
|
+
kebabCase: ( str ) => kebabCase( str ),
|
|
37
|
+
dashCase: ( str ) => kebabCase( str ),
|
|
38
|
+
kabobCase: ( str ) => kebabCase( str ),
|
|
39
|
+
pascalCase: ( str ) => pascalCase( str ),
|
|
40
|
+
properCase: ( str ) => pascalCase( str ),
|
|
41
|
+
deburr: ( str ) => deburr( str ),
|
|
42
|
+
lowerFirst: ( str ) => lowerFirst( str ),
|
|
43
|
+
startCase: ( str ) => startCase( str ),
|
|
44
|
+
noCase: ( str ) => noCase( str ),
|
|
45
|
+
capitalCase: ( str ) => capitalCase( str ),
|
|
46
|
+
pascalSnakeCase: ( str ) => pascalSnakeCase( str ),
|
|
47
|
+
trainCase: ( str ) => trainCase( str ),
|
|
48
|
+
trim: ( str ) => trim( str ),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//
|
|
52
|
+
// -----
|
|
53
|
+
//
|
|
54
|
+
|
|
55
|
+
// import { styleText } from "node:util"
|
|
56
|
+
// const typeDisplay = {
|
|
57
|
+
// function: styleText( "yellow", "->" ),
|
|
58
|
+
// add: styleText( "green", "++" ),
|
|
59
|
+
// addMany: styleText( "green", "+!" ),
|
|
60
|
+
// modify: `${`${styleText( "green", "+" )}${styleText( "red", "-" )}`}`,
|
|
61
|
+
// append: styleText( "green", "_+" ),
|
|
62
|
+
// skip: styleText( "green", "--" ),
|
|
63
|
+
// }
|
|
64
|
+
|
|
65
|
+
// function typeMap( name: keyof typeof typeDisplay, noMap: boolean = true ) {
|
|
66
|
+
// const dimType = styleText( "dim", name )
|
|
67
|
+
// return noMap ? dimType : typeDisplay[name] || dimType
|
|
68
|
+
// }
|
|
69
|
+
|
|
70
|
+
export class Hooks implements ActionHooks {
|
|
71
|
+
onComment( message: string ) {
|
|
72
|
+
console.log( message )
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
onSuccess( change: ActionHooksChanges ) {
|
|
76
|
+
// let line = ""
|
|
77
|
+
|
|
78
|
+
// if ( change.type ) {
|
|
79
|
+
// line += ` ${typeMap( change.type )}`
|
|
80
|
+
// }
|
|
81
|
+
|
|
82
|
+
// if ( change.path ) {
|
|
83
|
+
// line += ` ${change.path}`
|
|
84
|
+
// }
|
|
85
|
+
|
|
86
|
+
console.log( change )
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
onFailure( failure: ActionHooksFailures ) {
|
|
90
|
+
// let line = ""
|
|
91
|
+
|
|
92
|
+
// if ( failure.type ) {
|
|
93
|
+
// line += ` ${typeMap( failure.type )}`
|
|
94
|
+
// }
|
|
95
|
+
|
|
96
|
+
// if ( failure.path ) {
|
|
97
|
+
// line += ` ${failure.path}`
|
|
98
|
+
// }
|
|
99
|
+
|
|
100
|
+
// const errorMessage = failure.error || failure.message
|
|
101
|
+
|
|
102
|
+
// if ( errorMessage ) {
|
|
103
|
+
// line += ` ${errorMessage}`
|
|
104
|
+
// }
|
|
105
|
+
|
|
106
|
+
console.log( failure )
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//
|
|
111
|
+
// -----
|
|
112
|
+
//
|
|
113
|
+
|
|
114
|
+
const ReadOnlyProxyDescriptor = {
|
|
115
|
+
// deno-lint-ignore no-explicit-any ban-types
|
|
116
|
+
get<T extends object>( target: T, key: keyof T & (string & {}) ): any {
|
|
117
|
+
const value = target[key]
|
|
118
|
+
|
|
119
|
+
if ( !isObject( value ) ) {
|
|
120
|
+
return value
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return readonly( value )
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
set( _target: object, _key: string ) {
|
|
127
|
+
throw new ReadOnlyProxyWriteError( "Cannot write on read-only proxy" )
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
deleteProperty( _target: object, _key: string ) {
|
|
131
|
+
throw new ReadOnlyProxyWriteError( "Cannot delete on read-only proxy" )
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isObject( thing: unknown ) {
|
|
136
|
+
return thing !== null && typeof thing === "object"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class ReadOnlyProxyWriteError extends Error {
|
|
140
|
+
override name = "ReadOnlyProxyWriteError"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create a read-only proxy over an object.
|
|
144
|
+
// Sub-properties that are objects also return read-only proxies.
|
|
145
|
+
export function readonly<T extends object>( target: T ) {
|
|
146
|
+
return new Proxy<T>( target, ReadOnlyProxyDescriptor )
|
|
147
|
+
}
|
package/mod.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeandmoney/jargal",
|
|
3
|
+
"version": "0.0.2-RC.3",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"description": "Renderer",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Code & Money Team",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./mod.ts",
|
|
13
|
+
"./types": "./types.ts",
|
|
14
|
+
"./actions": "./actions/exports.ts"
|
|
15
|
+
},
|
|
16
|
+
"fmt": {
|
|
17
|
+
"semiColons": false,
|
|
18
|
+
"trailingCommas": "onlyMultiLine",
|
|
19
|
+
"quoteProps": "asNeeded",
|
|
20
|
+
"indentWidth": 2,
|
|
21
|
+
"lineWidth": 120,
|
|
22
|
+
"spaceAround": true
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^24.7.0",
|
|
26
|
+
"bun-types": "^1.2.23"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"change-case": "^5.4.4",
|
|
30
|
+
"enquirer": "^2.4.1",
|
|
31
|
+
"es-toolkit": "^1.40.0",
|
|
32
|
+
"handlebars": "^4.7.8",
|
|
33
|
+
"title-case": "^4.3.2",
|
|
34
|
+
"valibot": "^1.1.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/renderer.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// deno-lint-ignore-file no-explicit-any
|
|
2
|
+
|
|
3
|
+
import { get, set } from "es-toolkit/compat"
|
|
4
|
+
import handlebars from "handlebars"
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
GetReturnType,
|
|
8
|
+
HelperFn,
|
|
9
|
+
Helpers,
|
|
10
|
+
MappingScope,
|
|
11
|
+
Partials,
|
|
12
|
+
SetOptions,
|
|
13
|
+
Setter,
|
|
14
|
+
SetterScope,
|
|
15
|
+
} from "./types.ts"
|
|
16
|
+
import { readonly, textHelpers } from "./lib.ts"
|
|
17
|
+
|
|
18
|
+
export class Renderer {
|
|
19
|
+
#partials: Partials = {}
|
|
20
|
+
#helpers: Helpers = textHelpers
|
|
21
|
+
|
|
22
|
+
#mapping: { partial: Partials; helper: Helpers } = { partial: this.#partials, helper: this.#helpers }
|
|
23
|
+
|
|
24
|
+
#set<T extends SetterScope>( scope: T, config: Setter<T>, options?: SetOptions ): Renderer {
|
|
25
|
+
if ( !config.name ) {
|
|
26
|
+
throw new Error( "Name must be non-empty string" )
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const target = get( this.#mapping, scope )
|
|
30
|
+
|
|
31
|
+
if ( !target ) throw new Error( "No mapping" )
|
|
32
|
+
|
|
33
|
+
if ( config.name in target && !options?.override ) {
|
|
34
|
+
throw new Error( "Can't override" )
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
set( target, config.name, config.target )
|
|
38
|
+
|
|
39
|
+
return this
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public readonly partials: Partials = readonly( this.#partials )
|
|
43
|
+
public readonly helpers: Helpers = readonly( this.#helpers )
|
|
44
|
+
|
|
45
|
+
public get<T extends SetterScope>( scope: T, name: string ): GetReturnType<T> {
|
|
46
|
+
const target = get( this.#mapping, scope )
|
|
47
|
+
|
|
48
|
+
if ( !target ) {
|
|
49
|
+
throw new Error( "!" )
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return get( target, name )
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public list<T extends `${SetterScope}s`, O extends boolean = false>(
|
|
56
|
+
scope: T,
|
|
57
|
+
options?: { full?: O },
|
|
58
|
+
): O extends true ? MappingScope[T extends `${infer S}s` ? S : T][]
|
|
59
|
+
: string[] {
|
|
60
|
+
const target = get( this.#mapping, scope.slice( 0, scope.length - 1 ) )
|
|
61
|
+
|
|
62
|
+
if ( !target ) {
|
|
63
|
+
throw new Error( "No mapping" )
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if ( !options?.full ) {
|
|
67
|
+
return Object.keys( target ) as any
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
switch ( scope ) {
|
|
71
|
+
case "helpers":
|
|
72
|
+
return Object.entries( target ).map( ( [ name, helper ] ) => ( { name, helper } ) ) as any
|
|
73
|
+
|
|
74
|
+
case "partials":
|
|
75
|
+
return Object.entries( target ).map( ( [ name, partial ] ) => ( { name, partial } ) ) as any
|
|
76
|
+
|
|
77
|
+
default:
|
|
78
|
+
throw new Error( "can't find the scope" )
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public setPartial( name: string, partial: string, options?: SetOptions ): Renderer {
|
|
83
|
+
return this.#set( "partial", { target: partial, name }, options )
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public setHelper( name: string, helper: HelperFn, options?: SetOptions ): Renderer {
|
|
87
|
+
return this.#set( "helper", { target: helper, name }, options )
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public renderString( params: { template: string; data: Record<string, unknown> } ): string {
|
|
91
|
+
// TODO: do this once
|
|
92
|
+
for ( const [ name, helper ] of Object.entries( this.#helpers ) ) {
|
|
93
|
+
handlebars.registerHelper( name, helper )
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// TODO: do this once
|
|
97
|
+
for ( const [ name, partial ] of Object.entries( this.#partials ) ) {
|
|
98
|
+
handlebars.registerPartial( name, partial )
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const compiled = handlebars.compile( params.template )
|
|
102
|
+
|
|
103
|
+
return compiled( params.data )
|
|
104
|
+
}
|
|
105
|
+
}
|
package/runner.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { assert } from "@std/assert"
|
|
2
|
+
import { Renderer } from "./renderer.ts"
|
|
3
|
+
import * as v from "valibot"
|
|
4
|
+
|
|
5
|
+
import type { Action, Config, ExecuteActionParams, GeneratorParams } from "./types.ts"
|
|
6
|
+
import { selectGenerator } from "./actions/select_generator.ts"
|
|
7
|
+
|
|
8
|
+
export async function runGenerator( { context, generator, renderer }: GeneratorParams ): Promise<void> {
|
|
9
|
+
assert( generator )
|
|
10
|
+
assert( Array.isArray( generator.actions ) && generator.actions.length > 0 )
|
|
11
|
+
|
|
12
|
+
for ( const action of generator.actions ) {
|
|
13
|
+
await executeAction( { action, context, renderer } )
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function executeAction(
|
|
18
|
+
{ action, context, renderer }: ExecuteActionParams,
|
|
19
|
+
): Promise<void | Action | Action[]> {
|
|
20
|
+
if ( Array.isArray( action ) ) {
|
|
21
|
+
for ( const action_ of action ) {
|
|
22
|
+
await executeAction( { action: action_, context, renderer } )
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const executed = await action( { context, renderer } )
|
|
27
|
+
|
|
28
|
+
if ( !executed ) {
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return await execRecursive( executed, { context, renderer } )
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function execRecursive(
|
|
36
|
+
executed: Action | Action[],
|
|
37
|
+
{ context, renderer }: Omit<ExecuteActionParams, "action">,
|
|
38
|
+
): Promise<Action | Action[] | void> {
|
|
39
|
+
if ( Array.isArray( executed ) ) {
|
|
40
|
+
const executionResults: (Action)[] = []
|
|
41
|
+
|
|
42
|
+
for ( const action of executed ) {
|
|
43
|
+
const result = await executeAction( { action, context, renderer } )
|
|
44
|
+
|
|
45
|
+
if ( result ) {
|
|
46
|
+
if ( Array.isArray( result ) ) {
|
|
47
|
+
executionResults.push( ...result.flat() )
|
|
48
|
+
} else {
|
|
49
|
+
executionResults.push( result )
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return executionResults
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if ( typeof executed === "function" ) {
|
|
58
|
+
return await executeAction( { action: executed, context, renderer } )
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
assert( !executed )
|
|
62
|
+
|
|
63
|
+
return undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ConfigSchema = v.object( {
|
|
67
|
+
generators: v.pipe(
|
|
68
|
+
v.array(
|
|
69
|
+
v.object( {
|
|
70
|
+
name: v.string(),
|
|
71
|
+
description: v.optional( v.string() ),
|
|
72
|
+
actions: v.pipe( v.array( v.any() ), v.minLength( 1 ) ),
|
|
73
|
+
} ),
|
|
74
|
+
),
|
|
75
|
+
v.minLength( 1 ),
|
|
76
|
+
),
|
|
77
|
+
} )
|
|
78
|
+
|
|
79
|
+
export async function run( config_: Config ): Promise<void> {
|
|
80
|
+
const config = v.parse( ConfigSchema, config_ )
|
|
81
|
+
|
|
82
|
+
if ( config.generators.length === 1 ) {
|
|
83
|
+
return await runGenerator( {
|
|
84
|
+
context: { errors: [], answers: {} },
|
|
85
|
+
renderer: new Renderer(),
|
|
86
|
+
generator: config.generators[0],
|
|
87
|
+
} )
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return await runGenerator( {
|
|
91
|
+
context: { errors: [], answers: {} },
|
|
92
|
+
renderer: new Renderer(),
|
|
93
|
+
generator: {
|
|
94
|
+
name: "select",
|
|
95
|
+
actions: [
|
|
96
|
+
selectGenerator( config ),
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
} )
|
|
100
|
+
}
|
package/runner_test.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { run } from "./runner.ts"
|
|
2
|
+
import { assert } from "@std/assert"
|
|
3
|
+
import { join } from "@std/path"
|
|
4
|
+
import { blank1, blank2 } from "./actions/blank.ts"
|
|
5
|
+
|
|
6
|
+
import type { Config, GeneratorConfig } from "./types.ts"
|
|
7
|
+
import { loadTemplates } from "./actions/load_templates.ts"
|
|
8
|
+
import { context } from "./actions/context.ts"
|
|
9
|
+
|
|
10
|
+
Deno.test("simple", async () => {
|
|
11
|
+
const simple: GeneratorConfig = {
|
|
12
|
+
name: "simple",
|
|
13
|
+
description: "description",
|
|
14
|
+
actions: [
|
|
15
|
+
blank1,
|
|
16
|
+
blank2,
|
|
17
|
+
context( () => ( { kek: Math.random() } ) ),
|
|
18
|
+
console.log,
|
|
19
|
+
loadTemplates( join( Deno.cwd(), "actions" ) ),
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const config: Config = { generators: [ simple ] }
|
|
24
|
+
const result = await run( config )
|
|
25
|
+
|
|
26
|
+
assert( typeof result === "undefined" )
|
|
27
|
+
})
|
package/types.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// deno-lint-ignore-file
|
|
2
|
+
|
|
3
|
+
import type { Renderer } from "./renderer.ts"
|
|
4
|
+
|
|
5
|
+
export interface Config {
|
|
6
|
+
generators: GeneratorConfig[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Context extends Record<string, unknown> {
|
|
10
|
+
answers: Record<string, string | boolean | (string | boolean)[]>
|
|
11
|
+
errors: Error[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type DeepReadonly<T> = {
|
|
15
|
+
readonly [Key in keyof T]: T[Key] extends any[] ? T[Key]
|
|
16
|
+
: T[Key] extends object ? DeepReadonly<T[Key]>
|
|
17
|
+
: T[Key]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GeneratorParams {
|
|
21
|
+
context: Context
|
|
22
|
+
renderer: Renderer
|
|
23
|
+
generator: GeneratorConfig
|
|
24
|
+
hooks?: ActionHooks
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ActionParams {
|
|
28
|
+
context: Context
|
|
29
|
+
renderer: Renderer
|
|
30
|
+
hooks?: ActionHooks
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ExecuteActionParams {
|
|
34
|
+
context: Context
|
|
35
|
+
renderer: Renderer
|
|
36
|
+
action: Action
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface GeneratorConfig {
|
|
40
|
+
name: string
|
|
41
|
+
// TODO: implement commented out interface
|
|
42
|
+
description?: string // | (( params: unknown ) => string)
|
|
43
|
+
// TODO: implement commented out interface
|
|
44
|
+
actions: Action[] // | Promise<Action[]> | (( params: unknown ) => Action[] | Promise<Action[]>)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type HelperFn = ( str: string ) => string
|
|
48
|
+
|
|
49
|
+
export interface TextHelpers extends Record<string, HelperFn> {}
|
|
50
|
+
|
|
51
|
+
export type Action = ( params: ActionParams ) => void | Action | Action[] | Promise<void | Action | Action[]>
|
|
52
|
+
|
|
53
|
+
export interface ActionHooksFailures {
|
|
54
|
+
path: string
|
|
55
|
+
error: string
|
|
56
|
+
message?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ActionHooksChanges {
|
|
60
|
+
path: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ActionHooks {
|
|
64
|
+
onComment?: ( msg: string ) => void
|
|
65
|
+
onSuccess?: ( change: ActionHooksChanges ) => void
|
|
66
|
+
onFailure?: ( failure: ActionHooksFailures ) => void
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type SetterScope = "helper" | "partial"
|
|
70
|
+
|
|
71
|
+
export interface SetOptions {
|
|
72
|
+
override?: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface MappingScope {
|
|
76
|
+
helper: { name: string; target: HelperFn }
|
|
77
|
+
partial: { name: string; target: string }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface Mapping {
|
|
81
|
+
helper: HelperFn
|
|
82
|
+
partial: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type GetSetterType<Key extends SetterScope> = Mapping[Key]
|
|
86
|
+
|
|
87
|
+
export interface Setter<T extends SetterScope> {
|
|
88
|
+
target: GetSetterType<T>
|
|
89
|
+
name: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type GetReturnType<T extends SetterScope> = Mapping[T]
|
|
93
|
+
|
|
94
|
+
export interface Partials extends Record<string, string> {}
|
|
95
|
+
export interface Helpers extends Record<string, any> {}
|
|
96
|
+
|
|
97
|
+
export type ContextAction = ( context: DeepReadonly<Context> ) => Partial<Context> | Promise<Partial<Context>>
|