Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve TS developer experience #441

Merged
merged 20 commits into from Feb 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e30994b
feat(ts): Add Strict type flag for Document
eemeli Feb 12, 2023
edd9cdd
test: Convert tests/doc/anchors from JS to TS
eemeli Feb 12, 2023
2aced1c
fix(ts): Add undefined & Date handling to NodeType<T>
eemeli Feb 12, 2023
837bc32
test: Convert tests/doc/createNode from JS to TS
eemeli Feb 12, 2023
90ac31b
test: Convert tests/doc/errors from JS to TS
eemeli Feb 12, 2023
7e6b35c
fix(ts): Export FoldOptions from yaml/util
eemeli Feb 12, 2023
6e52136
test: Convert tests/doc/foldFlowLines from JS to TS
eemeli Feb 12, 2023
81b0f0a
test: Convert tests/doc/YAML-1.2.spec from JS to TS
eemeli Feb 12, 2023
90074bb
test: Convert tests/doc/YAML-1.1.spec from JS to TS
eemeli Feb 12, 2023
d0a1a55
test: Convert tests/doc/comments from JS to TS
eemeli Feb 12, 2023
d7ff81b
test: Fix remaining TS bugs & add test type checks to test:types script
eemeli Feb 12, 2023
8f6e75e
feat(ts): Add <Contents, Strict> generic types to Composer
eemeli Feb 12, 2023
0f1c7f0
test: Convert tests/doc/parse from JS to TS
eemeli Feb 12, 2023
d82e9b9
test: Convert tests/doc/stringify from JS to TS
eemeli Feb 12, 2023
c7cc418
test: Convert tests/doc/types from JS to TS
eemeli Feb 12, 2023
82fde88
chore: Update ESLint & TS configs for tests
eemeli Feb 12, 2023
52ce17a
chore: Drop support for TS 3.8 & add note to docs
eemeli Feb 13, 2023
f0a4afa
ci: Fix types for older TS versions
eemeli Feb 13, 2023
2489810
chore(ts): Use package.json typesVersions to find d.ts files from dist/
eemeli Feb 13, 2023
8407300
fix: Avoid polynomial regexp in stringifyString
eemeli Feb 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.yaml
Expand Up @@ -49,6 +49,7 @@ overrides:
jest: true
rules:
camelcase: 0
'@typescript-eslint/no-non-null-assertion': off
'@typescript-eslint/no-unsafe-return': off

