Skip to content

Commit

Permalink
Merge pull request #441 from eemeli/ts-fixes
Browse files Browse the repository at this point in the history
Improve TS developer experience
  • Loading branch information
eemeli committed Feb 15, 2023
2 parents c914dcc + 8407300 commit 6765cf5
Show file tree
Hide file tree
Showing 31 changed files with 612 additions and 438 deletions.
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

0 comments on commit 6765cf5

Please sign in to comment.