diff --git a/cli/package.json b/cli/package.json index 9a6b6f3e64..01b6a0b82c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -26,6 +26,7 @@ "prepublishOnly": "cp ../README.md .", "test": "yarn clean && yarn build && yarn test-quick", "test-quick": "jest && yarn lint && yarn flow", + "test:watch": "yarn clean && yarn build && yarn jest --watch", "watch": "mkdirp dist && babel --source-maps --watch=./src --out-dir=./dist" }, "dependencies": { diff --git a/cli/src/cli.js b/cli/src/cli.js index b4f5b59f08..8b174df25f 100755 --- a/cli/src/cli.js +++ b/cli/src/cli.js @@ -2,16 +2,17 @@ // @flow import yargs from 'yargs'; -import {fs, path} from './lib/node.js'; +import {fs, path} from './lib/node'; -import * as Install from './commands/install.js'; +import * as Install from './commands/install'; import * as CreateDef from './commands/create-def'; -import * as CreateStub from './commands/create-stub.js'; -import * as RunTests from './commands/runTests.js'; -import * as Search from './commands/search.js'; -import * as Update from './commands/update.js'; +import * as CreateStub from './commands/create-stub'; +import * as Outdated from './commands/outdated'; +import * as RunTests from './commands/runTests'; +import * as Search from './commands/search'; +import * as Update from './commands/update'; import * as UpdateCache from './commands/update-cache'; -import * as ValidateDefs from './commands/validateDefs.js'; +import * as ValidateDefs from './commands/validateDefs'; import type {Argv} from 'yargs'; import typeof Yargs from 'yargs'; @@ -30,6 +31,7 @@ export function runCLI() { CreateDef, CreateStub, Install, + Outdated, RunTests, Search, Update, diff --git a/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/.cli-metadata.json b/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/.cli-metadata.json new file mode 100644 index 0000000000..384d02d152 --- /dev/null +++ b/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/.cli-metadata.json @@ -0,0 +1,3 @@ +{ + "compatibleCLIRange": ">=*.*.*" +} diff --git a/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/flow-bin_v0.x.x/flow_v0.38.x-/flow-bin_v0.x.x.js b/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/flow-bin_v0.x.x/flow_v0.38.x-/flow-bin_v0.x.x.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/flow-bin_v0.x.x/test_flowbin.js b/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/flow-bin_v0.x.x/test_flowbin.js new file mode 100644 index 0000000000..9366824f47 --- /dev/null +++ b/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/flow-bin_v0.x.x/test_flowbin.js @@ -0,0 +1,187 @@ +/** @flow */ +import _ from 'underscore'; + +/** + * _.find + */ +_.find([1, 2, 3], x => x * 1 == 3); +// $FlowExpectedError number cannot be compared to string +_.find([1, 2, 3], x => x == 'a'); +// $FlowExpectedError number. This type is incompatible with function type. +_.find([1, 2, 3], 1); +// $FlowExpectedError property `y`. Property not found in object literal +_.find([{x:1}, {x:2}, {x:3}], v => v.y == 3); +_.find([{x:1}, {x:2}, {x:3}], v => v.x == 3); + +/** + * _.findWhere + */ +_.findWhere([{x: 1}, {y: 2}], {x: 2}); +// $FlowExpectedError number. This type is incompatible with function type. +_.findWhere([{x: 1}, {y: 2}], 1); +// XXX: It would be nice if Flow could catch this error. +// See https://github.com/facebook/flow/issues/946 +_.findWhere([{x:1}, {x:2}, {x:3}], v => v.x == 3); + + +/** + * _.clone + */ +_.clone({a: 1}).a == 1; +// $FlowExpectedError property `b`. Property not found in object literal +_.clone({a: 1}).b == 1 +// $FlowExpectedError number. This type is incompatible with function type. +_.clone({a: 1}).a == 'c'; + +/** + * _.isEqual + */ +_.isEqual('a', 'b'); +_.isEqual({x: 1}, {y: 2}); + +// Flow considers these compatible with isEqual(a: any, b: any). +// Reasonable people disagree about whether these should be considered legal calls. +// See https://github.com/splodingsocks/FlowTyped/pull/1#issuecomment-149345275 +// and https://github.com/facebook/flow/issues/956 +_.isEqual(1); +_.isEqual(1, 2, 3); + + +/** + * _.range + */ +_.range(0, 10)[4] == 4 +// $FlowExpectedError string. This type is incompatible with number +_.range(0, 'a'); +// $FlowExpectedError string cannot be compared to number +_.range(0, 10)[4] == 'a'; + + +/** + * _.extend + */ +_.extend({a: 1}, {b: 2}).a +_.extend({a: 1}, {b: 2}).b +// $FlowExpectedError property `c`. Property not found in object literal +_.extend({a: 1}, {b: 2}).c + + +/** + * _.zip + */ +_.zip(['a', 'b', 'c'], ['d', 'e', 'f'])[0].length; +_.zip(['a', 'b', 'c'], [1, 2, 3])[0].length; +_.zip(['a', 'b', 'c'], [1, 2, 3])[0][0] + 'a' +_.zip(['a', 'b', 'c'], [1, 2, 3])[0][1] * 10 +// $FlowExpectedError `x` property not found in Array +_.zip([{x:1}], [{x:2,y:1}])[0].x +// $FlowExpectedError `y` property not found in object literal +_.zip([{x:1}], [{x:2,y:1}])[0][0].y +_.zip([{x:1}], [{x:2,y:1}])[0][1].y + + + +/** + * _.any + */ +_.any([1, 2, 3], x => x == 1); +// $FlowExpectedError number cannot be compared to string. +_.any([1, 2, 3], x => x == 'a'); + + +/** + * _.find + */ +_.find([1, 2, 3], x => x == 1); +// $FlowExpectedError number. This type is incompatible with function type. +_.find([1, 2, 3], 1); +// $FlowExpectedError Callable signature not found in object literal +_.find([1, 2, 3], {val: 1}); + +(_.findIndex([1, 2, 3], function(i) { return i % 2 == 0} ): number); +// $FlowExpectedError number cannot be compared to string. +(_.findIndex([1, 2, 3], function(i) { return i == '0'} ): number); + +(_.indexOf(['a', 'b', 'c'], function(e) { return e == 'b'}): number); + +(_.contains(['a', 'b', 'c'], 'b'): boolean); + +(_.map(['hello', 'world'], function(e) { return e.length }): Array); +(_.map({hello: 1, world: 2}, function(v, k) { return k.length }): Array); +// $FlowExpectedError This type is incompatible with string +(_.map({hello: 1, world: 2}, function(v, k) { return k * 2 }): Array); + +(_.mapObject({foo: 1, bar: 2}, function (v, k) {return (k.length + v).toString()}): {[key: string]: string}); +// $FlowExpectedError This type is incompatible with number +(_.mapObject({foo: 1, bar: 2}, function (v, k) {return (k.length + v).toString()}): number); + +(_.pluck([{name: 'bob'}, {name: 'jane'}], 'name'): Array); +(_.reduce([1, 2, 3], function(m, o) { return m + o }, 0): number); +(_.all([2, 4, 5], function(i) { return i % 2 == 0 }): boolean); +// $FlowExpectedError Property not found in Number +(_.all([2, 4, 5], function(i) { return i.length }): boolean); +(_.some([2, 4, 5], function(i) { return i % 2 == 0 }): boolean); +(_.union(['a', 'b'], ['b']): Array); +(_.intersection(['a', 'b'], ['b']): Array); +(_.difference(['a', 'b'], ['b']): Array); +(_.first([1,2,3]): number); +(_.first([1,2,3], 2): Array); +(_.last([1,2,3]): number); +(_.last([1,2,3], 2): Array); +(_.sample([1,2,3]): number); +(_.sortBy(['hello', 'world'], function(e) { return e.length }): Array); +(_.uniq([1,2,2]): Array); +(_.compact([1, null]): Array); +(_.select([1,2,3], function(e) { return e % 2 == 0 }): Array); +(_.reject([1,2,3], function(e) { return e % 2 == 0 }): Array); +(_.without([1,2,3], 1, 2): Array); +(_.has({a: 1, b: 2}, 'b'): boolean); +(_.isArray([1, 2]): boolean); +(_.isArray(1): boolean); +(_.pick({a: 1, b: 2}, 'a'): {[key: string]: number}); +(_.omit({a: 1, b: 2}, 'a'): {[key: string]: number}); + +_.throttle(function(a) {a.length}, 10)('hello'); +_.debounce(function(a) {a.length}, 10)('hello'); + +_.memoize(function(){})(); +_.partial(function (a, b) { return a + b }, 1)(2); +_.defer(function(){}); + +( + + _.compose( + function (name:string):string { return name + ', hello!'; }, + function (user:Object):string { return user.name; } + ): (user: Object) => string + +); + +(_.partition([1,5,2,4], function(i: number) { return i<4 }): [Array, Array]); +(_.partition({x: 'foo', y: 'bar'}, function(v: string, k: string) { return k === 'bar' }): [Array, Array]); + +(_.size([1,2]): number); +(_.size({a: 1, b: 2}): number); + +_.template("a<%=b%>c")({b: "_"}); +// $FlowExpectedError `foo` property not found in Function +_.template(321).foo; +// $FlowExpectedError This type is incompatible with string +_.template(321)({b: "_"}); +// $FlowExpectedError This type is incompatible with string +_.template("a<%=b%>c")({b: 1}); + +_.isObject({}); +_.isArguments(null); +_.isFunction(() => {}); +_.isString(''); +_.isBoolean(true); +_.isNumber(1); +_.isFinite(1); +_.isBoolean(1); +_.isDate(new Date()); +_.isRegExp(/[a-z]/); +_.isError(new Error('?')); +_.isNaN(NaN); +_.isNull(null); +_.isUndefined(undefined); diff --git a/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/foo_v0.5.x/flow_v0.38.x-/foo_v0.5.x.js b/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/foo_v0.5.x/flow_v0.38.x-/foo_v0.5.x.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/foo_v0.5.x/test_foo-v0.5.x.js b/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/foo_v0.5.x/test_foo-v0.5.x.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/foo_v1.x.x/flow_v0.38.x-/foo_v1.x.x.js b/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/foo_v1.x.x/flow_v0.38.x-/foo_v1.x.x.js new file mode 100644 index 0000000000..0526ac0995 --- /dev/null +++ b/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/foo_v1.x.x/flow_v0.38.x-/foo_v1.x.x.js @@ -0,0 +1,3 @@ +declare module 'foo' { + // stuff +} diff --git a/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/foo_v1.x.x/test_foo-v1.js b/cli/src/commands/__tests__/__outdated-fixtures__/end-to-end/fakeCacheRepo/definitions/npm/foo_v1.x.x/test_foo-v1.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cli/src/commands/__tests__/install-test.js b/cli/src/commands/__tests__/install-test.js index aaa015b6bf..99647ae811 100644 --- a/cli/src/commands/__tests__/install-test.js +++ b/cli/src/commands/__tests__/install-test.js @@ -26,7 +26,6 @@ import {testProject} from '../../lib/TEST_UTILS'; import colors from 'colors/safe'; import { - _determineFlowVersion as determineFlowVersion, _installNpmLibDefs as installNpmLibDefs, _installNpmLibDef as installNpmLibDef, run, @@ -55,61 +54,6 @@ const defaultRunProps = { }; describe('install (command)', () => { - describe('determineFlowVersion', () => { - it('infers version from path if arg not passed', () => { - return testProject(async ROOT_DIR => { - const ARBITRARY_PATH = path.join(ROOT_DIR, 'some', 'arbitrary', 'path'); - await Promise.all([ - mkdirp(ARBITRARY_PATH), - touchFile(path.join(ROOT_DIR, '.flowconfig')), - writePkgJson(path.join(ROOT_DIR, 'package.json'), { - name: 'test', - devDependencies: { - 'flow-bin': '^0.40.0', - }, - }), - ]); - - const flowVer = await determineFlowVersion(ARBITRARY_PATH); - expect(flowVer).toEqual({ - kind: 'specific', - ver: { - major: 0, - minor: 40, - patch: 0, - prerel: null, - }, - }); - }); - }); - - it('uses explicitly specified version', async () => { - const explicitVer = await determineFlowVersion('/', '0.7.0'); - expect(explicitVer).toEqual({ - kind: 'specific', - ver: { - major: 0, - minor: 7, - patch: 0, - prerel: null, - }, - }); - }); - - it("uses 'v'-prefixed explicitly specified version", async () => { - const explicitVer = await determineFlowVersion('/', 'v0.7.0'); - expect(explicitVer).toEqual({ - kind: 'specific', - ver: { - major: 0, - minor: 7, - patch: 0, - prerel: null, - }, - }); - }); - }); - describe('installNpmLibDefs', () => { const origConsoleError = console.error; diff --git a/cli/src/commands/__tests__/outdated-test.js b/cli/src/commands/__tests__/outdated-test.js new file mode 100644 index 0000000000..b426772d2b --- /dev/null +++ b/cli/src/commands/__tests__/outdated-test.js @@ -0,0 +1,258 @@ +// @flow +(require('../../lib/git'): any).rebaseRepoMaster = jest.fn(); + +import {table} from 'table'; +import { + _clearCustomCacheDir as clearCustomCacheDir, + _setCustomCacheDir as setCustomCacheDir, +} from '../../lib/cacheRepoUtils'; + +import {copyDir, mkdirp} from '../../lib/fileUtils'; + +import { + add as gitAdd, + commit as gitCommit, + init as gitInit, + setLocalConfig as gitConfig, +} from '../../lib/git'; + +import {fs, path} from '../../lib/node'; + +import {testProject} from '../../lib/TEST_UTILS'; + +import {run as install} from '../install'; +import {run} from '../outdated'; + +const BASE_FIXTURE_ROOT = path.join(__dirname, '__outdated-fixtures__'); + +async function touchFile(filePath) { + await fs.close(await fs.open(filePath, 'w')); +} + +async function writePkgJson(filePath, pkgJson) { + await fs.writeJson(filePath, pkgJson); +} + +describe('outdated (command)', () => { + describe('end-to-end tests', () => { + const FIXTURE_ROOT = path.join(BASE_FIXTURE_ROOT, 'end-to-end'); + + const FIXTURE_FAKE_CACHE_REPO_DIR = path.join( + FIXTURE_ROOT, + 'fakeCacheRepo', + ); + + const origConsoleLog = console.log; + const origConsoleError = console.error; + + beforeEach(() => { + (console: any).log = jest.fn(); + (console: any).error = jest.fn(); + }); + + afterEach(() => { + (console: any).log = origConsoleLog; + (console: any).error = origConsoleError; + }); + + async function fakeProjectEnv(runTest) { + return await testProject(async ROOT_DIR => { + const FAKE_CACHE_DIR = path.join(ROOT_DIR, 'fakeCache'); + const FAKE_CACHE_REPO_DIR = path.join(FAKE_CACHE_DIR, 'repo'); + const FLOWPROJ_DIR = path.join(ROOT_DIR, 'flowProj'); + const FLOWTYPED_DIR = path.join(FLOWPROJ_DIR, 'flow-typed', 'npm'); + + await Promise.all([mkdirp(FAKE_CACHE_REPO_DIR), mkdirp(FLOWTYPED_DIR)]); + + await copyDir(FIXTURE_FAKE_CACHE_REPO_DIR, FAKE_CACHE_REPO_DIR); + + await gitInit(FAKE_CACHE_REPO_DIR), + await Promise.all([ + gitConfig(FAKE_CACHE_REPO_DIR, 'user.name', 'Test Author'), + gitConfig(FAKE_CACHE_REPO_DIR, 'user.email', 'test@flow-typed.org'), + ]); + await gitAdd(FAKE_CACHE_REPO_DIR, 'definitions'); + await gitCommit(FAKE_CACHE_REPO_DIR, 'FIRST'); + + setCustomCacheDir(FAKE_CACHE_DIR); + + // $FlowExpectedError[method-unbinding] + const origCWD = process.cwd; + (process: any).cwd = () => FLOWPROJ_DIR; + try { + await runTest(FLOWPROJ_DIR); + } finally { + (process: any).cwd = origCWD; + clearCustomCacheDir(); + } + }); + } + + it('reports stub as outdated', () => { + const fooStub = `// flow-typed signature: 57774713bd9f8b7a3059edf76e66a6e0 +// flow-typed version: <>/foo_v1.2.3/flow_v0.162.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'foo' + * + * Fill this stub out by replacing all the \`any\` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'foo' { + declare module.exports: any; +} +`; + + return fakeProjectEnv(async FLOWPROJ_DIR => { + // Create some dependencies + await Promise.all([ + touchFile(path.join(FLOWPROJ_DIR, '.flowconfig')), + writePkgJson(path.join(FLOWPROJ_DIR, 'package.json'), { + name: 'test', + devDependencies: { + 'flow-bin': '^0.162.0', + }, + dependencies: { + foo: '1.2.3', + }, + }), + mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'foo')), + mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'flow-bin')), + mkdirp(path.join(FLOWPROJ_DIR, 'flow-typed')), + mkdirp(path.join(FLOWPROJ_DIR, 'flow-typed', 'npm')), + touchFile( + path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_vx.x.x.js'), + ), + fs.writeFile( + path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_vx.x.x.js'), + fooStub, + ), + ]); + + await run({}); + + expect( + await Promise.all([ + fs.exists( + path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_vx.x.x.js'), + ), + ]), + ).toEqual([true]); + + expect(console.log).toHaveBeenCalledWith( + table([ + ['Name', 'Details'], + [ + 'foo', + 'A new libdef has been published to the registry replacing your stub install it with `flow-typed install foo`', + ], + ]), + ); + }); + }); + + it('reports outdated libdef as needing updates', () => { + const fooLibdef = `// flow-typed signature: 9caf6a2fac36ca585677b99fd5bf5036 +// flow-typed version: abc/foo_v1.x.x/flow_v0.162.0 + +declare module 'foo' {}`; + + return fakeProjectEnv(async FLOWPROJ_DIR => { + // Create some dependencies + await Promise.all([ + touchFile(path.join(FLOWPROJ_DIR, '.flowconfig')), + writePkgJson(path.join(FLOWPROJ_DIR, 'package.json'), { + name: 'test', + devDependencies: { + 'flow-bin': '^0.162.0', + }, + dependencies: { + foo: '1.2.3', + }, + }), + mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'foo')), + mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'flow-bin')), + mkdirp(path.join(FLOWPROJ_DIR, 'flow-typed')), + mkdirp(path.join(FLOWPROJ_DIR, 'flow-typed', 'npm')), + touchFile( + path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_v1.x.x.js'), + ), + fs.writeFile( + path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_v1.x.x.js'), + fooLibdef, + ), + ]); + + await run({}); + + expect( + await Promise.all([ + fs.exists( + path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_v1.x.x.js'), + ), + ]), + ).toEqual([true]); + + expect(console.log).toHaveBeenCalledWith( + table([ + ['Name', 'Details'], + [ + 'foo', + 'This libdef does not match what we found in the registry, update it with `flow-typed update`', + ], + ]), + ); + }); + }); + + it('reports a recently installed libdef as update to date', () => { + return fakeProjectEnv(async FLOWPROJ_DIR => { + // Create some dependencies + await Promise.all([ + touchFile(path.join(FLOWPROJ_DIR, '.flowconfig')), + writePkgJson(path.join(FLOWPROJ_DIR, 'package.json'), { + name: 'test', + devDependencies: { + 'flow-bin': '^0.162.0', + }, + dependencies: { + foo: '1.2.3', + }, + }), + mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'foo')), + mkdirp(path.join(FLOWPROJ_DIR, 'node_modules', 'flow-bin')), + ]); + + await install({ + overwrite: false, + verbose: false, + skip: false, + skipFlowRestart: true, + explicitLibDefs: [], + }); + + (console: any).log.mockClear(); + + await run({}); + + expect( + await Promise.all([ + fs.exists( + path.join(FLOWPROJ_DIR, 'flow-typed', 'npm', 'foo_v1.x.x.js'), + ), + ]), + ).toEqual([true]); + + expect(console.log).toHaveBeenCalledWith( + 'All your lib defs are up to date!', + ); + }); + }); + }); +}); diff --git a/cli/src/commands/install.js b/cli/src/commands/install.js index a5fe2bdc11..bf3b0dc0b4 100644 --- a/cli/src/commands/install.js +++ b/cli/src/commands/install.js @@ -1,6 +1,4 @@ // @flow - -import type {FlowSpecificVer} from '../lib/flowVersion'; import {signCodeStream} from '../lib/codeSign'; import {copyFile, mkdirp} from '../lib/fileUtils'; import {child_process} from '../lib/node'; @@ -9,7 +7,7 @@ import {findFlowRoot} from '../lib/flowProjectUtils'; import { toSemverString as flowVersionToSemver, - parseFlowSpecificVer, + determineFlowSpecificVersion, } from '../lib/flowVersion'; import type {FlowVersion} from '../lib/flowVersion'; @@ -25,7 +23,6 @@ import { } from '../lib/npm/npmLibDefs'; import { - findFlowSpecificVer, findWorkspacesPackages, getPackageJsonData, getPackageJsonDependencies, @@ -63,7 +60,7 @@ export type Args = { packageDir?: mixed, // string ignoreDeps?: mixed, // Array rootDir?: mixed, // string, - useCacheUntil?: mixed, // seconds + useCacheUntil?: mixed, // number (milliseconds) explicitLibDefs: mixed, // Array ... }; @@ -153,7 +150,10 @@ export async function run(args: Args): Promise { : process.cwd(); const packageDir = typeof args.packageDir === 'string' ? path.resolve(args.packageDir) : cwd; - const flowVersion = await determineFlowVersion(packageDir, args.flowVersion); + const flowVersion = await determineFlowSpecificVersion( + packageDir, + args.flowVersion, + ); const libdefDir = typeof args.libdefDir === 'string' ? args.libdefDir : 'flow-typed'; if (args.ignoreDeps !== undefined && !Array.isArray(args.ignoreDeps)) { @@ -218,34 +218,6 @@ export async function run(args: Args): Promise { return 0; } -async function determineFlowVersion( - cwd: string, - flowVersionArg?: mixed, -): Promise<{| - kind: 'specific', - ver: FlowSpecificVer, -|}> { - if (flowVersionArg && typeof flowVersionArg === 'string') { - // Be permissive if the prefix 'v' is left off - let flowVersionStr = - flowVersionArg[0] === 'v' ? flowVersionArg : `v${flowVersionArg}`; - - if (/^v[0-9]+\.[0-9]+$/.test(flowVersionStr)) { - flowVersionStr = `${flowVersionStr}.0`; - } - - return { - kind: 'specific', - ver: parseFlowSpecificVer(flowVersionStr), - }; - } else { - return { - kind: 'specific', - ver: await findFlowSpecificVer(cwd), - }; - } -} - async function installCoreLibDefs(): Promise { // TODO... return 0; @@ -666,7 +638,6 @@ async function installNpmLibDef( } export { - determineFlowVersion as _determineFlowVersion, installNpmLibDefs as _installNpmLibDefs, installNpmLibDef as _installNpmLibDef, }; diff --git a/cli/src/commands/outdated.js b/cli/src/commands/outdated.js new file mode 100644 index 0000000000..05835622e0 --- /dev/null +++ b/cli/src/commands/outdated.js @@ -0,0 +1,204 @@ +// @flow +import path from 'path'; +import typeof Yargs from 'yargs'; +import {table} from 'table'; + +import {findFlowRoot} from '../lib/flowProjectUtils'; +import { + findNpmLibDef, + getCacheNpmLibDefs, + getInstalledNpmLibDefs, + getNpmLibDefVersionHash, +} from '../lib/npm/npmLibDefs'; +import {fs} from '../lib/node'; +import {determineFlowSpecificVersion} from '../lib/flowVersion'; +import {signCodeStream} from '../lib/codeSign'; +import {CACHE_REPO_EXPIRY, getCacheRepoDir} from '../lib/cacheRepoUtils'; + +export const name = 'outdated'; +export const description = + 'Update the flow-typed cache and print any outdated libdefs in current project'; + +export function setup(yargs: Yargs): Yargs { + return yargs + .usage(`$0 ${name}`) + .options({ + flowVersion: { + alias: 'f', + describe: + 'The Flow version that outdated libdefs must be compatible with', + type: 'string', + }, + useCacheUntil: { + alias: 'u', + describe: 'Use cache until specified time in milliseconds', + type: 'number', + }, + libdefDir: { + alias: 'l', + describe: 'Scan currently installed libdefs from a custom directory', + type: 'string', + demandOption: false, + }, + rootDir: { + alias: 'r', + describe: 'Directory of .flowconfig relative to node_modules', + type: 'string', + }, + packageDir: { + alias: 'p', + describe: + 'The relative path of package.json where flow-bin is installed', + type: 'string', + }, + }) + .example('$0 outdated', '') + .help('h') + .alias('h', 'help'); +} + +type Args = { + flowVersion?: mixed, // string + useCacheUntil?: mixed, // number (milliseconds) + libdefDir?: mixed, // string + rootDir?: mixed, // string + packageDir?: mixed, // string + ... +}; + +/** + * 1. Update and pull the cache + * 2. Compare current installed with what's in the cache + * 3. Create a list to print out + */ +export async function run(args: Args): Promise { + const cwd = + typeof args.rootDir === 'string' + ? path.resolve(args.rootDir) + : process.cwd(); + const flowProjectRoot = await findFlowRoot(cwd); + const packageDir = + typeof args.packageDir === 'string' ? path.resolve(args.packageDir) : cwd; + const flowVersion = await determineFlowSpecificVersion( + packageDir, + args.flowVersion, + ); + if (flowProjectRoot === null) { + console.error( + 'Error: Unable to find a flow project in the current dir or any of ' + + "it's parent dirs!\n" + + 'Please run this command from within a Flow project.', + ); + return 1; + } + + const cachedLibDefs = await getCacheNpmLibDefs( + Number(args.useCacheUntil) || CACHE_REPO_EXPIRY, + true, + ); + const installedLibDefs = await getInstalledNpmLibDefs( + flowProjectRoot, + args.libdefDir ? String(args.libdefDir) : undefined, + ); + + let outdatedList: Array<{ + name: string, + message: string, + }> = []; + + await Promise.all( + cachedLibDefs.map(async cachedDef => { + await Promise.all( + [...installedLibDefs.values()].map(async installedDef => { + // For each cached def we'll check if it's installed as a stub + // if so then we should mark as outdated + if ( + installedDef.kind === 'Stub' && + installedDef.name.startsWith(`${cachedDef.name}_`) + ) { + const stubName = installedDef.name.substring( + 0, + installedDef.name.indexOf('_'), + ); + outdatedList.push({ + name: stubName, + message: `A new libdef has been published to the registry replacing your stub install it with \`flow-typed install ${stubName}\``, + }); + } + if ( + installedDef.kind === 'LibDef' && + installedDef.libDef.name === cachedDef.name && + installedDef.libDef.scope === cachedDef.scope + ) { + const definitionFullName = installedDef.libDef.scope + ? `${installedDef.libDef.scope}/${installedDef.libDef.name}` + : installedDef.libDef.name; + // If we've found a match we need to know if definition has changed + // We can just find a compatible matching library and then + // figure out if the flow signatures has changed + const npmLibDef = await findNpmLibDef( + definitionFullName, + installedDef.libDef.version, + flowVersion, + args.useCacheUntil ? Number(args.useCacheUntil) : undefined, + undefined, + cachedLibDefs, + ); + + if (npmLibDef) { + const pullSignature = v => v.split('\n').slice(0, 2); + + const file = await fs.readFile( + path.join(cwd, installedDef.libDef.path), + 'utf8', + ); + const installedSignatureArray = pullSignature(file); + + const repoVersion = await getNpmLibDefVersionHash( + getCacheRepoDir(), + npmLibDef, + ); + const codeSignPreprocessor = signCodeStream(repoVersion); + const content = fs.readFileSync(npmLibDef.path, 'utf-8'); + const cacheSignatureArray = pullSignature( + codeSignPreprocessor(content), + ); + + if ( + installedSignatureArray[0] !== cacheSignatureArray[0] || + installedSignatureArray[1] !== cacheSignatureArray[1] + ) { + outdatedList.push({ + name: definitionFullName, + message: + 'This libdef does not match what we found in the registry, update it with `flow-typed update`', + }); + } + } + } + }), + ); + }), + ); + + if (outdatedList.length > 0) { + // Cleanup duplicated dependencies which come from nested libraries that ship flow + outdatedList = outdatedList.reduce((acc, cur) => { + if (acc.find(o => o.name === cur.name)) { + return acc; + } + return [...acc, cur]; + }, []); + + console.log( + table([ + ['Name', 'Details'], + ...outdatedList.map(o => [o.name, o.message]), + ]), + ); + } else { + console.log('All your lib defs are up to date!'); + } + + return 0; +} diff --git a/cli/src/lib/__tests__/flowVersion-test.js b/cli/src/lib/__tests__/flowVersion-test.js index c72d0bbc2d..b8e89033d9 100644 --- a/cli/src/lib/__tests__/flowVersion-test.js +++ b/cli/src/lib/__tests__/flowVersion-test.js @@ -1,11 +1,14 @@ // @flow - +import {fs, path} from '../../lib/node'; +import {testProject} from '../TEST_UTILS'; +import {mkdirp} from '../fileUtils'; import { parseDirString, __parseVersion as parseVersion, toSemverString, toDirString, compareFlowVersionAsc, + determineFlowSpecificVersion, } from '../flowVersion'; describe('flowVersion', () => { @@ -559,4 +562,67 @@ describe('flowVersion', () => { expect(compareFlowVersionAsc(a, b)).toBe(expected); }); }); + + describe('determineFlowSpecificVersion', () => { + async function touchFile(filePath) { + await fs.close(await fs.open(filePath, 'w')); + } + + async function writePkgJson(filePath, pkgJson) { + await fs.writeJson(filePath, pkgJson); + } + + it('infers version from path if arg not passed', () => { + return testProject(async ROOT_DIR => { + const ARBITRARY_PATH = path.join(ROOT_DIR, 'some', 'arbitrary', 'path'); + await Promise.all([ + mkdirp(ARBITRARY_PATH), + touchFile(path.join(ROOT_DIR, '.flowconfig')), + writePkgJson(path.join(ROOT_DIR, 'package.json'), { + name: 'test', + devDependencies: { + 'flow-bin': '^0.40.0', + }, + }), + ]); + + const flowVer = await determineFlowSpecificVersion(ARBITRARY_PATH); + expect(flowVer).toEqual({ + kind: 'specific', + ver: { + major: 0, + minor: 40, + patch: 0, + prerel: null, + }, + }); + }); + }); + + it('uses explicitly specified version', async () => { + const explicitVer = await determineFlowSpecificVersion('/', '0.7.0'); + expect(explicitVer).toEqual({ + kind: 'specific', + ver: { + major: 0, + minor: 7, + patch: 0, + prerel: null, + }, + }); + }); + + it("uses 'v'-prefixed explicitly specified version", async () => { + const explicitVer = await determineFlowSpecificVersion('/', 'v0.7.0'); + expect(explicitVer).toEqual({ + kind: 'specific', + ver: { + major: 0, + minor: 7, + patch: 0, + prerel: null, + }, + }); + }); + }); }); diff --git a/cli/src/lib/__tests__/libDefs-test.js b/cli/src/lib/__tests__/libDefs-test.js index 42b3eacc39..e7fa6080f2 100644 --- a/cli/src/lib/__tests__/libDefs-test.js +++ b/cli/src/lib/__tests__/libDefs-test.js @@ -4,6 +4,8 @@ jest.unmock('../libDefs.js'); jest.unmock('../semver.js'); jest.unmock('semver'); jest.unmock('../flowVersion'); +jest.unmock('colors/lib/styles'); +jest.unmock('wrappy'); import {fs} from '../node.js'; diff --git a/cli/src/lib/flowVersion.js b/cli/src/lib/flowVersion.js index 035899852b..0ca57926dd 100644 --- a/cli/src/lib/flowVersion.js +++ b/cli/src/lib/flowVersion.js @@ -1,4 +1,5 @@ // @flow +import {findFlowSpecificVer} from './npm/npmProjectUtils'; import {ValidationError} from './ValidationError'; @@ -207,6 +208,34 @@ export function parseFlowSpecificVer(verStr: string): FlowSpecificVer { }; } +export async function determineFlowSpecificVersion( + cwd: string, + flowVersionArg?: mixed, +): Promise<{| + kind: 'specific', + ver: FlowSpecificVer, +|}> { + if (flowVersionArg && typeof flowVersionArg === 'string') { + // Be permissive if the prefix 'v' is left off + let flowVersionStr = + flowVersionArg[0] === 'v' ? flowVersionArg : `v${flowVersionArg}`; + + if (/^v[0-9]+\.[0-9]+$/.test(flowVersionStr)) { + flowVersionStr = `${flowVersionStr}.0`; + } + + return { + kind: 'specific', + ver: parseFlowSpecificVer(flowVersionStr), + }; + } else { + return { + kind: 'specific', + ver: await findFlowSpecificVer(cwd), + }; + } +} + /** * Given two version ranges a and b, determine whether a is before b. */ diff --git a/docs/_media/outdated.png b/docs/_media/outdated.png new file mode 100644 index 0000000000..79df84eaf5 Binary files /dev/null and b/docs/_media/outdated.png differ diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 7845dd4ba7..c68d9a648f 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -5,6 +5,7 @@ - [Contributing](contributing.md) - CLI commands - [install](install.md) + - [outdated](outdated.md) - [create-stub](stub.md) - [run-tests](tests.md) - [search](search.md) diff --git a/docs/outdated.md b/docs/outdated.md new file mode 100644 index 0000000000..1092845d46 --- /dev/null +++ b/docs/outdated.md @@ -0,0 +1,21 @@ +# outdated + +Updates the global local cache and prints a table of all outdated currently installed definitions in the ./flow-typed directory. + +![example](_media/outdated.png) + +### Examples + +``` +flow-typed outdated +``` + +### Flags + +|Shorthand|Longhand|Description|Type| +|---------|--------|-----------|----| +|-f|--flowVersion|The Flow version that outdated libdefs must be compatible with|string| +|-l|--libdefDir|Scan currently installed libdefs from a custom directory (instead of the default, `./flow-typed`)|string| +|-p|--packageDir|The relative path of package.json where flow-bin is installed|string| +|-r|--rootDir|Directory of .flowconfig relative to node_modules|string| +|-u|--useCacheUntil|Use cache until specified time in milliseconds|number|