- files: [tests/doc/**]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Expand Up @@ -25,8 +25,8 @@ jobs:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
- run: npm run test:types
- run: npm run test:dist
- run: npm run test:types
- run: npm run test:dist:types

lint:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/typescript.yml
Expand Up @@ -27,6 +27,6 @@ jobs:
- run: for d in node_modules/@types/*; do [[ $d == *node ]] || rm -r $d; done
- run: npm run test:dist:types

- run: npm install --no-save typescript@3.8
- run: npm install --no-save typescript@3.9
- run: for d in node_modules/@types/*; do [[ $d == *node ]] || rm -r $d; done
- run: npm run test:dist:types
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -14,6 +14,10 @@ It has no external dependencies and runs on Node.js as well as modern browsers.
For the purposes of versioning, any changes that break any of the documented endpoints or APIs will be considered semver-major breaking changes.
Undocumented library internals may change between minor versions, and previous APIs may be deprecated (but not removed).

The minimum supported TypeScript version of the included typings is 3.9;
for use in earlier versions you may need to set `skipLibCheck: true` in your config.
This requirement may be updated between minor versions of the library.

For more information, see the project's documentation site: [**eemeli.org/yaml**](https://eemeli.org/yaml/)

To install:
Expand Down
4 changes: 4 additions & 0 deletions docs/01_intro.md
Expand Up @@ -32,6 +32,10 @@ It has no external dependencies and runs on Node.js as well as modern browsers.
For the purposes of versioning, any changes that break any of the endpoints or APIs documented here will be considered semver-major breaking changes.
Undocumented library internals may change between minor versions, and previous APIs may be deprecated (but not removed).

The minimum supported TypeScript version of the included typings is 3.9;
for use in earlier versions you may need to set `skipLibCheck: true` in your config.
This requirement may be updated between minor versions of the library.

**Note:** These docs are for `yaml@2`. For v1, see the [v1.10.0 tag](https://github.com/eemeli/yaml/tree/v1.10.0) for the source and [eemeli.org/yaml/v1](https://eemeli.org/yaml/v1/) for the documentation.

## API Overview
Expand Down
10 changes: 8 additions & 2 deletions package.json
Expand Up @@ -14,7 +14,6 @@
"files": [
"browser/",
"dist/",
"util.d.ts",
"util.js"
],
"type": "commonjs",
Expand All @@ -35,6 +34,13 @@
"default": "./browser/dist/util.js"
}
},
"typesVersions": {
"*": {
"*": [
"dist/*"
]
}
},
"scripts": {
"build": "npm run build:node && npm run build:browser",
"build:browser": "rollup -c config/rollup.browser-config.mjs",
Expand All @@ -49,7 +55,7 @@
"test:browsers": "cd playground && npm test",
"test:dist": "npm run build:node && jest --config config/jest.config.js",
"test:dist:types": "tsc --allowJs --moduleResolution node --noEmit --target es5 dist/index.js",
"test:types": "tsc --noEmit",
"test:types": "tsc --noEmit && tsc --noEmit -p tests/tsconfig.json",
"docs:install": "cd docs-slate && bundle install",
"docs:deploy": "cd docs-slate && ./deploy.sh",
"docs": "cd docs-slate && bundle exec middleman server",
Expand Down
9 changes: 7 additions & 2 deletions src/compose/compose-doc.ts
@@ -1,5 +1,6 @@
import type { Directives } from '../doc/directives.js'
import { Document } from '../doc/Document.js'
import type { ParsedNode } from '../nodes/Node.js'
import type {
DocumentOptions,
ParseOptions,
Expand All @@ -15,14 +16,17 @@ import type { ComposeErrorHandler } from './composer.js'
import { resolveEnd } from './resolve-end.js'
import { resolveProps } from './resolve-props.js'

export function composeDoc(
export function composeDoc<
Contents extends ParsedNode = ParsedNode,
Strict extends boolean = true
>(
options: ParseOptions & DocumentOptions & SchemaOptions,
directives: Directives,
{ offset, start, value, end }: CST.Document,
onError: ComposeErrorHandler
) {
const opts = Object.assign({ _directives: directives }, options)
const doc = new Document(undefined, opts) as Document.Parsed
const doc = new Document(undefined, opts) as Document.Parsed<Contents, Strict>
const ctx: ComposeContext = {
atRoot: true,
directives: doc.directives,
Expand All @@ -49,6 +53,7 @@ export function composeDoc(
'Block collection cannot start on same line with directives-end marker'
)
}
// @ts-expect-error If Contents is set, let's trust the user
doc.contents = value
? composeNode(ctx, value, props, onError)
: composeEmptyNode(ctx, props.end, start, null, props, onError)
Expand Down
18 changes: 12 additions & 6 deletions src/compose/composer.ts
@@ -1,7 +1,7 @@
import { Directives } from '../doc/directives.js'
import { Document } from '../doc/Document.js'
import { ErrorCode, YAMLParseError, YAMLWarning } from '../errors.js'
import { isCollection, isPair, Range } from '../nodes/Node.js'
import { isCollection, isPair, ParsedNode, Range } from '../nodes/Node.js'
import type {
DocumentOptions,
ParseOptions,
Expand Down Expand Up @@ -69,9 +69,12 @@ function parsePrelude(prelude: string[]) {
* const docs = new Composer().compose(tokens)
* ```
*/
export class Composer {
export class Composer<
Contents extends ParsedNode = ParsedNode,
Strict extends boolean = true
> {
private directives: Directives
private doc: Document.Parsed | null = null
private doc: Document.Parsed<Contents, Strict> | null = null
private options: ParseOptions & DocumentOptions & SchemaOptions
private atDirectives = false
private prelude: string[] = []
Expand All @@ -90,7 +93,7 @@ export class Composer {
else this.errors.push(new YAMLParseError(pos, code, message))
}

private decorate(doc: Document.Parsed, afterDoc: boolean) {
private decorate(doc: Document.Parsed<Contents, Strict>, afterDoc: boolean) {
const { comment, afterEmptyLine } = parsePrelude(this.prelude)
//console.log({ dc: doc.comment, prelude, comment })
if (comment) {
Expand Down Expand Up @@ -162,7 +165,7 @@ export class Composer {
this.atDirectives = true
break
case 'document': {
const doc = composeDoc(
const doc = composeDoc<Contents, Strict>(
this.options,
this.directives,
token,
Expand Down Expand Up @@ -247,7 +250,10 @@ export class Composer {
this.doc = null
} else if (forceDoc) {
const opts = Object.assign({ _directives: this.directives }, this.options)
const doc = new Document(undefined, opts) as Document.Parsed
const doc = new Document(undefined, opts) as Document.Parsed<
Contents,
Strict
>
if (this.atDirectives)
this.onError(
endOffset,
Expand Down
63 changes: 37 additions & 26 deletions src/doc/Document.ts
Expand Up @@ -36,13 +36,21 @@ import { Directives } from './directives.js'
export type Replacer = any[] | ((key: any, value: any) => unknown)

export declare namespace Document {
interface Parsed<T extends ParsedNode = ParsedNode> extends Document<T> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/** @ts-ignore The typing of directives fails in TS <= 4.2 */
interface Parsed<
Contents extends ParsedNode = ParsedNode,
Strict extends boolean = true
> extends Document<Contents, Strict> {
directives: Directives
range: Range
}
}

export class Document<T extends Node = Node> {
export class Document<
Contents extends Node = Node,
Strict extends boolean = true
> {
declare readonly [NODE_TYPE]: symbol

/** A comment before this Document */
Expand All @@ -52,9 +60,9 @@ export class Document<T extends Node = Node> {
comment: string | null = null

/** The document contents. */
contents: T | null
contents: Strict extends true ? Contents | null : Contents

directives?: Directives
directives: Strict extends true ? Directives | undefined : Directives

/** Errors encountered during parsing. */
errors: YAMLError[] = []
Expand Down Expand Up @@ -131,19 +139,18 @@ export class Document<T extends Node = Node> {
} else this.directives = new Directives({ version })
this.setSchema(version, options)

if (value === undefined) this.contents = null
else {
this.contents = this.createNode(value, _replacer, options) as unknown as T
}
// @ts-expect-error We can't really know that this matches Contents.
this.contents =
value === undefined ? null : this.createNode(value, _replacer, options)
}

/**
* Create a deep copy of this Document and its contents.
*
* Custom Node values that inherit from `Object` still refer to their original instances.
*/
clone(): Document<T> {
const copy: Document<T> = Object.create(Document.prototype, {
clone(): Document<Contents, Strict> {
const copy: Document<Contents, Strict> = Object.create(Document.prototype, {
[NODE_TYPE]: { value: DOC }
})
copy.commentBefore = this.commentBefore
Expand All @@ -153,8 +160,9 @@ export class Document<T extends Node = Node> {
copy.options = Object.assign({}, this.options)
if (this.directives) copy.directives = this.directives.clone()
copy.schema = this.schema.clone()
// @ts-expect-error We can't really know that this matches Contents.
copy.contents = isNode(this.contents)
? (this.contents.clone(copy.schema) as unknown as T)
? this.contents.clone(copy.schema)
: this.contents
if (this.range) copy.range = this.range.slice() as Document['range']
return copy
Expand All @@ -179,7 +187,10 @@ export class Document<T extends Node = Node> {
* `name` will be used as a prefix for a new unique anchor.
* If `name` is undefined, the generated anchor will use 'a' as a prefix.
*/
createAlias(node: Scalar | YAMLMap | YAMLSeq, name?: string): Alias {
createAlias(
node: Strict extends true ? Scalar | YAMLMap | YAMLSeq : Node,
name?: string
): Alias {
if (!node.anchor) {
const prev = anchorNames(this)
node.anchor =
Expand Down Expand Up @@ -276,6 +287,7 @@ export class Document<T extends Node = Node> {
deleteIn(path: Iterable<unknown> | null): boolean {
if (isEmptyPath(path)) {
if (this.contents == null) return false
// @ts-expect-error Presumed impossible if Strict extends false
this.contents = null
return true
}
Expand All @@ -289,7 +301,7 @@ export class Document<T extends Node = Node> {
* scalar values from their surrounding node; to disable set `keepScalar` to
* `true` (collections are always returned intact).
*/
get(key: unknown, keepScalar?: boolean): unknown {
get(key: unknown, keepScalar?: boolean): Strict extends true ? unknown : any {
return isCollection(this.contents)
? this.contents.get(key, keepScalar)
: undefined
Expand All @@ -300,7 +312,10 @@ export class Document<T extends Node = Node> {
* scalar values from their surrounding node; to disable set `keepScalar` to
* `true` (collections are always returned intact).
*/
getIn(path: Iterable<unknown> | null, keepScalar?: boolean): unknown {
getIn(
path: Iterable<unknown> | null,
keepScalar?: boolean
): Strict extends true ? unknown : any {
if (isEmptyPath(path))
return !keepScalar && isScalar(this.contents)
? this.contents.value
Expand Down Expand Up @@ -331,11 +346,8 @@ export class Document<T extends Node = Node> {
*/
set(key: any, value: unknown): void {
if (this.contents == null) {
this.contents = collectionFromPath(
this.schema,
[key],
value
) as unknown as T
// @ts-expect-error We can't really know that this matches Contents.
this.contents = collectionFromPath(this.schema, [key], value)
} else if (assertCollection(this.contents)) {
this.contents.set(key, value)
}
Expand All @@ -346,13 +358,12 @@ export class Document<T extends Node = Node> {
* boolean to add/remove the item from the set.
*/
setIn(path: Iterable<unknown> | null, value: unknown): void {
if (isEmptyPath(path)) this.contents = value as T
else if (this.contents == null) {
this.contents = collectionFromPath(
this.schema,
Array.from(path),
value
) as unknown as T
if (isEmptyPath(path)) {
// @ts-expect-error We can't really know that this matches Contents.
this.contents = value
} else if (this.contents == null) {
// @ts-expect-error We can't really know that this matches Contents.
this.contents = collectionFromPath(this.schema, Array.from(path), value)
} else if (assertCollection(this.contents)) {
this.contents.setIn(path, value)
}
Expand Down
7 changes: 5 additions & 2 deletions src/doc/anchors.ts
Expand Up @@ -20,7 +20,7 @@ export function anchorIsValid(anchor: string): true {
return true
}

export function anchorNames(root: Document | Node) {
export function anchorNames(root: Document<Node, boolean> | Node) {
const anchors = new Set<string>()
visit(root, {
Value(_key: unknown, node: Scalar | YAMLMap | YAMLSeq) {
Expand All @@ -38,7 +38,10 @@ export function findNewAnchor(prefix: string, exclude: Set<string>) {
}
}

export function createNodeAnchors(doc: Document, prefix: string) {
export function createNodeAnchors(
doc: Document<Node, boolean>,
prefix: string
) {
const aliasObjects: unknown[] = []
const sourceObjects: CreateNodeContext['sourceObjects'] = new Map()
let prevAnchors: Set<string> | null = null
Expand Down
10 changes: 9 additions & 1 deletion src/nodes/Node.ts
Expand Up @@ -14,8 +14,16 @@ export type Node<T = unknown> =
| YAMLSeq<T>

/** Utility type mapper */
export type NodeType<T> = T extends string | number | bigint | boolean | null
export type NodeType<T> = T extends
| string
| number
| bigint
| boolean
| null
| undefined
? Scalar<T>
: T extends Date
? Scalar<string | Date>
: T extends Array<any>
? YAMLSeq<NodeType<T[number]>>
: T extends { [key: string]: any }
Expand Down
2 changes: 1 addition & 1 deletion src/nodes/toJS.ts
Expand Up @@ -10,7 +10,7 @@ export interface AnchorData {

export interface ToJSContext {
anchors: Map<Node, AnchorData>
doc: Document
doc: Document<Node, boolean>
keep: boolean
mapAsMap: boolean
mapKeyWarned: boolean
Expand Down