yini-cli 1.0.0-alpha.2

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.
Files changed (41) hide show
  1. package/.github/workflows/publish.yml +30 -0
  2. package/.github/workflows/run-all-tests.yml +51 -0
  3. package/.github/workflows/run-smoke-tests.yml +33 -0
  4. package/.nvmrc +1 -0
  5. package/.vscode/settings.json +3 -0
  6. package/CONTRIBUTING.md +6 -0
  7. package/README.md +68 -0
  8. package/dist/commands/info.d.ts +1 -0
  9. package/dist/commands/parse.d.ts +2 -0
  10. package/dist/commands/validate.d.ts +7 -0
  11. package/dist/config/env.d.ts +34 -0
  12. package/dist/descriptions.d.ts +6 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +162 -0
  15. package/dist/types.d.ts +8 -0
  16. package/dist/utils/print.d.ts +14 -0
  17. package/docs/contributing.md +28 -0
  18. package/docs/project-setup.md +32 -0
  19. package/eslint.config.js +40 -0
  20. package/package.json +93 -0
  21. package/prettier.config.cjs +33 -0
  22. package/sample.yini +19 -0
  23. package/samples/basic.yini +11 -0
  24. package/samples/nested.yini +26 -0
  25. package/src/commands/info.ts +19 -0
  26. package/src/commands/parse.ts +88 -0
  27. package/src/commands/validate.ts +48 -0
  28. package/src/config/env.ts +85 -0
  29. package/src/descriptions.ts +6 -0
  30. package/src/index.ts +182 -0
  31. package/src/types.ts +9 -0
  32. package/src/utils/print.ts +49 -0
  33. package/tests/fixtures/corrupt-config-1.yini +8 -0
  34. package/tests/fixtures/invalid-config-1.yini +2 -0
  35. package/tests/fixtures/nested-config-1.yini +7 -0
  36. package/tests/fixtures/valid-config-1.yini +5 -0
  37. package/tests/general.test.ts +86 -0
  38. package/tests/smoke.test.ts +145 -0
  39. package/tests/test-helpers.ts +9 -0
  40. package/tsconfig.json +16 -0
  41. package/vitest.config.ts +8 -0
