Skip to content

Commit

Permalink
feat: introduce legacy compat layer
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Jan 11, 2024
1 parent 61eebee commit e571b7b
Show file tree
Hide file tree
Showing 12 changed files with 426 additions and 46 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ Breaking changes from `@typescript/twoslash`:

// TODO:

### Backward Compatibility Layer

// TODO:

## TODOs

- [ ] Support `showEmit` option
- [ ] Test coverage
- [ ] Compat-layer maybe?
- [x] Test coverage
- [x] Compat-layer maybe?

## Benchmark

Expand Down
18 changes: 13 additions & 5 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type TS = typeof import('typescript')
// TODO: Make them configurable maybe
const reConfigBoolean = /^\/\/\s?@(\w+)$/mg
const reConfigValue = /^\/\/\s?@(\w+):\s?(.+)$/mg
const reAnnonateMarkers = /^\s*\/\/\s*\^(\?|\||\^+)( .*)?\n?$/mg
const reAnnonateMarkers = /^\s*\/\/\s*\^(\?|\||\^+)( .*)?$/mg

const cutString = '// ---cut---\n'
const cutAfterString = '// ---cut-after---\n'
Expand Down Expand Up @@ -267,12 +267,20 @@ export function createTwoSlasher(createOptions: CreateTwoSlashOptions = {}): Two
return undefined
const range: Range = [token.start, token.start + token.length]
// Turn static info to query if in range
if (targetsQuery.find(target => isInRanges(target, [range])))
token.type = 'query'
if (targetsQuery.find(target => isInRanges(target, [range]))) {
_tokens.push({
...token,
type: 'query',
} as any)
}

// Turn static info to completion if in range
else if (targetsHighlights.find(target => isInRanges(target[0], [range]) || isInRanges(target[1], [range])))
token.type = 'highlight'
else if (targetsHighlights.find(target => isInRanges(target[0], [range]) || isInRanges(target[1], [range]))) {
_tokens.push({
...token,
type: 'highlight',
} as any)
}
})
// #endregion

Expand Down
18 changes: 18 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import ts from 'typescript'
import type { TwoSlashOptions } from './core'
import { createTwoSlasher as _createTwoSlasher, twoslasher as _twoslasher } from './core'
import { convertLegacyOptions, convertLegacyReturn } from './legacy'
import type { TwoSlashOptionsLegacy, TwoSlashReturnLegacy } from './legacy'

export * from './public'
export * from './legacy'

// eslint-disable-next-line node/prefer-global/process
const cwd = /* @__PURE__ */ typeof process !== 'undefined' && typeof process.cwd === 'function' ? process.cwd() : ''
Expand All @@ -22,3 +25,18 @@ export function createTwoSlasher(opts?: TwoSlashOptions) {
...opts,
})
}

/**
* Compatability wrapper to align with `@typescript/twoslash`'s input/output
*
* @deprecated migrate to `twoslasher` instead
*/
export function twoslasherLegacy(code: string, lang: string, opts?: TwoSlashOptionsLegacy): TwoSlashReturnLegacy {
return convertLegacyReturn(
_twoslasher(code, lang, convertLegacyOptions({
vfsRoot: cwd,
tsModule: ts,
...opts,
})),
)
}
180 changes: 180 additions & 0 deletions src/legacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import type { CompilerOptions } from 'typescript'
import type { HandbookOptions, TwoSlashExecuteOptions, TwoSlashOptions, TwoSlashReturn } from './types'

Check failure on line 2 in src/legacy.ts

View workflow job for this annotation

GitHub Actions / lint

'TwoSlashOptions' is defined but never used
import { createPositionConverter } from './utils'

Check failure on line 3 in src/legacy.ts

View workflow job for this annotation

GitHub Actions / lint

'createPositionConverter' is defined but never used

export interface TwoSlashOptionsLegacy extends TwoSlashExecuteOptions {
/**
* @deprecated, use `handbookOptions` instead
*/
defaultOptions?: Partial<HandbookOptions>
/**
* @deprecated, use `compilerOptions` instead
*/
defaultCompilerOptions?: CompilerOptions
}

