Skip to content

Commit

Permalink
feat: add typescript codegen utils (#3)
Browse files Browse the repository at this point in the history
* feat: add typescript codegen utils

* docs: typo
  • Loading branch information
danielroe committed Mar 4, 2022
1 parent b229745 commit b74fbe2
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 17 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,30 @@ console.log(genExport('pkg', ['a', 'b']))
console.log(genExport('pkg', { name: '*', as: 'bar' }))
```

**Generating TS:**

```js
import { genInterface, genAugmentation, genInlineTypeImport, genTypeImport, genTypeExport } from 'knitwork'

// interface FooInterface extends A, B {
// name: boolean
// optional?: string
// }
console.log(genInterface('FooInterface', { name: 'boolean', 'optional?': 'string' }, { extends: ['A', 'B'] }))
// declare module "my-module" {
// interface MyInterface {}
// }
console.log(genAugmentation('my-module', { MyInterface: {} }))
// typeof import("my-module").genString'
console.log(genInlineTypeImport('my-module', 'genString'))
// typeof import("my-module").default'
console.log(genInlineTypeImport('my-module'))
// import type { test as value } from "my-module";
console.log(genTypeImport('my-module', [{ name: 'test', as: 'value' }]))
// export type { test } from "my-module";
console.log(genTypeExport('my-module', ['test']))
```

**Serializing JS objects:**

```js
Expand Down
14 changes: 13 additions & 1 deletion src/esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ export function genImport (specifier: string, imports?: ESMImport | ESMImport[],
return _genStatement('import', specifier, imports, opts)
}

export function genTypeImport (specifier: string, imports: ESMImport[], opts: CodegenOptions = {}) {
return _genStatement('import type', specifier, imports, opts)
}

export function genTypeExport (specifier: string, imports: ESMImport[], opts: CodegenOptions = {}) {
return _genStatement('export type', specifier, imports, opts)
}

export const genInlineTypeImport = (specifier: string, name = 'default', opts: CodegenOptions = {}) => {
return `typeof ${genDynamicImport(specifier, { ...opts, wrapper: false })}.${name}`
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export
// https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-exports
export type ESMExport = string | { name: string, as?: string }
Expand All @@ -19,7 +31,7 @@ export function genExport (specifier: string, exports?: ESMExport | ESMExport[],
}

type ESMImportOrExport = ESMImport | ESMExport
function _genStatement (type: 'import' | 'export', specifier: string, names?: ESMImportOrExport | ESMImportOrExport[], opts: CodegenOptions = {}) {
function _genStatement (type: 'import' | 'export' | 'import type' | 'export type', specifier: string, names?: ESMImportOrExport | ESMImportOrExport[], opts: CodegenOptions = {}) {
const specifierStr = genString(specifier, opts)
if (!names) {
return `${type} ${specifierStr};`
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './esm'
export * from './object'
export * from './string'
export * from './types'
export * from './typescript'
16 changes: 1 addition & 15 deletions src/object.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { genString } from './string'
import { genObjectKey, wrapInDelimiters } from './utils'

export function genObjectFromRaw (obj: Record<string, any>, indent = ''): string {
return genObjectFromRawEntries(Object.entries(obj), indent)
Expand All @@ -16,14 +16,6 @@ export function genObjectFromRawEntries (array: [key: string, value: any][], ind

// --- Internals ---

function wrapInDelimiters (lines: string[], indent = '', delimiters = '{}') {
if (!lines.length) {
return delimiters
}
const [start, end] = delimiters
return `${start}\n` + lines.join(',\n') + `\n${indent}${end}`
}

function genRawValue (value: unknown, indent = ''): string {
if (typeof value === 'undefined') {
return 'undefined'
Expand All @@ -39,9 +31,3 @@ function genRawValue (value: unknown, indent = ''): string {
}
return value.toString()
}

const VALID_IDENTIFIER_RE = /^[$_]?[\w\d]*$/

function genObjectKey (key: string) {
return key.match(VALID_IDENTIFIER_RE) ? key : genString(key)
}
40 changes: 40 additions & 0 deletions src/typescript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@

import { genString } from './string'
import { genObjectKey, wrapInDelimiters } from './utils'

export type TypeObject = {
[key: string]: string | TypeObject
}
export interface GenInterfaceOptions {
extends?: string | string[]
export?: boolean
}

export const genTypeObject = (obj: TypeObject, indent = '') => {
const newIndent = indent + ' '
return wrapInDelimiters(Object.entries(obj).map(([key, value]) => {
const [, k = key, optional = ''] = key.match(/^(.*[^?])(\?)?$/) /* c8 ignore next */ || []
if (typeof value === 'string') {
return `${newIndent}${genObjectKey(k)}${optional}: ${value}`
}
return `${newIndent}${genObjectKey(k)}${optional}: ${genTypeObject(value, newIndent)}`
}), indent, '{}', false)
}

export const genInterface = (name: string, contents?: TypeObject, options: GenInterfaceOptions = {}) => {
const result = [
options.export && 'export',
`interface ${name}`,
options.extends && `extends ${Array.isArray(options.extends) ? options.extends.join(', ') : options.extends}`,
contents ? genTypeObject(contents) : '{}'
].filter(Boolean).join(' ')
return result
}

export const genAugmentation = (specifier: string, interfaces?: Record<string, TypeObject | [TypeObject, Omit<GenInterfaceOptions, 'export'>]>) => {
return `declare module ${genString(specifier)} ${wrapInDelimiters(
Object.entries(interfaces || {}).map(
([key, entry]) => ' ' + (Array.isArray(entry) ? genInterface(key, ...entry) : genInterface(key, entry))
)
)}`
}
15 changes: 15 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { genString } from './string'

export function wrapInDelimiters (lines: string[], indent = '', delimiters = '{}', withComma = true) {
if (!lines.length) {
return delimiters
}
const [start, end] = delimiters
return `${start}\n` + lines.join(withComma ? ',\n' : '\n') + `\n${indent}${end}`
}

const VALID_IDENTIFIER_RE = /^[$_]?[\w\d]*$/

export function genObjectKey (key: string) {
return key.match(VALID_IDENTIFIER_RE) ? key : genString(key)
}
103 changes: 102 additions & 1 deletion test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, describe, it } from 'vitest'
import { genImport, genExport, genDynamicImport, genObjectFromRaw, genObjectFromRawEntries } from '../src'
import { genImport, genExport, genDynamicImport, genObjectFromRaw, genObjectFromRawEntries, genInterface, genAugmentation, genInlineTypeImport, genTypeImport, genTypeExport } from '../src'

const genImportTests = [
{ names: 'foo', code: 'import foo from "pkg";' },
Expand Down Expand Up @@ -108,3 +108,104 @@ describe('genObjectFromRawEntries', () => {
})
}
})

const genInterfaceTests: Array<{ input: Parameters<typeof genInterface>, code: string }> = [
{ input: ['FooInterface'], code: 'interface FooInterface {}' },
{ input: ['FooInterface', undefined, { extends: ['Other'] }], code: 'interface FooInterface extends Other {}' },
{ input: ['FooInterface', undefined, { extends: 'Other' }], code: 'interface FooInterface extends Other {}' },
{
input: ['FooInterface', { name: 'boolean', 'other name"': { value: '() => {}' } }],
code:
`interface FooInterface {
name: boolean
"other name\\"": {
value: () => {}
}
}`
},
{
input: ['FooInterface', { "na'me?": 'boolean' }],
code:
`interface FooInterface {
"na'me"?: boolean
}`
}
]

describe('genInterface', () => {
for (const t of genInterfaceTests) {
it(t.code, () => {
const code = genInterface(...t.input)
expect(code).to.equal(t.code)
})
}
})

const genAugmentationTests: Array<{ input: Parameters<typeof genAugmentation>, code: string }> = [
{ input: ['@nuxt/utils'], code: 'declare module "@nuxt/utils" {}' },
{
input: ['@nuxt/utils', { MyInterface: {} }],
code:
`declare module "@nuxt/utils" {
interface MyInterface {}
}`
},
{
input: ['@nuxt/utils', { MyInterface: [{}, { extends: ['OtherInterface', 'FurtherInterface'] }] }],
code:
`declare module "@nuxt/utils" {
interface MyInterface extends OtherInterface, FurtherInterface {}
}`
}
]

describe('genAugmentation', () => {
for (const t of genAugmentationTests) {
it(t.code, () => {
const code = genAugmentation(...t.input)
expect(code).to.equal(t.code)
})
}
})

const genInlineTypeImportTests: Array<{ input: Parameters<typeof genInlineTypeImport>, code: string }> = [
{ input: ['@nuxt/utils'], code: 'typeof import("@nuxt/utils").default' },
{ input: ['@nuxt/utils', 'genString'], code: 'typeof import("@nuxt/utils").genString' }
]

describe('genInlineTypeImport', () => {
for (const t of genInlineTypeImportTests) {
it(t.code, () => {
const code = genInlineTypeImport(...t.input)
expect(code).to.equal(t.code)
})
}
})

const genTypeImportTests: Array<{ input: Parameters<typeof genTypeImport>, code: string }> = [
{ input: ['@nuxt/utils', ['test']], code: 'import type { test } from "@nuxt/utils";' },
{ input: ['@nuxt/utils', [{ name: 'test', as: 'value' }]], code: 'import type { test as value } from "@nuxt/utils";' }
]

describe('genTypeImport', () => {
for (const t of genTypeImportTests) {
it(t.code, () => {
const code = genTypeImport(...t.input)
expect(code).to.equal(t.code)
})
}
})

const genTypeExportTests: Array<{ input: Parameters<typeof genTypeExport>, code: string }> = [
{ input: ['@nuxt/utils', ['test']], code: 'export type { test } from "@nuxt/utils";' },
{ input: ['@nuxt/utils', [{ name: 'test', as: 'value' }]], code: 'export type { test as value } from "@nuxt/utils";' }
]

describe('genTypeExport', () => {
for (const t of genTypeExportTests) {
it(t.code, () => {
const code = genTypeExport(...t.input)
expect(code).to.equal(t.code)
})
}
})

0 comments on commit b74fbe2

Please sign in to comment.