diff --git a/package.json b/package.json index 57771d0..d0ae8bc 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "typescript-doctest": "src/main.js" }, "scripts": { + "preinstall": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { @@ -25,7 +26,6 @@ }, "homepage": "https://github.com/danr/typescript-doctest#readme", "dependencies": { - "fs": "0.0.1-security", "typescript": "^2.6.1" }, "devDependencies": { diff --git a/src/main.ts b/src/main.ts index 6b7ac05..99f90aa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,9 @@ #!/usr/bin/env node import * as ts from 'typescript' -import * as fs from "fs" +import * as fs from 'fs' -const is_doctest = (s: string) => s.match(/\/\/[ \t]*=>/) != null +const is_doctest = (s: string) => s.match( /\/\/[ \t]*=>/) != null +const doctest_rhs = (s: string) => s.match(/^\s*\/\/[ \t]*=>([^\n]*)/m) function replicate(i: number, x: A): A[] { const out = [] as A[] @@ -10,6 +11,14 @@ function replicate(i: number, x: A): A[] { return out } +function flatMap(xs: A[], f: (a: A) => B[]): B[] { + return ([] as B[]).concat(...xs.map(f)) +} + +function flatten(xss: A[][]): A[] { + return ([] as A[]).concat(...xss) +} + type Top = {filename: string, defs: Defs}[] type Defs = Def[] @@ -25,25 +34,18 @@ interface Def { kind: string, } -function walk(defs: Defs, f: (def: Def, d: number) => void, d0: number = 0): void { - defs.forEach(d => (f(d, d0), walk(d.children, f, d0 + 1))) +function walk(defs: Defs, f: (def: Def, d: number) => A[], d0: number = 0): A[] { + return flatten(defs.map(d => f(d, d0).concat(...walk(d.children, f, d0 + 1)))) } -function flatMap(xs: A[], f: (a: A) => B[]): B[] { - return ([] as B[]).concat(...xs.map(f)) -} +/** Generate documentation for all classes in a set of .ts files -function flatten(xss: A[][]): A[] { - return ([] as A[]).concat(...xss) -} - -/** Generate documentation for all classes in a set of .ts files */ -function generateDocumentation(fileNames: string[], options: ts.CompilerOptions): Top { - const program = ts.createProgram(fileNames, options) +Adapted from TS wiki about using the compiler API */ +function generateDocumentation(program: ts.Program, filenames: string[]): Top { const checker = program.getTypeChecker() const printer = ts.createPrinter() return program.getSourceFiles() - .filter(file => -1 != fileNames.indexOf(file.fileName)) + .filter(file => -1 != filenames.indexOf(file.fileName)) .map(file => ({ filename: file.fileName, defs: flatten(file.statements.map(visits)) @@ -128,10 +130,10 @@ function generateDocumentation(fileNames: string[], options: ts.CompilerOptions) } } -const pwoc = ts.createPrinter({removeComments: true}) - -function script(s: string): string[] { - const f = ts.createSourceFile('test.ts', s, ts.ScriptTarget.ES5, true, ts.ScriptKind.TS) +function script(program: ts.Program, filename: string, s: string): string[] { + const pwoc = ts.createPrinter({removeComments: true}) + const f = ts.createSourceFile('_doctest_' + filename, s, ts.ScriptTarget.ES5, true, ts.ScriptKind.TS) + ts.getPreEmitDiagnostics(program, f) const out = f.statements.map( (now, i) => { @@ -139,7 +141,7 @@ function script(s: string): string[] { const next = f.statements[i+1] // zip with next const [a, z] = next ? [next.pos, next.end] : [now.end, f.end] const after = f.text.slice(a, z) - const m = after.match(/^\s*\/\/[ \t]*=>([^\n]*)/m) + const m = doctest_rhs(after) if (m && m[1]) { const lhs = pwoc.printNode(ts.EmitHint.Expression, now.expression, f) const rhs = m[1].trim() @@ -151,93 +153,127 @@ function script(s: string): string[] { return out } -const headers = ["import * as test from 'tape'"] -const filenames = [] as string[] -let fileout = null -const argv = process.argv -for (let i = 2; i < argv.length; i++) { - const arg = argv[i] - if (arg == '-o') { - fileout = argv[++i] - } else if (arg == '-i') { - headers.push(argv[++i]) - } else { - filenames.push(arg) - } -} - -const top = generateDocumentation(filenames, { - target: ts.ScriptTarget.ES5, module: ts.ModuleKind.CommonJS -}) - - -if (fileout != null) { - const out = headers.slice() - - top.forEach(({filename, defs}) => { - walk(defs, d => { - let tests = 0 - d.doc.split(/\n\n+/m).map(s => { - if (is_doctest(s)) { - out.push( - 'test(' + JSON.stringify(d.name + ' ' + ++tests) + ', assert => {', - ...script(s).map(l => ' ' + l), - ' assert.end()', - '})', - '' - ) - } - }) - }) +function test_script_one(program: ts.Program, filename: string, d: Def): string[] { + const out = [] as string[] + let tests = 0 + d.doc.split(/\n\n+/m).map(s => { + if (is_doctest(s)) { + // todo: typecheck s now + out.push( + 'test(' + JSON.stringify(d.name + ' ' + ++tests) + ', assert => {', + ...script(program, filename, s).map(l => ' ' + l), + ' assert.end()', + '})', + '' + ) + } }) - - fs.writeFileSync(fileout, out.join('\n')); + return out } -const readme = [] as string[] +function test_script(program: ts.Program, top: Top) { + return ["import * as test from 'tape'"].concat(...top.map( + ({filename, defs}) => walk(defs, (d) => test_script_one(program, filename, d))) + ) +} -readme.push('## API overview') function prettyKind(kind: string) { return kind.replace('Declaration', '').toLowerCase() } -top.forEach(({defs}) => { - walk(defs, (def, i) => { - if (def.exported || i > 0) { - readme.push( - replicate(i, ' ').join('') + - '* ' + - (def.children.length == 0 ? '' : (prettyKind(def.kind) + ' ')) + - def.name) - } - }) -}) +function toc_one(def: Def, i: number): string[] { + if (def.exported || i > 0) { + return [ + replicate(i, ' ').join('') + + '* ' + + (def.children.length == 0 ? '' : (prettyKind(def.kind) + ' ')) + + def.name + ] + } else { + return [] + } +} -top.forEach(({defs}) => - walk(defs, (def, i) => { - if (def.exported || i > 0) { - let indent = '' - if (def.children.length == 0) { - //const method = (def.kind == 'MethodDeclaration') ? 'method ' : '' - readme.push('* ' + '**' + def.name + '**: `' + def.type + '`') - indent = ' ' - } else { - readme.push('### ' + prettyKind(def.kind) + ' ' + def.name) +function toc(top: Top): string[] { + return flatten(top.map(({defs}) => walk(defs, toc_one))) +} + +function doc_one(def: Def, i: number): string[] { + const out = [] as string[] + if (def.exported || i > 0) { + let indent = '' + if (def.children.length == 0) { + //const method = (def.kind == 'MethodDeclaration') ? 'method ' : '' + out.push('* ' + '**' + def.name + '**: `' + def.type + '`') + indent = ' ' + } else { + out.push('### ' + prettyKind(def.kind) + ' ' + def.name) + } + def.doc.split(/\n\n+/).forEach(s => { + out.push('') + if (is_doctest(s)) { + out.push(indent + '```typescript') } - def.doc.split(/\n\n+/).forEach(s => { - readme.push('') - if (is_doctest(s)) { - readme.push(indent + '```typescript') - } - readme.push(indent + s.trim().split('\n').join('\n' + indent)) - if (is_doctest(s)) { - readme.push(indent + '```') - } - }) + out.push(indent + s.trim().split('\n').join('\n' + indent)) + if (is_doctest(s)) { + out.push(indent + '```') + } + }) + } + return out +} + +function doc(top: Top) { + return flatten(top.map(({defs}) => walk(defs, doc_one))) +} + +const filenames = [] as string[] +const argv = process.argv.slice(2) +const outputs = [] as ((top: Top) => string[])[] + +{ + let program: ts.Program + for (let i = 0; i < argv.length; i++) { + const arg = argv[i] + if (arg == '-t' || arg == '--test-script') { + outputs.push(top => test_script(program, top)) + } else if (arg == '-d' || arg == '--doc') { + outputs.push(doc) + } else if (arg == '--toc' || arg == '--toc') { + outputs.push(toc) + } else if (arg == '-i' || arg == '--include') { + outputs.push(_top => [fs.readFileSync(argv[++i]).toString()]) + } else if (arg == '-s' || arg == '--string') { + outputs.push(_top => [argv[++i]]) + } else { + filenames.push(arg) } - }) -) + } -console.log(readme.join('\n')) + if (outputs.length == 0) { + console.log(`typescript-doctests + Each entry in may be: + [-t|--test-script] // write tape test script on stdout + [-d|--doc] // write markdown api documentation on stdout + [--toc] // write markdown table of contents on stdout + [-i|--include] FILENAME // write the contents of a file on stdout + [-s|--string] STRING // write a string literally on stdout + FILENAME // typescript files to look for docstrings in + Example usages: + + typescript-doctests src/*.ts -s 'import * as App from "../src/App"' -t > test/App.doctest.ts + + typescript-doctests src/*.ts -i Header.md --toc --doc -i Footer.md > README.md + `) + } else { + program = ts.createProgram(filenames, { + target: ts.ScriptTarget.ES5, + module: ts.ModuleKind.CommonJS + }) + const top = generateDocumentation(program, filenames) + + outputs.forEach(m => m(top).forEach(line => console.log(line))) + } +}