@@ -0,0 +1,33 @@
1
+ // prettier.config.js
2
+ /** @type {import("prettier").Config} */
3
+ module.exports = {
4
+ // ────────────────────────────────────────────────────────────────
5
+ // Formatting Options
6
+ // ─
7
+ useTabs: false,
8
+
9
+ // Maximum line length before Prettier wraps.
10
+ // "printWidth": 120,
11
+ printWidth: 80,
12
+
13
+ // Number of spaces per indentation level.
14
+ tabWidth: 4,
15
+ singleQuote: true,
16
+ trailingComma: "all",
17
+ jsxBracketSameLine: true,
18
+ semi: false,
19
+ // Whether to add a space between brackets in object literals
20
+ bracketSpacing: true,
21
+ // Since prettier 3.0, manually specifying plugins is required
22
+ plugins: ['@ianvs/prettier-plugin-sort-imports'],
23
+ // This plugin's options
24
+ importOrder: [
25
+ '^react$', // Put React first
26
+ '<THIRD_PARTY_MODULES>', // Then other external dependencies
27
+ '^[@/](.*)$', // Then “@/…” or “@alias/…” imports
28
+ '^[./]', // Then relative imports
29
+ ],
30
+ importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
31
+ importOrderTypeScriptVersion: '5.0.0',
32
+ importOrderCaseSensitive: false,
33
+ }
package/sample.yini ADDED
@@ -0,0 +1,19 @@
1
+ @YINI
2
+
3
+ /*
4
+ Example of a YINI document.
5
+ */
6
+
7
+ ^ Service // Defines a section named Server.
8
+ Enabled = true
9
+
10
+ ^^ Cache
11
+ Type = "redis" // Defines Cache, a sub-section of Server.
12
+ TTL = 3600
13
+
14
+ ^^^ Options // Defines Options, a sub-section of Cache.
15
+ Host = "127.0.0.1"
16
+ Port = 6379
17
+
18
+ ^ Env // Defines a section named Env.
19
+ code = "dev"
@@ -0,0 +1,11 @@
1
+ @yini
2
+
3
+ ^ App
4
+ title = 'My App'
5
+ items = 10
6
+ debug = ON
7
+
8
+ ^ Server
9
+ host = "localhost"
10
+ port = 8080
11
+ useTLS = OFF
@@ -0,0 +1,26 @@
1
+ /*
2
+ nested.yini
3
+ Demonstrates nested sections in YINI format.
4
+ */
5
+
6
+ @yini
7
+
8
+ ^ App // Top-level section: App
9
+ name = 'Nested Demo App'
10
+ version = "1.2.3"
11
+
12
+ ^^ Theme // Nested under App: App.Theme
13
+ primaryColor = #336699
14
+ darkMode = true
15
+
16
+
17
+ ^^^ Overrides // Nested under Theme: App.Theme.Overrides
18
+ darkMode = false
19
+ fontSize = 14
20
+
21
+ ^ Database // Another top-level section: Database
22
+ host = "db.local"
23
+ port = 5432
24
+
25
+ ^^ Credentials // Nested under Database: Database.Credentials username = "admin"
26
+ password = "secret"
@@ -0,0 +1,19 @@
1
+ // import pkg from '../../package.json'
2
+ import { createRequire } from 'module'
3
+
4
+ const require = createRequire(import.meta.url)
5
+ const pkg = require('../../package.json')
6
+
7
+ // import pkg from '../../package.json' assert { type: 'json' }
8
+
9
+ export const printInfo = () => {
10
+ console.log(`** YINI CLI **`)
11
+ console.log(`yini-cli: ${pkg.version}`)
12
+ console.log(
13
+ `yini-parser: ${pkg.dependencies['yini-parser'].replace('^', '')}`,
14
+ )
15
+ console.log(`Author: ${pkg.author}`)
16
+ console.log(`License: ${pkg.license}`)
17
+ console.log(`Homepage: ${pkg.homepage}`)
18
+ console.log('Repo: https://github.com/YINI-lang/yini-cli')
19
+ }
@@ -0,0 +1,88 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import util from 'util'
4
+ import YINI from 'yini-parser'
5
+ import { ICLIParseOptions, TBailSensitivity } from '../types.js'
6
+ import { printObject, toPrettyJSON } from '../utils/print.js'
7
+
8
+ type TOutputStype = 'JS-style' | 'Pretty-JSON' | 'Console.log' | 'JSON-compact'
9
+
10
+ export const parseFile = (file: string, options: ICLIParseOptions) => {
11
+ const outputFile = options.output || ''
12
+ const isStrictMode = !!options.strict
13
+ let outputStyle: TOutputStype = 'JS-style'
14
+
15
+ console.log('file = ' + file)
16
+ console.log('output = ' + options.output)
17
+ console.log('options:')
18
+ printObject(options)
19
+
20
+ if (options.pretty) {
21
+ outputStyle = 'Pretty-JSON'
22
+ } else if (options.log) {
23
+ outputStyle = 'Console.log'
24
+ } else if (options.json) {
25
+ outputStyle = 'JSON-compact'
26
+ } else {
27
+ outputStyle = 'JS-style'
28
+ }
29
+
30
+ doParseFile(file, outputStyle, isStrictMode, outputFile)
31
+ }
32
+
33
+ const doParseFile = (
34
+ file: string,
35
+ outputStyle: TOutputStype,
36
+ isStrictMode = false,
37
+ outputFile = '',
38
+ ) => {
39
+ // let strictMode = !!options.strict
40
+ let bailSensitivity: TBailSensitivity = 'auto'
41
+ let includeMetaData = false
42
+
43
+ console.log('File = ' + file)
44
+ console.log('outputStyle = ' + outputStyle)
45
+
46
+ // try {
47
+ // const raw = fs.readFileSync(file, 'utf-8')
48
+ // const parsed = YINI.parseFile(
49
+ //const parsed = YINI.parseFile(file)
50
+ const parsed = YINI.parseFile(
51
+ file,
52
+ isStrictMode,
53
+ bailSensitivity,
54
+ includeMetaData,
55
+ )
56
+ // const parsed = YINI.parse(raw)
57
+
58
+ // const output = options.pretty
59
+ // ? // ? JSON.stringify(parsed, null, 2)
60
+ // toPrettyJSON(parsed)
61
+ // : JSON.stringify(parsed)
62
+ let output = ''
63
+ switch (outputStyle) {
64
+ case 'Pretty-JSON':
65
+ output = toPrettyJSON(parsed)
66
+ break
67
+ case 'Console.log':
68
+ output = '<todo>'
69
+ break
70
+ case 'JSON-compact':
71
+ output = JSON.stringify(parsed)
72
+ break
73
+ default:
74
+ output = util.inspect(parsed, { depth: null, colors: false })
75
+ }
76
+
77
+ if (outputFile) {
78
+ // Write JSON output to file instead of stdout.
79
+ fs.writeFileSync(path.resolve(outputFile), output, 'utf-8')
80
+ console.log(`Output written to ${outputFile}`)
81
+ } else {
82
+ console.log(output)
83
+ }
84
+ // } catch (err: any) {
85
+ // console.error(`Error: ${err.message}`)
86
+ // process.exit(1)
87
+ // }
88
+ }
@@ -0,0 +1,48 @@
1
+ import fs from 'node:fs'
2
+ import { exit } from 'node:process'
3
+ import YINI from 'yini-parser'
4
+ import { printObject } from '../utils/print.js'
5
+
6
+ interface IValidateOptions {
7
+ strict?: boolean
8
+ details?: boolean
9
+ silent?: boolean
10
+ }
11
+
12
+ export const validateFile = (file: string, options: IValidateOptions = {}) => {
13
+ try {
14
+ const content = fs.readFileSync(file, 'utf-8')
15
+ const isMeta = true
16
+ const parsed = YINI.parse(
17
+ content,
18
+ options.strict ?? false,
19
+ 'auto',
20
+ isMeta,
21
+ )
22
+
23
+ if (!options.silent) {
24
+ console.log(
25
+ `✔ File is valid${options.strict ? ' (strict mode)' : ''}.`,
26
+ )
27
+
28
+ if (options.details) {
29
+ //@todo format parsed.meta to details as
30
+ /*
31
+ * Details:
32
+ * - YINI version: 1.0.0-beta.6
33
+ * - Mode: strict
34
+ * - Keys: 42
35
+ * - Sections: 6
36
+ * - Nesting depth: 3
37
+ * - Has @yini: true
38
+ */
39
+
40
+ printObject(parsed.meta)
41
+ }
42
+ }
43
+ exit(0)
44
+ } catch (err: any) {
45
+ console.error(`✖ Validation failed: ${err.message}`)
46
+ exit(1)
47
+ }
48
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * NODE_ENV - Defacto Node.js modes (environments)
3
+ *
4
+ * Used in many JS frameworks and tools, for special purposes.
5
+ * Some even only know 'production' and treat everything else as 'development'.
6
+ * Also Jest sets NODE_ENV automatically to 'test'.
7
+ */
8
+ type TNodeEnv = 'development' | 'production' | 'test'
9
+
10
+ /**
11
+ * APP_ENV - More custom envs (more finer-grained control) for this project.
12
+ * @note Since this is a library (as opposed to a Web/App), we don't use "staging".
13
+ */
14
+ type TAppEnv = 'local' | 'ci' | 'production' // Note: 'staging' is omitted by purpose.
15
+
16
+ const localNodeEnv = (process.env.NODE_ENV || 'production') as TNodeEnv
17
+ const localAppEnv = (process.env.APP_ENV || 'production') as TAppEnv
18
+
19
+ // export const initEnvs = () => {
20
+ // const localNodeEnv = (process.env.NODE_ENV || 'production') as TNodeEnv
21
+ // const localAppEnv = (process.env?.APP_ENV || 'production') as TAppEnv
22
+
23
+ // return { localNodeEnv, localAppEnv }
24
+ // }
25
+
26
+ // const { localNodeEnv, localAppEnv } = initEnvs()
27
+
28
+ /** Are we running in the environment "development"? Will be based on the (global) environment variable process.env.NODE_ENV. */
29
+ export const isDevEnv = (): boolean => localNodeEnv === 'development'
30
+
31
+ /** Are we running in the environment "production"? Will be based on the (global) environment variable process.env.NODE_ENV. */
32
+ export const isProdEnv = (): boolean => localNodeEnv === 'production'
33
+
34
+ /** Are we running in the environment "test"? Will be based on the (global) variable process.env.NODE_ENV. */
35
+ export const isTestEnv = (): boolean => localNodeEnv === 'test'
36
+
37
+ /** Will be based on the local argument when this process was launched.
38
+ * @returns True if the DEV flag is set.
39
+ * @example npm run start -- isDev=1
40
+ * @example node dist/index.js isDev=1
41
+ */
42
+ export const isDev = (): boolean => {
43
+ const len = process.argv.length
44
+
45
+ // NOTE: We will start with index 2, since the first element will be
46
+ // execPath. The second element will be the path to the
47
+ // JavaScript file being executed.
48
+ for (let i = 2; i < len; i++) {
49
+ const val: string = process.argv[i] || ''
50
+ if (
51
+ val.toLowerCase() === 'isdev=1' ||
52
+ val.toLowerCase() === 'isdev=true'
53
+ ) {
54
+ return true
55
+ }
56
+ }
57
+
58
+ return false
59
+ }
60
+
61
+ /** Will be based on the local argument when this process was launched.
62
+ * @returns True if the DEBUG flag is set.
63
+ * @example npm run start -- isDebug=1
64
+ * @example node dist/index.js isDebug=1
65
+ */
66
+ export const isDebug = (): boolean => {
67
+ const len = process.argv.length
68
+
69
+ // NOTE: We will start with index 2, since the first element will be
70
+ // execPath. The second element will be the path to the
71
+ // JavaScript file being executed.
72
+ for (let i = 2; i < len; i++) {
73
+ const val: string = process.argv[i] || ''
74
+ if (
75
+ val.toLowerCase() === 'isdebug=1' ||
76
+ val.toLowerCase() === 'isdebug=true'
77
+ ) {
78
+ return true
79
+ }
80
+ }
81
+
82
+ return false
83
+ }
84
+
85
+ export { localNodeEnv, localAppEnv }
@@ -0,0 +1,6 @@
1
+ export const descriptions = {
2
+ yini: 'CLI for parsing and validating YINI config files',
3
+ 'For-command-info': 'Show extended information (details, links, etc.)',
4
+ 'For-command-parse': 'Parse a YINI file and print the result',
5
+ 'For-command-validate': 'Checks if the file can be parsed as valid YINI',
6
+ }
package/src/index.ts ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ // (!) NOTE: Leave above shebang as first line!
3
+
4
+ // import pkg from '../package.json'
5
+ import { createRequire } from 'module'
6
+ import { Command } from 'commander'
7
+ import { printInfo } from './commands/info.js'
8
+ import { parseFile } from './commands/parse.js'
9
+ import { validateFile } from './commands/validate.js'
10
+ import { isDebug } from './config/env.js'
11
+ import { descriptions as descripts } from './descriptions.js'
12
+ import { debugPrint, toPrettyJSON } from './utils/print.js'
13
+
14
+ const require = createRequire(import.meta.url)
15
+ const pkg = require('../package.json')
16
+
17
+ const program = new Command()
18
+
19
+ /*
20
+
21
+ Idea/suggestion
22
+ yini [parse] [--strict] [--pretty] [--output]
23
+
24
+ Current suggestion:
25
+ * yini parse config.yini
26
+ JS-style object using printObject()
27
+ to stdout
28
+ * yini parse config.yini --pretty
29
+ Pretty JSON using JSON.stringify(obj, null, 4)
30
+ to stdout
31
+ * yini parse config.yini --output out.txt
32
+ JS-style object
33
+ to out.txt
34
+ * yini parse config.yini --pretty --output out.json
35
+ Pretty JSON
36
+ to out.json
37
+
38
+ New suggestion:
39
+ Current suggestion:
40
+ * yini parse config.yini
41
+ JS-style object using printObject(obj) (using using util.inspect)
42
+ to stdout
43
+ * yini parse config.yini --pretty
44
+ Pretty JSON using JSON.stringify(obj, null, 4) (formatted, readable)
45
+ to stdout
46
+ * yini parse config.yini --log
47
+ Intended for quick output using console.log (nested object may get compacted/abbreviate)
48
+ to stdout
49
+ * yini parse config.yini --json
50
+ Stringigies JSON using using JSON.stringify(obj) (compact, machine-parseable)
51
+ to stdout
52
+ * yini parse config.yini --output out.txt
53
+ JS-style object
54
+ to out.txt
55
+ * yini parse config.yini --pretty --output out.json
56
+ Pretty JSON
57
+ to out.json
58
+
59
+ */
60
+
61
+ // Display help for command
62
+ program.name('yini').description(descripts.yini).version(pkg.version)
63
+
64
+ program.addHelpText(
65
+ 'before',
66
+ `YINI CLI (Yet another INI)
67
+
68
+ For parsing and validating YINI configuration files.
69
+ A human-friendly config format - like INI, but with type-safe values,
70
+ nested sections, comments, minimal syntax noise, and optional strict mode.
71
+
72
+ Crafted for clarity, consistency, and the simple joy of it. :)`,
73
+ )
74
+ program.addHelpText(
75
+ 'after',
76
+ `
77
+ Examples:
78
+ $ yini parse config.yini
79
+ $ yini validate config.yini --strict
80
+ $ yini parse config.yini --pretty --output out.json
81
+
82
+ More info: https://github.com/YINI-lang/yini-parser
83
+ `,
84
+ )
85
+
86
+ //program.command('help [command]').description('Display help for command')
87
+
88
+ // Command info
89
+ program
90
+ .command('info')
91
+ // .command('')
92
+ .description(descripts['For-command-info'])
93
+ // .option('info')
94
+ .action((options) => {
95
+ debugPrint('Run command "info"')
96
+ if (isDebug()) {
97
+ console.log('options:')
98
+ console.log(toPrettyJSON(options))
99
+ }
100
+ printInfo()
101
+ })
102
+
103
+ /**
104
+ *
105
+ * Maybe later, to as default command: parse <parse>
106
+ */
107
+ // program
108
+ // .argument('<file>', 'File to parse')
109
+ // .option('--strict', 'Parse YINI in strict-mode')
110
+ // .option('--pretty', 'Pretty-print output as JSON')
111
+ // // .option('--log', 'Use console.log output format (compact, quick view)')
112
+ // .option('--json', 'Compact JSON output using JSON.stringify')
113
+ // .option('--output <file>', 'Write output to a specified file')
114
+ // .action((file, options) => {
115
+ // if (file) {
116
+ // parseFile(file, options)
117
+ // } else {
118
+ // program.help()
119
+ // }
120
+ // })
121
+
122
+ // Explicit "parse" command
123
+ program
124
+ .command('parse <file>')
125
+ .description(descripts['For-command-parse'])
126
+ .option('--strict', 'Parse YINI in strict-mode')
127
+ .option('--pretty', 'Pretty-print output as JSON')
128
+ // .option('--log', 'Use console.log output format (compact, quick view)')
129
+ .option('--json', 'Compact JSON output using JSON.stringify')
130
+ .option('--output <file>', 'Write output to a specified file')
131
+ .action((file, options) => {
132
+ debugPrint('Run command "parse"')
133
+ debugPrint(`<file> = ${file}`)
134
+ if (isDebug()) {
135
+ console.log('options:')
136
+ console.log(toPrettyJSON(options))
137
+ }
138
+ if (file) {
139
+ parseFile(file, options)
140
+ } else {
141
+ program.help()
142
+ }
143
+ })
144
+
145
+ /**
146
+ * To handle command validate, e.g.:
147
+ * yini validate config.yini
148
+ * yini validate config.yini --strict
149
+ * yini validate config.yini --details
150
+ * yini validate config.yini --silent
151
+ *
152
+ * If details:
153
+ * Details:
154
+ * - YINI version: 1.0.0-beta.6
155
+ * - Mode: strict
156
+ * - Keys: 42
157
+ * - Sections: 6
158
+ * - Nesting depth: 3
159
+ * - Has @yini: true
160
+ */
161
+ program
162
+ .command('validate <file>')
163
+ .description(descripts['For-command-validate'])
164
+ .option('--strict', 'Enable parsing in strict-mode')
165
+ .option(
166
+ '--details',
167
+ 'Print detailed meta-data info (e.g., key count, nesting, etc.)',
168
+ )
169
+ .option('--silent', 'Suppress output')
170
+ .action((file, options) => {
171
+ //@todo add debugPrint
172
+ if (file) {
173
+ validateFile(file, options)
174
+ } else {
175
+ program.help()
176
+ }
177
+ })
178
+
179
+ // NOTE: Converting YINI files to other formats than json and js.
180
+ // Other format should go into a new CLI-command called 'yini-convert'.
181
+
182
+ program.parseAsync()
package/src/types.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface ICLIParseOptions {
2
+ strict?: boolean
3
+ pretty?: boolean
4
+ log?: boolean
5
+ json?: boolean
6
+ output?: string
7
+ }
8
+
9
+ export type TBailSensitivity = 'auto' | 0 | 1 | 2
@@ -0,0 +1,49 @@
1
+ /**
2
+ * This file contains general system helper functions (utils).
3
+ * @note More specific YINI helper functions should go into yiniHelpers.ts-file.
4
+ */
5
+ import util from 'util'
6
+ import { isDebug, isDev, isProdEnv, isTestEnv } from '../config/env.js'
7
+
8
+ // import { isDebug, isDev, isProdEnv, isTestEnv } from '../config/env'
9
+
10
+ export const debugPrint = (str: any = '') => {
11
+ isDebug() && console.debug('DEBUG: ' + str)
12
+ console.debug('DEBUG: ' + str)
13
+ }
14
+
15
+ export const devPrint = (str: any = '') => {
16
+ isDev() && !isTestEnv() && console.log('DEV: ' + str)
17
+ console.log('DEV: ' + str)
18
+ }
19
+
20
+ export const toJSON = (obj: any): string => {
21
+ const str = JSON.stringify(obj)
22
+ return str
23
+ }
24
+
25
+ export const toPrettyJSON = (obj: any): string => {
26
+ const str = JSON.stringify(obj, null, 4)
27
+ return str
28
+ }
29
+
30
+ /** Pretty-prints a JavaScript object as formatted JSON to the console.
31
+ * Strict JSON, all keys are enclosed in ", etc.
32
+ */
33
+ export const printJSON = (obj: any) => {
34
+ if (isProdEnv() || (isTestEnv() && !isDebug())) return
35
+
36
+ const str = toPrettyJSON(obj)
37
+ console.log(str)
38
+ }
39
+
40
+ /**
41
+ * Print a full JavaScript object in a human-readable way (not as JSON).
42
+ * Not strict JSON, and shows functions, symbols, getters/setters, and class names.
43
+ * @param isColors If true, the output is styled with ANSI color codes.
44
+ */
45
+ export const printObject = (obj: any, isColors = true) => {
46
+ if (isProdEnv() || (isTestEnv() && !isDebug())) return
47
+
48
+ console.log(util.inspect(obj, { depth: null, colors: isColors }))
49
+ }
@@ -0,0 +1,8 @@
1
+ /*
2
+ corrupt yini
3
+ In strict should throw error while lenient should pass (given that bailSensitivity = 'auto').
4
+ */
5
+
6
+ ^ Section
7
+ value = 42
8
+ 333 = "missing key name" // (!) Invalid key!
@@ -0,0 +1,2 @@
1
+ <garbage-content-in-this-file>
2
+
@@ -0,0 +1,7 @@
1
+ @yini
2
+
3
+ ^ App
4
+ name = "Nested"
5
+
6
+ ^^ Database
7
+ host = "localhost"
@@ -0,0 +1,5 @@
1
+ @yini
2
+
3
+ ^ App
4
+ title = "My App"
5
+ enabled = ON