@o3co/ts.hocon 0.1.0 → 0.1.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.
- package/README.ja.md +49 -0
- package/README.md +49 -0
- package/dist/config-D4EyvNd8.d.cts +31 -0
- package/dist/config-D4EyvNd8.d.ts +31 -0
- package/dist/index.cjs +891 -0
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +847 -0
- package/dist/zod.cjs +39 -0
- package/dist/zod.d.cts +7 -0
- package/dist/zod.d.ts +7 -0
- package/dist/zod.js +13 -0
- package/package.json +13 -10
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -29
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
- package/.github/pull_request_template.md +0 -17
- package/.github/workflows/lint.yml +0 -33
- package/.github/workflows/release.yml +0 -41
- package/.github/workflows/test.yml +0 -53
- package/CHANGELOG.md +0 -22
- package/CONTRIBUTING.md +0 -62
- package/SECURITY.md +0 -20
- package/docs/superpowers/plans/2026-03-20-ts-hocon.md +0 -2412
- package/docs/superpowers/specs/2026-03-20-ts-hocon-design.md +0 -368
- package/src/config.ts +0 -108
- package/src/errors.ts +0 -34
- package/src/index.ts +0 -5
- package/src/internal/lexer/lexer.ts +0 -168
- package/src/internal/lexer/token.ts +0 -20
- package/src/internal/parser/ast.ts +0 -17
- package/src/internal/parser/parser.ts +0 -229
- package/src/internal/resolver/resolver.ts +0 -428
- package/src/parse.ts +0 -79
- package/src/value.ts +0 -5
- package/src/zod.ts +0 -12
- package/tests/config.test.ts +0 -100
- package/tests/errors.test.ts +0 -43
- package/tests/lexer.test.ts +0 -141
- package/tests/lightbend/lightbend.test.ts +0 -39
- package/tests/lightbend/testdata/a_1.conf +0 -1
- package/tests/lightbend/testdata/b_2.conf +0 -1
- package/tests/lightbend/testdata/beanconfig/beanconfig01.conf +0 -131
- package/tests/lightbend/testdata/bom.conf +0 -2
- package/tests/lightbend/testdata/cycle.conf +0 -1
- package/tests/lightbend/testdata/env-variables.conf +0 -16
- package/tests/lightbend/testdata/equiv01/comments.conf +0 -45
- package/tests/lightbend/testdata/equiv01/equals.conf +0 -41
- package/tests/lightbend/testdata/equiv01/no-commas.conf +0 -55
- package/tests/lightbend/testdata/equiv01/no-root-braces.conf +0 -40
- package/tests/lightbend/testdata/equiv01/no-whitespace.json +0 -1
- package/tests/lightbend/testdata/equiv01/omit-colons.conf +0 -44
- package/tests/lightbend/testdata/equiv01/original.json +0 -41
- package/tests/lightbend/testdata/equiv01/path-keys.conf +0 -29
- package/tests/lightbend/testdata/equiv01/properties-style.conf +0 -37
- package/tests/lightbend/testdata/equiv01/substitutions.conf +0 -41
- package/tests/lightbend/testdata/equiv01/unquoted.conf +0 -41
- package/tests/lightbend/testdata/equiv02/original.json +0 -17
- package/tests/lightbend/testdata/equiv02/path-keys-weird-whitespace.conf +0 -32
- package/tests/lightbend/testdata/equiv02/path-keys.conf +0 -9
- package/tests/lightbend/testdata/equiv03/includes.conf +0 -7
- package/tests/lightbend/testdata/equiv03/letters/a.conf +0 -6
- package/tests/lightbend/testdata/equiv03/letters/b.json +0 -3
- package/tests/lightbend/testdata/equiv03/letters/c.conf +0 -2
- package/tests/lightbend/testdata/equiv03/letters/c.properties +0 -1
- package/tests/lightbend/testdata/equiv03/letters/numbers/1.conf +0 -1
- package/tests/lightbend/testdata/equiv03/letters/numbers/2.properties +0 -1
- package/tests/lightbend/testdata/equiv03/original.json +0 -15
- package/tests/lightbend/testdata/equiv03/root/foo.conf +0 -1
- package/tests/lightbend/testdata/equiv04/missing-substitutions.conf +0 -4
- package/tests/lightbend/testdata/equiv04/original.json +0 -3
- package/tests/lightbend/testdata/equiv05/original.json +0 -16
- package/tests/lightbend/testdata/equiv05/triple-quotes.conf +0 -20
- package/tests/lightbend/testdata/file-include.conf +0 -5
- package/tests/lightbend/testdata/include-from-list.conf +0 -5
- package/tests/lightbend/testdata/onclasspath.conf +0 -2
- package/tests/lightbend/testdata/subdir/bar-file.conf +0 -1
- package/tests/lightbend/testdata/subdir/bar.conf +0 -1
- package/tests/lightbend/testdata/subdir/baz.conf +0 -1
- package/tests/lightbend/testdata/subdir/foo.conf +0 -5
- package/tests/lightbend/testdata/test01.conf +0 -96
- package/tests/lightbend/testdata/test01.json +0 -4
- package/tests/lightbend/testdata/test01.properties +0 -5
- package/tests/lightbend/testdata/test02.conf +0 -13
- package/tests/lightbend/testdata/test03-included.conf +0 -9
- package/tests/lightbend/testdata/test03.conf +0 -36
- package/tests/lightbend/testdata/test04.conf +0 -305
- package/tests/lightbend/testdata/test05.conf +0 -154
- package/tests/lightbend/testdata/test06.conf +0 -14
- package/tests/lightbend/testdata/test07.conf +0 -4
- package/tests/lightbend/testdata/test08.conf +0 -6
- package/tests/lightbend/testdata/test09.conf +0 -17
- package/tests/lightbend/testdata/test10.conf +0 -11
- package/tests/lightbend/testdata/test11.conf +0 -7
- package/tests/lightbend/testdata/test12.conf +0 -43
- package/tests/lightbend/testdata/test13-application-override-substitutions.conf +0 -1
- package/tests/lightbend/testdata/test13-reference-bad-substitutions.conf +0 -1
- package/tests/lightbend/testdata/test13-reference-with-substitutions.conf +0 -2
- package/tests/lightbend/testdata/validate-invalid.conf +0 -30
- package/tests/lightbend/testdata/validate-reference.conf +0 -32
- package/tests/parse.test.ts +0 -44
- package/tests/parser.test.ts +0 -154
- package/tests/resolver.test.ts +0 -159
- package/tests/zod.test.ts +0 -57
- package/tsconfig.json +0 -16
- package/tsup.config.ts +0 -12
- package/vitest.config.ts +0 -7
package/src/parse.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { tokenize } from './internal/lexer/lexer.js'
|
|
2
|
-
import { parseTokens } from './internal/parser/parser.js'
|
|
3
|
-
import { resolve } from './internal/resolver/resolver.js'
|
|
4
|
-
import { Config } from './config.js'
|
|
5
|
-
import type { ResolveOptions } from './internal/resolver/resolver.js'
|
|
6
|
-
import * as fs from 'node:fs'
|
|
7
|
-
import * as path from 'node:path'
|
|
8
|
-
|
|
9
|
-
export type ParseOptions = {
|
|
10
|
-
baseDir?: string
|
|
11
|
-
env?: Record<string, string>
|
|
12
|
-
readFile?: (filePath: string) => Promise<string>
|
|
13
|
-
readFileSync?: (filePath: string) => string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function getEnv(opts: ParseOptions): Record<string, string> {
|
|
17
|
-
if (opts.env !== undefined) return opts.env
|
|
18
|
-
if (typeof process !== 'undefined' && process.env) return process.env as Record<string, string>
|
|
19
|
-
return {}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function defaultReadFileSync(filePath: string): string {
|
|
23
|
-
return fs.readFileSync(filePath, 'utf-8')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function defaultReadFile(filePath: string): Promise<string> {
|
|
27
|
-
return fs.promises.readFile(filePath, 'utf-8')
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function parse(input: string, opts: ParseOptions = {}): Config {
|
|
31
|
-
const tokens = tokenize(input)
|
|
32
|
-
const ast = parseTokens(tokens)
|
|
33
|
-
const resolveOpts: ResolveOptions = {
|
|
34
|
-
env: getEnv(opts),
|
|
35
|
-
baseDir: opts.baseDir,
|
|
36
|
-
readFileSync: opts.readFileSync ?? defaultReadFileSync,
|
|
37
|
-
}
|
|
38
|
-
const value = resolve(ast, resolveOpts)
|
|
39
|
-
if (value.kind !== 'object') throw new Error('resolved value is not an object')
|
|
40
|
-
return new Config(value)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Async wrapper around `parse()`. Returns a resolved Promise — the parsing
|
|
45
|
-
* itself is synchronous. Use `parseFileAsync()` for async file I/O.
|
|
46
|
-
* Note: `include` directives in string input are resolved synchronously
|
|
47
|
-
* via `readFileSync`; `readFile` is not used by this function.
|
|
48
|
-
*/
|
|
49
|
-
export async function parseAsync(input: string, opts: ParseOptions = {}): Promise<Config> {
|
|
50
|
-
return Promise.resolve(parse(input, opts))
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function parseFile(filePath: string, opts: ParseOptions = {}): Config {
|
|
54
|
-
if (typeof process === 'undefined' && !opts.readFileSync) {
|
|
55
|
-
throw new Error('parseFile is not supported in browser environments. Provide opts.readFileSync or use parse() instead.')
|
|
56
|
-
}
|
|
57
|
-
const resolvedPath = path.resolve(filePath)
|
|
58
|
-
const readFileSync = opts.readFileSync ?? defaultReadFileSync
|
|
59
|
-
const input = readFileSync(resolvedPath)
|
|
60
|
-
return parse(input, {
|
|
61
|
-
...opts,
|
|
62
|
-
baseDir: opts.baseDir ?? path.dirname(resolvedPath),
|
|
63
|
-
readFileSync,
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function parseFileAsync(filePath: string, opts: ParseOptions = {}): Promise<Config> {
|
|
68
|
-
if (typeof process === 'undefined' && !opts.readFile) {
|
|
69
|
-
throw new Error('parseFileAsync is not supported in browser environments. Provide opts.readFile or use parseAsync() instead.')
|
|
70
|
-
}
|
|
71
|
-
const resolvedPath = path.resolve(filePath)
|
|
72
|
-
const readFile = opts.readFile ?? defaultReadFile
|
|
73
|
-
const input = await readFile(resolvedPath)
|
|
74
|
-
return parse(input, {
|
|
75
|
-
...opts,
|
|
76
|
-
baseDir: opts.baseDir ?? path.dirname(resolvedPath),
|
|
77
|
-
readFileSync: opts.readFileSync ?? defaultReadFileSync,
|
|
78
|
-
})
|
|
79
|
-
}
|
package/src/value.ts
DELETED
package/src/zod.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { ZodType } from 'zod'
|
|
2
|
-
import type { Config } from './config.js'
|
|
3
|
-
|
|
4
|
-
export function validate<T>(config: Config, schema: ZodType<T>): T {
|
|
5
|
-
const plain = config.toObject()
|
|
6
|
-
return schema.parse(plain)
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getValidated<T>(config: Config, path: string, schema: ZodType<T>): T {
|
|
10
|
-
const val = config.get(path)
|
|
11
|
-
return schema.parse(val)
|
|
12
|
-
}
|
package/tests/config.test.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// tests/config.test.ts
|
|
2
|
-
import { describe, it, expect } from 'vitest'
|
|
3
|
-
import { Config } from '../src/config.js'
|
|
4
|
-
import type { HoconValue } from '../src/value.js'
|
|
5
|
-
import { ConfigError } from '../src/errors.js'
|
|
6
|
-
|
|
7
|
-
function makeConfig(obj: Record<string, HoconValue>): Config {
|
|
8
|
-
return new Config({ kind: 'object', fields: new Map(Object.entries(obj)) })
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('Config', () => {
|
|
12
|
-
it('get() returns value at path', () => {
|
|
13
|
-
const c = makeConfig({ host: { kind: 'scalar', value: 'localhost' } })
|
|
14
|
-
expect(c.get('host')).toBe('localhost')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('get() returns nested value at dot-path', () => {
|
|
18
|
-
const inner = new Map([['host', { kind: 'scalar', value: 'localhost' } satisfies HoconValue]])
|
|
19
|
-
const c = new Config({ kind: 'object', fields: new Map([['server', { kind: 'object', fields: inner }]]) })
|
|
20
|
-
expect(c.get('server.host')).toBe('localhost')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('get() returns undefined for missing path', () => {
|
|
24
|
-
const c = makeConfig({})
|
|
25
|
-
expect(c.get('missing')).toBeUndefined()
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('getString() returns string value', () => {
|
|
29
|
-
const c = makeConfig({ host: { kind: 'scalar', value: 'localhost' } })
|
|
30
|
-
expect(c.getString('host')).toBe('localhost')
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('getString() throws ConfigError for non-string', () => {
|
|
34
|
-
const c = makeConfig({ port: { kind: 'scalar', value: 8080 } })
|
|
35
|
-
expect(() => c.getString('port')).toThrow(ConfigError)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('getNumber() returns number value', () => {
|
|
39
|
-
const c = makeConfig({ port: { kind: 'scalar', value: 8080 } })
|
|
40
|
-
expect(c.getNumber('port')).toBe(8080)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('getNumber() throws ConfigError for non-number', () => {
|
|
44
|
-
const c = makeConfig({ host: { kind: 'scalar', value: 'localhost' } })
|
|
45
|
-
expect(() => c.getNumber('host')).toThrow(ConfigError)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('getBoolean() returns boolean value', () => {
|
|
49
|
-
const c = makeConfig({ debug: { kind: 'scalar', value: true } })
|
|
50
|
-
expect(c.getBoolean('debug')).toBe(true)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('getConfig() returns sub-config', () => {
|
|
54
|
-
const inner = new Map([['host', { kind: 'scalar', value: 'localhost' } satisfies HoconValue]])
|
|
55
|
-
const c = new Config({ kind: 'object', fields: new Map([['server', { kind: 'object', fields: inner }]]) })
|
|
56
|
-
const sub = c.getConfig('server')
|
|
57
|
-
expect(sub).toBeInstanceOf(Config)
|
|
58
|
-
expect(sub.getString('host')).toBe('localhost')
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('getList() returns array as unknown[]', () => {
|
|
62
|
-
const items: HoconValue[] = [{ kind: 'scalar', value: 1 }, { kind: 'scalar', value: 2 }]
|
|
63
|
-
const c = makeConfig({ list: { kind: 'array', items } })
|
|
64
|
-
expect(c.getList('list')).toEqual([1, 2])
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('has() returns true for existing key', () => {
|
|
68
|
-
const c = makeConfig({ host: { kind: 'scalar', value: 'localhost' } })
|
|
69
|
-
expect(c.has('host')).toBe(true)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('has() returns true for null value', () => {
|
|
73
|
-
const c = makeConfig({ x: { kind: 'scalar', value: null } })
|
|
74
|
-
expect(c.has('x')).toBe(true)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('has() returns false for missing key', () => {
|
|
78
|
-
const c = makeConfig({})
|
|
79
|
-
expect(c.has('missing')).toBe(false)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('keys() returns top-level keys in order', () => {
|
|
83
|
-
const c = new Config({ kind: 'object', fields: new Map([['b', { kind: 'scalar', value: 2 }], ['a', { kind: 'scalar', value: 1 }]]) })
|
|
84
|
-
expect(c.keys()).toEqual(['b', 'a'])
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('withFallback() receiver wins', () => {
|
|
88
|
-
const c1 = makeConfig({ host: { kind: 'scalar', value: 'prod' } })
|
|
89
|
-
const c2 = makeConfig({ host: { kind: 'scalar', value: 'dev' }, port: { kind: 'scalar', value: 8080 } })
|
|
90
|
-
const merged = c1.withFallback(c2)
|
|
91
|
-
expect(merged.getString('host')).toBe('prod')
|
|
92
|
-
expect(merged.getNumber('port')).toBe(8080)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('toObject() converts to plain JS object recursively', () => {
|
|
96
|
-
const inner = new Map([['host', { kind: 'scalar', value: 'localhost' } satisfies HoconValue]])
|
|
97
|
-
const c = new Config({ kind: 'object', fields: new Map([['server', { kind: 'object', fields: inner }]]) })
|
|
98
|
-
expect(c.toObject()).toEqual({ server: { host: 'localhost' } })
|
|
99
|
-
})
|
|
100
|
-
})
|
package/tests/errors.test.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { ParseError, ResolveError, ConfigError } from '../src/errors.js'
|
|
3
|
-
|
|
4
|
-
describe('ParseError', () => {
|
|
5
|
-
it('is an Error with line/col', () => {
|
|
6
|
-
const e = new ParseError('unexpected token', 3, 10)
|
|
7
|
-
expect(e).toBeInstanceOf(Error)
|
|
8
|
-
expect(e.message).toBe('unexpected token')
|
|
9
|
-
expect(e.line).toBe(3)
|
|
10
|
-
expect(e.col).toBe(10)
|
|
11
|
-
expect(e.file).toBeUndefined()
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
it('accepts optional file path', () => {
|
|
15
|
-
const e = new ParseError('bad syntax', 1, 1, 'app.conf')
|
|
16
|
-
expect(e.file).toBe('app.conf')
|
|
17
|
-
})
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
describe('ResolveError', () => {
|
|
21
|
-
it('carries path and position', () => {
|
|
22
|
-
const e = new ResolveError('unresolved substitution', 'server.host', 5, 3)
|
|
23
|
-
expect(e).toBeInstanceOf(Error)
|
|
24
|
-
expect(e.path).toBe('server.host')
|
|
25
|
-
expect(e.line).toBe(5)
|
|
26
|
-
expect(e.col).toBe(3)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('line/col can be 0 (no position info)', () => {
|
|
30
|
-
const e = new ResolveError('include failed', 'path', 0, 0, 'app.conf')
|
|
31
|
-
expect(e.line).toBe(0)
|
|
32
|
-
expect(e.col).toBe(0)
|
|
33
|
-
expect(e.file).toBe('app.conf')
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
describe('ConfigError', () => {
|
|
38
|
-
it('carries path', () => {
|
|
39
|
-
const e = new ConfigError('key not found', 'server.port')
|
|
40
|
-
expect(e).toBeInstanceOf(Error)
|
|
41
|
-
expect(e.path).toBe('server.port')
|
|
42
|
-
})
|
|
43
|
-
})
|
package/tests/lexer.test.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { tokenize } from '../src/internal/lexer/lexer.js'
|
|
3
|
-
import { ParseError } from '../src/errors.js'
|
|
4
|
-
|
|
5
|
-
describe('tokenize', () => {
|
|
6
|
-
it('tokenizes empty string', () => {
|
|
7
|
-
const tokens = tokenize('')
|
|
8
|
-
expect(tokens).toHaveLength(1)
|
|
9
|
-
expect(tokens[0].kind).toBe('eof')
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it('tokenizes braces and brackets', () => {
|
|
13
|
-
const tokens = tokenize('{}[]')
|
|
14
|
-
expect(tokens.map(t => t.kind)).toEqual(['lbrace', 'rbrace', 'lbracket', 'rbracket', 'eof'])
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('tokenizes = and +=', () => {
|
|
18
|
-
const tokens = tokenize('=+=')
|
|
19
|
-
expect(tokens[0].kind).toBe('equals')
|
|
20
|
-
expect(tokens[1].kind).toBe('plus_equals')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('tokenizes : and ,', () => {
|
|
24
|
-
const tokens = tokenize(':,')
|
|
25
|
-
expect(tokens.map(t => t.kind)).toEqual(['colon', 'comma', 'eof'])
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('skips // comments, keeps newline', () => {
|
|
29
|
-
const tokens = tokenize('// comment\nfoo')
|
|
30
|
-
expect(tokens[0].kind).toBe('newline')
|
|
31
|
-
expect(tokens[1].kind).toBe('unquoted')
|
|
32
|
-
expect(tokens[1].value).toBe('foo')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('skips # comments', () => {
|
|
36
|
-
const tokens = tokenize('# comment\nfoo')
|
|
37
|
-
expect(tokens[0].kind).toBe('newline')
|
|
38
|
-
expect(tokens[1].value).toBe('foo')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('tokenizes quoted strings', () => {
|
|
42
|
-
const [t] = tokenize('"hello world"')
|
|
43
|
-
expect(t.kind).toBe('string')
|
|
44
|
-
expect(t.value).toBe('hello world')
|
|
45
|
-
expect(t.isQuoted).toBe(true)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('handles escape sequences', () => {
|
|
49
|
-
const [t] = tokenize('"a\\nb\\tc"')
|
|
50
|
-
expect(t.value).toBe('a\nb\tc')
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('handles \\u unicode escapes', () => {
|
|
54
|
-
const [t] = tokenize('"\\u0041"')
|
|
55
|
-
expect(t.value).toBe('A')
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('tokenizes triple-quoted strings', () => {
|
|
59
|
-
const [t] = tokenize('"""hello\nworld"""')
|
|
60
|
-
expect(t.kind).toBe('triple_string')
|
|
61
|
-
expect(t.value).toBe('hello\nworld')
|
|
62
|
-
expect(t.isQuoted).toBe(true)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('strips leading newline from triple-quoted strings', () => {
|
|
66
|
-
const [t] = tokenize('"""\nhello"""')
|
|
67
|
-
expect(t.value).toBe('hello')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('tokenizes unquoted strings', () => {
|
|
71
|
-
const [t] = tokenize('localhost')
|
|
72
|
-
expect(t.kind).toBe('unquoted')
|
|
73
|
-
expect(t.value).toBe('localhost')
|
|
74
|
-
expect(t.isQuoted).toBe(false)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('tokenizes numbers as unquoted', () => {
|
|
78
|
-
const [t] = tokenize('8080')
|
|
79
|
-
expect(t.kind).toBe('unquoted')
|
|
80
|
-
expect(t.value).toBe('8080')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('tokenizes substitutions', () => {
|
|
84
|
-
const [t] = tokenize('${server.host}')
|
|
85
|
-
expect(t.kind).toBe('subst')
|
|
86
|
-
expect(t.value).toBe('server.host')
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('tokenizes optional substitutions', () => {
|
|
90
|
-
const [t] = tokenize('${?foo}')
|
|
91
|
-
expect(t.kind).toBe('opt_subst')
|
|
92
|
-
expect(t.value).toBe('foo')
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('tokenizes newlines', () => {
|
|
96
|
-
const tokens = tokenize('a\nb')
|
|
97
|
-
expect(tokens[1].kind).toBe('newline')
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('deduplicates consecutive newlines', () => {
|
|
101
|
-
const tokens = tokenize('a\n\n\nb')
|
|
102
|
-
const newlines = tokens.filter(t => t.kind === 'newline')
|
|
103
|
-
expect(newlines).toHaveLength(1)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('tracks line and col', () => {
|
|
107
|
-
const tokens = tokenize('a\nb')
|
|
108
|
-
expect(tokens[0].line).toBe(1)
|
|
109
|
-
expect(tokens[0].col).toBe(1)
|
|
110
|
-
expect(tokens[2].line).toBe(2)
|
|
111
|
-
expect(tokens[2].col).toBe(1)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('sets precedingSpace on token after whitespace', () => {
|
|
115
|
-
const tokens = tokenize('a b')
|
|
116
|
-
expect(tokens[1].precedingSpace).toBe(true)
|
|
117
|
-
expect(tokens[0].precedingSpace).toBe(false)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('strips UTF-8 BOM', () => {
|
|
121
|
-
const tokens = tokenize('\uFEFFfoo')
|
|
122
|
-
expect(tokens[0].value).toBe('foo')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('stops unquoted scan at $ for concat', () => {
|
|
126
|
-
const tokens = tokenize('foo${bar}')
|
|
127
|
-
expect(tokens[0].kind).toBe('unquoted')
|
|
128
|
-
expect(tokens[0].value).toBe('foo')
|
|
129
|
-
expect(tokens[1].kind).toBe('subst')
|
|
130
|
-
expect(tokens[1].value).toBe('bar')
|
|
131
|
-
expect(tokens[1].precedingSpace).toBe(false)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('throws ParseError on unterminated string', () => {
|
|
135
|
-
expect(() => tokenize('"unterminated')).toThrow(ParseError)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('throws ParseError on unterminated substitution', () => {
|
|
139
|
-
expect(() => tokenize('${foo')).toThrow(ParseError)
|
|
140
|
-
})
|
|
141
|
-
})
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// tests/lightbend/lightbend.test.ts
|
|
2
|
-
//
|
|
3
|
-
// Lightbend HOCON spec compliance tests, mirroring go.hocon's lightbend_test.go.
|
|
4
|
-
// Tests equiv01-05 directories (each .conf compared against original.json).
|
|
5
|
-
import { describe, it, expect } from 'vitest'
|
|
6
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
7
|
-
import { join } from 'node:path'
|
|
8
|
-
import { parse } from '../../src/parse.js'
|
|
9
|
-
|
|
10
|
-
const dataDir = new URL('./testdata', import.meta.url).pathname
|
|
11
|
-
|
|
12
|
-
// Known skip reasons for specific test files
|
|
13
|
-
const equivSkip = new Set([
|
|
14
|
-
'equiv03/includes.conf', // requires .properties file support
|
|
15
|
-
])
|
|
16
|
-
|
|
17
|
-
describe('Lightbend HOCON equiv tests', () => {
|
|
18
|
-
for (let i = 1; i <= 5; i++) {
|
|
19
|
-
const dir = join(dataDir, `equiv${String(i).padStart(2, '0')}`)
|
|
20
|
-
const jsonPath = join(dir, 'original.json')
|
|
21
|
-
if (!existsSync(jsonPath)) continue
|
|
22
|
-
|
|
23
|
-
const expected = JSON.parse(readFileSync(jsonPath, 'utf-8'))
|
|
24
|
-
const entries = readdirSync(dir)
|
|
25
|
-
|
|
26
|
-
for (const entry of entries) {
|
|
27
|
-
if (!entry.endsWith('.conf')) continue
|
|
28
|
-
const rel = `equiv${String(i).padStart(2, '0')}/${entry}`
|
|
29
|
-
if (equivSkip.has(rel)) continue
|
|
30
|
-
|
|
31
|
-
it(rel, () => {
|
|
32
|
-
const confPath = join(dir, entry)
|
|
33
|
-
const confContent = readFileSync(confPath, 'utf-8')
|
|
34
|
-
const config = parse(confContent, { baseDir: dir })
|
|
35
|
-
expect(config.toObject()).toEqual(expected)
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
})
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
a=1
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
b=2
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"booleans" : {
|
|
3
|
-
"trueVal" : true,
|
|
4
|
-
"trueValAgain" : ${booleans.trueVal},
|
|
5
|
-
"falseVal" : false,
|
|
6
|
-
"falseValAgain" : ${booleans.falseVal},
|
|
7
|
-
"on" : "on",
|
|
8
|
-
"off" : "off"
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
"numbers" : {
|
|
12
|
-
"byteVal" : "1",
|
|
13
|
-
"byteObj" : ${numbers.byteVal},
|
|
14
|
-
"shortVal" : "2",
|
|
15
|
-
"shortObj" : ${numbers.shortVal},
|
|
16
|
-
"intVal" : "3",
|
|
17
|
-
"intObj" : ${numbers.intVal},
|
|
18
|
-
"longVal" : "4",
|
|
19
|
-
"longObj" : ${numbers.longVal},
|
|
20
|
-
"doubleVal" : "1.0",
|
|
21
|
-
"doubleObj" : ${numbers.doubleVal}
|
|
22
|
-
}
|
|
23
|
-
//
|
|
24
|
-
"strings" : {
|
|
25
|
-
"abcd" : "abcd",
|
|
26
|
-
"abcd-again" : ${strings.a}${strings.b}${strings.c}${strings.d},
|
|
27
|
-
"a" : "a",
|
|
28
|
-
"b" : "b",
|
|
29
|
-
"c" : "c",
|
|
30
|
-
"d" : "d",
|
|
31
|
-
"concatenated" : null bar 42 baz true 3.14 hi,
|
|
32
|
-
"double" : "3.14",
|
|
33
|
-
"number" : "57",
|
|
34
|
-
"null" : "null",
|
|
35
|
-
"true" : "true",
|
|
36
|
-
"yes" : "yes",
|
|
37
|
-
"false" : "false",
|
|
38
|
-
"no" : "no"
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
"arrays" : {
|
|
42
|
-
"empty" : [],
|
|
43
|
-
"ofInt" : [1, 2, 3],
|
|
44
|
-
"ofString" : [ ${strings.a}, ${strings.b}, ${strings.c} ],
|
|
45
|
-
"of-double" : [3.14, 4.14, 5.14],
|
|
46
|
-
"of-long" : { "1" : 32, "2" : 42, "3" : 52 }, // object-to-list conversion
|
|
47
|
-
"ofNull" : [null, null, null],
|
|
48
|
-
"ofBoolean" : [true, false],
|
|
49
|
-
"ofArray" : [${arrays.ofString}, ${arrays.ofString}, ${arrays.ofString}],
|
|
50
|
-
"ofObject" : [${numbers}, ${booleans}, ${strings}],
|
|
51
|
-
"ofConfig" : [${numbers}, ${booleans}, ${strings}],
|
|
52
|
-
"ofConfigObject" : [${numbers}, ${booleans}, ${strings}],
|
|
53
|
-
"ofConfigValue" : [1, 2, "a"],
|
|
54
|
-
"ofDuration" : [1, 2h, 3 days],
|
|
55
|
-
"ofMemorySize" : [1024, 1M, 1G],
|
|
56
|
-
"ofStringBean" : [
|
|
57
|
-
{
|
|
58
|
-
abcd : "testAbcdOne"
|
|
59
|
-
yes : "testYesOne"
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
abcd : "testAbcdTwo"
|
|
63
|
-
yes : "testYesTwo"
|
|
64
|
-
}
|
|
65
|
-
]
|
|
66
|
-
},
|
|
67
|
-
"bytes" : {
|
|
68
|
-
"kilobyte" : "1kB",
|
|
69
|
-
"kibibyte" : "1K",
|
|
70
|
-
"thousandBytes" : "1000B"
|
|
71
|
-
},
|
|
72
|
-
"durations" : {
|
|
73
|
-
"second" : 1s,
|
|
74
|
-
"secondAsNumber" : 1000,
|
|
75
|
-
"halfSecond" : 0.5s
|
|
76
|
-
},
|
|
77
|
-
"validation" : {
|
|
78
|
-
"shouldBeInt" : true,
|
|
79
|
-
"should-be-boolean" : 42,
|
|
80
|
-
"should-be-list" : "hello"
|
|
81
|
-
},
|
|
82
|
-
"preferCamelNames" : {
|
|
83
|
-
"foo-bar" : "no",
|
|
84
|
-
"fooBar" : "yes",
|
|
85
|
-
"baz-bar" : "no",
|
|
86
|
-
"bazBar" : "yes"
|
|
87
|
-
},
|
|
88
|
-
"values" : {
|
|
89
|
-
"obj" : 42,
|
|
90
|
-
"config" : ${strings},
|
|
91
|
-
"configObj" : ${numbers},
|
|
92
|
-
"configValue" : "hello world",
|
|
93
|
-
"list" : [1,2,3],
|
|
94
|
-
"unwrappedMap" : ${validation}
|
|
95
|
-
},
|
|
96
|
-
"enums" : {
|
|
97
|
-
"problem" : "P1",
|
|
98
|
-
"solutions" : ["S1", "S3"]
|
|
99
|
-
},
|
|
100
|
-
"objects" : {
|
|
101
|
-
"valueObject": {
|
|
102
|
-
"mandatoryValue": "notNull"
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
"sets" : {
|
|
106
|
-
"empty" : [],
|
|
107
|
-
"ofInt" : [1, 2, 3, 2, 3],
|
|
108
|
-
"ofString" : [ ${strings.a}, ${strings.b}, ${strings.c} ],
|
|
109
|
-
"of-double" : [3.14, 4.14, 4.14, 5.14],
|
|
110
|
-
"of-long" : { "1" : 32, "2" : 42, "3" : 52 }, // object-to-list conversion
|
|
111
|
-
"ofNull" : [null, null, null],
|
|
112
|
-
"ofBoolean" : [true, false, false],
|
|
113
|
-
"ofArray" : [${arrays.ofString}, ${arrays.ofString}, ${arrays.ofString}],
|
|
114
|
-
"ofObject" : [${numbers}, ${booleans}, ${strings}],
|
|
115
|
-
"ofConfig" : [${numbers}, ${booleans}, ${strings}],
|
|
116
|
-
"ofConfigObject" : [${numbers}, ${booleans}, ${strings}],
|
|
117
|
-
"ofConfigValue" : [1, 2, "a"],
|
|
118
|
-
"ofDuration" : [1, 2h, 3 days],
|
|
119
|
-
"ofMemorySize" : [1024, 1M, 1G],
|
|
120
|
-
"ofStringBean" : [
|
|
121
|
-
{
|
|
122
|
-
abcd : "testAbcdOne"
|
|
123
|
-
yes : "testYesOne"
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
abcd : "testAbcdTwo"
|
|
127
|
-
yes : "testYesTwo"
|
|
128
|
-
}
|
|
129
|
-
]
|
|
130
|
-
}
|
|
131
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
include "cycle.conf"
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
secret = a
|
|
2
|
-
secret = ${?SECRET_A}
|
|
3
|
-
secrets = ["b", "c"]
|
|
4
|
-
secrets = [${?SECRET_B}, ${?SECRET_C}]
|
|
5
|
-
|
|
6
|
-
myList = ${MY_LIST[]}
|
|
7
|
-
myOptionalList = ${?MY_LIST[]}
|
|
8
|
-
myOptionalUndefinedList = ${?UNDEFINED_LIST[]}
|
|
9
|
-
numList = ${NUM_LIST[]}
|
|
10
|
-
|
|
11
|
-
# list concatenation with env var list expansion
|
|
12
|
-
prependedList = ["x", "y"] ${?MY_LIST[]}
|
|
13
|
-
appendedList = ${?MY_LIST[]} ["x", "y"]
|
|
14
|
-
selfAppendedList = ["x"]
|
|
15
|
-
selfAppendedList = ${?selfAppendedList} ${?MY_LIST[]}
|
|
16
|
-
appendedUndefined = ["x", "y"] ${?UNDEFINED_LIST[]}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
{ // These are some integers
|
|
2
|
-
"ints" : { # we can comment them with # too
|
|
3
|
-
"fortyTwo" : 42, ## double-# !
|
|
4
|
-
"fortyTwoAgain" : 42 # { }
|
|
5
|
-
},
|
|
6
|
-
######## COMMENT COMMENT COMMENT
|
|
7
|
-
/// I COMMENT YOU
|
|
8
|
-
"floats" : {
|
|
9
|
-
"fortyTwoPointOne" : 42.1,
|
|
10
|
-
"fortyTwoPointOneAgain" : 42.1
|
|
11
|
-
// I CAN HAS COMMENT
|
|
12
|
-
},
|
|
13
|
-
|
|
14
|
-
"strings" : {
|
|
15
|
-
"abcd" : // I got yr comment right here
|
|
16
|
-
"abcd",
|
|
17
|
-
"abcdAgain" : "abcd",
|
|
18
|
-
"a" : "a",
|
|
19
|
-
"b" : "b",
|
|
20
|
-
"c" : "c",
|
|
21
|
-
"d" : "d",
|
|
22
|
-
"concatenated" # more to say?
|
|
23
|
-
: "null bar 42 baz true 3.14 hi"
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
"arrays" : {
|
|
27
|
-
"empty" : [],
|
|
28
|
-
"1" : [ 1 ],
|
|
29
|
-
"12" : [1, 2],
|
|
30
|
-
"123" : [1, 2, 3],
|
|
31
|
-
"ofString" : [ "a", "b", "c" ]
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
"booleans" : {
|
|
35
|
-
"true" : true,
|
|
36
|
-
"trueAgain" : true,
|
|
37
|
-
"false" : false,
|
|
38
|
-
"falseAgain" : false
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
"nulls" : {
|
|
42
|
-
"null" : null,
|
|
43
|
-
"nullAgain" : null
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"ints" = {
|
|
3
|
-
"fortyTwo" = 42,
|
|
4
|
-
"fortyTwoAgain" = 42
|
|
5
|
-
},
|
|
6
|
-
|
|
7
|
-
"floats" = {
|
|
8
|
-
"fortyTwoPointOne" = 42.1,
|
|
9
|
-
"fortyTwoPointOneAgain" = 42.1
|
|
10
|
-
},
|
|
11
|
-
|
|
12
|
-
"strings" = {
|
|
13
|
-
"abcd" = "abcd",
|
|
14
|
-
"abcdAgain" = "abcd",
|
|
15
|
-
"a" = "a",
|
|
16
|
-
"b" = "b",
|
|
17
|
-
"c" = "c",
|
|
18
|
-
"d" = "d",
|
|
19
|
-
"concatenated" = "null bar 42 baz true 3.14 hi"
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
"arrays" = {
|
|
23
|
-
"empty" = [],
|
|
24
|
-
"1" = [ 1 ],
|
|
25
|
-
"12" = [1, 2],
|
|
26
|
-
"123" = [1, 2, 3],
|
|
27
|
-
"ofString" = [ "a", "b", "c" ]
|
|
28
|
-
},
|
|
29
|
-
|
|
30
|
-
"booleans" = {
|
|
31
|
-
"true" = true,
|
|
32
|
-
"trueAgain" = true,
|
|
33
|
-
"false" = false,
|
|
34
|
-
"falseAgain" = false
|
|
35
|
-
},
|
|
36
|
-
|
|
37
|
-
"nulls" = {
|
|
38
|
-
"null" = null,
|
|
39
|
-
"nullAgain" = null
|
|
40
|
-
}
|
|
41
|
-
}
|