diff --git a/packages/jest-docblock/README.md b/packages/jest-docblock/README.md index 41f021a912c2..85a740b8e6aa 100644 --- a/packages/jest-docblock/README.md +++ b/packages/jest-docblock/README.md @@ -30,6 +30,7 @@ Pragmas can also take arguments: `jest-docblock` can: * extract the docblock from some code as a string * parse a docblock string's pragmas into an object +* print an object and some comments back to a string ## Installation ```sh @@ -56,13 +57,18 @@ const code = ` } `; -const { extract, parse } = require("jest-docblock"); +const { extract, parse, parseWithComments, print } = require("jest-docblock"); const docblock = extract(code); console.log(docblock); // "/**\n * Everything is awesome!\n * \n * @everything is:awesome\n * @flow\n */" const pragmas = parse(docblock); console.log(pragmas); // { everything: "is:awesome", flow: "" } + +const parsed = parseWithComments(docblock); +console.log(parsed); // { comments: "Everything is awesome!", pragmas: { everything: "is:awesome", flow: "" } } + +console.log(print({pragmas, comments: "hi!"})) // /**\n * hi!\n *\n * @everything is:awesome\n * @flow\n */; ``` ## API Documentation @@ -72,3 +78,9 @@ Extracts a docblock from some file contents. Returns the docblock contained in ` ### `parse(docblock: string): {[key: string]: string}` Parses the pragmas in a docblock string into an object whose keys are the pragma tags and whose values are the arguments to those pragmas. + +### `parseWithComments(docblock: string): { comments: string, pragmas: {[key: string]: string} }` +Similar to `parse` except this method also returns the comments from the docblock. Useful when used with `print()`. + +### `print({ comments?: string, pragmas?: {[key: string]: string} }): string` +Prints an object of key-value pairs back into a docblock. If `comments` are provided, they will be positioned on the top of the docblock. \ No newline at end of file diff --git a/packages/jest-docblock/package.json b/packages/jest-docblock/package.json index 1c8921a08ade..6739455f2511 100644 --- a/packages/jest-docblock/package.json +++ b/packages/jest-docblock/package.json @@ -6,5 +6,8 @@ "url": "https://github.com/facebook/jest.git" }, "license": "MIT", - "main": "build/index.js" + "main": "build/index.js", + "dependencies": { + "detect-newline": "^2.1.0" + } } diff --git a/packages/jest-docblock/src/__tests__/index.test.js b/packages/jest-docblock/src/__tests__/index.test.js index c44de6118221..b2097b3ba484 100644 --- a/packages/jest-docblock/src/__tests__/index.test.js +++ b/packages/jest-docblock/src/__tests__/index.test.js @@ -10,8 +10,8 @@ 'use strict'; -const os = require('os'); -const docblock = require('../'); +import os from 'os'; +import * as docblock from '..'; describe('docblock', () => { it('extracts valid docblock with line comment', () => { @@ -212,4 +212,182 @@ describe('docblock', () => { providesModule: 'apple/banana', }); }); + + it('extracts comments from docblock', () => { + const code = + '/**' + + os.EOL + + ' * hello world' + + os.EOL + + ' * @flow yes' + + os.EOL + + ' */'; + expect(docblock.parseWithComments(code)).toEqual({ + comments: 'hello world', + pragmas: {flow: 'yes'}, + }); + }); + + it('extracts multiline comments from docblock', () => { + const code = + '/**' + + os.EOL + + ' * hello' + + os.EOL + + ' * world' + + os.EOL + + ' * @flow yes' + + os.EOL + + ' */'; + expect(docblock.parseWithComments(code)).toEqual({ + comments: 'hello' + os.EOL + 'world', + pragmas: {flow: 'yes'}, + }); + }); + + it('extracts comments from beginning and end of docblock', () => { + const code = + '/**' + + os.EOL + + ' * hello' + + os.EOL + + ' * @flow yes' + + os.EOL + + ' * ' + + os.EOL + + ' * world' + + os.EOL + + ' */'; + expect(docblock.parseWithComments(code)).toEqual({ + comments: 'hello' + os.EOL + os.EOL + 'world', + pragmas: {flow: 'yes'}, + }); + }); + + it('extracts docblock comments as CRLF when docblock contains CRLF', () => { + const code = '/**\r\n * foo\r\n * bar\r\n*/'; + expect(docblock.parseWithComments(code)).toEqual({ + comments: 'foo\r\nbar', + pragmas: {}, + }); + }); + + it('extracts docblock comments as LF when docblock contains LF', () => { + const code = '/**\n * foo\n * bar\n*/'; + expect(docblock.parseWithComments(code)).toEqual({ + comments: 'foo\nbar', + pragmas: {}, + }); + }); + + it('prints docblocks with no pragmas as empty string', () => { + const pragmas = {}; + expect(docblock.print({pragmas})).toEqual(''); + }); + + it('prints docblocks with one pragma on one line', () => { + const pragmas = {flow: ''}; + expect(docblock.print({pragmas})).toEqual('/** @flow */'); + }); + + it('prints docblocks with multiple pragmas on multiple lines', () => { + const pragmas = { + flow: '', + format: '', + }; + expect(docblock.print({pragmas})).toEqual( + '/**' + os.EOL + ' * @flow' + os.EOL + ' * @format' + os.EOL + ' */', + ); + }); + + it('prints docblocks with pragmas', () => { + const pragmas = { + flow: 'foo', + providesModule: 'x/y/z', + }; + expect(docblock.print({pragmas})).toEqual( + '/**' + + os.EOL + + ' * @flow foo' + + os.EOL + + ' * @providesModule x/y/z' + + os.EOL + + ' */', + ); + }); + + it('prints docblocks with comments', () => { + const pragmas = {flow: 'foo'}; + const comments = 'hello'; + expect(docblock.print({comments, pragmas})).toEqual( + '/**' + + os.EOL + + ' * hello' + + os.EOL + + ' *' + + os.EOL + + ' * @flow foo' + + os.EOL + + ' */', + ); + }); + + it('prints docblocks with comments and no keys', () => { + const pragmas = {}; + const comments = 'Copyright 2004-present Facebook. All Rights Reserved.'; + expect(docblock.print({comments, pragmas})).toEqual( + '/**' + os.EOL + ' * ' + comments + os.EOL + ' */', + ); + }); + + it('prints docblocks with multiline comments', () => { + const pragmas = {}; + const comments = 'hello' + os.EOL + 'world'; + expect(docblock.print({comments, pragmas})).toEqual( + '/**' + os.EOL + ' * hello' + os.EOL + ' * world' + os.EOL + ' */', + ); + }); + + it('prints docblocks that are parseable', () => { + const pragmas = {a: 'b', c: ''}; + const comments = 'hello world!'; + const formatted = docblock.print({comments, pragmas}); + const parsed = docblock.parse(formatted); + expect(parsed).toEqual(pragmas); + }); + + it('can augment existing docblocks with comments', () => { + const before = + '/**' + os.EOL + ' * Legalese' + os.EOL + ' * @flow' + os.EOL + ' */'; + const {comments, pragmas} = docblock.parseWithComments(before); + pragmas.format = ''; + const after = docblock.print({comments, pragmas}); + expect(after).toEqual( + '/**' + + os.EOL + + ' * Legalese' + + os.EOL + + ' *' + + os.EOL + + ' * @flow' + + os.EOL + + ' * @format' + + os.EOL + + ' */', + ); + }); + + it('prints docblocks using CRLF if comments contains CRLF', () => { + const pragmas = {}; + const comments = 'hello\r\nworld'; + const formatted = docblock.print({comments, pragmas}); + expect(formatted).toEqual('/**\r\n * hello\r\n * world\r\n */'); + }); + + it('prints docblocks using LF if comments contains LF', () => { + const pragmas = {}; + const comments = 'hello\nworld'; + const formatted = docblock.print({comments, pragmas}); + expect(formatted).toEqual('/**\n * hello\n * world\n */'); + }); }); diff --git a/packages/jest-docblock/src/index.js b/packages/jest-docblock/src/index.js index 714c20032fdf..7281a6941f77 100644 --- a/packages/jest-docblock/src/index.js +++ b/packages/jest-docblock/src/index.js @@ -7,6 +7,9 @@ * @flow */ +import detectNewline from 'detect-newline'; +import {EOL} from 'os'; + const commentEndRe = /\*\/$/; const commentStartRe = /^\/\*\*/; const docblockRe = /^\s*(\/\*\*?(.|\r?\n)*?\*\/)/; @@ -15,14 +18,23 @@ const ltrimRe = /^\s*/; const multilineRe = /(?:^|\r?\n) *(@[^\r\n]*?) *\r?\n *([^@\r\n\s][^@\r\n]+?) *\r?\n/g; const propertyRe = /(?:^|\r?\n) *@(\S+) *([^\r\n]*)/g; const stringStartRe = /(\r?\n|^) *\*/g; +const lineStartRe = /(\r?\n|^) */g; const wsRe = /[\t ]+/g; -function extract(contents: string): string { +export function extract(contents: string): string { const match = contents.match(docblockRe); return match ? match[0].replace(ltrimRe, '') || '' : ''; } -function parse(docblock: string): {[key: string]: string} { +export function parse(docblock: string): {[key: string]: string} { + return parseWithComments(docblock).pragmas; +} + +export function parseWithComments( + docblock: string, +): {comments: string, pragmas: {[key: string]: string}} { + const line = detectNewline(docblock) || EOL; + docblock = docblock .replace(commentStartRe, '') .replace(commentEndRe, '') @@ -34,17 +46,62 @@ function parse(docblock: string): {[key: string]: string} { let prev = ''; while (prev !== docblock) { prev = docblock; - docblock = docblock.replace(multilineRe, '\n$1 $2\n'); + docblock = docblock.replace(multilineRe, `${line}$1 $2${line}`); } docblock = docblock.trim(); const result = Object.create(null); + const comments = docblock.replace(propertyRe, '').replace(lineStartRe, line); let match; while ((match = propertyRe.exec(docblock))) { result[match[1]] = match[2]; } - return result; + return {comments: comments.trim(), pragmas: result}; } -exports.extract = extract; -exports.parse = parse; +export function print({ + comments = '', + pragmas = {}, +}: { + comments?: string, + pragmas?: {[key: string]: string}, +}): string { + const line = detectNewline(comments) || EOL; + const head = '/**'; + const start = ' *'; + const tail = ' */'; + + const keys = Object.keys(pragmas); + + const printedObject = keys + .map(key => start + ' ' + printKeyValue(key, pragmas[key]) + line) + .join(''); + + if (!comments) { + if (keys.length === 0) { + return ''; + } + if (keys.length === 1) { + return `${head} ${printKeyValue(keys[0], pragmas[keys[0]])}${tail}`; + } + } + + const printedComments = + comments + .split(line) + .map(textLine => `${start} ${textLine}`) + .join(line) + line; + + return ( + head + + line + + (comments ? printedComments : '') + + (comments && keys.length ? start + line : '') + + printedObject + + tail + ); +} + +function printKeyValue(key, value) { + return `@${key} ${value}`.trim(); +} diff --git a/packages/jest-haste-map/src/worker.js b/packages/jest-haste-map/src/worker.js index 1b6edcf963a3..8e39d12cfa32 100644 --- a/packages/jest-haste-map/src/worker.js +++ b/packages/jest-haste-map/src/worker.js @@ -11,7 +11,7 @@ import type {SerializableError} from 'types/TestResult'; import type {HasteImpl, WorkerMessage, WorkerCallback} from './types'; import path from 'path'; -import docblock from 'jest-docblock'; +import * as docblock from 'jest-docblock'; import fs from 'graceful-fs'; import H from './constants'; import extractRequires from './lib/extract_requires'; diff --git a/packages/jest-runner/src/run_test.js b/packages/jest-runner/src/run_test.js index 5bb9e878e023..eb0a50fcd7fe 100644 --- a/packages/jest-runner/src/run_test.js +++ b/packages/jest-runner/src/run_test.js @@ -24,7 +24,7 @@ import { } from 'jest-util'; import jasmine2 from 'jest-jasmine2'; import {getTestEnvironment} from 'jest-config'; -import docblock from 'jest-docblock'; +import * as docblock from 'jest-docblock'; // The default jest-runner is required because it is the default test runner // and required implicitly through the `testRunner` ProjectConfig option. diff --git a/yarn.lock b/yarn.lock index f37aaa12e644..59fcae6047ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1971,6 +1971,10 @@ detect-indent@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + detective@^4.0.0: version "4.5.0" resolved "https://registry.yarnpkg.com/detective/-/detective-4.5.0.tgz#6e5a8c6b26e6c7a254b1c6b6d7490d98ec91edd1"