-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
286 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { test } from 'tap' | ||
import { parse, stringify } from 'yaml' | ||
|
||
import { classTag, functionTag } from '.' | ||
|
||
test('parse valid', t => { | ||
const res: { new (): any } = parse( | ||
`!class |- | ||
class X extends Y { | ||
constructor() { | ||
this.a = 1 | ||
throw new Error('bad idea to actually run this') | ||
} | ||
z = 3 | ||
} | ||
`, | ||
{ customTags: [classTag, functionTag] } | ||
) | ||
t.type(res, 'function') | ||
t.equal( | ||
res.toString(), | ||
`class X extends Y { | ||
constructor() { | ||
this.a = 1 | ||
throw new Error('bad idea to actually run this') | ||
} | ||
z = 3 | ||
}` | ||
) | ||
t.equal(res.name, 'X') | ||
const inst = new res() | ||
t.notMatch( | ||
inst, | ||
{ | ||
a: Number, | ||
z: Number | ||
}, | ||
'does not actually run the code specified' | ||
) | ||
t.end() | ||
}) | ||
|
||
test('unnamed class', t => { | ||
// it's actually kind of tricky to get a class that V8 won't | ||
// assign *some* sort of intelligible name to. It has to never be | ||
// assigned to any variable, or directly pass as an argument to | ||
// a named function at its point of creation, hence this line noise. | ||
const res = stringify((() => class {})(), { | ||
customTags: [classTag, functionTag] | ||
}) | ||
t.equal( | ||
res, | ||
`!class |- | ||
class {\n } | ||
` | ||
) | ||
t.end() | ||
}) | ||
|
||
test('parse completely empty value', t => { | ||
const src = `!class |-\n` | ||
const res: { new (): any } = parse(src, { customTags: [classTag] }) | ||
t.type(res, 'function') | ||
t.equal(res.name, undefined) | ||
t.equal(res.toString(), '') | ||
t.end() | ||
}) | ||
|
||
class Foo extends Boolean {} | ||
test('stringify a class', t => { | ||
const res = stringify(Foo, { customTags: [classTag, functionTag] }) | ||
// don't test the actual class body, because that will break | ||
// if/when TypeScript is updated. | ||
t.ok( | ||
res.startsWith(`!class |- | ||
class Foo extends Boolean {`), | ||
'shows class toString value' | ||
) | ||
t.end() | ||
}) | ||
|
||
test('stringify not a class for identify coverage', t => { | ||
const res = stringify(() => {}, { customTags: [classTag, functionTag] }) | ||
t.equal( | ||
res, | ||
`!function |- | ||
"" | ||
() => { } | ||
` | ||
) | ||
t.end() | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { Scalar, ScalarTag } from 'yaml' | ||
import { stringifyString } from 'yaml/util' | ||
|
||
const options: { defaultType: Scalar.Type } = { | ||
defaultType: 'BLOCK_LITERAL' | ||
} | ||
|
||
/** | ||
* `!class` A YAML representation of JavaScript classes | ||
* | ||
* Stringified as a block literal string, prefixed with the class name. | ||
* | ||
* When parsing, a no-op class with matching name and toString() is | ||
* returned. It is not possible to construct an actual JavaScript class by | ||
* evaluating YAML, and it is unsafe to attempt. | ||
*/ | ||
export const classTag = { | ||
identify(value) { | ||
const cls = value as { new (): any } | ||
try { | ||
return typeof value === 'function' && Boolean(class extends cls {}) | ||
} catch { | ||
return false | ||
} | ||
}, | ||
tag: '!class', | ||
resolve(str) { | ||
const f = class {} | ||
f.toString = () => str | ||
const m = str.trim().match(/^class(?:\s+([^{ \s]*?)[{\s])/) | ||
Object.defineProperty(f, 'name', { | ||
value: m?.[1], | ||
enumerable: false, | ||
configurable: true, | ||
writable: true | ||
}) | ||
return f | ||
}, | ||
options, | ||
stringify(i, ctx, onComment, onChompKeep) { | ||
const { type: originalType, value: originalValue } = i | ||
const cls = originalValue as { new (...a: any[]): any } | ||
const value = cls.toString() | ||
// better to just always put classes on a new line. | ||
const type: Scalar.Type = originalType || options.defaultType | ||
return stringifyString({ ...i, type, value }, ctx, onComment, onChompKeep) | ||
} | ||
} satisfies ScalarTag & { options: typeof options } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { test } from 'tap' | ||
import { parse, parseDocument, stringify } from 'yaml' | ||
|
||
import { functionTag } from '.' | ||
|
||
test('parse valid', t => { | ||
const res: Function = parse( | ||
`!function |- | ||
"foo" | ||
() => 'bar' | ||
`, | ||
{ customTags: [functionTag] } | ||
) | ||
t.type(res, 'function') | ||
t.equal(res.toString(), `() => 'bar'`) | ||
t.equal(res.name, 'foo') | ||
t.equal(res(), undefined, 'does not actually run the code specified') | ||
t.end() | ||
}) | ||
|
||
test('parse invalid name serialization', t => { | ||
const doc = parseDocument( | ||
`!function |- | ||
name: "invalid | ||
blah | ||
`, | ||
{ customTags: [functionTag] } | ||
) | ||
t.has(doc.errors, { length: 1, 0: { code: 'TAG_RESOLVE_FAILED' } }) | ||
t.end() | ||
}) | ||
|
||
function foo() { | ||
return 1 | ||
} | ||
test('stringify', t => { | ||
const res = stringify(foo, { customTags: [functionTag] }) | ||
// the divergent toString is because typescript parses this test | ||
// prior to executing it, and it outputs JS with 4-space indentation | ||
// and extraneous semicolons. | ||
t.equal( | ||
res, | ||
`!function |- | ||
"foo" | ||
function foo() { | ||
return 1; | ||
} | ||
` | ||
) | ||
t.end() | ||
}) | ||
|
||
test('unnamed function', t => { | ||
// it's actually kind of tricky to get a function that V8 won't | ||
// assign *some* sort of intelligible name to. It has to never be | ||
// assigned to any variable, or directly pass as an argument to | ||
// a named function at its point of creation, hence this line noise. | ||
const res = stringify((() => () => {})(), { customTags: [functionTag] }) | ||
t.equal( | ||
res, | ||
`!function |- | ||
"" | ||
() => { } | ||
` | ||
) | ||
t.end() | ||
}) | ||
|
||
test('parse completely empty value', t => { | ||
const src = `!function |-\n` | ||
const res: Function = parse(src, { customTags: [functionTag] }) | ||
t.type(res, 'function') | ||
t.equal(res.name, undefined) | ||
t.equal(res(), undefined) | ||
t.end() | ||
}) | ||
|
||
class Foo extends Boolean {} | ||
test('stringify a class', t => { | ||
const res = stringify(Foo, { customTags: [functionTag] }) | ||
// don't test the actual class body, because that will break | ||
// if/when TypeScript is updated. | ||
t.ok( | ||
res.startsWith(`!function |- | ||
"Foo" | ||
class Foo extends Boolean {`), | ||
'shows class toString value' | ||
) | ||
t.end() | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { Scalar, ScalarTag } from 'yaml' | ||
import { stringifyString } from 'yaml/util' | ||
|
||
const options: { defaultType: Scalar.Type } = { | ||
defaultType: 'BLOCK_LITERAL' | ||
} | ||
|
||
/** | ||
* `!function` A YAML representation of JavaScript functions | ||
* | ||
* Stringified as a block literal string, prefixed with the function name. | ||
* | ||
* When parsing, a no-op function with matching name and toString() is | ||
* returned. It is not possible to construct an actual JavaScript function by | ||
* evaluating YAML, and it is unsafe to attempt. | ||
*/ | ||
export const functionTag = { | ||
identify: value => typeof value === 'function', | ||
tag: '!function', | ||
resolve(str) { | ||
const src = str.split('\n') | ||
const n = src.shift() | ||
const name = n ? JSON.parse(n) : undefined | ||
const code = src.join('\n') | ||
const f = function () {} | ||
Object.defineProperty(f, 'name', { | ||
value: name, | ||
enumerable: false, | ||
configurable: true | ||
}) | ||
f.toString = () => code | ||
return f | ||
}, | ||
options, | ||
stringify(i, ctx, onComment, onChompKeep) { | ||
const { type: originalType, value: originalValue } = i | ||
const fn = originalValue as (...a: any[]) => any | ||
const value = JSON.stringify(fn.name) + '\n' + fn.toString() | ||
// better to just always put functions on a new line. | ||
const type: Scalar.Type = originalType || options.defaultType | ||
return stringifyString({ ...i, type, value }, ctx, onComment, onChompKeep) | ||
} | ||
} satisfies ScalarTag & { options: typeof options } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters