From bdd701401156b67c0744986c0d702c177088cbf4 Mon Sep 17 00:00:00 2001 From: mgechev Date: Sat, 31 Mar 2018 13:36:10 -0700 Subject: [PATCH] Initial commit --- .editorconfig | 5 + .gitignore | 8 + .npmignore | 12 + .travis.yml | 12 + LICENSE | 21 + README.md | 53 ++ appveyor.yml | 40 ++ build/buildDocs.ts | 192 +++++++ build/package.ts | 32 ++ build/processFiles.ts | 23 + build/vars.ts | 14 + index.js | 6 + manual_typings/chai-spies.d.ts | 5 + migrate-tslint.json | 8 + package-lock.json | 724 ++++++++++++++++++++++++++ package.json | 60 +++ src/collapseRxjsImportsRule.ts | 110 ++++ src/index.ts | 3 + src/migrateToPipeableOperatorsRule.ts | 471 +++++++++++++++++ src/updateRxjsImportsRule.ts | 170 ++++++ test/collapseRxjsImportsRule.spec.ts | 78 +++ test/testHelper.ts | 306 +++++++++++ test/updateRxjsImportsRule.spec.ts | 297 +++++++++++ test/utils.ts | 133 +++++ tsconfig-release.json | 26 + tsconfig.json | 23 + tslint.json | 55 ++ 27 files changed, 2887 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 appveyor.yml create mode 100644 build/buildDocs.ts create mode 100644 build/package.ts create mode 100644 build/processFiles.ts create mode 100644 build/vars.ts create mode 100644 index.js create mode 100644 manual_typings/chai-spies.d.ts create mode 100644 migrate-tslint.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/collapseRxjsImportsRule.ts create mode 100644 src/index.ts create mode 100644 src/migrateToPipeableOperatorsRule.ts create mode 100644 src/updateRxjsImportsRule.ts create mode 100644 test/collapseRxjsImportsRule.spec.ts create mode 100644 test/testHelper.ts create mode 100644 test/updateRxjsImportsRule.spec.ts create mode 100644 test/utils.ts create mode 100644 tsconfig-release.json create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0020fc0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c69e46b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +typings +dist +.idea +npm-debug.log +.vscode +yarn.lock + diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..5686c6a --- /dev/null +++ b/.npmignore @@ -0,0 +1,12 @@ +.*.swp +._* +.DS_Store +.git +.hg +.npmrc +.lock-wscript +.svn +.wafpickle-* +config.gypi +CVS +npm-debug.log diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0b20ef7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: node_js + +node_js: + - stable +os: + - linux + +branches: + only: master + +script: npm run tscv && npm run lint && npm t + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..06f2f49 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Minko Gechev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6878293 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# RxJS Migrations + +TSLint rules for migration to the latest version of RxJS. + +## Rules + +This repository provides the following rules: + +| Rule Name | Configuration | Description | +| :-----------------------------: | :-----------: | :-----------------------------------------------------: | +| `collapse-rxjs-imports` | none | Collapses multiple imports from `rxjs` to a single one. | +| `migrate-to-pipeable-operators` | none | Migrates side-effect operators to pipeables. | +| `update-rxjs-imports` | none | Updates RxJS 5.x.x imports to RxJS 6.0 | + +## Usage with Angular CLI + +1. Build the project: + +```bash +git clone https://github.com/mgechev/rxjs-migrate +cd rxjs-migrate && npm i +npm run build +``` + +2. In your project's directory, create a file called `migrate-rxjs.tslint.json` with the following content: + +```json +{ + "rulesDirectory": ["path/to/the/compiled/rules"], + "rules": { + "update-rxjs-imports": true, + "migrate-to-pipeable-operators": true, + "collapse-rxjs-imports": true + } +} +``` + +3. Run tslint: + +```bash +./node_modules/.bin/tslint -c migrate-rxjs.tslint.json --project src/tsconfig.app.json +``` + +4. Enjoy! 😎 + +### Notes + +* Once you run all the migrations check the diff and make sure that everything looks as expected. If you see any issues, open an issue at https://github.com/angular/angular-cli. +* Although the migration will format your source code, it's likely that that the style is not consistent with the rest of your project. To make sure that everything is properly following your project's style guide, use a formatter such as prettier or clang-format. + +## License + +MIT diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..1cd44ee --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,40 @@ +# AppVeyor file +# http://www.appveyor.com/docs/appveyor-yml +# This file: cloned from https://github.com/gruntjs/grunt/blob/master/appveyor.yml + +# Build version format +version: "{build}" + +# Test against this version of Node.js +environment: + nodejs_version: "Stable" + +build: off + +clone_depth: 10 + +# Fix line endings on Windows +init: + - git config --global core.autocrlf true + +install: + - ps: Install-Product node $env:nodejs_version + - npm install -g npm@3.10.8 + - ps: $env:path = $env:appdata + "\npm;" + $env:path + - npm install + +test_script: + # Output useful info for debugging. + - node --version && npm --version + # We test multiple Windows shells because of prior stdout buffering issues + # filed against Grunt. https://github.com/joyent/node/issues/3584 + - ps: "npm --version # PowerShell" # Pass comment to PS for easier debugging + - npm t + +notifications: + - provider: Webhook + url: https://webhooks.gitter.im/e/cfd8ce5ddee6f3a0b0c9 + on_build_success: false + on_build_failure: true + on_build_status_changed: true + diff --git a/build/buildDocs.ts b/build/buildDocs.ts new file mode 100644 index 0000000..a9de8a2 --- /dev/null +++ b/build/buildDocs.ts @@ -0,0 +1,192 @@ +/* + * Copyright 2016 Palantir Technologies, Inc. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * This TS script reads the metadata from each TSLint built-in rule + * and serializes it in a format appropriate for the docs website. + * + * This script expects there to be a tslint-gh-pages directory + * parallel to the main tslint directory. The tslint-gh-pages should + * have the gh-pages branch of the TSLint repo checked out. + * One easy way to do this is with the following Git command: + * + * ``` + * git worktree add -b gh-pages ../tslint-gh-pages origin/gh-pages + * ``` + * + * See http://palantir.github.io/tslint/develop/docs/ for more info + * + */ + +import * as fs from 'fs'; +import * as glob from 'glob'; +import stringify = require('json-stringify-pretty-compact'); +import * as yaml from 'js-yaml'; +import * as path from 'path'; + +import {IFormatterMetadata, IRuleMetadata} from 'tslint'; + +type Metadata = IRuleMetadata | IFormatterMetadata; + +interface Documented { + metadata: Metadata; +} + +interface IDocumentation { + /** + * File name for the json data file listing. + */ + dataFileName: string; + + /** + * Exported item name from each file. + */ + exportName: string; + + /** + * Pattern matching files to be documented. + */ + globPattern: string; + + /** + * Key of the item's name within the metadata object. + */ + nameMetadataKey: string; + + /** + * Function to generate individual documentation pages. + */ + pageGenerator: (metadata: any) => string; + + /** + * Documentation subdirectory to output to. + */ + subDirectory: string; +} + +const DOCS_DIR = '../docs'; + +process.chdir('./build'); + +/** + * Documentation definition for rule modules. + */ +const ruleDocumentation: IDocumentation = { + dataFileName: 'rules.json', + exportName: 'Rule', + globPattern: '../dist/src/*Rule.js', + nameMetadataKey: 'ruleName', + pageGenerator: generateRuleFile, + subDirectory: path.join(DOCS_DIR, 'rules'), +}; + +/** + * Documentation definition for formatter modules. + */ +const formatterDocumentation: IDocumentation = { + dataFileName: 'formatters.json', + exportName: 'Formatter', + globPattern: '../lib/formatters/*Formatter.js', + nameMetadataKey: 'formatterName', + pageGenerator: generateFormatterFile, + subDirectory: path.join(DOCS_DIR, 'formatters'), +}; + +/** + * Builds complete documentation. + */ +function buildDocumentation(documentation: IDocumentation) { + // Create each module's documentation file. + const paths = glob.sync(documentation.globPattern); + const metadataJson = paths.map((path: string) => { + return buildSingleModuleDocumentation(documentation, path); + }); + + // Create a data file with details of every module. + buildDocumentationDataFile(documentation, metadataJson); +} + +/** + * Produces documentation for a single file/module. + */ +function buildSingleModuleDocumentation(documentation: IDocumentation, modulePath: string): Metadata { + // Load the module. + // tslint:disable-next-line:no-var-requires + const module = require(modulePath); + const DocumentedItem = module[documentation.exportName] as Documented; + if (DocumentedItem !== null && DocumentedItem.metadata !== null) { + // Build the module's page. + const { metadata } = DocumentedItem; + const fileData = documentation.pageGenerator(metadata); + + // Ensure a directory exists and write the module's file. + const moduleName = (metadata as any)[documentation.nameMetadataKey]; + const fileDirectory = path.join(documentation.subDirectory, moduleName); + if (!fs.existsSync(documentation.subDirectory)) { + fs.mkdirSync(documentation.subDirectory); + } + if (!fs.existsSync(fileDirectory)) { + fs.mkdirSync(fileDirectory); + } + fs.writeFileSync(path.join(fileDirectory, 'index.html'), fileData); + + return metadata; + } +} + +function buildDocumentationDataFile(documentation: IDocumentation, metadataJson: any[]) { + const dataJson = JSON.stringify(metadataJson, undefined, 2); + fs.writeFileSync(path.join(DOCS_DIR, '_data', documentation.dataFileName), dataJson); +} + +/** + * Generates Jekyll data from any item's metadata. + */ +function generateJekyllData(metadata: any, layout: string, type: string, name: string): any { + return { + ...metadata, + layout, + title: `${type}: ${name}`, + }; +} + +/** + * Based off a rule's metadata, generates a Jekyll 'HTML' file + * that only consists of a YAML front matter block. + */ +function generateRuleFile(metadata: IRuleMetadata): string { + if (metadata.optionExamples) { + metadata = { ...metadata }; + metadata.optionExamples = (metadata.optionExamples as any[]).map((example) => + typeof example === 'string' ? example : stringify(example)); + } + + const yamlData = generateJekyllData(metadata, 'rule', 'Rule', metadata.ruleName); + yamlData.optionsJSON = JSON.stringify(metadata.options, undefined, 2); + return `---\n${yaml.safeDump(yamlData, {lineWidth: 140} as any)}---`; +} + +/** + * Based off a formatter's metadata, generates a Jekyll 'HTML' file + * that only consists of a YAML front matter block. + */ +function generateFormatterFile(metadata: IFormatterMetadata): string { + const yamlData = generateJekyllData(metadata, 'formatter', 'TSLint formatter', metadata.formatterName); + return `---\n${yaml.safeDump(yamlData, {lineWidth: 140} as any)}---`; +} + +buildDocumentation(ruleDocumentation); +buildDocumentation(formatterDocumentation); diff --git a/build/package.ts b/build/package.ts new file mode 100644 index 0000000..e59f4ec --- /dev/null +++ b/build/package.ts @@ -0,0 +1,32 @@ +process.stdin.setEncoding('utf8'); + +const blacklist = [ + 'scripts', + 'devDependencies' +]; + +process.stdin.resume(); +process.stdin.setEncoding('utf8'); + +let packageJson = ''; +process.stdin.on('data', (chunk: string) => { + packageJson += chunk; +}); +process.stdin.on('end', () => { + let parsed: any; + try { + parsed = JSON.parse(packageJson); + } catch (e) { + console.error('Cannot parse to JSON'); + process.exit(1); + } + const result = {}; + Object.keys(parsed).forEach((key: string) => { + if (blacklist.indexOf(key) < 0) { + result[key] = parsed[key]; + } + }); + process.stdout.write(JSON.stringify(result, null, 2)); + packageJson = ''; +}); + diff --git a/build/processFiles.ts b/build/processFiles.ts new file mode 100644 index 0000000..e98bc18 --- /dev/null +++ b/build/processFiles.ts @@ -0,0 +1,23 @@ +import {readFileSync, writeFileSync, readdirSync, lstatSync} from 'fs'; +import {join} from 'path'; + +interface Replacement { + regexp: RegExp; + replace: any; +} + +const processFileContent = (content: string, replacement: Replacement) => { + return content.replace(replacement.regexp, replacement.replace); +}; + +export const processFiles = (path: string, replacement: Replacement) => { + const files = readdirSync(path); + files.forEach((fileName: string) => { + const filePath = join(path, fileName); + if (/\.js$/.test(fileName) && lstatSync(filePath).isFile()) { + writeFileSync(filePath, processFileContent(readFileSync(filePath).toString(), replacement)); + } else if (lstatSync(filePath).isDirectory()) { + processFiles(filePath, replacement); + } + }); +}; diff --git a/build/vars.ts b/build/vars.ts new file mode 100644 index 0000000..37431ff --- /dev/null +++ b/build/vars.ts @@ -0,0 +1,14 @@ +import {processFiles} from './processFiles'; + +const ARGS = require('minimist')(process.argv.slice(2)); +const SRC = ARGS.src; +const BUILD_TYPE = process.env.BUILD_TYPE; + +try { + processFiles(SRC, { + regexp: /<%=\s*BUILD_TYPE\s*%>/g, + replace: BUILD_TYPE + }); +} catch (e) { + console.error(e); +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..8487640 --- /dev/null +++ b/index.js @@ -0,0 +1,6 @@ +// Unopinionated extensable tslint configuration +// Loads rules for extending packages, but does not enable any +module.exports = { + rulesDirectory: "./", +}; + diff --git a/manual_typings/chai-spies.d.ts b/manual_typings/chai-spies.d.ts new file mode 100644 index 0000000..b54e647 --- /dev/null +++ b/manual_typings/chai-spies.d.ts @@ -0,0 +1,5 @@ +declare var spy: any; + +declare module "chai-spies" { + export = spy; +} diff --git a/migrate-tslint.json b/migrate-tslint.json new file mode 100644 index 0000000..d3e342b --- /dev/null +++ b/migrate-tslint.json @@ -0,0 +1,8 @@ +{ + "rulesDirectory": ["dist/src"], + "rules": { + "update-rxjs-imports": true, + "migrate-to-pipeable-operators": true, + "collapse-rxjs-imports": true + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ac7ff2e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,724 @@ +{ + "name": "tslintx", + "version": "0.0.7", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/chai": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-3.5.2.tgz", + "integrity": "sha1-wRzSgX06QBt7oPWkIPNcVhObHB4=", + "dev": true + }, + "@types/mocha": { + "version": "2.2.41", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.41.tgz", + "integrity": "sha1-4nzwgXFT658nE7LT9saPHhw8pgg=", + "dev": true + }, + "@types/node": { + "version": "6.0.85", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.85.tgz", + "integrity": "sha512-6qLZpfQFO/g5Ns2e7RsW6brk0Q6Xzwiw7kVVU/XiQNOiJXSojhX76GP457PBYIsNMH2WfcGgcnZB4awFDHrwpA==", + "dev": true + }, + "@types/sprintf-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/sprintf-js/-/sprintf-js-1.1.0.tgz", + "integrity": "sha512-AQSq0X2Pj+2UbLIAxRmnj7Tll4btd1fj3hFf0XxB3/ZT7qjQ2WA3/JKtehu8UXEbkNCcysG6ZnYs58F1e/FboA==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + } + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "assertion-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "babel-code-frame": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "chai": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", + "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", + "dev": true, + "requires": { + "assertion-error": "1.0.2", + "deep-eql": "0.1.3", + "type-detect": "1.0.0" + } + }, + "chai-spies": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/chai-spies/-/chai-spies-0.7.1.tgz", + "integrity": "sha1-ND2Z9RJEIS6LF+ZLk5lv97LCqbE=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + }, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": "1.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "deep-eql": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", + "dev": true, + "requires": { + "type-detect": "0.1.1" + }, + "dependencies": { + "type-detect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true + } + } + }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.5.tgz", + "integrity": "sha1-tCAqaQmbu00pKnwblbZoK2fr3JU=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "dev": true, + "requires": { + "parse-passwd": "1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.1.tgz", + "integrity": "sha512-CbcG379L1e+mWBnLvHWWeLs8GyV/EMw862uLI3c+GxVyDHWZcjZinwuBd3iW2pgxgIlksW/1vNJa4to+RvDOww==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, + "json-stringify-pretty-compact": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-1.0.4.tgz", + "integrity": "sha1-1RYRMb4n/ZdIORNgWX/MolDGxc4=" + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "3.0.1", + "lodash.keys": "3.1.2" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "3.2.0", + "lodash._basecreate": "3.0.3", + "lodash._isiterateecall": "3.0.9" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "3.9.1", + "lodash.isarguments": "3.1.0", + "lodash.isarray": "3.0.4" + } + }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.0.2.tgz", + "integrity": "sha1-Y6l/Phj00+ZZ1HphdnfQiYdFV/A=", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.2.0", + "diff": "1.4.0", + "escape-string-regexp": "1.0.5", + "glob": "7.0.5", + "growl": "1.9.2", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "resolve": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", + "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "rimraf": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true, + "requires": { + "glob": "7.0.5" + } + }, + "semver": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "0.5.7" + } + }, + "sprintf-js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", + "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "1.0.0" + } + }, + "ts-node": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.3.0.tgz", + "integrity": "sha1-wTxqMCTjC+EYDdUwOPwgkonUv2k=", + "dev": true, + "requires": { + "arrify": "1.0.1", + "chalk": "2.3.2", + "diff": "3.5.0", + "make-error": "1.3.4", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "source-map-support": "0.4.18", + "tsconfig": "6.0.0", + "v8flags": "3.0.2", + "yn": "2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", + "integrity": "sha512-ZM4j2/ld/YZDc3Ma8PgN7gyAk+kHMMMyzLNryCPGhWrsfAuDVeuid5bpRFTDgMH9JBK2lA4dyyAkkZYF/WcqDQ==", + "dev": true, + "requires": { + "ansi-styles": "3.2.1", + "escape-string-regexp": "1.0.5", + "supports-color": "5.3.0" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "supports-color": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", + "integrity": "sha512-0aP01LLIskjKs3lq52EC0aGBAJhLq7B2Rd8HC/DR/PtNNpcLilNmHC12O+hu0usQpo7wtHNRqtrhBwtDb0+dNg==", + "dev": true, + "requires": { + "has-flag": "3.0.0" + } + } + } + }, + "tsconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz", + "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=", + "dev": true, + "requires": { + "strip-bom": "3.0.0", + "strip-json-comments": "2.0.1" + } + }, + "tslib": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", + "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=", + "dev": true + }, + "tslint": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.5.0.tgz", + "integrity": "sha1-EOjas+MGH6YelELozuOYKs8gpqo=", + "dev": true, + "requires": { + "babel-code-frame": "6.22.0", + "colors": "1.1.2", + "commander": "2.9.0", + "diff": "3.3.0", + "glob": "7.1.2", + "minimatch": "3.0.4", + "resolve": "1.4.0", + "semver": "5.4.1", + "tslib": "1.7.1", + "tsutils": "2.25.0" + }, + "dependencies": { + "diff": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.0.tgz", + "integrity": "sha512-w0XZubFWn0Adlsapj9EAWX0FqWdO4tz8kc3RiYdWLh4k/V8PTb6i0SMgXt0vRM3zyKnT8tKO7mUlieRQHIjMNg==", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } + }, + "tsutils": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.25.0.tgz", + "integrity": "sha512-SPgUlOAUAe6fCyPi0QR4U0jRuDsHHKvzIR6/hHd0YR0bb8MzeLJgCagkPSmZeJjWImnpJ0xq6XHa9goTvMBBCQ==", + "requires": { + "tslib": "1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==" + } + } + }, + "type-detect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", + "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "dev": true + }, + "typescript": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.8.1.tgz", + "integrity": "sha512-Ao/f6d/4EPLq0YwzsQz8iXflezpTkQzqAyenTiw4kCUGr1uPiFLC3+fZ+gMZz6eeI/qdRUqvC+HxIJzUAzEFdg==", + "dev": true + }, + "v8flags": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.2.tgz", + "integrity": "sha512-6sgSKoFw1UpUPd3cFdF7QGnrH6tDeBgW1F3v9gy8gLY0mlbiBXq8soy8aQpY6xeeCjH5K+JvC62Acp7gtl7wWA==", + "dev": true, + "requires": { + "homedir-polyfill": "1.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..67d2a3a --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "rxjs-migrate", + "version": "0.0.7", + "description": "Migrations for RxJS", + "main": "index.js", + "scripts": { + "docs": "ts-node build/buildDocs.ts", + "lint": "tslint -c tslint.json \"src/**/*.ts\" \"test/**/*.ts\"", + "lint:fix": "npm run lint -- --fix", + "release": + "npm run build && rimraf dist && tsc -p tsconfig-release.json && npm run copy:common && npm run prepare:package && BUILD_TYPE=prod npm run set:vars", + "build": "rimraf dist && tsc && npm run lint && npm t", + "copy:common": "cp README.md dist", + "prepare:package": "cat package.json | ts-node build/package.ts > dist/package.json", + "test": "rimraf dist && tsc && mocha -R nyan dist/test --recursive", + "test:watch": + "rimraf dist && tsc && BUILD_TYPE=dev npm run set:vars && mocha -R nyan dist/test --watch --recursive", + "set:vars": "ts-node build/vars.ts --src ./dist", + "tscv": "tsc --version", + "tsc": "tsc", + "tsc:watch": "tsc --w" + }, + "contributors": ["Minko Gechev "], + "repository": { + "type": "git", + "url": "git+https://github.com/mgechev/tslint-rules.git" + }, + "keywords": ["lint", "tslint"], + "author": { + "name": "Minko Gechev", + "email": "mgechev@gmail.com" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/mgechev/tslint-rules/issues" + }, + "homepage": "https://github.com/mgechev/tslint-rules#readme", + "devDependencies": { + "@types/chai": "^3.4.33", + "@types/mocha": "^2.2.32", + "@types/node": "^6.0.41", + "@types/sprintf-js": "^1.1.0", + "chai": "^3.5.0", + "chai-spies": "^0.7.1", + "js-yaml": "^3.8.4", + "mocha": "3.0.2", + "rimraf": "^2.5.2", + "ts-node": "^3.3.0", + "tslint": "^5.0.0", + "typescript": "^2.7.0" + }, + "peerDependencies": { + "tslint": "^5.0.0" + }, + "dependencies": { + "json-stringify-pretty-compact": "^1.0.4", + "sprintf-js": "^1.1.1", + "tsutils": "^2.25.0" + } +} diff --git a/src/collapseRxjsImportsRule.ts b/src/collapseRxjsImportsRule.ts new file mode 100644 index 0000000..032b4c4 --- /dev/null +++ b/src/collapseRxjsImportsRule.ts @@ -0,0 +1,110 @@ +// Original author Bowen Ni +// Modifications mgechev. + +import * as Lint from 'tslint'; +import * as tsutils from 'tsutils'; +import * as ts from 'typescript'; + +const FAILURE_STRING = 'duplicate RxJS import'; +/** + * A rule to combine the duplicate imports of rxjs. + */ +export class Rule extends Lint.Rules.AbstractRule { + static metadata: Lint.IRuleMetadata = { + ruleName: 'collapse-rxjs-imports', + description: + `In RxJS v6.0 most imports are just ` + + `"import {...} from 'rxjs';". This TSLint rule collapses the ` + + `duplicate imports of rxjs into one import statement.`, + rationale: '', + options: null, + optionsDescription: '', + type: 'style', + typescriptOnly: true + }; + apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, walk); + } +} + +interface RxJSImport { + namedImports: string; + importStatements: ts.ImportDeclaration[]; +} + +const RXJS_IMPORTS = 'rxjs'; + +function walk(ctx: Lint.WalkContext) { + const allRxjsImports = new Map(); + // Collect all imports from RxJS + for (const statement of ctx.sourceFile.statements) { + if (!tsutils.isImportDeclaration(statement)) { + continue; + } + if (!statement.importClause) { + continue; + } + if (!statement.importClause.namedBindings) { + continue; + } + if (!tsutils.isNamedImports(statement.importClause.namedBindings)) { + continue; + } + if (!tsutils.isLiteralExpression(statement.moduleSpecifier)) { + continue; + } + const moduleSpecifier = statement.moduleSpecifier.text; + if (!moduleSpecifier.startsWith(RXJS_IMPORTS)) { + continue; + } + const existingImport = allRxjsImports.get(moduleSpecifier); + // namedBindings is a named import. e.g. {foo as bar, baz} + // Strip the braces. + const namedImports = statement.importClause.namedBindings.getText(ctx.sourceFile).slice(1, -1); + if (!existingImport) { + allRxjsImports.set(moduleSpecifier, { + namedImports, + importStatements: [statement] + }); + } else { + // Collect all named imports and collapse them into one. + existingImport.namedImports += `, ${namedImports}`; + existingImport.importStatements.push(statement); + } + } + // For every import path if there are more than one import statement collapse + // them. + const entries = allRxjsImports.entries(); + while (true) { + let current = entries.next(); + if (current.done) { + break; + } + const [path, imports] = current.value; + if (imports.importStatements.length === 1) { + continue; + } + const fixes: Lint.Replacement[] = [ + Lint.Replacement.replaceNode( + imports.importStatements[0].importClause!.namedBindings!, + `{${imports.namedImports}}` + ) + ]; + for (const duplicateImport of imports.importStatements.slice(1)) { + // Only remove trailing comments for the removed import statements because + // those comments are mostly likely comments (which should not be needed in + // the first place). Keep leading comments because that probably contains + // something meaningful. + let end = duplicateImport.end; + tsutils.forEachComment( + duplicateImport, + (fullText: string, comment: ts.CommentRange) => { + end = end < comment.end ? comment.end : end; + }, + ctx.sourceFile + ); + fixes.push(Lint.Replacement.deleteFromTo(duplicateImport.getFullStart(), end)); + } + ctx.addFailureAtNode(imports.importStatements[0], FAILURE_STRING, fixes); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..f89794b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export { Rule as CollapseRxjsImports } from './collapseRxjsImportsRule'; +export { Rule as UpdateRxjsImports } from './updateRxjsImportsRule'; +export { Rule as MigrateToPipeableOperators } from './migrateToPipeableOperatorsRule'; diff --git a/src/migrateToPipeableOperatorsRule.ts b/src/migrateToPipeableOperatorsRule.ts new file mode 100644 index 0000000..f4978c8 --- /dev/null +++ b/src/migrateToPipeableOperatorsRule.ts @@ -0,0 +1,471 @@ +// Original author Bowen Ni +// Modifications mgechev. + +import * as Lint from 'tslint'; +import * as tsutils from 'tsutils'; +import * as ts from 'typescript'; +/** + * A typed TSLint rule that inspects observable chains using patched RxJs + * operators and turns them into a pipeable operator chain. + */ +export class Rule extends Lint.Rules.TypedRule { + static metadata: Lint.IRuleMetadata = { + ruleName: 'migrate-to-pipeable-operators', + description: `Pipeable operators offer a new way of composing observable chains and + they have advantages for both application developers and library + authors.`, + rationale: 'go/pipeable-operators', + optionsDescription: '', + options: null, + typescriptOnly: true, + type: 'functionality' + }; + static FAILURE_STRING = 'Prefer pipeable operators. See go/pipeable-operators'; + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { + return this.applyWithFunction(sourceFile, ctx => this.walk(ctx, program)); + } + private walk(ctx: Lint.WalkContext, program: ts.Program) { + this.removePatchedOperatorImports(ctx); + const sourceFile = ctx.sourceFile; + const typeChecker = program.getTypeChecker(); + const insertionStart = computeInsertionIndexForImports(sourceFile); + let rxjsOperatorImports = findImportedRxjsOperators(sourceFile); + /** + * Creates a lint failure with suggested replacements if an observable chain + * of patched operators is found. + * + *

