├── .eslintrc ├── .gitignore ├── .husky └── pre-commit ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── lerna.json ├── package.json ├── packages ├── babel-plugin-idx │ ├── .babelrc │ ├── package.json │ └── src │ │ ├── babel-plugin-idx.js │ │ └── babel-plugin-idx.test.js └── idx │ ├── .babelrc │ ├── .flowconfig │ ├── package.json │ ├── scripts │ └── build.sh │ └── src │ ├── idx.d.ts │ ├── idx.js │ ├── idx.js.flow │ ├── idx.test.js │ └── idx.test.ts └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parser": "hermes-eslint", 4 | "plugins": ["jest"], 5 | "root": true, 6 | "rules": { 7 | "max-len": 0 8 | }, 9 | "overrides": [ 10 | { 11 | "files": ["packages/babel-plugin-idx/src/*.js"], 12 | "env": { 13 | "node": true 14 | } 15 | }, 16 | { 17 | "files": ["packages/idx/src/*.js"], 18 | "env": { 19 | "commonjs": true 20 | } 21 | }, 22 | { 23 | "files": ["**/*.test.js"], 24 | "env": { 25 | "jest/globals": true 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | lib/ 3 | node_modules/ 4 | packages/*/LICENSE 5 | packages/*/README.md 6 | *.log 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.0.3 / 2024-01-10 2 | 3 | - Update `README` in preparation for archiving the project. 4 | 5 | # 3.0.2 / 2023-07-06 6 | 7 | - Remove extraneous dependencies from `babel-plugin-idx`. 8 | 9 | # 3.0.1 / 2023-07-06 10 | 11 | - Restore `LICENSE` and omit tests from published package. 12 | 13 | # 3.0.0 / 2023-07-06 14 | 15 | - **Breaking Change:** New Flow type definition requires `flow-bin@0.211.0`. 16 | - See the [updated documentation for `idx`](https://github.com/facebook/idx#static-typing) for additional configuration steps. 17 | - In the future, older versions of `idx` may no longer be compatible with newer versions of `flow-bin`. (https://github.com/facebook/idx/pull/854) 18 | 19 | # 2.5.6 / 2019-05-05 20 | 21 | - TypeScript: Refactor `DeepRequiredObject` using `extends`. 22 | 23 | # 2.5.5 / 2019-03-13 24 | 25 | - TypeScript: Make `DeepRequiredObject` omit primitive types. 26 | - TypeScript: Update `UnboxDeepRequired` to check for objects and arrays before primitives. 27 | - Moved `typescript` dependency down to `idx` package. 28 | 29 | # 2.5.4 / 2019-02-21 30 | 31 | - TypeScript: Fix bugs with the `UnboxDeepRequired` type. 32 | 33 | # 2.5.3 / 2019-02-09 34 | 35 | - TypeScript: Fix return type with new `UnboxDeepRequired` type. 36 | - Upgraded multiple dependency version. 37 | 38 | # 2.5.2 / 2018-11-26 39 | 40 | - TypeScript: Allow nullable values. 41 | 42 | # 2.5.1 / 2018-11-23 43 | 44 | - TypeScript: Carry over argument types in methods. 45 | 46 | # 2.5.0 / 2018-11-16 47 | 48 | - TypeScript: Support `strictNullCheck` flag. 49 | 50 | # 2.4.0 / 2018-06-11 51 | 52 | - Fix a bug with `babel-plugin-idx` when dealing with nested `idx` calls. 53 | - Upgraded to Flow strict. 54 | 55 | # 2.3.0 / 2018-04-13 56 | 57 | - Fix detection in browsers with capitalized `NULL` or `UNDEFINED`. 58 | 59 | # 2.2.0 / 2017-10-27 60 | 61 | - Added TypeScript definitions for `idx`. 62 | - Relicensed `babel-plugin-idx` and `idx` as MIT. 63 | 64 | # 2.1.0 / 2017-10-09 65 | 66 | - Simplify `idx` error message parsing and remove `Function` constructor use. 67 | - Export `idx` as `default` for use with `import`. 68 | 69 | # 2.0.0 / 2017-08-31 70 | 71 | - Disallow call expressions from within `idx` (originally introduced in 1.1.0). 72 | - Disallow invalid type imports from `idx`. 73 | - Change `babel-plugin-idx` to stop hardcoding `idx` (so it can be imported as any identifier). 74 | - Fix `idx` calls in async methods. 75 | 76 | # 1.5.1 / 2017-04-11 77 | 78 | - Fix `babel-plugin-idx` when `idx`'s parent is a scope-creating expression. 79 | 80 | # 1.5.0 / 2017-04-09 81 | 82 | - Improve `babel-plugin-idx` to use only one temporary variable. 83 | - Add fast path for source files without references to "idx". 84 | 85 | # 1.4.0 / 2017-03-30 86 | 87 | - Better `babel-plugin-idx` error messages. 88 | - Add fast path for source files without references to "idx". 89 | 90 | # 1.3.0 / 2017-03-29 91 | 92 | - Fix `babel-plugin-idx` for async functions. 93 | 94 | # 1.2.0 / 2017-03-22 95 | 96 | - Strip `@providesModule` from `idx.js`. 97 | 98 | # 1.1.0 / 2017-03-13 99 | 100 | - Added support for method calls (eg. `idx(foo, _ => _.bar())`). 101 | 102 | # 1.0.1 / 2017-03-13 103 | 104 | - Initial release. 105 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.fb.com/codeofconduct) so that you can understand what actions will and will not be tolerated. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to idx 2 | 3 | We want to make contributing to this project as easy and transparent as 4 | possible. 5 | 6 | ## Pull Requests 7 | 8 | We actively welcome your pull requests. 9 | 10 | 1. Fork the repo and create your branch from `main`. 11 | 2. If you've added code that should be tested, add tests. 12 | 3. If you've changed APIs, update the documentation. 13 | 4. Ensure the test suite passes. 14 | 5. Make sure your code lints. 15 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 16 | 17 | ## Contributor License Agreement ("CLA") 18 | 19 | In order to accept your pull request, we need you to submit a CLA. You only need 20 | to do this once to work on any of Facebook's open source projects. 21 | 22 | Complete your CLA here: 23 | 24 | ## Issues 25 | 26 | We use GitHub issues to track public bugs. Please ensure your description is 27 | clear and has sufficient instructions to be able to reproduce the issue. 28 | 29 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 30 | disclosure of security bugs. In those cases, please go through the process 31 | outlined on that page and do not file a public issue. 32 | 33 | ## License 34 | 35 | By contributing to idx, you agree that your contributions will be licensed 36 | under the LICENSE file in the root directory of this source tree. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # idx 2 | 3 | **This module is deprecated and no longer maintained. Use [optional chaining](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Optional_chaining) instead.** 4 | 5 | `idx` is a utility function for traversing properties on objects and arrays, 6 | where intermediate properties may be null or undefined. 7 | 8 | One notable difference between `idx` and optional chaining is what happens when 9 | an intermediate property is null or undefined. With `idx`, the null or undefined 10 | value is returned, whereas optional chaining would resolve to undefined. 11 | 12 | ## Install 13 | 14 | ```shell 15 | $ npm install idx babel-plugin-idx 16 | ``` 17 | 18 | or 19 | 20 | ```shell 21 | $ yarn add idx babel-plugin-idx 22 | ``` 23 | 24 | [Configure Babel](https://babeljs.io/docs/en/configuration) to include the 25 | `babel-plugin-idx` Babel plugin. 26 | 27 | ```javascript 28 | { 29 | plugins: [['babel-plugin-idx']]; 30 | } 31 | ``` 32 | 33 | This is necessary for `idx` to behave correctly 34 | with minimal performance impact. 35 | 36 | ## Usage 37 | 38 | Consider the following type for `props`: 39 | 40 | ```javascript 41 | type User = { 42 | user: ?{ 43 | name: string, 44 | friends: ?Array, 45 | }, 46 | }; 47 | ``` 48 | 49 | Getting to the friends of my first friend would resemble: 50 | 51 | ```javascript 52 | props.user && 53 | props.user.friends && 54 | props.user.friends[0] && 55 | props.user.friends[0].friends; 56 | ``` 57 | 58 | Instead, `idx` allows us to safely write: 59 | 60 | ```javascript 61 | idx(props, _ => _.user.friends[0].friends); 62 | ``` 63 | 64 | The second argument must be a function that returns one or more nested member 65 | expressions. Any other expression has undefined behavior. 66 | 67 | ## Static Typing 68 | 69 | [Flow](https://flow.org/) and [TypeScript](https://www.typescriptlang.org/) 70 | understand the `idx` idiom: 71 | 72 | ```javascript 73 | // @flow 74 | 75 | import idx from 'idx'; 76 | 77 | function getName(props: User): ?string { 78 | return idx(props, _ => _.user.name); 79 | } 80 | ``` 81 | 82 | **If you use `idx@3+`,** you may need to add the following to your `.flowconfig`: 83 | 84 | ``` 85 | [options] 86 | conditional_type=true 87 | mapped_type=true 88 | ``` 89 | 90 | ## Babel Plugin 91 | 92 | The `idx` runtime function exists for the purpose of illustrating the expected 93 | behavior and is not meant to be executed. The `idx` function requires the use of 94 | a Babel plugin that replaces it with an implementation that does not depend on 95 | details related to browser error messages. 96 | 97 | This Babel plugin searches for requires or imports to the `idx` module and 98 | replaces all its usages, so this code: 99 | 100 | ```javascript 101 | import idx from 'idx'; 102 | 103 | function getFriends() { 104 | return idx(props, _ => _.user.friends[0].friends); 105 | } 106 | ``` 107 | 108 | gets transformed to something like: 109 | 110 | ```javascript 111 | function getFriends() { 112 | return props.user == null 113 | ? props.user 114 | : props.user.friends == null 115 | ? props.user.friends 116 | : props.user.friends[0] == null 117 | ? props.user.friends[0] 118 | : props.user.friends[0].friends; 119 | } 120 | ``` 121 | 122 | Note that the original `import` gets also removed. 123 | 124 | It's possible to customize the name of the import/require, so code that is not 125 | directly requiring the `idx` npm package can also get transformed: 126 | 127 | ```javascript 128 | { 129 | plugins: [ 130 | [ 131 | 'babel-plugin-idx', 132 | { 133 | importName: './idx', 134 | }, 135 | ], 136 | ]; 137 | } 138 | ``` 139 | 140 | ## License 141 | 142 | `idx` is [MIT licensed](./LICENSE). 143 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "3.0.3", 4 | "npmClient": "yarn" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "scripts": { 7 | "build": "lerna run build", 8 | "lint": "eslint --ignore-path .gitignore .", 9 | "prepare": "husky install", 10 | "test": "lerna run test", 11 | "upgrade-all": "yarn workspaces run upgrade && yarn upgrade --latest" 12 | }, 13 | "prettier": { 14 | "arrowParens": "avoid", 15 | "bracketSpacing": false, 16 | "singleQuote": true, 17 | "trailingComma": "all" 18 | }, 19 | "lint-staged": { 20 | "**/*": "prettier --write --ignore-unknown" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^8.39.0", 24 | "eslint-plugin-jest": "^27.2.1", 25 | "hermes-eslint": "^0.13.1", 26 | "husky": "^8.0.3", 27 | "lerna": "^7.1.1", 28 | "lint-staged": "^13.2.3", 29 | "prettier": "^3.0.0" 30 | }, 31 | "resolutions": { 32 | "semver": "^7.5.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/babel-plugin-idx/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-idx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-idx", 3 | "version": "3.0.3", 4 | "description": "Babel plugin for transforming the idx utility function.", 5 | "main": "lib/babel-plugin-idx.js", 6 | "files": [ 7 | "README.md", 8 | "lib/" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/facebook/idx.git", 13 | "directory": "packages/babel-plugin-idx" 14 | }, 15 | "license": "MIT", 16 | "scripts": { 17 | "build": "babel src --out-dir lib --copy-files; rm lib/*.test.js", 18 | "prepublish": "yarn run build && cp ../../{LICENSE,README.md} .", 19 | "test": "jest", 20 | "upgrade": "yarn upgrade --latest" 21 | }, 22 | "devDependencies": { 23 | "@babel/cli": "^7.21.0", 24 | "@babel/core": "^7.21.4", 25 | "@babel/plugin-transform-async-to-generator": "^7.20.7", 26 | "@babel/plugin-transform-flow-strip-types": "^7.21.0", 27 | "@babel/preset-env": "^7.21.4", 28 | "babel-jest": "^29.5.0", 29 | "jest": "^29.5.0" 30 | }, 31 | "jest": { 32 | "testEnvironment": "node", 33 | "rootDir": "src" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/babel-plugin-idx/src/babel-plugin-idx.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | 'use strict'; // eslint-disable-line strict 11 | 12 | module.exports = context => { 13 | const t = context.types; 14 | 15 | const idxRe = /\bidx\b/; 16 | 17 | function checkIdxArguments(file, node) { 18 | const args = node.arguments; 19 | if (args.length !== 2) { 20 | throw file.buildCodeFrameError( 21 | node, 22 | 'The `idx` function takes exactly two arguments.', 23 | ); 24 | } 25 | const arrowFunction = args[1]; 26 | if (!t.isArrowFunctionExpression(arrowFunction)) { 27 | throw file.buildCodeFrameError( 28 | arrowFunction, 29 | 'The second argument supplied to `idx` must be an arrow function.', 30 | ); 31 | } 32 | if (!t.isExpression(arrowFunction.body)) { 33 | throw file.buildCodeFrameError( 34 | arrowFunction.body, 35 | 'The body of the arrow function supplied to `idx` must be a single ' + 36 | 'expression (without curly braces).', 37 | ); 38 | } 39 | if (arrowFunction.params.length !== 1) { 40 | throw file.buildCodeFrameError( 41 | arrowFunction.params[2] || arrowFunction, 42 | 'The arrow function supplied to `idx` must take exactly one parameter.', 43 | ); 44 | } 45 | const input = arrowFunction.params[0]; 46 | if (!t.isIdentifier(input)) { 47 | throw file.buildCodeFrameError( 48 | arrowFunction.params[0], 49 | 'The parameter supplied to `idx` must be an identifier.', 50 | ); 51 | } 52 | } 53 | 54 | function checkIdxBindingNode(file, node) { 55 | if (t.isImportDeclaration(node)) { 56 | // E.g. `import '...'` 57 | if (node.specifiers.length === 0) { 58 | throw file.buildCodeFrameError( 59 | node, 60 | 'The idx import must have a value.', 61 | ); 62 | } 63 | // E.g. `import A, {B} from '...'` 64 | // `import A, * as B from '...'` 65 | // `import {A, B} from '...'` 66 | if (node.specifiers.length > 1) { 67 | throw file.buildCodeFrameError( 68 | node.specifiers[1], 69 | 'The idx import must be a single specifier.', 70 | ); 71 | } 72 | // `import {default as idx} from '...'` or `import idx from '...'` are ok. 73 | // `import idx, * as idx2 from '...'` is not ok but would've been caught 74 | // above. 75 | if (!t.isSpecifierDefault(node.specifiers[0])) { 76 | throw file.buildCodeFrameError( 77 | node.specifiers[0], 78 | 'The idx import must be a default import.', 79 | ); 80 | } 81 | // `importKind` is not a property unless flow syntax is enabled. 82 | // On specifiers, `importKind` is not "value" when it's not a type, it's 83 | // `null`. 84 | // E.g. `import type {...} from '...'` 85 | // `import typeof {...} from '...'` 86 | // `import {type ...} from '...'`. 87 | // `import {typeof ...} from '...'` 88 | if ( 89 | node.importKind === 'type' || 90 | node.importKind === 'typeof' || 91 | node.specifiers[0].importKind === 'type' || 92 | node.specifiers[0].importKind === 'typeof' 93 | ) { 94 | throw file.buildCodeFrameError( 95 | node, 96 | 'The idx import must be a value import.', 97 | ); 98 | } 99 | } else if (t.isVariableDeclarator(node)) { 100 | // E.g. var {idx} or var [idx] 101 | if (!t.isIdentifier(node.id)) { 102 | throw file.buildCodeFrameError( 103 | node.specifiers[0], 104 | 'The idx declaration must be an identifier.', 105 | ); 106 | } 107 | } 108 | } 109 | 110 | function makeCondition(node, state, inside) { 111 | if (inside) { 112 | return t.ConditionalExpression( 113 | t.BinaryExpression( 114 | '!=', 115 | t.AssignmentExpression('=', state.temp, node), 116 | t.NullLiteral(), 117 | ), 118 | inside, 119 | state.temp, 120 | ); 121 | } else { 122 | return node; 123 | } 124 | } 125 | 126 | function makeChain(node, state, inside) { 127 | if (t.isMemberExpression(node)) { 128 | return makeChain( 129 | node.object, 130 | state, 131 | makeCondition( 132 | t.MemberExpression(state.temp, node.property, node.computed), 133 | state, 134 | inside, 135 | ), 136 | ); 137 | } else if (t.isIdentifier(node)) { 138 | if (node.name !== state.base.name) { 139 | throw state.file.buildCodeFrameError( 140 | node, 141 | 'The parameter of the arrow function supplied to `idx` must match ' + 142 | 'the base of the body expression.', 143 | ); 144 | } 145 | return makeCondition(state.input, state, inside); 146 | } else { 147 | throw state.file.buildCodeFrameError( 148 | node, 149 | 'idx callbacks may only access properties on the callback parameter.', 150 | ); 151 | } 152 | } 153 | 154 | function visitIdxCallExpression(path, state) { 155 | const node = path.node; 156 | checkIdxArguments(state.file, node); 157 | const temp = path.scope.generateUidIdentifier('ref'); 158 | const replacement = makeChain(node.arguments[1].body, { 159 | file: state.file, 160 | input: node.arguments[0], 161 | base: node.arguments[1].params[0], 162 | temp, 163 | }); 164 | path.replaceWith(replacement); 165 | // Hoist to the top if it's an async method. 166 | if (path.scope.path.isClassMethod({async: true})) { 167 | path.scope.push({id: temp, _blockHoist: 3}); 168 | } else { 169 | path.scope.push({id: temp}); 170 | } 171 | } 172 | 173 | function isIdxImportOrRequire(node, name) { 174 | if (t.isImportDeclaration(node)) { 175 | return t.isStringLiteral(node.source, {value: name}); 176 | } else if (t.isVariableDeclarator(node)) { 177 | return ( 178 | t.isCallExpression(node.init) && 179 | t.isIdentifier(node.init.callee, {name: 'require'}) && 180 | t.isLiteral(node.init.arguments[0], {value: name}) 181 | ); 182 | } else { 183 | return false; 184 | } 185 | } 186 | 187 | const declareVisitor = { 188 | 'ImportDeclaration|VariableDeclarator'(path, state) { 189 | if (!isIdxImportOrRequire(path.node, state.importName)) { 190 | return; 191 | } 192 | 193 | checkIdxBindingNode(state.file, path.node); 194 | 195 | const bindingName = t.isImportDeclaration(path.node) 196 | ? path.node.specifiers[0].local.name 197 | : path.node.id.name; 198 | const idxBinding = path.scope.getOwnBinding(bindingName); 199 | 200 | idxBinding.constantViolations.forEach(refPath => { 201 | throw state.file.buildCodeFrameError( 202 | refPath.node, 203 | '`idx` cannot be redefined.', 204 | ); 205 | }); 206 | 207 | let didTransform = false; 208 | let didSkip = false; 209 | 210 | // Traverse the references backwards to process inner calls before 211 | // outer calls. 212 | idxBinding.referencePaths 213 | .slice() 214 | .reverse() 215 | .forEach(refPath => { 216 | if (refPath.node === idxBinding.node) { 217 | // Do nothing... 218 | } else if (refPath.parentPath.isCallExpression()) { 219 | visitIdxCallExpression(refPath.parentPath, state); 220 | didTransform = true; 221 | } else { 222 | // Should this throw? 223 | didSkip = true; 224 | } 225 | }); 226 | if (didTransform && !didSkip) { 227 | path.remove(); 228 | } 229 | }, 230 | }; 231 | 232 | return { 233 | visitor: { 234 | Program(path, state) { 235 | const importName = state.opts.importName || 'idx'; 236 | // If there can't reasonably be an idx call, exit fast. 237 | if (importName !== 'idx' || idxRe.test(state.file.code)) { 238 | // We're very strict about the shape of idx. Some transforms, like 239 | // "babel-plugin-transform-async-to-generator", will convert arrow 240 | // functions inside async functions into regular functions. So we do 241 | // our transformation before any one else interferes. 242 | const newState = {file: state.file, importName}; 243 | path.traverse(declareVisitor, newState); 244 | } 245 | }, 246 | }, 247 | }; 248 | }; 249 | -------------------------------------------------------------------------------- /packages/babel-plugin-idx/src/babel-plugin-idx.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | 'use strict'; // eslint-disable-line strict 11 | 12 | jest.autoMockOff(); 13 | 14 | const {transform: babelTransform} = require('@babel/core'); 15 | const babelPluginIdx = require('./babel-plugin-idx'); 16 | const transformAsyncToGenerator = require('@babel/plugin-transform-async-to-generator'); 17 | const syntaxFlow = require('@babel/plugin-syntax-flow'); 18 | const vm = require('vm'); 19 | 20 | function transform(source, plugins, options) { 21 | return babelTransform(source, { 22 | plugins: plugins || [[babelPluginIdx, options]], 23 | babelrc: false, 24 | }).code; 25 | } 26 | 27 | const asyncToGeneratorHelperCode = ` 28 | function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { 29 | try { 30 | var info = gen[key](arg); 31 | var value = info.value; 32 | } catch (error) { 33 | reject(error); 34 | return; 35 | } 36 | if (info.done) { 37 | resolve(value); 38 | } else { 39 | Promise.resolve(value).then(_next, _throw); 40 | } 41 | } 42 | 43 | function _asyncToGenerator(fn) { 44 | return function() { 45 | var self = this, 46 | args = arguments; 47 | return new Promise(function(resolve, reject) { 48 | var gen = fn.apply(self, args); 49 | function _next(value) { 50 | asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); 51 | } 52 | function _throw(err) { 53 | asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); 54 | } 55 | _next(undefined); 56 | }); 57 | }; 58 | } 59 | `; 60 | 61 | describe('babel-plugin-idx', () => { 62 | beforeEach(() => { 63 | function stringByTrimmingSpaces(string) { 64 | return string.replace(/\s+/g, ''); 65 | } 66 | 67 | expect.extend({ 68 | toTransformInto(input, expected) { 69 | const plugins = typeof input === 'string' ? null : input.plugins; 70 | const options = typeof input === 'string' ? undefined : input.options; 71 | const code = typeof input === 'string' ? input : input.code; 72 | const actual = transform(code, plugins, options); 73 | const pass = 74 | stringByTrimmingSpaces(actual) === stringByTrimmingSpaces(expected); 75 | return { 76 | pass, 77 | message: () => 78 | 'Expected input to transform into:\n' + 79 | expected + 80 | '\n' + 81 | 'Instead, got:\n' + 82 | actual, 83 | }; 84 | }, 85 | toThrowTransformError(input, expected) { 86 | try { 87 | const plugins = typeof input === 'string' ? null : input.plugins; 88 | const options = typeof input === 'string' ? undefined : input.options; 89 | const code = typeof input === 'string' ? input : input.code; 90 | transform(code, plugins, options); 91 | } catch (error) { 92 | const actual = error.message.substring( 93 | 14, // Strip "unknown file: ". 94 | error.message.indexOf('\n', expected.length), // Strip code snippet. 95 | ); 96 | return { 97 | pass: actual === expected, 98 | message: () => 99 | 'Expected transform to throw "' + 100 | expected + 101 | '", but instead ' + 102 | 'got "' + 103 | actual + 104 | '".', 105 | }; 106 | } 107 | return { 108 | pass: false, 109 | message: () => 'Expected transform to throw "' + expected + '".', 110 | }; 111 | }, 112 | toReturn(input, expected) { 113 | const code = transform(input, undefined); 114 | const actual = vm.runInNewContext(code); 115 | return { 116 | pass: actual === expected, 117 | message: () => 118 | 'Expected "' + expected + '" but got "' + actual + '".', 119 | }; 120 | }, 121 | }); 122 | }); 123 | 124 | it('transforms member expressions', () => { 125 | expect(` 126 | const idx = require('idx'); 127 | idx(base, _ => _.b.c.d.e); 128 | `).toTransformInto(` 129 | var _ref; 130 | (_ref = base) != null ? 131 | (_ref = _ref.b) != null ? 132 | (_ref = _ref.c) != null ? 133 | (_ref = _ref.d) != null ? 134 | _ref.e : 135 | _ref : 136 | _ref : 137 | _ref : 138 | _ref; 139 | `); 140 | }); 141 | 142 | it('throws on call expressions', () => { 143 | expect(` 144 | const idx = require('idx'); 145 | idx(base, _ => _.b.c(...foo)().d(bar, null, [...baz])); 146 | `).toThrowTransformError( 147 | 'idx callbacks may only access properties on the callback parameter.', 148 | ); 149 | }); 150 | 151 | it('transforms bracket notation', () => { 152 | expect(` 153 | const idx = require('idx'); 154 | idx(base, _ => _["b"][0][c + d]); 155 | `).toTransformInto(` 156 | var _ref; 157 | (_ref = base) != null ? 158 | (_ref = _ref["b"]) != null ? 159 | (_ref = _ref[0]) != null ? 160 | _ref[c + d] : 161 | _ref : 162 | _ref : 163 | _ref; 164 | `); 165 | }); 166 | 167 | it('throws on bracket notation call expressions', () => { 168 | expect(` 169 | const idx = require('idx'); 170 | idx(base, _ => _["b"](...foo)()[0][c + d](bar, null, [...baz])); 171 | `).toThrowTransformError( 172 | 'idx callbacks may only access properties on the callback parameter.', 173 | ); 174 | }); 175 | 176 | it('transforms combination of both member access notations', () => { 177 | expect(` 178 | const idx = require('idx'); 179 | idx(base, _ => _.a["b"].c[d[e[f]]].g); 180 | `).toTransformInto(` 181 | var _ref; 182 | (_ref = base) != null ? 183 | (_ref = _ref.a) != null ? 184 | (_ref = _ref["b"]) != null ? 185 | (_ref = _ref.c) != null ? 186 | (_ref = _ref[d[e[f]]]) != null ? 187 | _ref.g : 188 | _ref : 189 | _ref : 190 | _ref : 191 | _ref : 192 | _ref; 193 | `); 194 | }); 195 | 196 | it('transforms if the base is an expression', () => { 197 | expect(` 198 | const idx = require('idx'); 199 | idx(this.props.base[5], _ => _.property); 200 | `).toTransformInto(` 201 | var _ref; 202 | (_ref = this.props.base[5]) != null ? 203 | _ref.property : 204 | _ref; 205 | `); 206 | }); 207 | 208 | it('throws if the arrow function has more than one param', () => { 209 | expect(` 210 | const idx = require('idx'); 211 | idx(base, (a, b) => _.property); 212 | `).toThrowTransformError( 213 | 'The arrow function supplied to `idx` must take exactly one parameter.', 214 | ); 215 | }); 216 | 217 | it('throws if the arrow function has an invalid base', () => { 218 | expect(` 219 | const idx = require('idx'); 220 | idx(base, a => b.property) 221 | `).toThrowTransformError( 222 | 'The parameter of the arrow function supplied to `idx` must match the ' + 223 | 'base of the body expression.', 224 | ); 225 | }); 226 | 227 | it('throws if the arrow function expression has non-properties/methods', () => { 228 | expect(` 229 | const idx = require('idx'); 230 | idx(base, _ => (_.a++).b.c); 231 | `).toThrowTransformError( 232 | 'idx callbacks may only access properties on the callback parameter.', 233 | ); 234 | }); 235 | 236 | it('throws if the body of the arrow function is not an expression', () => { 237 | expect(` 238 | const idx = require('idx'); 239 | idx(base, _ => {}) 240 | `).toThrowTransformError( 241 | 'The body of the arrow function supplied to `idx` must be a single ' + 242 | 'expression (without curly braces).', 243 | ); 244 | }); 245 | 246 | it('ignores non-function call idx', () => { 247 | expect(` 248 | const idx = require('idx'); 249 | result = idx; 250 | `).toTransformInto(` 251 | const idx = require('idx'); 252 | result = idx; 253 | `); 254 | }); 255 | 256 | it('throws if idx is called with zero arguments', () => { 257 | expect(` 258 | const idx = require('idx'); 259 | idx(); 260 | `).toThrowTransformError('The `idx` function takes exactly two arguments.'); 261 | }); 262 | 263 | it('throws if idx is called with one argument', () => { 264 | expect(` 265 | const idx = require('idx'); 266 | idx(1); 267 | `).toThrowTransformError('The `idx` function takes exactly two arguments.'); 268 | }); 269 | 270 | it('throws if idx is called with three arguments', () => { 271 | expect(` 272 | const idx = require('idx'); 273 | idx(1, 2, 3); 274 | `).toThrowTransformError('The `idx` function takes exactly two arguments.'); 275 | }); 276 | 277 | it('transforms idx calls as part of another expressions', () => { 278 | expect(` 279 | const idx = require('idx'); 280 | paddingStatement(); 281 | a = idx(base, _ => _.b[c]); 282 | `).toTransformInto(` 283 | var _ref; 284 | paddingStatement(); 285 | a = 286 | (_ref = base) != null ? 287 | (_ref = _ref.b) != null ? 288 | _ref[c] : 289 | _ref : 290 | _ref; 291 | `); 292 | }); 293 | 294 | it('transforms nested idx calls', () => { 295 | expect(` 296 | const idx = require('idx'); 297 | idx( 298 | idx( 299 | idx(base, _ => _.a.b), 300 | _ => _.c.d 301 | ), 302 | _ => _.e.f 303 | ); 304 | `).toTransformInto(` 305 | var _ref, _ref2, _ref3; 306 | (_ref3 = 307 | (_ref2 = 308 | (_ref = base) != null 309 | ? (_ref = _ref.a) != null 310 | ? _ref.b 311 | : _ref 312 | : _ref) != null 313 | ? (_ref2 = _ref2.c) != null 314 | ? _ref2.d 315 | : _ref2 316 | : _ref2) != null 317 | ? (_ref3 = _ref3.e) != null 318 | ? _ref3.f 319 | : _ref3 320 | : _ref3; 321 | `); 322 | }); 323 | 324 | it('transforms idx calls inside async functions (plugin order #1)', () => { 325 | expect({ 326 | plugins: [babelPluginIdx, transformAsyncToGenerator], 327 | code: ` 328 | const idx = require('idx'); 329 | async function f() { 330 | idx(base, _ => _.b.c.d.e); 331 | } 332 | `, 333 | }).toTransformInto(` 334 | ${asyncToGeneratorHelperCode} 335 | 336 | function f() { 337 | return _f.apply(this, arguments); 338 | } 339 | 340 | function _f() { 341 | _f = _asyncToGenerator(function*() { 342 | var _ref; 343 | 344 | (_ref = base) != null 345 | ? (_ref = _ref.b) != null 346 | ? (_ref = _ref.c) != null 347 | ? (_ref = _ref.d) != null 348 | ? _ref.e 349 | : _ref 350 | : _ref 351 | : _ref 352 | : _ref; 353 | }); 354 | return _f.apply(this, arguments); 355 | } 356 | `); 357 | }); 358 | 359 | it('transforms idx calls inside async functions (plugin order #2)', () => { 360 | expect({ 361 | plugins: [transformAsyncToGenerator, babelPluginIdx], 362 | code: ` 363 | const idx = require('idx'); 364 | async function f() { 365 | idx(base, _ => _.b.c.d.e); 366 | } 367 | `, 368 | }).toTransformInto(` 369 | ${asyncToGeneratorHelperCode} 370 | 371 | function f() { 372 | return _f.apply(this, arguments); 373 | } 374 | 375 | function _f() { 376 | _f = _asyncToGenerator(function*() { 377 | var _ref; 378 | 379 | (_ref = base) != null 380 | ? (_ref = _ref.b) != null 381 | ? (_ref = _ref.c) != null 382 | ? (_ref = _ref.d) != null 383 | ? _ref.e 384 | : _ref 385 | : _ref 386 | : _ref 387 | : _ref; 388 | }); 389 | return _f.apply(this, arguments); 390 | } 391 | `); 392 | }); 393 | 394 | it('transforms idx calls in async methods', () => { 395 | expect({ 396 | plugins: [transformAsyncToGenerator, babelPluginIdx], 397 | code: ` 398 | const idx = require('idx'); 399 | class Foo { 400 | async bar() { 401 | idx(base, _ => _.b); 402 | return this; 403 | } 404 | } 405 | `, 406 | }).toTransformInto(` 407 | ${asyncToGeneratorHelperCode} 408 | 409 | class Foo { 410 | bar() { 411 | var _this = this; 412 | return _asyncToGenerator(function* () { 413 | var _ref; 414 | (_ref = base) != null ? _ref.b : _ref; 415 | return _this; 416 | })(); 417 | } 418 | } 419 | `); 420 | }); 421 | 422 | it('transforms idx calls when an idx import binding is in scope', () => { 423 | expect(` 424 | import idx from 'idx'; 425 | idx(base, _ => _.b); 426 | `).toTransformInto(` 427 | var _ref; 428 | (_ref = base) != null ? _ref.b : _ref; 429 | `); 430 | }); 431 | 432 | it('transforms idx calls when an idx const binding is in scope', () => { 433 | expect(` 434 | const idx = require('idx'); 435 | idx(base, _ => _.b); 436 | `).toTransformInto(` 437 | var _ref; 438 | (_ref = base) != null ? _ref.b : _ref; 439 | `); 440 | }); 441 | 442 | it('transforms deep idx calls when an idx import binding is in scope', () => { 443 | expect(` 444 | import idx from 'idx'; 445 | function f() { 446 | idx(base, _ => _.b); 447 | } 448 | `).toTransformInto(` 449 | function f() { 450 | var _ref; 451 | (_ref = base) != null ? _ref.b : _ref; 452 | } 453 | `); 454 | }); 455 | 456 | it('transforms deep idx calls when an idx const binding is in scope', () => { 457 | expect(` 458 | const idx = require('idx'); 459 | function f() { 460 | idx(base, _ => _.b); 461 | } 462 | `).toTransformInto(` 463 | function f() { 464 | var _ref; 465 | (_ref = base) != null ? _ref.b : _ref; 466 | } 467 | `); 468 | }); 469 | 470 | it('throws on base call expressions', () => { 471 | expect(` 472 | const idx = require('idx'); 473 | idx(base, _ => _().b.c); 474 | `).toThrowTransformError( 475 | 'idx callbacks may only access properties on the callback parameter.', 476 | ); 477 | }); 478 | 479 | it('transforms when the idx parent is a scope creating expression', () => { 480 | expect(` 481 | const idx = require('idx'); 482 | (() => idx(base, _ => _.b)); 483 | `).toTransformInto(` 484 | () => { 485 | var _ref; 486 | return (_ref = base) != null ? _ref.b : _ref; 487 | }; 488 | `); 489 | }); 490 | 491 | it('throws if redefined before use', () => { 492 | expect(` 493 | let idx = require('idx'); 494 | idx = null; 495 | idx(base, _ => _.b); 496 | `).toThrowTransformError('`idx` cannot be redefined.'); 497 | }); 498 | 499 | it('throws if redefined after use', () => { 500 | expect(` 501 | let idx = require('idx'); 502 | idx(base, _ => _.b); 503 | idx = null; 504 | `).toThrowTransformError('`idx` cannot be redefined.'); 505 | }); 506 | 507 | it('handles sibling scopes with unique idx', () => { 508 | expect(` 509 | function aaa() { 510 | const idx = require('idx'); 511 | idx(base, _ => _.b); 512 | } 513 | function bbb() { 514 | const idx = require('idx'); 515 | idx(base, _ => _.b); 516 | } 517 | `).toTransformInto(` 518 | function aaa() { 519 | var _ref; 520 | (_ref = base) != null ? _ref.b : _ref; 521 | } 522 | function bbb() { 523 | var _ref2; 524 | (_ref2 = base) != null ? _ref2.b : _ref2; 525 | } 526 | `); 527 | }); 528 | 529 | it('handles sibling scopes with and without idx', () => { 530 | expect(` 531 | function aaa() { 532 | const idx = require('idx'); 533 | idx(base, _ => _.b); 534 | } 535 | function bbb() { 536 | idx(base, _ => _.b); 537 | } 538 | `).toTransformInto(` 539 | function aaa() { 540 | var _ref; 541 | (_ref = base) != null ? _ref.b : _ref; 542 | } 543 | function bbb() { 544 | idx(base, _ => _.b); 545 | } 546 | `); 547 | }); 548 | 549 | it('handles nested scopes with shadowing', () => { 550 | expect(` 551 | const idx = require('idx'); 552 | idx(base, _ => _.b); 553 | function aaa() { 554 | idx(base, _ => _.b); 555 | function bbb(idx) { 556 | idx(base, _ => _.b); 557 | } 558 | } 559 | `).toTransformInto(` 560 | var _ref2; 561 | (_ref2 = base) != null ? _ref2.b : _ref2; 562 | function aaa() { 563 | var _ref; 564 | (_ref = base) != null ? _ref.b : _ref; 565 | function bbb(idx) { 566 | idx(base, _ => _.b); 567 | } 568 | } 569 | `); 570 | }); 571 | 572 | it('throws on type imports', () => { 573 | expect({ 574 | plugins: [babelPluginIdx, syntaxFlow], 575 | code: ` 576 | import type idx from 'idx'; 577 | idx(base, _ => _.b); 578 | `, 579 | }).toThrowTransformError('The idx import must be a value import.'); 580 | }); 581 | 582 | it('throws on typeof imports', () => { 583 | expect({ 584 | plugins: [babelPluginIdx, syntaxFlow], 585 | code: ` 586 | import typeof idx from 'idx'; 587 | idx(base, _ => _.b); 588 | `, 589 | }).toThrowTransformError('The idx import must be a value import.'); 590 | }); 591 | 592 | it('throws on type import specifier', () => { 593 | expect({ 594 | plugins: [babelPluginIdx, syntaxFlow], 595 | code: ` 596 | import {type idx} from 'idx'; 597 | idx(base, _ => _.b); 598 | `, 599 | }).toThrowTransformError('The idx import must be a default import.'); 600 | }); 601 | 602 | it('throws on typeof import specifier', () => { 603 | expect({ 604 | plugins: [babelPluginIdx, syntaxFlow], 605 | code: ` 606 | import {typeof idx} from 'idx'; 607 | idx(base, _ => _.b); 608 | `, 609 | }).toThrowTransformError('The idx import must be a default import.'); 610 | }); 611 | 612 | it('throws on type default import specifier', () => { 613 | expect({ 614 | plugins: [babelPluginIdx, syntaxFlow], 615 | code: ` 616 | import {type default as idx} from 'idx'; 617 | idx(base, _ => _.b); 618 | `, 619 | }).toThrowTransformError('The idx import must be a value import.'); 620 | }); 621 | 622 | it('throws on typeof default import specifier', () => { 623 | expect({ 624 | plugins: [babelPluginIdx, syntaxFlow], 625 | code: ` 626 | import {typeof default as idx} from 'idx'; 627 | idx(base, _ => _.b); 628 | `, 629 | }).toThrowTransformError('The idx import must be a value import.'); 630 | }); 631 | 632 | it('throws on named idx import', () => { 633 | expect(` 634 | import {idx} from 'idx'; 635 | idx(base, _ => _.b); 636 | `).toThrowTransformError('The idx import must be a default import.'); 637 | }); 638 | 639 | it('throws on namespace idx import', () => { 640 | expect(` 641 | import * as idx from 'idx'; 642 | idx(base, _ => _.b); 643 | `).toThrowTransformError('The idx import must be a default import.'); 644 | }); 645 | 646 | it('throws on default plus named import', () => { 647 | expect(` 648 | import idx, {foo} from 'idx'; 649 | idx(base, _ => _.b); 650 | `).toThrowTransformError('The idx import must be a single specifier.'); 651 | }); 652 | 653 | it('throws on default plus namespace import', () => { 654 | expect(` 655 | import idx, * as foo from 'idx'; 656 | idx(base, _ => _.b); 657 | `).toThrowTransformError('The idx import must be a single specifier.'); 658 | }); 659 | 660 | it('throws on named default plus other import', () => { 661 | expect(` 662 | import {default as idx, foo} from 'idx'; 663 | idx(base, _ => _.b); 664 | `).toThrowTransformError('The idx import must be a single specifier.'); 665 | }); 666 | 667 | it('handles named default imports', () => { 668 | expect(` 669 | import {default as idx} from 'idx'; 670 | idx(base, _ => _.b); 671 | `).toTransformInto(` 672 | var _ref; 673 | (_ref = base) != null ? _ref.b : _ref; 674 | `); 675 | }); 676 | 677 | it('unused idx import should be left alone', () => { 678 | expect(` 679 | import idx from 'idx'; 680 | `).toTransformInto(` 681 | import idx from 'idx'; 682 | `); 683 | }); 684 | 685 | it('allows configuration of the import name', () => { 686 | expect({ 687 | code: ` 688 | import i_d_x from 'i_d_x'; 689 | i_d_x(base, _ => _.b); 690 | `, 691 | options: {importName: 'i_d_x'}, 692 | }).toTransformInto(` 693 | var _ref; 694 | (_ref = base) != null ? _ref.b : _ref; 695 | `); 696 | }); 697 | 698 | it('follows configuration of the import name', () => { 699 | expect({ 700 | code: ` 701 | import idx from 'idx'; 702 | import i_d_x from 'i_d_x'; 703 | i_d_x(base, _ => _.b); 704 | idx(base, _ => _.c); 705 | `, 706 | options: {importName: 'i_d_x'}, 707 | }).toTransformInto(` 708 | var _ref; 709 | import idx from 'idx'; 710 | (_ref = base) != null ? _ref.b : _ref; 711 | idx(base, _ => _.c); 712 | `); 713 | }); 714 | 715 | it('allows configuration of the require name', () => { 716 | expect({ 717 | code: ` 718 | const i_d_x = require('i_d_x'); 719 | i_d_x(base, _ => _.b); 720 | `, 721 | options: {importName: 'i_d_x'}, 722 | }).toTransformInto(` 723 | var _ref; 724 | (_ref = base) != null ? _ref.b : _ref; 725 | `); 726 | }); 727 | 728 | it('follows configuration of the require name', () => { 729 | expect({ 730 | code: ` 731 | const idx = require('idx'); 732 | const i_d_x = require('i_d_x'); 733 | i_d_x(base, _ => _.b); 734 | idx(base, _ => _.c); 735 | `, 736 | options: {importName: 'i_d_x'}, 737 | }).toTransformInto(` 738 | var _ref; 739 | const idx = require('idx'); 740 | (_ref = base) != null ? _ref.b : _ref; 741 | idx(base, _ => _.c); 742 | `); 743 | }); 744 | 745 | describe('functional', () => { 746 | it('works with only properties', () => { 747 | expect(` 748 | const idx = require('idx'); 749 | const base = {a: {b: {c: 2}}}; 750 | idx(base, _ => _.a.b.c); 751 | `).toReturn(2); 752 | }); 753 | 754 | it('works with missing properties', () => { 755 | expect(` 756 | const idx = require('idx'); 757 | const base = {a: {b: {}}}; 758 | idx(base, _ => _.a.b.c); 759 | `).toReturn(undefined); 760 | }); 761 | 762 | it('works with null properties', () => { 763 | expect(` 764 | const idx = require('idx'); 765 | const base = {a: {b: null}}; 766 | idx(base, _ => _.a.b.c); 767 | `).toReturn(null); 768 | }); 769 | 770 | it('works with nested idx calls', () => { 771 | expect(` 772 | const idx = require('idx'); 773 | const base = {a: {b: {c: {d: {e: {f: 2}}}}}}; 774 | idx( 775 | idx( 776 | idx(base, _ => _.a.b), 777 | _ => _.c.d 778 | ), 779 | _ => _.e.f 780 | ); 781 | `).toReturn(2); 782 | }); 783 | 784 | it('works with nested idx calls with missing properties', () => { 785 | expect(` 786 | const idx = require('idx'); 787 | const base = {a: {b: {c: null}}}; 788 | idx( 789 | idx( 790 | idx(base, _ => _.a.b), 791 | _ => _.c.d 792 | ), 793 | _ => _.e.f 794 | ); 795 | `).toReturn(null); 796 | }); 797 | }); 798 | }); 799 | -------------------------------------------------------------------------------- /packages/idx/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript" 5 | ], 6 | "plugins": ["@babel/transform-flow-strip-types"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/idx/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | conditional_type=true 11 | mapped_type=true 12 | 13 | [strict] 14 | -------------------------------------------------------------------------------- /packages/idx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "idx", 3 | "version": "3.0.3", 4 | "description": "Utility function for traversing properties on objects and arrays.", 5 | "main": "lib/idx.js", 6 | "types": "lib/idx.d.ts", 7 | "files": [ 8 | "README.md", 9 | "lib/" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/facebook/idx.git", 14 | "directory": "packages/idx" 15 | }, 16 | "license": "MIT", 17 | "scripts": { 18 | "build": "scripts/build.sh", 19 | "prepublish": "yarn run build && cp ../../{LICENSE,README.md} .", 20 | "test": "jest idx.test.js && yarn run tsc", 21 | "tsc": "tsc --noEmit --strict src/idx.test.ts", 22 | "upgrade": "yarn upgrade --latest" 23 | }, 24 | "devDependencies": { 25 | "@babel/cli": "^7.21.0", 26 | "@babel/core": "^7.21.4", 27 | "@babel/plugin-transform-flow-strip-types": "^7.21.0", 28 | "@babel/preset-env": "^7.21.4", 29 | "@babel/preset-typescript": "^7.21.4", 30 | "babel-jest": "^29.5.0", 31 | "flow-bin": "^0.211.0", 32 | "jest": "^29.5.0", 33 | "typescript": "^5.0.4" 34 | }, 35 | "jest": { 36 | "testEnvironment": "node", 37 | "rootDir": "src" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/idx/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) Facebook, Inc. and its affiliates. 3 | # 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | 7 | # 8 | # Build script for `idx`. Run using `npm run build`. 9 | # 10 | 11 | set -e 12 | set -u 13 | 14 | ROOT_DIR=$(dirname "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)") 15 | 16 | rm -rf "${ROOT_DIR:?}/lib" 17 | babel "$ROOT_DIR/src" --out-dir "$ROOT_DIR/lib" --copy-files 18 | rm "$ROOT_DIR"/lib/idx.test.{js,ts} 19 | 20 | # Strip `@providesModule` from lib/**/*.js. 21 | find "$ROOT_DIR/lib" -type f -name '*.js' -exec \ 22 | sed -i '' '/^ \* @providesModule /d' {} \; 23 | -------------------------------------------------------------------------------- /packages/idx/src/idx.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | /** 9 | * DeepRequiredArray 10 | * Nested array condition handler 11 | */ 12 | interface DeepRequiredArray extends Array>> {} 13 | 14 | /** 15 | * Return type of calling `idx()`. `idx` always returns an optional value 16 | * @template T Value can be null or undefined 17 | */ 18 | export type IDXOptional = T | null | undefined; 19 | 20 | /** 21 | * DeepRequiredObject 22 | * Nested object condition handler 23 | */ 24 | type DeepRequiredObject = { 25 | [P in keyof T]-?: DeepRequired>; 26 | }; 27 | 28 | /** 29 | * Function that has deeply required return type 30 | */ 31 | type FunctionWithRequiredReturnType< 32 | T extends (...args: any[]) => any 33 | > = T extends (...args: infer A) => infer R 34 | ? (...args: A) => DeepRequired 35 | : never; 36 | 37 | /** 38 | * DeepRequired 39 | * Required that works for deeply nested structure 40 | */ 41 | type DeepRequired = T extends any[] 42 | ? DeepRequiredArray 43 | : T extends (...args: any[]) => any 44 | ? FunctionWithRequiredReturnType 45 | : T extends object 46 | ? DeepRequiredObject 47 | : NonNullable; 48 | 49 | /** 50 | * UnboxDeepRequired 51 | * Unbox type wrapped with DeepRequired 52 | */ 53 | type UnboxDeepRequired = T extends DeepRequired ? R : T; 54 | 55 | /** 56 | * Traverses properties on objects and arrays. If an intermediate property is 57 | * either null or undefined, it is instead returned. The purpose of this method 58 | * is to simplify extracting properties from a chain of maybe-typed properties. 59 | * 60 | * Consider the following type: 61 | * 62 | * const props: { 63 | * user?: { 64 | * name: string, 65 | * friends?: Array, 66 | * } 67 | * }; 68 | * 69 | * Getting to the friends of my first friend would resemble: 70 | * 71 | * props.user && 72 | * props.user.friends && 73 | * props.user.friends[0] && 74 | * props.user.friends[0].friends 75 | * 76 | * Instead, `idx` allows us to safely write: 77 | * 78 | * idx(props, _ => _.user.friends[0].friends) 79 | * 80 | * The second argument must be a function that returns one or more nested member 81 | * expressions. Any other expression has undefined behavior. 82 | * 83 | * @param prop - Parent object 84 | * @param accessor - Accessor function 85 | * @return the property accessed if accessor function could reach to property, 86 | * null or undefined otherwise 87 | */ 88 | declare function idx( 89 | prop: T1, 90 | accessor: (prop: NonNullable>) => T2, 91 | ): IDXOptional>; 92 | 93 | export default idx; 94 | -------------------------------------------------------------------------------- /packages/idx/src/idx.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @providesModule idx 8 | * @flow strict 9 | * @format 10 | */ 11 | 12 | 'use strict'; // eslint-disable-line strict 13 | 14 | /** 15 | * Traverses properties on objects and arrays. If an intermediate property is 16 | * either null or undefined, it is instead returned. The purpose of this method 17 | * is to simplify extracting properties from a chain of maybe-typed properties. 18 | * 19 | * === EXAMPLE === 20 | * 21 | * Consider the following type: 22 | * 23 | * const props: { 24 | * user: ?{ 25 | * name: string, 26 | * friends: ?Array, 27 | * } 28 | * }; 29 | * 30 | * Getting to the friends of my first friend would resemble: 31 | * 32 | * props.user && 33 | * props.user.friends && 34 | * props.user.friends[0] && 35 | * props.user.friends[0].friends 36 | * 37 | * Instead, `idx` allows us to safely write: 38 | * 39 | * idx(props, _ => _.user.friends[0].friends) 40 | * 41 | * The second argument must be a function that returns one or more nested member 42 | * expressions. Any other expression has undefined behavior. 43 | * 44 | * === NOTE === 45 | * 46 | * The code below exists for the purpose of illustrating expected behavior and 47 | * is not meant to be executed. The `idx` function is used in conjunction with a 48 | * Babel transform that replaces it with better performing code: 49 | * 50 | * props.user == null ? props.user : 51 | * props.user.friends == null ? props.user.friends : 52 | * props.user.friends[0] == null ? props.user.friends[0] : 53 | * props.user.friends[0].friends 54 | * 55 | * All this machinery exists due to the fact that an existential operator does 56 | * not currently exist in JavaScript. 57 | */ 58 | function idx(input: Ti, accessor: (input: Ti) => Tv): ?Tv { 59 | try { 60 | return accessor(input); 61 | } catch (error) { 62 | if (error instanceof TypeError) { 63 | if (nullPattern.test(error.message)) { 64 | return null; 65 | } else if (undefinedPattern.test(error.message)) { 66 | return undefined; 67 | } 68 | } 69 | throw error; 70 | } 71 | } 72 | 73 | /** 74 | * Some actual error messages for null: 75 | * 76 | * TypeError: Cannot read property 'bar' of null 77 | * TypeError: Cannot convert null value to object 78 | * TypeError: foo is null 79 | * TypeError: null has no properties 80 | * TypeError: null is not an object (evaluating 'foo.bar') 81 | * TypeError: null is not an object (evaluating '(" undefined ", null).bar') 82 | */ 83 | const nullPattern = /^null | null$|^[^(]* null /i; 84 | const undefinedPattern = /^undefined | undefined$|^[^(]* undefined /i; 85 | 86 | idx.default = idx; 87 | module.exports = idx; 88 | -------------------------------------------------------------------------------- /packages/idx/src/idx.js.flow: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow strict 8 | */ 9 | 10 | declare opaque type DeepRequiredArray<+T>: $ReadOnlyArray< 11 | DeepRequired<$NonMaybeType>, 12 | >; 13 | 14 | declare opaque type DeepRequiredObject<+T: interface {}>: Required<{ 15 | +[K in keyof T]: DeepRequired, 16 | }>; 17 | 18 | declare opaque type DeepRequired: T extends empty 19 | ? $FlowFixMe // If something can pass empty, it's already unsafe 20 | : T extends $ReadOnlyArray 21 | ? DeepRequiredArray 22 | : T extends (...$ReadOnlyArray) => mixed 23 | ? T 24 | : T extends interface {} 25 | ? DeepRequiredObject 26 | : $NonMaybeType; 27 | 28 | type UnboxDeepRequired = T extends DeepRequired ? V : T; 29 | 30 | /** 31 | * @see https://github.com/facebook/idx 32 | * 33 | * If you entered the file with the hope to understand why something doesn't 34 | * type check, you should stop, and migrate your code away from idx first. 35 | * idx is deprecated, and you should always use optional chaining instead. 36 | */ 37 | declare module.exports: ( 38 | prop: T1, 39 | accessor: (prop: $NonMaybeType>) => T2, 40 | ) => ?UnboxDeepRequired; 41 | -------------------------------------------------------------------------------- /packages/idx/src/idx.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | 'use strict'; // eslint-disable-line strict 11 | 12 | jest.unmock('./idx'); 13 | 14 | const idx = require('./idx'); 15 | 16 | describe('idx', () => { 17 | it('returns properties that exist', () => { 18 | const a = {b: {c: 123}}; 19 | expect(idx(a, _ => _.b.c)).toEqual(123); 20 | }); 21 | 22 | it('throws non-"property access" errors', () => { 23 | const error = new Error('Expected error.'); 24 | const a = {}; 25 | Object.defineProperty(a, 'b', { 26 | get() { 27 | throw error; 28 | }, 29 | }); 30 | expect(() => idx(a, _ => _.b.c)).toThrow(error); 31 | }); 32 | 33 | it('throws a `TypeError` when calling non-methods', () => { 34 | const a = {b: 'I am a string'}; 35 | expect(() => idx(a, _ => _.b())).toThrow( 36 | new TypeError('_.b is not a function'), 37 | ); 38 | }); 39 | 40 | it('returns null for intermediate null properties', () => { 41 | const a = {b: null}; 42 | expect(idx(a, _ => _.b.c)).toEqual(null); 43 | }); 44 | 45 | it('returns undefined for intermediate undefined properties', () => { 46 | const a = {b: undefined}; 47 | expect(idx(a, _ => _.b.c)).toEqual(undefined); 48 | }); 49 | 50 | it('returns undefined for intermediate undefined array indexes', () => { 51 | const a = {b: []}; 52 | expect(idx(a, _ => _.b[0].c)).toEqual(undefined); 53 | }); 54 | 55 | it('returns null for error in capital case', () => { 56 | const error = new TypeError('b is NULL'); 57 | const a = {}; 58 | Object.defineProperty(a, 'b', { 59 | get() { 60 | throw error; 61 | }, 62 | }); 63 | expect(idx(a, _ => _.b.c)).toEqual(null); 64 | }); 65 | 66 | it('returns undefined for error in capital case', () => { 67 | const error = new TypeError('b is UNDEFINED'); 68 | const a = {}; 69 | Object.defineProperty(a, 'b', { 70 | get() { 71 | throw error; 72 | }, 73 | }); 74 | expect(idx(a, _ => _.b.c)).toEqual(undefined); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/idx/src/idx.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import idx, {IDXOptional} from './idx'; 9 | 10 | /** 11 | * Test functions are not run in runtime. They are only type checked with 12 | * TypeScript compiler 13 | */ 14 | declare function it(description: string, test: () => void): void; 15 | 16 | interface Item { 17 | t?: T; 18 | inner?: { 19 | item?: string; 20 | }; 21 | } 22 | 23 | interface MethodReturnType { 24 | optional?: {member?: Item}; 25 | } 26 | 27 | interface DeepStructure { 28 | str?: string; 29 | undef?: undefined; 30 | null?: null; 31 | generic?: T; 32 | arr?: {inner?: string}[]; 33 | foo?: { 34 | bar?: { 35 | baz?: { 36 | arr?: Item[]; 37 | }; 38 | }; 39 | }; 40 | requiredInner?: { 41 | inner: boolean; 42 | }; 43 | method?(): MethodReturnType; 44 | args?(a: string, b: number, c?: boolean): Item; 45 | requiredReturnType?(): {inner: number}; 46 | } 47 | 48 | let deep: DeepStructure = {}; 49 | 50 | it('can access deep properties without null type assertion', () => { 51 | let str: IDXOptional = idx(deep, _ => _.str); 52 | let undef: undefined | null = idx(deep, _ => _.undef); 53 | let null_: undefined | null = idx(deep, _ => _.null); 54 | let arr: IDXOptional = idx(deep, _ => _.foo.bar.baz.arr); 55 | }); 56 | 57 | it('can call deep methods without null type assertion', () => { 58 | let member: IDXOptional = idx(deep, _ => _.method().optional.member); 59 | member = idx(deep, _ => _.args('', 1, true)); 60 | member = idx(deep, _ => _.args('', 1)); 61 | }); 62 | 63 | it('can tap into optional structures (array and objects)', () => { 64 | let str: IDXOptional = idx( 65 | deep, 66 | _ => _.foo.bar.baz.arr[0].inner.item, 67 | ); 68 | }); 69 | 70 | it('returns optional object while maintaining the original type of the object structure', () => { 71 | let req = idx(deep, _ => _.requiredInner); 72 | if (req) { 73 | req.inner.valueOf(); // can safely call because inner is not optional 74 | } 75 | }); 76 | 77 | it('returns optional array while maintaining the original type of array item type', () => { 78 | // inner property type did not become `string | null | undefined` 79 | let arr: IDXOptional> = idx(deep, _ => _.arr); 80 | }); 81 | 82 | it('maintains the return type of method calls', () => { 83 | let req = idx(deep, _ => _.requiredReturnType()); 84 | if (req) { 85 | req.inner.toFixed(); // can safely call because inner is not optional 86 | } 87 | }); 88 | 89 | it('can unbox enums', () => { 90 | enum Enum { 91 | ONE = 'ONE', 92 | } 93 | type WithEnum = { 94 | foo?: { 95 | enum?: Enum; 96 | }; 97 | }; 98 | 99 | let e: IDXOptional = idx({} as WithEnum, _ => _.foo.enum); 100 | }); 101 | 102 | it('can unbox function', () => { 103 | const returnValue = idx(deep, _ => _.method()); 104 | const control: MethodReturnType = {optional: {}}; 105 | const treatment: typeof returnValue = {optional: {}}; 106 | }); 107 | --------------------------------------------------------------------------------