@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.
@@ -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
@@ -0,0 +1,2 @@
1
+ 1. Write tests
2
+ 2. Document
package/actions/ai.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { Action } from "../types.ts"
2
+
3
+ export function ai(): Action {
4
+ return function execute( _params ) {
5
+ throw new Error( "Action is not implemented" )
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ import type { Action } from "../types.ts"
2
+
3
+ export function answer(): Action {
4
+ return function execute( _params ) {
5
+ throw new Error( "Action is not implemented" )
6
+ }
7
+ }
@@ -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,7 @@
1
+ import type { Action } from "../types.ts"
2
+
3
+ export function config(): Action {
4
+ return function execute( _params ) {
5
+ throw new Error( "Action is not implemented" )
6
+ }
7
+ }
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import type { Action } from "../types.ts"
2
+
3
+ export function echo( message?: string ): Action {
4
+ return function execute() {
5
+ console.log( message ?? "Hello, World!" )
6
+ }
7
+ }
@@ -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,7 @@
1
+ import type { Action } from "../types.ts"
2
+
3
+ export function modify(): Action {
4
+ return function execute( _params ) {
5
+ throw new Error( "Action is not implemented" )
6
+ }
7
+ }
@@ -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
+ }
@@ -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
@@ -0,0 +1,8 @@
1
+ // Run
2
+ export { run } from "./runner.ts"
3
+
4
+ // Helpers
5
+ export { textHelpers } from "./lib.ts"
6
+
7
+ // Renderer
8
+ export { Renderer } from "./renderer.ts"
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>>