The expression + *

const subs = foo.do(console.log)
+     *                      .map( x =>2*x)
+     *                      .do(console.log)
+     *                      .switchMap( y => z)
+     *                      .subscribe(fn);
+     * 
+ * + * should produce a failure at the underlined section below: + *
const subs = foo.do(console.log)
+     *                      ----------------
+     *                      .map( x =>2*x)
+     *                      --------------
+     *                      .do(console.log)
+     *                      ----------------
+     *                      .switchMap( y => z)
+     *                      -------------------
+     *                      .subscribe(fn);
+     * 
+ * and suggest replacements that would produce text like + *
const subs = foo.pipe(
+     *                          tap(console.log),
+     *                          map( x =>2*x),
+     *                          tap(console.log),
+     *                          switchMap( y => z),
+     *                      )
+     *                      .subscribe(fn);
+     * 
+ */ + function checkPatchableOperatorUsage(node: ts.Node): void { + // Navigate up the expression tree until a call expression with an rxjs + // operator is found. + // If the parent expression is also an rxjs operator call expression, + // continue. + // If not, then verify that the parent is indeed an observable. + // files the node with the expression 'foo'. + // Using the example above, the traversal would stop at 'foo'. + if (!isRxjsInstanceOperatorCallExpression(node, typeChecker)) { + return ts.forEachChild(node, checkPatchableOperatorUsage); + } + const immediateParent = (node as ts.CallExpression).expression as ts.PropertyAccessExpression; + // Get the preceeding expression (specific child node) to which the + // current node was chained to. If node represents text like + // foo.do(console.log).map( x =>2*x), then preceedingNode would have the + // text foo.do(console.log). + const preceedingNode = immediateParent.expression; + // If the preceeding node is also an RxJS call then continue traversal. + if (isRxjsInstanceOperatorCallExpression(preceedingNode, typeChecker)) { + return ts.forEachChild(node, checkPatchableOperatorUsage); + } + // Some Rxjs operators have same names as array operators, and could be + // chained array operators that return an observable instead. These nodes + // should be skipped. + // eg.functionReturningArray().reduce(functionProducingObservable) + // or arrayObject.reduce(functionProducingObservable) + if (tsutils.isCallExpression(preceedingNode) || tsutils.isNewExpression(preceedingNode)) { + if (!returnsObservable(preceedingNode, typeChecker)) { + return ts.forEachChild(node, checkPatchableOperatorUsage); + } + } else if (!isObservable(typeChecker.getTypeAtLocation(preceedingNode), typeChecker)) { + return ts.forEachChild(node, checkPatchableOperatorUsage); + } + const failureStart = immediateParent.getStart(sourceFile) + immediateParent.getText(sourceFile).lastIndexOf('.'); + const lastNode = findLastObservableExpression(preceedingNode, typeChecker); + const failureEnd = lastNode.getEnd(); + const pipeReplacement = Lint.Replacement.appendText(preceedingNode.getEnd(), '.pipe('); + const operatorsToImport = new Set(); + const operatorReplacements = replaceWithPipeableOperators(preceedingNode, lastNode, operatorsToImport); + const operatorsToAdd = subtractSets(operatorsToImport, rxjsOperatorImports); + const importReplacements = createImportReplacements(operatorsToAdd, insertionStart); + rxjsOperatorImports = concatSets(rxjsOperatorImports, operatorsToAdd); + const allReplacements = [pipeReplacement, ...operatorReplacements, ...importReplacements]; + ctx.addFailure(failureStart, failureEnd, Rule.FAILURE_STRING, allReplacements); + return ts.forEachChild(node, checkPatchableOperatorUsage); + } + return ts.forEachChild(ctx.sourceFile, checkPatchableOperatorUsage); + } + /** + * Generates replacements to remove imports for patched operators. + */ + private removePatchedOperatorImports(ctx: Lint.WalkContext): void { + const sourceFile = ctx.sourceFile; + for (const importStatement of sourceFile.statements.filter(tsutils.isImportDeclaration)) { + const moduleSpecifier = importStatement.moduleSpecifier.getText(); + if (!moduleSpecifier.startsWith(`'rxjs/operator/`) && !moduleSpecifier.startsWith(`'rxjs/add/operator/`)) { + continue; + } + const importStatementStart = importStatement.getStart(sourceFile); + const importStatementEnd = importStatement.getEnd(); + ctx.addFailure( + importStatementStart, + importStatementEnd, + Rule.FAILURE_STRING, + Lint.Replacement.deleteFromTo(importStatementStart, importStatementEnd) + ); + } + } +} +/** + * Returns true if the {@link type} is an Observable or one of its sub-classes. + */ +function isObservable(type: ts.Type, tc: ts.TypeChecker): boolean { + if (tsutils.isTypeReference(type)) { + type = type.target; + } + if (type.symbol !== undefined && type.symbol.name === 'Observable') { + return true; + } + if (tsutils.isUnionOrIntersectionType(type)) { + return type.types.some(t => isObservable(t, tc)); + } + const bases = type.getBaseTypes(); + return bases !== undefined && bases.some(t => isObservable(t, tc)); +} +/** + * Returns true if the return type of the expression represented by the {@link + * node} is an Observable or one of its subclasses. + */ +function returnsObservable(node: ts.CallLikeExpression, tc: ts.TypeChecker) { + const signature = tc.getResolvedSignature(node); + if (signature === undefined) { + return false; + } + const returnType = tc.getReturnTypeOfSignature(signature); + return isObservable(returnType, tc); +} +/** + * Returns true if the identifier of the current expression is an RxJS instance + * operator like map, switchMap etc. + */ +function isRxjsInstanceOperator(node: ts.PropertyAccessExpression) { + return 'Observable' !== node.expression.getText() && RXJS_OPERATORS.has(node.name.getText()); +} +/** + * Returns true if {@link node} is a call expression containing an RxJs instance + * operator and returns an observable. eg map(fn), switchMap(fn) + */ +function isRxjsInstanceOperatorCallExpression(node: ts.Node, typeChecker: ts.TypeChecker) { + // Expression is of the form fn() + if (!tsutils.isCallExpression(node)) { + return false; + } + // Expression is of the form foo.fn + if (!tsutils.isPropertyAccessExpression(node.expression)) { + return false; + } + // fn is one of RxJs instance operators + if (!isRxjsInstanceOperator(node.expression)) { + return false; + } + // fn(): k. Checks if k is an observable. Required to distinguish between + // array operators with same name as RxJs operators. + if (!returnsObservable(node, typeChecker)) { + return false; + } + return true; +} +/** + * Finds all pipeable operators that are imported in the {@link sourceFile}. + * + *

Searches for import statements of the type + * import {map} from 'rxjs/operators/map; + * and collects the named bindings. + */ +function findImportedRxjsOperators(sourceFile: ts.SourceFile): Set { + return new Set( + sourceFile.statements.filter(tsutils.isImportDeclaration).reduce((current, decl) => { + if (!decl.importClause) { + return current; + } + if (!decl.moduleSpecifier.getText().startsWith(`'rxjs/operators`)) { + return current; + } + if (!decl.importClause.namedBindings) { + return current; + } + const bindings = decl.importClause.namedBindings; + if (ts.isNamedImports(bindings)) { + return [ + ...current, + ...(Array.from(bindings.elements) || []).map(element => { + return element.name.getText(); + }) + ]; + } + return current; + }, []) + ); +} +/** + * Returns the index to be used for inserting import statements potentially + * after a leading file overview comment (separated from the file with \n\n). + */ +function computeInsertionIndexForImports(sourceFile: ts.SourceFile): number { + const comments = ts.getLeadingCommentRanges(sourceFile.getFullText(), 0) || []; + if (comments.length > 0) { + const commentEnd = comments[0].end; + if (sourceFile.text.substring(commentEnd, commentEnd + 2) === '\n\n') { + return commentEnd + 2; + } + } + return sourceFile.getFullStart(); +} +/** + * Generates an array of {@link Lint.Replacement} representing import statements + * for the {@link operatorsToAdd}. + * + * @param operatorsToAdd Set of Rxjs operators that need to be imported + * @param startIndex Position where the {@link Lint.Replacement} can be inserted + */ +function createImportReplacements(operatorsToAdd: Set, startIndex: number): Lint.Replacement[] { + return [...Array.from(operatorsToAdd.values())].map(operator => + Lint.Replacement.appendText(startIndex, `\nimport {${operator}} from 'rxjs/operators/${operator}';\n`) + ); +} +/** + * Returns a new Set that contains elements present in the {@link source} but + * not present in {@link target} + */ +function subtractSets(source: Set, target: Set): Set { + return new Set([...Array.from(source.values())].filter(x => !target.has(x))); +} +/** + * Returns a new Set that contains union of the two input sets. + */ +function concatSets(set1: Set, set2: Set): Set { + return new Set([...Array.from(set1.values()), ...Array.from(set2.values())]); +} +/** + * Returns the last chained RxJS call expression by walking up the AST. + * + *

For an expression like foo.map(Fn).switchMap(Fn) - the function starts + * with node = foo. node.parent - represents the property expression foo.map and + * node.parent.parent represents the call expression foo.map(). + * + */ +function findLastObservableExpression(node: ts.Node, typeChecker: ts.TypeChecker): ts.Node { + let currentNode = node; + while (isAncestorRxjsOperatorCall(currentNode, typeChecker)) { + currentNode = currentNode.parent!.parent!; + } + return currentNode; +} +/** + * Returns true if the grandfather of the {@link node} is a call expression of + * an RxJs instance operator. + */ +function isAncestorRxjsOperatorCall(node: ts.Node, typeChecker: ts.TypeChecker): boolean { + // If this is the only operator in the chain. + if (!node.parent) { + return false; + } + // Do not overstep the boundary of an arrow function. + if (ts.isArrowFunction(node.parent)) { + return false; + } + if (!node.parent.parent) { + return false; + } + return isRxjsInstanceOperatorCallExpression(node.parent.parent, typeChecker); +} +/** + * Recursively generates {@link Lint.Replacement} to convert a chained rxjs call + * expression to an expression using pipeable rxjs operators. + * + * @param currentNode The node in the chained expression being processed + * @param lastNode The last node of the chained expression + * @param operatorsToImport Collects the operators encountered in the expression + * so far + * @param notStart Whether the {@link currentNode} is the first expression in + * the chain. + */ +function replaceWithPipeableOperators( + currentNode: ts.Node, + lastNode: ts.Node, + operatorsToImport: Set, + notStart = false +): Lint.Replacement[] { + // Reached the root of the expression, nothing to replace. + if (!currentNode.parent || !currentNode.parent.parent) { + return []; + } + // For an arbitrary expression like + // foo.do(console.log).map( x =>2*x).do(console.log).switchMap( y => z); + // if currentNode is foo.do(console.log), + // immediateParent = foo.do(console.log).map + const immediateParent = currentNode.parent; + const immediateParentText = immediateParent.getText(); + const identifierStart = immediateParentText.lastIndexOf('.'); + const identifierText = immediateParentText.slice(identifierStart + 1); + const pipeableOperator = PIPEABLE_OPERATOR_MAPPING[identifierText] || identifierText; + operatorsToImport.add(pipeableOperator); + // Generates a replacement that would replace .map with map using absolute + // position of the text to be replaced. + const operatorReplacement = Lint.Replacement.replaceFromTo( + immediateParent.getEnd() - identifierText.length - 1, + immediateParent.getEnd(), + pipeableOperator + ); + // parentNode = foo.do(console.log).map( x =>2*x) + const parentNode = currentNode.parent.parent; + const moreReplacements = + parentNode === lastNode + ? [Lint.Replacement.appendText(parentNode.getEnd(), notStart ? ',)' : ')')] + : replaceWithPipeableOperators(parentNode, lastNode, operatorsToImport, true); + // Generates a replacement for adding a ',' after the call expression + const separatorReplacements = notStart ? [Lint.Replacement.appendText(currentNode.getEnd(), ',')] : []; + return [operatorReplacement, ...separatorReplacements, ...moreReplacements]; +} +/** + * Set of all instance operators, including those renamed as part of lettable + * operator migration. Source:(RxJS v5) + * https://github.com/ReactiveX/rxjs/tree/stable/src/operators + */ +const RXJS_OPERATORS = new Set([ + 'audit', + 'auditTime', + 'buffer', + 'bufferCount', + 'bufferTime', + 'bufferToggle', + 'bufferWhen', + 'catchError', + 'combineAll', + 'combineLatest', + 'concat', + 'concatAll', + 'concatMap', + 'concatMapTo', + 'count', + 'debounce', + 'debounceTime', + 'defaultIfEmpty', + 'delay', + 'delayWhen', + 'dematerialize', + 'distinct', + 'distinctUntilChanged', + 'distinctUntilKeyChanged', + 'elementAt', + 'every', + 'exhaust', + 'exhaustMap', + 'expand', + 'filter', + 'finalize', + 'find', + 'findIndex', + 'first', + 'groupBy', + 'ignoreElements', + 'isEmpty', + 'last', + 'map', + 'mapTo', + 'materialize', + 'max', + 'merge', + 'mergeAll', + 'mergeMap', + 'mergeMapTo', + 'mergeScan', + 'min', + 'multicast', + 'observeOn', + 'onErrorResumeNext', + 'pairwise', + 'partition', + 'pluck', + 'publish', + 'publishBehavior', + 'publishLast', + 'publishReplay', + 'race', + 'reduce', + 'refCount', + 'repeat', + 'repeatWhen', + 'retry', + 'retryWhen', + 'sample', + 'sampleTime', + 'scan', + 'sequenceEqual', + 'share', + 'shareReplay', + 'single', + 'skip', + 'skipLast', + 'skipUntil', + 'skipWhile', + 'startWith', + 'subscribeOn', + 'switchAll', + 'switchMap', + 'switchMapTo', + 'take', + 'takeLast', + 'takeUntil', + 'takeWhile', + 'tap', + 'throttle', + 'throttleTime', + 'timeInterval', + 'timeout', + 'timeoutWith', + 'timestamp', + 'toArray', + 'window', + 'windowCount', + 'windowTime', + 'windowToggle', + 'windowWhen', + 'withLatestFrom', + 'zip', + 'zipAll', + 'do', + 'catch', + 'flatMap', + 'flatMapTo', + 'finally', + 'switch' +]); +/** + * Represents the mapping for pipeable version of some operators whose name has + * changed due to conflict with JavaScript keyword restrictions. + */ +const PIPEABLE_OPERATOR_MAPPING: { [key: string]: string } = { + do: 'tap', + catch: 'catchError', + flatMap: 'mergeMap', + flatMapTo: 'mergeMapTo', + finally: 'finalize', + switch: 'switchAll' +}; diff --git a/src/updateRxjsImportsRule.ts b/src/updateRxjsImportsRule.ts new file mode 100644 index 0000000..5d6d3d3 --- /dev/null +++ b/src/updateRxjsImportsRule.ts @@ -0,0 +1,170 @@ +import * as Lint from 'tslint'; +import * as ts from 'typescript'; + +const ImportMap = new Map([ + ['rxjs/util/', 'rxjs/internal/util/'], + ['rxjs/testing/', 'rxjs/internal/testing/'], + ['rxjs/scheduler/', 'rxjs/internal/scheduler/'], + ['rxjs/interfaces', 'rxjs'], + ['rxjs/AsyncSubject', 'rxjs'], + ['rxjs/BehaviorSubject', 'rxjs'], + ['rxjs/Notification', 'rxjs'], + ['rxjs/Observable', 'rxjs'], + ['rxjs/Observer', 'rxjs'], + ['rxjs/Operator', 'rxjs'], + ['rxjs/ReplaySubject', 'rxjs'], + ['rxjs/Subject', 'rxjs'], + ['rxjs/Subscriber', 'rxjs'], + ['rxjs/Scheduler', 'rxjs'], + ['rxjs/Subscription', 'rxjs'], + ['rxjs/observable/bindCallback', 'rxjs'], + ['rxjs/observable/combineLatest', 'rxjs'], + ['rxjs/observable/concat', 'rxjs'], + ['rxjs/observable/ConnectableObservable', 'rxjs'], + ['rxjs/observable/defer', 'rxjs'], + ['rxjs/observable/forkJoin', 'rxjs'], + ['rxjs/observable/from', 'rxjs'], + ['rxjs/observable/fromEvent', 'rxjs'], + ['rxjs/observable/fromEventPattern', 'rxjs'], + ['rxjs/observable/interval', 'rxjs'], + ['rxjs/observable/merge', 'rxjs'], + ['rxjs/observable/of', 'rxjs'], + ['rxjs/observable/race', 'rxjs'], + ['rxjs/observable/range', 'rxjs'], + ['rxjs/observable/timer', 'rxjs'], + ['rxjs/observable/zip', 'rxjs'], + ['rxjs/observable/fromPromise', 'rxjs'], + ['rxjs/observable/if', 'rxjs'], + ['rxjs/observable/throw', 'rxjs'], + ['rxjs/observable/never', 'rxjs'], + ['rxjs/observable/empty', 'rxjs'], + ['rxjs/observable/FromEventObservable', 'rxjs/internal/observable/fromEvent'] +]); + +const OperatorsPathRe = /^rxjs\/operators\/.*$/; +const NewOperatorsPath = 'rxjs/operators'; + +interface ImportReplacement { + path: string; + symbol: string; + newPath: string; + newSymbol: string; +} + +const ImportReplacements = [ + { + path: 'rxjs/observable/empty', + symbol: 'empty', + newPath: 'rxjs', + newSymbol: 'EMPTY' + }, + { + path: 'rxjs/observable/never', + symbol: 'never', + newPath: 'rxjs', + newSymbol: 'NEVER' + }, + { + path: 'rxjs/Subscription', + symbol: 'AnonymousSubscription', + newPath: 'rxjs', + newSymbol: 'Unsubscribable' + }, + { + path: 'rxjs/Subscription', + symbol: 'ISubscription', + newPath: 'rxjs', + newSymbol: 'SubscriptionLike' + } +]; + +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: Lint.IRuleMetadata = { + ruleName: 'update-rxjs-imports', + type: 'functionality', + description: 'Updates the paths of the rxjs imports to the version 6', + rationale: 'RxJS version 6 updated their API which requires changes in some of the import paths.', + options: null, + optionsDescription: 'Not configurable.', + typescriptOnly: true + }; + + static RuleFailure = 'outdated import path'; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new UpdateOutdatedImportsWalker(sourceFile, this.getOptions())); + } +} + +class UpdateOutdatedImportsWalker extends Lint.RuleWalker { + visitImportDeclaration(node: ts.ImportDeclaration): void { + if (ts.isStringLiteral(node.moduleSpecifier) && node.importClause) { + const specifier = node.moduleSpecifier; + const path = (specifier as ts.StringLiteral).text; + const start = specifier.getStart() + 1; + const end = specifier.text.length; + const replacementStart = start; + const replacementEnd = specifier.text.length; + let replacement = null; + + // Try to find updated symbol names. + ImportReplacements.forEach(r => (r.path === path ? this._migrateExportedSymbols(r, node) : void 0)); + + // Try to migrate entire import path updates. + if (ImportMap.has(path)) { + replacement = ImportMap.get(path); + + // Try to migrate import path prefix updates in case + // of `rxjs/operators/*`. + } else if (OperatorsPathRe.test(path)) { + replacement = NewOperatorsPath; + } + + if (replacement !== null) { + return this.addFailureAt( + start, + end, + Rule.RuleFailure, + this.createReplacement(replacementStart, replacementEnd, replacement) + ); + } + } + } + + private _migrateExportedSymbols(re: ImportReplacement, node: ts.ImportDeclaration) { + const importClause = node.importClause as ts.ImportClause; + const bindings = importClause.namedBindings as ts.NamedImports | null; + if (!bindings || bindings.kind !== ts.SyntaxKind.NamedImports) { + return; + } + + // Users may import more than a single symbol from `rxjs/Subscription` + // So we need to iterate over all the import specifiers and replace + // only the ones which were updated. All `rxjs/Subscription` exports + // are now under `rxjs` and there are two symbols renamed. + bindings.elements.forEach((e: ts.ImportSpecifier | null) => { + if (!e || e.kind !== ts.SyntaxKind.ImportSpecifier) { + return; + } + + let toReplace = e.name; + // We don't want to introduce type errors so we alias the old new symbol. + let replacement = `${re.newSymbol} as ${re.symbol}`; + if (e.propertyName) { + toReplace = e.propertyName; + replacement = re.newSymbol; + } + + if (toReplace.getText() !== re.symbol) { + return; + } + + return this.addFailureAt( + toReplace.getStart(), + toReplace.getWidth(), + 'imported symbol no longer exists', + this.createReplacement(toReplace.getStart(), toReplace.getWidth(), replacement) + ); + }); + } +} diff --git a/test/collapseRxjsImportsRule.spec.ts b/test/collapseRxjsImportsRule.spec.ts new file mode 100644 index 0000000..5135da6 --- /dev/null +++ b/test/collapseRxjsImportsRule.spec.ts @@ -0,0 +1,78 @@ +import { + assertSuccess, + assertAnnotated, + assertMultipleAnnotated, + assertFailures, + assertReplacements +} from './testHelper'; +import { assert } from 'chai'; +import { RuleFailure } from 'tslint'; + +describe('collapse-rxjs-imports', () => { + it('should collapse imports', () => { + const source = ` + import { foo } from 'rxjs'; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + import { baz } from '@angular/core'; + import { bar } from 'rxjs'; + `; + + const err = assertMultipleAnnotated({ + ruleName: 'collapse-rxjs-imports', + failures: [ + { + msg: 'duplicate RxJS import', + char: '~' + } + ], + source + }); + + const before = ` + import { foo } from 'rxjs'; + import { baz } from '@angular/core'; + import { bar } from 'rxjs'; + `; + const after = ` + import { foo , bar } from 'rxjs'; + import { baz } from '@angular/core'; + `; + + assertReplacements(err as RuleFailure[], before, after); + }); + + it('should collapse imports properly if there are following imports', () => { + const source = ` + import { foo } from 'rxjs'; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + import { baz } from '@angular/core'; + import { bar } from 'rxjs'; + import { qux } from '@angular/core'; + `; + + const err = assertMultipleAnnotated({ + ruleName: 'collapse-rxjs-imports', + failures: [ + { + msg: 'duplicate RxJS import', + char: '~' + } + ], + source + }); + + const before = ` + import { foo } from 'rxjs'; + import { baz } from '@angular/core'; + import { bar } from 'rxjs'; + import { qux } from '@angular/core'; + `; + const after = ` + import { foo , bar } from 'rxjs'; + import { baz } from '@angular/core'; + import { qux } from '@angular/core'; + `; + + assertReplacements(err as RuleFailure[], before, after); + }); +}); diff --git a/test/testHelper.ts b/test/testHelper.ts new file mode 100644 index 0000000..c63ee60 --- /dev/null +++ b/test/testHelper.ts @@ -0,0 +1,306 @@ +import * as tslint from 'tslint'; +import * as Lint from 'tslint'; +import chai = require('chai'); +import * as ts from 'typescript'; +import { IOptions, RuleFailure } from 'tslint'; +import { loadRules, convertRuleOptions } from './utils'; + +const fs = require('fs'); +const path = require('path'); + +interface ISourcePosition { + line: number; + character: number; +} + +export interface IExpectedFailure { + message: string; + startPosition: ISourcePosition; + endPosition: ISourcePosition; +} + +/** + * A helper function for specs. Lints the given `source` string against the `ruleName` with + * `options`. + * + * You're unlikely to use these in actual specs. Usually you'd use some of the following: + * - `assertAnnotated` or + * - `assertSuccess`. + * + * @param ruleName the name of the rule which is being tested + * @param source the source code, as a string + * @param options additional options for the lint rule + * @returns {LintResult} the result of linting + */ +function lint(ruleName: string, source: string | ts.SourceFile, options: any): tslint.LintResult { + let configuration = { + extends: [], + rules: new Map>(), + jsRules: new Map>(), + rulesDirectory: [] + }; + if (!options) { + options = []; + } + const ops: Partial = { ruleName, ruleArguments: options, disabledIntervals: [] }; + configuration.rules.set(ruleName, ops); + const linterOptions: tslint.ILinterOptions = { + formatter: 'json', + rulesDirectory: './dist/src', + formattersDirectory: null, + fix: false + }; + + let linter = new tslint.Linter(linterOptions, undefined); + if (typeof source === 'string') { + linter.lint('file.ts', source, configuration); + } else { + const rules = loadRules(convertRuleOptions(configuration.rules), linterOptions.rulesDirectory, false); + const res = [].concat.apply([], rules.map(r => r.apply(source))) as tslint.RuleFailure[]; + const errCount = res.filter(r => !r.getRuleSeverity || r.getRuleSeverity() === 'error').length; + return { + errorCount: errCount, + warningCount: res.length - errCount, + output: '', + format: null, + fixes: [].concat.apply(res.map(r => r.getFix())), + failures: res + }; + } + return linter.getResult(); +} + +export interface AssertConfig { + ruleName: string; + source: string; + options?: any; + message?: string; +} + +export interface AssertMultipleConfigs { + ruleName: string; + source: string; + options?: any; + failures: { char: string; msg: string }[]; +} + +/** + * When testing a failure, we also test to see if the linter will report the correct place where + * the source code doesn't match the rule. + * + * For example, if you use a private property in your template, the linter should report _where_ + * did it happen. Because it's tedious to supply actual line/column number in the spec, we use + * some custom syntax with "underlining" the problematic part with tildes: + * + * ``` + * template: '{{ foo }}' + * ~~~ + * ``` + * + * When giving a spec which we expect to fail, we give it "source code" such as above, with tildes. + * We call this kind of source code "annotated". This source code cannot be compiled (and thus + * cannot be linted/tested), so we use this function to get rid of tildes, but maintain the + * information about where the linter is supposed to catch error. + * + * The result of the function contains "cleaned" source (`.source`) and a `.failure` object which + * contains the `.startPosition` and `.endPosition` of the tildes. + * + * @param source The annotated source code with tildes. + * @param message Passed to the result's `.failure.message` property. + * @param specialChar The character to look for; in the above example that's ~. + * @param otherChars All other characters which should be ignored. Used when asserting multiple + * failures where there are multiple invalid characters. + * @returns {{source: string, failure: {message: string, startPosition: null, endPosition: any}}} + */ +const parseInvalidSource = (source: string, message: string, specialChar: string = '~', otherChars: string[] = []) => { + otherChars.forEach(char => source.replace(new RegExp(char, 'g'), ' ')); + let start = null; + let end; + let line = 0; + let col = 0; + let lastCol = 0; + let lastLine = 0; + for (let i = 0; i < source.length; i += 1) { + if (source[i] === specialChar && source[i - 1] !== '/' && start === null) { + start = { + line: line - 1, + character: col + }; + } + if (source[i] === '\n') { + col = 0; + line += 1; + } else { + col += 1; + } + if (source[i] === specialChar && source[i - 1] !== '/') { + lastCol = col; + lastLine = line - 1; + } + } + end = { + line: lastLine, + character: lastCol + }; + source = source.replace(new RegExp(specialChar, 'g'), ''); + return { + source: source, + failure: { + message: message, + startPosition: start, + endPosition: end + } + }; +}; + +/** + * Helper function used in specs for asserting an annotated failure. + * See explanation given in `parseInvalidSource` about annotated source code. * + * + * @param config + */ +export function assertAnnotated(config: AssertConfig) { + if (config.message) { + const parsed = parseInvalidSource(config.source, config.message); + return assertFailure(config.ruleName, parsed.source, parsed.failure, config.options); + } else { + return assertSuccess(config.ruleName, config.source, config.options); + } +} + +/** + * Helper function which asserts multiple annotated failures. + * @param configs + */ +export function assertMultipleAnnotated(configs: AssertMultipleConfigs): Lint.RuleFailure[] { + return [].concat.apply( + [], + configs.failures + .map((failure, index) => { + const otherCharacters = configs.failures.map(message => message.char).filter(x => x !== failure.char); + if (failure.msg) { + const parsed = parseInvalidSource(configs.source, failure.msg, failure.char, otherCharacters); + return assertFailure(configs.ruleName, parsed.source, parsed.failure, configs.options, index).filter(f => { + const start = f.getStartPosition().getLineAndCharacter(); + const end = f.getEndPosition().getLineAndCharacter(); + return ( + start.character === parsed.failure.startPosition.character && + start.line === parsed.failure.endPosition.line && + end.character === parsed.failure.endPosition.character && + end.line === parsed.failure.endPosition.line + ); + }); + } else { + assertSuccess(configs.ruleName, configs.source, configs.options); + return null; + } + }) + .filter(r => r !== null) + ); +} + +/** + * A helper function used in specs to assert a failure (meaning that the code contains a lint error). + * Consider using `assertAnnotated` instead. + * + * @param ruleName + * @param source + * @param fail + * @param options + * @param onlyNthFailure When there are multiple failures in code, we might want to test only some. + * This is 0-based index of the error that will be tested for. 0 by default. + * @returns {any} + */ +export function assertFailure( + ruleName: string, + source: string, + fail: IExpectedFailure, + options = null, + onlyNthFailure: number = 0 +): Lint.RuleFailure[] { + let result: Lint.LintResult; + try { + result = lint(ruleName, source, options); + } catch (e) { + console.log(e.stack); + } + chai.assert(result.failures && result.failures.length > 0, 'no failures'); + const ruleFail = result.failures[onlyNthFailure]; + chai.assert.equal(fail.message, ruleFail.getFailure(), `error messages don't match`); + chai.assert.deepEqual( + fail.startPosition, + ruleFail.getStartPosition().getLineAndCharacter(), + `start char doesn't match for "${fail.message}"` + ); + chai.assert.deepEqual( + fail.endPosition, + ruleFail.getEndPosition().getLineAndCharacter(), + `end char doesn't match for "${fail.message}"` + ); + if (result) { + return result.failures; + } + return undefined; +} + +/** + * A helper function used in specs to assert more than one failure. + * Consider using `assertAnnotated` instead. + * + * @param ruleName + * @param source + * @param fails + * @param options + */ +export function assertFailures(ruleName: string, source: string, fails: IExpectedFailure[], options = null) { + let result; + try { + result = lint(ruleName, source, options); + } catch (e) { + console.log(e.stack); + } + chai.assert(result.failures && result.failures.length > 0, 'no failures'); + result.failures.forEach((ruleFail, index) => { + chai.assert.equal(fails[index].message, ruleFail.getFailure(), `error messages don't match`); + chai.assert.deepEqual( + fails[index].startPosition, + ruleFail.getStartPosition().getLineAndCharacter(), + `start char doesn't match` + ); + chai.assert.deepEqual( + fails[index].endPosition, + ruleFail.getEndPosition().getLineAndCharacter(), + `end char doesn't match` + ); + }); + return result.failures; +} + +/** + * A helper function used in specs to assert a success (meaning that there are no lint errors). + * + * @param ruleName + * @param source + * @param options + */ +export function assertSuccess(ruleName: string, source: string | ts.SourceFile, options = null) { + const result = lint(ruleName, source, options); + chai.assert.isTrue(result && result.failures.length === 0); +} + +export const assertReplacements = (err: RuleFailure[], before: string, after: string) => { + if (err instanceof Array) { + let fixes = []; + err.forEach(e => { + let f = e.getFix(); + if (!Array.isArray(f)) { + f = [f]; + } + fixes = fixes.concat(f); + }); + before = fixes + .sort((a, b) => (b.end !== a.end ? b.end - a.end : b.start - a.start)) + .reduce((a, c) => c.apply(a), before); + chai.assert(before === after, 'Replacements are not applied properly: ' + before); + } +}; diff --git a/test/updateRxjsImportsRule.spec.ts b/test/updateRxjsImportsRule.spec.ts new file mode 100644 index 0000000..b9baaae --- /dev/null +++ b/test/updateRxjsImportsRule.spec.ts @@ -0,0 +1,297 @@ +import { + assertSuccess, + assertAnnotated, + assertMultipleAnnotated, + assertFailures, + assertReplacements +} from './testHelper'; +import { assert } from 'chai'; +import { RuleFailure } from 'tslint'; + +describe('update-rxjs-imports', () => { + describe('invalid import', () => { + it('should update the old import', () => { + const source = ` + import { foo } from 'rxjs/Subscriber'; + ~~~~~~~~~~~~~~~ + `; + const err = assertAnnotated({ + ruleName: 'update-rxjs-imports', + message: 'outdated import path', + source + }); + + const before = ` + import { foo } from 'rxjs/Subscriber'; + `; + const after = ` + import { foo } from 'rxjs'; + `; + + assertReplacements(err as RuleFailure[], before, after); + }); + }); + + describe('operators import', () => { + it('should work', () => { + const source = ` + import {do} from 'rxjs/operators/do'; + ~~~~~~~~~~~~~~~~~ + `; + + const err = assertAnnotated({ + ruleName: 'update-rxjs-imports', + message: 'outdated import path', + source + }); + + const before = ` + import {do} from 'rxjs/operators/do'; + `; + const after = ` + import {do} from 'rxjs/operators'; + `; + + assertReplacements(err as RuleFailure[], before, after); + }); + + it('should not replace side-effect imports', () => { + const source = ` + import 'rxjs/operators/do'; + `; + + assertSuccess('update-rxjs-imports', source); + }); + }); + + describe('never & empty', () => { + it('should migrate empty', () => { + const source = ` + import { empty } from 'rxjs/observable/empty'; + `; + const after = ` + import { EMPTY as empty } from 'rxjs'; + `; + + const err = assertFailures('update-rxjs-imports', source, [ + { + startPosition: { + line: 1, + character: 17 + }, + endPosition: { + line: 1, + character: 22 + }, + message: 'imported symbol no longer exists' + }, + { + startPosition: { + line: 1, + character: 31 + }, + endPosition: { + line: 1, + character: 52 + }, + message: 'outdated import path' + } + ]); + + assertReplacements(err as RuleFailure[], source, after); + }); + + it('should migrate empty with aliases', () => { + const source = ` + import { empty as Empty } from 'rxjs/observable/empty'; + `; + const after = ` + import { EMPTY as Empty } from 'rxjs'; + `; + + const err = assertFailures('update-rxjs-imports', source, [ + { + startPosition: { + line: 1, + character: 17 + }, + endPosition: { + line: 1, + character: 22 + }, + message: 'imported symbol no longer exists' + }, + { + startPosition: { + line: 1, + character: 40 + }, + endPosition: { + line: 1, + character: 61 + }, + message: 'outdated import path' + } + ]); + + assertReplacements(err as RuleFailure[], source, after); + }); + + it('should migrate never', () => { + const source = ` + import { never } from 'rxjs/observable/never'; + `; + const after = ` + import { NEVER as never } from 'rxjs'; + `; + + const err = assertFailures('update-rxjs-imports', source, [ + { + startPosition: { + line: 1, + character: 17 + }, + endPosition: { + line: 1, + character: 22 + }, + message: 'imported symbol no longer exists' + }, + { + startPosition: { + line: 1, + character: 31 + }, + endPosition: { + line: 1, + character: 52 + }, + message: 'outdated import path' + } + ]); + + assertReplacements(err as RuleFailure[], source, after); + }); + + it('should migrate never with aliases', () => { + const source = ` + import { never as Bar } from 'rxjs/observable/never'; + `; + const after = ` + import { NEVER as Bar } from 'rxjs'; + `; + + const err = assertFailures('update-rxjs-imports', source, [ + { + startPosition: { + line: 1, + character: 17 + }, + endPosition: { + line: 1, + character: 22 + }, + message: 'imported symbol no longer exists' + }, + { + startPosition: { + line: 1, + character: 38 + }, + endPosition: { + line: 1, + character: 59 + }, + message: 'outdated import path' + } + ]); + + assertReplacements(err as RuleFailure[], source, after); + }); + }); + + describe('AnonymousSubscription', () => { + it('should migrate AnonymousSubscription', () => { + const source = ` + import { AnonymousSubscription } from 'rxjs/Subscription'; + `; + const after = ` + import { Unsubscribable as AnonymousSubscription } from 'rxjs'; + `; + + const err = assertFailures('update-rxjs-imports', source, [ + { + startPosition: { + line: 1, + character: 17 + }, + endPosition: { + line: 1, + character: 38 + }, + message: 'imported symbol no longer exists' + }, + { + startPosition: { + line: 1, + character: 47 + }, + endPosition: { + line: 1, + character: 64 + }, + message: 'outdated import path' + } + ]); + + assertReplacements(err as RuleFailure[], source, after); + }); + + it('should migrate AnonymousSubscription with ISubscription', () => { + const source = ` + import { AnonymousSubscription, ISubscription } from 'rxjs/Subscription'; + `; + const after = ` + import { Unsubscribable as AnonymousSubscription, SubscriptionLike as ISubscription } from 'rxjs'; + `; + + const err = assertFailures('update-rxjs-imports', source, [ + { + startPosition: { + line: 1, + character: 17 + }, + endPosition: { + line: 1, + character: 38 + }, + message: 'imported symbol no longer exists' + }, + { + startPosition: { + line: 1, + character: 40 + }, + endPosition: { + line: 1, + character: 53 + }, + message: 'imported symbol no longer exists' + }, + { + startPosition: { + line: 1, + character: 62 + }, + endPosition: { + line: 1, + character: 79 + }, + message: 'outdated import path' + } + ]); + + assertReplacements(err as RuleFailure[], source, after); + }); + }); +}); diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..e6e8264 --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,133 @@ +import { IOptions, IRule } from 'tslint'; +import * as fs from 'fs'; +import * as path from 'path'; + +export function convertRuleOptions(ruleConfiguration: Map>): IOptions[] { + const output: IOptions[] = []; + ruleConfiguration.forEach(({ ruleArguments, ruleSeverity }, ruleName) => { + const options: IOptions = { + disabledIntervals: [], // deprecated, so just provide an empty array. + ruleArguments: ruleArguments !== null ? ruleArguments : [], + ruleName, + ruleSeverity: ruleSeverity !== null ? ruleSeverity : 'error', + }; + output.push(options); + }); + return output; +} + +const cachedRules = new Map(); + +export function camelize(stringWithHyphens: string): string { + return stringWithHyphens.replace(/-(.)/g, (_, nextLetter) => (nextLetter as string).toUpperCase()); +} + +function transformName(name: string): string { + // camelize strips out leading and trailing underscores and dashes, so make sure they aren't passed to camelize + // the regex matches the groups (leading underscores and dashes)(other characters)(trailing underscores and dashes) + const nameMatch = name.match(/^([-_]*)(.*?)([-_]*)$/); + if (nameMatch === null) { + return `${name}Rule`; + } + return `${nameMatch[1]}${camelize(nameMatch[2])}${nameMatch[3]}Rule`; +} + +/** + * @param directory - An absolute path to a directory of rules + * @param ruleName - A name of a rule in filename format. ex) "someLintRule" + */ +function loadRule(directory: string, ruleName: string): any | 'not-found' { + const fullPath = path.join(directory, ruleName); + if (fs.existsSync(`${fullPath}.js`)) { + const ruleModule = require(fullPath) as { Rule: any } | undefined; + if (ruleModule !== undefined) { + return ruleModule.Rule; + } + } + return 'not-found'; +} + +export function getRelativePath(directory?: string | null, relativeTo?: string) { + if (directory !== null) { + const basePath = relativeTo !== undefined ? relativeTo : process.cwd(); + return path.resolve(basePath, directory); + } + return undefined; +} + +export function arrayify(arg?: T | T[]): T[] { + if (Array.isArray(arg)) { + return arg; + } else if (arg !== null) { + return [arg]; + } else { + return []; + } +} + +function loadCachedRule(directory: string, ruleName: string, isCustomPath?: boolean): any | undefined { + // use cached value if available + const fullPath = path.join(directory, ruleName); + const cachedRule = cachedRules.get(fullPath); + if (cachedRule !== undefined) { + return cachedRule === 'not-found' ? undefined : cachedRule; + } + + // get absolute path + let absolutePath: string | undefined = directory; + if (isCustomPath) { + absolutePath = getRelativePath(directory); + if (absolutePath !== undefined && !fs.existsSync(absolutePath)) { + throw new Error(`Could not find custom rule directory: ${directory}`); + } + } + + const Rule = absolutePath === undefined ? 'not-found' : loadRule(absolutePath, ruleName); + + cachedRules.set(fullPath, Rule); + return Rule === 'not-found' ? undefined : Rule; +} + +export function find(inputs: T[], getResult: (t: T) => U | undefined): U | undefined { + for (const element of inputs) { + const result = getResult(element); + if (result !== undefined) { + return result; + } + } + return undefined; +} + +function findRule(name: string, rulesDirectories?: string | string[]): any | undefined { + const camelizedName = transformName(name); + return find(arrayify(rulesDirectories), (dir) => loadCachedRule(dir, camelizedName, true)); +} + +export function loadRules(ruleOptionsList: IOptions[], + rulesDirectories?: string | string[], + isJs = false): IRule[] { + const rules: IRule[] = []; + const notFoundRules: string[] = []; + const notAllowedInJsRules: string[] = []; + + for (const ruleOptions of ruleOptionsList) { + if (ruleOptions.ruleSeverity === 'off') { + // Perf: don't bother finding the rule if it's disabled. + continue; + } + + const ruleName = ruleOptions.ruleName; + const Rule = findRule(ruleName, rulesDirectories); + if (Rule === undefined) { + notFoundRules.push(ruleName); + } else if (isJs && Rule.metadata !== undefined && Rule.metadata.typescriptOnly) { + notAllowedInJsRules.push(ruleName); + } else { + const rule = new Rule(ruleOptions); + if (rule.isEnabled()) { + rules.push(rule); + } + } + } + return rules; +} diff --git a/tsconfig-release.json b/tsconfig-release.json new file mode 100644 index 0000000..d274027 --- /dev/null +++ b/tsconfig-release.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "target": "es5", + "module": "commonjs", + "declaration": true, + "noImplicitAny": false, + "removeComments": true, + "lib": ["es6", "es2015", "dom"], + "noLib": false, + "outDir": "./dist", + "typeRoots": [ + "./node_modules/@types", + "./node_modules" + ], + "types": [ + "mocha", + "chai", + "node", + "sprintf-js" + ] + }, + "files": [ + "src/index.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0c92d83 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "target": "es5", + "module": "commonjs", + "declaration": false, + "noImplicitAny": false, + "removeComments": true, + "lib": ["es6", "es2015", "dom"], + "noLib": false, + "outDir": "./dist", + "typeRoots": [ + "./node_modules/@types", + "./node_modules" + ], + "types": [ + "mocha", + "chai", + "node", + "sprintf-js" + ] + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..cc76a44 --- /dev/null +++ b/tslint.json @@ -0,0 +1,55 @@ +{ + "rules": { + "class-name": true, + "curly": false, + "eofline": true, + "indent": [true, "spaces"], + "max-line-length": [true, 140], + "member-ordering": [true, {"order": [ + "public-static-field", + "public-static-method", + "protected-static-field", + "protected-static-method", + "private-static-field", + "private-static-method", + "public-instance-field", + "protected-instance-field", + "private-instance-field", + "public-constructor", + "protected-constructor", + "private-constructor", + "public-instance-method", + "protected-instance-method", + "private-instance-method" + ]}], + "no-arg": true, + "no-consecutive-blank-lines": [true, 2], + "no-construct": true, + "no-duplicate-variable": true, + "no-eval": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-var-keyword": true, + "one-line": [true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "quotemark": [true, "single"], + "semicolon": true, + "triple-equals": true, + "variable-name": false, + "space-before-function-paren": [true, "never"], + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-module", + "check-operator", + "check-preblock", + "check-separator", + "check-type" + ] + } +}