export interface TwoSlashReturnLegacy {
/** The output code, could be TypeScript, but could also be a JS/JSON/d.ts */
code: string

/** The new extension type for the code, potentially changed if they've requested emitted results */
extension: string

/** Requests to highlight a particular part of the code */
highlights: {
kind: 'highlight'
/** The index of the text in the file */
start: number
/** What line is the highlighted identifier on? */
line: number
/** At what index in the line does the caret represent */
offset: number
/** The text of the token which is highlighted */
text?: string
/** The length of the token */
length: number
}[]

/** An array of LSP responses identifiers in the sample */
staticQuickInfos: {
/** The string content of the node this represents (mainly for debugging) */
targetString: string
/** The base LSP response (the type) */
text: string
/** Attached JSDoc info */
docs: string | undefined
/** The index of the text in the file */
start: number
/** how long the identifier */
length: number
/** line number where this is found */
line: number
/** The character on the line */
character: number
}[]

/** Requests to use the LSP to get info for a particular symbol in the source */
queries: {
kind: 'query' | 'completions'
/** What line is the highlighted identifier on? */
line: number
/** At what index in the line does the caret represent */
offset: number
/** The text of the token which is highlighted */
text?: string
/** Any attached JSDocs */
docs?: string | undefined
/** The token start which the query indicates */
start: number
/** The length of the token */
length: number
/** Results for completions at a particular point */
completions?: import('typescript').CompletionEntry[]
/* Completion prefix e.g. the letters before the cursor in the word so you can filter */
completionsPrefix?: string
}[]

/** The extracted twoslash commands for any custom tags passed in via customTags */
tags: {
/** What was the name of the tag */
name: string
/** Where was it located in the original source file */
line: number
/** What was the text after the `// @tag: ` string (optional because you could do // @tag on it's own line without the ':') */
annotation?: string
}[]

/** Diagnostic error messages which came up when creating the program */
errors: {
renderedMessage: string
id: string
category: 0 | 1 | 2 | 3
code: number
start: number | undefined
length: number | undefined
line: number | undefined
character: number | undefined
}[]

/** The URL for this sample in the playground */
playgroundURL: string
}

export function convertLegacyOptions<T extends TwoSlashOptionsLegacy>(opts: T): Omit<T, 'defaultOptions' | 'defaultCompilerOptions'> {
return {
...opts,
handbookOptions: opts.handbookOptions || opts.defaultOptions,
compilerOptions: opts.compilerOptions || opts.defaultCompilerOptions,
}
}

/**
* Covert the new return type to the old one
*/
export function convertLegacyReturn(result: TwoSlashReturn): TwoSlashReturnLegacy {
return {
code: result.code,
extension: result.meta.extension,

staticQuickInfos: result.hovers
.map((i): TwoSlashReturnLegacy['staticQuickInfos'][0] => ({
text: i.text,
docs: i.docs || '',
start: i.start,
length: i.length,
line: i.line,
character: i.character,
targetString: i.target,
})),

tags: result.tags,

highlights: result.highlights
.map((h): TwoSlashReturnLegacy['highlights'][0] => ({
kind: 'highlight',
offset: h.character,
start: h.start,
length: h.length,
line: h.line,
text: h.text,
})),

queries: ([
...result.queries
.map((q): TwoSlashReturnLegacy['queries'][0] => ({
kind: 'query',
docs: q.docs || '',
offset: q.character,
start: q.start,
length: q.length,
line: q.line + 1,
text: q.text,
})),
...result.completions
.map((q): TwoSlashReturnLegacy['queries'][0] => ({
kind: 'completions',
offset: q.character,
start: q.start,
length: q.length,
line: q.line + 1,
completions: q.completions,
completionsPrefix: q.completionsPrefix,
})),
] as TwoSlashReturnLegacy['queries'])
.sort((a, b) => a.start - b.start),

errors: result.errors
.map((e): TwoSlashReturnLegacy['errors'][0] => ({
id: e.id,
code: e.code,
start: e.start,
length: e.length,
line: e.line,
character: e.character,
renderedMessage: e.text,
category: e.level,
})),

playgroundURL: '',
}
}
44 changes: 44 additions & 0 deletions test/legacy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fs from 'node:fs/promises'
import { basename } from 'node:path'
import { twoslasher as twoslasherOriginal } from '@typescript/twoslash'
import { describe, expect, it } from 'vitest'
import { twoslasherLegacy } from '../src'

describe('legacy', async () => {
await compareCode('./fixtures/examples/cuts_out_unnecessary_code.ts')
await compareCode('./fixtures/examples/errorsWithGenerics.ts')
await compareCode('./fixtures/examples/completions.ts')

async function compareCode(path: string) {
const code = await fs.readFile(new URL(path, import.meta.url), 'utf-8')

it(`compare ${basename(path)}`, async () => {
const us = twoslasherLegacy(code, 'ts')
const result = twoslasherOriginal(code, 'ts')

function cleanup(t: any) {
delete t.playgroundURL

// We have different calculations for queries, that are not trivial to map back
t.queries.forEach((i: any) => {
delete i.start
delete i.length
delete i.text
delete i.completions
})

t.errors.forEach((i: any) => {
delete i.id
})

return t
}

expect(
cleanup(us),
).toStrictEqual(
cleanup(result),
)
})
}
})
Loading

0 comments on commit e571b7b

Please sign in to comment.