├── .gitignore ├── packages ├── ts-optchain.macro │ ├── macro.d.ts │ ├── .gitignore │ ├── jest.config.js │ ├── __tests__ │ │ ├── entry.macro.js │ │ └── macro.test.ts │ ├── .prettierrc │ ├── README.md │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── package.json │ └── src │ │ └── macro.ts └── babel-plugin-ts-optchain │ ├── .gitignore │ ├── jest.config.js │ ├── tsconfig.json │ ├── .prettierrc │ ├── README.md │ ├── src │ ├── runtime.ts │ └── plugin.ts │ ├── tsconfig.build.json │ ├── package.json │ ├── __tests__ │ ├── runtime.test.ts │ ├── upstream-ts-optchain.test.js │ ├── upstream-ts-optchain-tests.source │ └── plugin.test.ts │ └── tools │ └── compile-upstream-tests.ts ├── .vscode ├── settings.json └── launch.json ├── lerna.json ├── tsconfig.json ├── CONTRIBUTING.md ├── jest.config.js ├── tsconfig.base.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /packages/ts-optchain.macro/macro.d.ts: -------------------------------------------------------------------------------- 1 | export {oc} from "ts-optchain"; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /packages/ts-optchain.macro/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | lib 4 | package-lock.json -------------------------------------------------------------------------------- /packages/ts-optchain.macro/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("../../jest.config"); 2 | -------------------------------------------------------------------------------- /packages/ts-optchain.macro/__tests__/entry.macro.js: -------------------------------------------------------------------------------- 1 | module.exports = require("../src/macro"); 2 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | lib 4 | package-lock.json -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("../../jest.config"); 2 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json" 3 | } 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "npm", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "1.1.5" 7 | } 8 | -------------------------------------------------------------------------------- /packages/ts-optchain.macro/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "trailingComma": "all", 4 | "tabWidth": 4 5 | } 6 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "trailingComma": "all", 4 | "tabWidth": 4 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "references": [ 4 | { "path": "packages/babel-plugin-ts-optchain" }, 5 | { "path": "packages/ts-optchain.macro" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/ts-optchain.macro/README.md: -------------------------------------------------------------------------------- 1 | # ts-optchain.macro 2 | 3 | Babel Macro for [ts-optchain][]. See the main [README][] for documentation. 4 | 5 | [ts-optchain]: https://github.com/rimeto/ts-optchain 6 | [readme]: https://github.com/epeli/babel-plugin-ts-optchain 7 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-ts-optchain 2 | 3 | Babel plugin for [ts-optchain][]. See the main [README][] for documentation. 4 | 5 | [ts-optchain]: https://github.com/rimeto/ts-optchain 6 | [readme]: https://github.com/epeli/babel-plugin-ts-optchain 7 | -------------------------------------------------------------------------------- /packages/ts-optchain.macro/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "babel-plugin-ts-optchain": ["../babel-plugin-ts-optchain"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/src/runtime.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inspired by 3 | * https://github.com/burakcan/mb 4 | */ 5 | export function oc(data: any, path: string[], defaultValue?: any) { 6 | path.map(key => (data = (data || {})[key])); 7 | return data == null ? defaultValue : data; 8 | } 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Install lerna 4 | 5 | npm i -g lerna 6 | 7 | Install shared dev deps 8 | 9 | npm i 10 | 11 | Install package deps and link them 12 | 13 | lerna bootstrap 14 | 15 | Build packages 16 | 17 | lerna run build 18 | 19 | Run tests 20 | 21 | lerna run test 22 | -------------------------------------------------------------------------------- /packages/ts-optchain.macro/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["__dtslint__"], 4 | "compilerOptions": { 5 | "sourceMap": true, 6 | "noEmit": false, 7 | "outDir": "./build", 8 | "declaration": true, 9 | "declarationDir": "./build" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["__dtslint__"], 4 | "compilerOptions": { 5 | "sourceMap": true, 6 | "noEmit": false, 7 | "outDir": "./build", 8 | "declaration": true, 9 | "declarationDir": "./build" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["ts", "tsx", "js"], 3 | transform: { 4 | "^.+\\.(ts|tsx)$": "ts-jest", 5 | }, 6 | globals: { 7 | "ts-jest": { 8 | tsConfig: "tsconfig.json", 9 | }, 10 | }, 11 | testMatch: ["**/?(*.)+(spec|test).(ts|js)?(x)"], 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noEmit": true, 6 | "jsx": "react", 7 | "lib": ["esnext", "dom"], 8 | "moduleResolution": "node", 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "esModuleInterop": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "devDependencies": { 5 | "lerna": "^3.13.1", 6 | "@babel/core": "^7.4.0", 7 | "@babel/preset-env": "^7.4.2", 8 | "@babel/preset-typescript": "^7.3.3", 9 | "@babel/traverse": "^7.4.0", 10 | "@babel/types": "^7.4.0", 11 | "@types/babel__traverse": "^7.0.6", 12 | "@types/dedent": "^0.7.0", 13 | "@types/jest": "^24.0.11", 14 | "babel-plugin-macros": "^2.5.1", 15 | "dedent": "^0.7.0", 16 | "jest": "^24.5.0", 17 | "prettier": "^1.16.4", 18 | "ts-jest": "^24.0.1", 19 | "ts-node": "^8.0.3", 20 | "typescript": "^3.4.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/ts-optchain.macro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-optchain.macro", 3 | "version": "1.1.5", 4 | "description": "Babel Macro for ts-optchain", 5 | "main": "lib/macro.js", 6 | "types": "macro.d.ts", 7 | "keywords": [ 8 | "babel-plugin-macros", 9 | "typescript" 10 | ], 11 | "files": [ 12 | "macro.d.ts", 13 | "lib" 14 | ], 15 | "repository": { 16 | "url": "https://github.com/epeli/babel-plugin-ts-optchain" 17 | }, 18 | "scripts": { 19 | "build": "rm -rf build && tsc --project tsconfig.build.json && rm -rf lib && mv build/src lib && rm -rf build", 20 | "test": "jest" 21 | }, 22 | "author": "", 23 | "license": "ISC", 24 | "dependencies": { 25 | "babel-plugin-ts-optchain": "^1.1.5", 26 | "ts-optchain": "^0.1.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-ts-optchain", 3 | "version": "1.1.5", 4 | "description": "Babel Plugin for ts-optchain", 5 | "keywords": [ 6 | "babel-plugin", 7 | "typescript" 8 | ], 9 | "main": "lib/plugin.js", 10 | "types": "lib/plugin.d.ts", 11 | "files": [ 12 | "lib" 13 | ], 14 | "repository": { 15 | "url": "https://github.com/epeli/babel-plugin-ts-optchain" 16 | }, 17 | "scripts": { 18 | "compile-upstream-tests": "ts-node tools/compile-upstream-tests.ts > __tests__/upstream-ts-optchain.test.js", 19 | "build": "rm -rf build && tsc --project tsconfig.build.json && rm -rf lib && mv build/src lib && rm -rf build", 20 | "test": "npm run compile-upstream-tests && jest" 21 | }, 22 | "author": "", 23 | "license": "ISC" 24 | } 25 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/__tests__/runtime.test.ts: -------------------------------------------------------------------------------- 1 | import {oc} from "../src/runtime"; 2 | 3 | test("runtime oc can access path", () => { 4 | const data = { 5 | foo: 1, 6 | }; 7 | 8 | expect(oc(data, ["foo"])).toEqual(1); 9 | }); 10 | 11 | test("runtime oc handles missing data", () => { 12 | const data = { 13 | foo: 1, 14 | }; 15 | 16 | expect(oc(data, ["foo", "bar"])).toBeUndefined(); 17 | }); 18 | 19 | test("runtime oc can have default", () => { 20 | const data = { 21 | foo: 1, 22 | }; 23 | 24 | expect(oc(data, ["foo", "bar"], "default")).toBe("default"); 25 | }); 26 | 27 | test("runtime oc defaults handles falsy values", () => { 28 | const data = { 29 | foo: { 30 | bar: 0, 31 | }, 32 | }; 33 | 34 | expect(oc(data, ["foo", "bar"], "default")).toBe(0); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/tools/compile-upstream-tests.ts: -------------------------------------------------------------------------------- 1 | import {transform} from "@babel/core"; 2 | import fs from "fs"; 3 | 4 | function main() { 5 | const source = fs 6 | .readFileSync("__tests__/upstream-ts-optchain-tests.source") 7 | .toString(); 8 | const res = transform(source, { 9 | babelrc: false, 10 | filename: "test.ts", 11 | presets: [ 12 | "@babel/preset-typescript", 13 | [ 14 | "@babel/preset-env", 15 | { 16 | targets: {node: "current"}, 17 | }, 18 | ], 19 | ], 20 | plugins: [ 21 | [ 22 | __dirname + "/../src/plugin.ts", 23 | { 24 | target: "../index", 25 | runtime: "../src/runtime", 26 | }, 27 | ], 28 | ], 29 | }); 30 | 31 | if (!res) { 32 | throw new Error("plugin failed"); 33 | } 34 | 35 | console.log(res.code); 36 | } 37 | 38 | main(); 39 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Jest: Plugin", 8 | "program": "${workspaceFolder}/node_modules/.bin/jest", 9 | "windows": { 10 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 11 | }, 12 | "args": ["--runInBand", "packages/babel-plugin-ts-optchain/__tests__/"], 13 | "console": "internalConsole", 14 | "internalConsoleOptions": "openOnSessionStart" 15 | }, 16 | { 17 | "type": "node", 18 | "request": "launch", 19 | "name": "Jest: Macro", 20 | "program": "${workspaceFolder}/node_modules/.bin/jest", 21 | "cwd": "${workspaceFolder}/packages/ts-optchain.macro/", 22 | "windows": { 23 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 24 | }, 25 | "args": ["--runInBand", "packages/ts-optchain.macro/__tests__/"], 26 | "console": "internalConsole", 27 | "internalConsoleOptions": "openOnSessionStart" 28 | }, 29 | { 30 | "type": "node", 31 | "request": "launch", 32 | "name": "Jest: Current File", 33 | "program": "${workspaceFolder}/node_modules/.bin/jest", 34 | "windows": { 35 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 36 | }, 37 | "args": ["${relativeFile}"], 38 | "console": "internalConsole", 39 | "internalConsoleOptions": "openOnSessionStart" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-ts-optchain 2 | 3 | Babel plugin for transpiling legacy browser support to [ts-optchain][] by 4 | removing Proxy usage. 5 | 6 | Input: 7 | 8 | ```ts 9 | import { oc } from "ts-optchain"; 10 | oc(data).foo.bar("default"); 11 | ``` 12 | 13 | Output 14 | 15 | ```ts 16 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 17 | oc(data, ["foo", "bar"], "default"); 18 | ``` 19 | 20 | The `babel-plugin-ts-optchain/lib/runtime` is a tiny [mb][] like function. 21 | 22 | ## Install 23 | 24 | npm install babel-plugin-ts-optchain 25 | 26 | and add it to your `.babelrc` with `@babel/preset-typescript` 27 | 28 | ```json 29 | { 30 | "presets": ["@babel/preset-typescript"], 31 | "plugins": ["ts-optchain"] 32 | } 33 | ``` 34 | 35 | ## Babel Macro 36 | 37 | There's also a [Babel Macro](https://github.com/kentcdodds/babel-plugin-macros) variant for [Create React App](https://facebook.github.io/create-react-app/) and other users who cannot use or don't want to use custom Babel Plugins. 38 | 39 | Install the macro with 40 | 41 | npm install ts-optchain.macro 42 | 43 | Then just in your code import `oc` from `ts-optchain.macro` instead of 44 | `ts-optchain`. There's no need to install `ts-optchain` separately. 45 | 46 | ```ts 47 | import { oc } from "ts-optchain.macro"; 48 | oc(data).foo.bar("default"); 49 | ``` 50 | 51 | ## Limitations 52 | 53 | You must call `oc()` in a single chain. Eg. this does not work: 54 | 55 | ```ts 56 | const x = oc(data); 57 | const bar = x.foo.bar(); 58 | ``` 59 | 60 | Write it like this 61 | 62 | ```ts 63 | const bar = oc(data).foo.bar(); 64 | ``` 65 | 66 | [ts-optchain]: https://github.com/rimeto/ts-optchain 67 | [mb]: https://github.com/burakcan/mb 68 | -------------------------------------------------------------------------------- /packages/ts-optchain.macro/src/macro.ts: -------------------------------------------------------------------------------- 1 | import {NodePath} from "@babel/traverse"; 2 | import * as BabelTypes from "@babel/types"; 3 | 4 | import { 5 | PluginOptions, 6 | Babel, 7 | transformOptchainCall, 8 | RUNTIME_IMPORT, 9 | } from "babel-plugin-ts-optchain"; 10 | 11 | const {createMacro} = require("babel-plugin-macros"); 12 | 13 | interface Macro { 14 | babel: Babel; 15 | state: PluginOptions; 16 | references: Record; 17 | } 18 | 19 | function findImportDeclaration( 20 | t: typeof BabelTypes, 21 | name: string, 22 | path: NodePath, 23 | ): BabelTypes.ImportDeclaration | undefined { 24 | const bindings = path.scope.bindings; 25 | 26 | const binding = bindings[name]; 27 | 28 | if (!binding) { 29 | return findImportDeclaration(t, name, path.scope.parent.path); 30 | } 31 | 32 | if (t.isImportDeclaration(binding.path.parentPath.node)) { 33 | return binding.path.parentPath.node; 34 | } 35 | } 36 | 37 | function transfromMacroImport( 38 | t: typeof BabelTypes, 39 | path: NodePath, 40 | ) { 41 | if (!t.isIdentifier(path.node.callee)) { 42 | return; 43 | } 44 | 45 | const name = path.node.callee.name; 46 | 47 | const importDecl = findImportDeclaration(t, name, path); 48 | 49 | if (importDecl) { 50 | importDecl.source.value = RUNTIME_IMPORT; 51 | } 52 | } 53 | 54 | export default createMacro(function tsOptChainMacro(macro: Macro) { 55 | const t = macro.babel.types; 56 | 57 | for (const key of Object.keys(macro.references)) { 58 | const paths = macro.references[key]; 59 | for (const path of paths) { 60 | if (t.isCallExpression(path.parent)) { 61 | const callPath = path.parentPath as NodePath< 62 | BabelTypes.CallExpression 63 | >; 64 | transfromMacroImport(t, callPath); 65 | transformOptchainCall(t, callPath); 66 | } 67 | } 68 | } 69 | 70 | return { 71 | keepImports: true, 72 | }; 73 | }); 74 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/__tests__/upstream-ts-optchain.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _runtime = require("../src/runtime"); 4 | 5 | /** 6 | * Copyright (C) 2018-present, Rimeto, LLC. 7 | * 8 | * This source code is licensed under the MIT license found in the 9 | * LICENSE file in the root directory of this source tree. 10 | */ 11 | describe("ts-optchain", () => { 12 | it("sanity checks", () => { 13 | const x = { 14 | a: "hello", 15 | b: { 16 | d: "world" 17 | }, 18 | c: [-100, 200, -300], 19 | d: null, 20 | e: { 21 | f: false 22 | } 23 | }; 24 | expect((0, _runtime.oc)(x, ["a"])).toEqual("hello"); 25 | expect((0, _runtime.oc)(x, ["b", "d"])).toEqual("world"); 26 | expect((0, _runtime.oc)(x, ["c", 0])).toEqual(-100); 27 | expect((0, _runtime.oc)(x, ["c", 100])).toBeUndefined(); 28 | expect((0, _runtime.oc)(x, ["c", 100], 1234)).toEqual(1234); 29 | expect((0, _runtime.oc)(x, ["d", "e"])).toBeUndefined(); 30 | expect((0, _runtime.oc)(x, ["d", "e"], "optional default value")).toEqual("optional default value"); 31 | expect((0, _runtime.oc)(x, ["e", "f"])).toEqual(false); 32 | expect((0, _runtime.oc)(x, ["y", "z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k"])).toBeUndefined(); 33 | }); 34 | it("optional chaining equivalence", () => { 35 | const x = { 36 | a: "hello", 37 | b: { 38 | d: "world" 39 | }, 40 | c: [{ 41 | u: { 42 | v: -100 43 | } 44 | }, { 45 | u: { 46 | v: 200 47 | } 48 | }, {}, { 49 | u: { 50 | v: -300 51 | } 52 | }] 53 | }; 54 | expect((0, _runtime.oc)(x, ["a"])).toEqual(x.a); 55 | expect((0, _runtime.oc)(x, ["b", "d"])).toEqual(x.b && x.b.d); 56 | expect((0, _runtime.oc)(x, ["c", 0, "u", "v"])).toEqual(x.c && x.c[0] && x.c[0].u && x.c[0].u.v); 57 | expect((0, _runtime.oc)(x, ["c", 100, "u", "v"])).toEqual(x.c && x.c[100] && x.c[100].u && x.c[100].u.v); 58 | expect((0, _runtime.oc)(x, ["c", 100, "u", "v"], 1234)).toEqual(x.c && x.c[100] && x.c[100].u && x.c[100].u.v || 1234); 59 | expect((0, _runtime.oc)(x, ["e", "f"])).toEqual(x.e && x.e.f); 60 | expect((0, _runtime.oc)(x, ["e", "f"], "optional default value")).toEqual(x.e && x.e.f || "optional default value"); 61 | expect((0, _runtime.oc)(x, ["e", "g"], () => "Yo Yo")()).toEqual((x.e && x.e.g || (() => "Yo Yo"))()); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /packages/ts-optchain.macro/__tests__/macro.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import {transform} from "@babel/core"; 3 | 4 | function runPlugin(code: string) { 5 | const res = transform(code, { 6 | babelrc: false, 7 | filename: "test.ts", 8 | root: __dirname, 9 | plugins: ["babel-plugin-macros"], 10 | }); 11 | 12 | if (!res) { 13 | throw new Error("plugin failed"); 14 | } 15 | 16 | return res; 17 | } 18 | 19 | test("oc can be imported from the macro", async () => { 20 | const code = dedent` 21 | import { oc } from "./__tests__/entry.macro"; 22 | oc(data).foo.bar.baz.last(); 23 | `; 24 | 25 | const res = runPlugin(code); 26 | expect(res.code).toEqual(dedent` 27 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 28 | oc(data, ["foo", "bar", "baz", "last"]); 29 | `); 30 | }); 31 | 32 | test("macro can convert multiple oc uses", async () => { 33 | const code = dedent` 34 | import { oc } from "./__tests__/entry.macro"; 35 | oc(data).foo.bar.baz.last(); 36 | oc(data).other.thing(); 37 | `; 38 | 39 | const res = runPlugin(code); 40 | expect(res.code).toEqual(dedent` 41 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 42 | oc(data, ["foo", "bar", "baz", "last"]); 43 | oc(data, ["other", "thing"]); 44 | `); 45 | }); 46 | 47 | test("can handle binding from parent scope", async () => { 48 | const code = dedent` 49 | import { oc } from "./__tests__/entry.macro"; 50 | function fun(a) { 51 | oc(a).b(); 52 | } 53 | `; 54 | 55 | const res = runPlugin(code); 56 | expect(res.code).toEqual(dedent` 57 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 58 | 59 | function fun(a) { 60 | oc(a, ["b"]); 61 | } 62 | `); 63 | }); 64 | 65 | test("can handle binding from parent scope through multiple levels", async () => { 66 | const code = dedent` 67 | import { oc } from "./__tests__/entry.macro"; 68 | function fun(a) { 69 | function inner() { 70 | oc(a).b(); 71 | } 72 | } 73 | `; 74 | 75 | const res = runPlugin(code); 76 | expect(res.code).toEqual(dedent` 77 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 78 | 79 | function fun(a) { 80 | function inner() { 81 | oc(a, ["b"]); 82 | } 83 | } 84 | `); 85 | }); 86 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/__tests__/upstream-ts-optchain-tests.source: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2018-present, Rimeto, LLC. 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 {oc} from "../index"; 9 | 10 | describe("ts-optchain", () => { 11 | it("sanity checks", () => { 12 | interface X { 13 | a: string; 14 | b: {d: string}; 15 | c: number[]; 16 | d: {e: string} | null; 17 | e: {f: boolean} | null; 18 | } 19 | 20 | const x: X = { 21 | a: "hello", 22 | b: { 23 | d: "world", 24 | }, 25 | c: [-100, 200, -300], 26 | d: null, 27 | e: {f: false}, 28 | }; 29 | 30 | expect(oc(x).a()).toEqual("hello"); 31 | expect(oc(x).b.d()).toEqual("world"); 32 | expect(oc(x).c[0]()).toEqual(-100); 33 | expect(oc(x).c[100]()).toBeUndefined(); 34 | expect(oc(x).c[100](1234)).toEqual(1234); 35 | expect(oc(x).d.e()).toBeUndefined(); 36 | expect(oc(x).d.e("optional default value")).toEqual( 37 | "optional default value", 38 | ); 39 | expect(oc(x).e.f()).toEqual(false); 40 | expect(oc(x as any).y.z.a.b.c.d.e.f.g.h.i.j.k()).toBeUndefined(); 41 | }); 42 | 43 | it("optional chaining equivalence", () => { 44 | interface X { 45 | a?: string; 46 | b?: { 47 | d?: string; 48 | }; 49 | c?: Array<{ 50 | u?: { 51 | v?: number; 52 | }; 53 | }>; 54 | e?: { 55 | f?: string; 56 | g?: () => string; 57 | }; 58 | } 59 | 60 | const x: X = { 61 | a: "hello", 62 | b: { 63 | d: "world", 64 | }, 65 | c: [{u: {v: -100}}, {u: {v: 200}}, {}, {u: {v: -300}}], 66 | }; 67 | 68 | expect(oc(x).a()).toEqual(x.a); 69 | expect(oc(x).b.d()).toEqual(x.b && x.b.d); 70 | expect(oc(x).c[0].u.v()).toEqual( 71 | x.c && x.c[0] && x.c[0].u && (x as any).c[0].u.v, 72 | ); 73 | expect(oc(x).c[100].u.v()).toEqual( 74 | x.c && x.c[100] && x.c[100].u && (x as any).c[100].u.v, 75 | ); 76 | expect(oc(x).c[100].u.v(1234)).toEqual( 77 | (x.c && x.c[100] && x.c[100].u && (x as any).c[100].u.v) || 1234, 78 | ); 79 | expect(oc(x).e.f()).toEqual(x.e && x.e.f); 80 | expect(oc(x).e.f("optional default value")).toEqual( 81 | (x.e && x.e.f) || "optional default value", 82 | ); 83 | expect(oc(x).e.g(() => "Yo Yo")()).toEqual( 84 | ((x.e && x.e.g) || (() => "Yo Yo"))(), 85 | ); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import * as BabelTypes from "@babel/types"; 2 | import {Visitor, NodePath} from "@babel/traverse"; 3 | 4 | type CallValue = BabelTypes.CallExpression["arguments"][0]; 5 | 6 | export const RUNTIME_IMPORT = "babel-plugin-ts-optchain/lib/runtime"; 7 | 8 | export interface PluginOptions { 9 | opts?: { 10 | target?: string; 11 | runtime?: string; 12 | }; 13 | file: { 14 | path: NodePath; 15 | }; 16 | } 17 | 18 | export interface Babel { 19 | types: typeof BabelTypes; 20 | } 21 | 22 | function getAccessorExpressionPath( 23 | t: typeof BabelTypes, 24 | path: NodePath, 25 | accessorExpressionPath?: BabelTypes.Expression[], 26 | ): { 27 | accessorExpressionPath: BabelTypes.Expression[]; 28 | endNodePath: NodePath; 29 | defaultValue?: CallValue; 30 | } { 31 | if (!accessorExpressionPath) { 32 | accessorExpressionPath = []; 33 | } 34 | 35 | if (!t.isMemberExpression(path.container)) { 36 | let defaultValue: CallValue | undefined = undefined; 37 | 38 | if (t.isCallExpression(path.parent)) { 39 | defaultValue = path.parent.arguments[0]; 40 | } else { 41 | if (t.isMemberExpression(path.node)) { 42 | throw new Error( 43 | "Last property accessor in ts-optchain must be a function call. " + 44 | `Add () to .${path.node.property.name};`, 45 | ); 46 | } else { 47 | throw new Error( 48 | "You must add at least one property accessor to oc() calls", 49 | ); 50 | } 51 | } 52 | 53 | return { 54 | accessorExpressionPath, 55 | endNodePath: path.parentPath, 56 | defaultValue: defaultValue, 57 | }; 58 | } 59 | 60 | const prop = path.container.property; 61 | let expression: BabelTypes.Expression; 62 | 63 | if (path.container.computed) { 64 | // Pass computed properties as is. 65 | // Ex. oc(data)[ding()]() -> ding() 66 | expression = prop; 67 | } else { 68 | // Convert static property accessors to strings 69 | // Ex. oc().foo() -> "foo" 70 | expression = t.stringLiteral(prop.name); 71 | } 72 | 73 | return getAccessorExpressionPath( 74 | t, 75 | path.parentPath, 76 | accessorExpressionPath.concat(expression), 77 | ); 78 | } 79 | 80 | export function transformOptchainCall( 81 | t: Babel["types"], 82 | path: NodePath, 83 | ) { 84 | // Avoid infinite recursion on already transformed nodes 85 | if (path.node.arguments.length > 1) { 86 | return; 87 | } 88 | 89 | const { 90 | accessorExpressionPath, 91 | endNodePath, 92 | defaultValue, 93 | } = getAccessorExpressionPath(t, path); 94 | 95 | const callArgs = [ 96 | path.node.arguments[0], 97 | t.arrayExpression(accessorExpressionPath), 98 | ]; 99 | 100 | if (defaultValue) { 101 | callArgs.push(defaultValue); 102 | } 103 | 104 | endNodePath.replaceWith(t.callExpression(path.node.callee, callArgs)); 105 | } 106 | 107 | export default function tsOptChainPlugin( 108 | babel: Babel, 109 | ): {visitor: Visitor} { 110 | const t = babel.types; 111 | 112 | /** 113 | * Local name of the oc import from ts-optchain if any 114 | */ 115 | let name: string | null = null; 116 | 117 | return { 118 | visitor: { 119 | Program() { 120 | // Reset import name state when entering a new file 121 | name = null; 122 | }, 123 | 124 | ImportDeclaration(path, state) { 125 | const opts = state.opts || {}; 126 | 127 | const target = opts.target || "ts-optchain"; 128 | 129 | if (path.node.source.value !== target) { 130 | return; 131 | } 132 | 133 | path.node.source.value = opts.runtime || RUNTIME_IMPORT; 134 | 135 | for (const s of path.node.specifiers) { 136 | if (!t.isImportSpecifier(s)) { 137 | continue; 138 | } 139 | 140 | if (s.imported.name === "oc") { 141 | name = s.local.name; 142 | } 143 | } 144 | }, 145 | 146 | CallExpression(path) { 147 | // Disable if no ts-optchain is imported 148 | if (!name) { 149 | return; 150 | } 151 | 152 | // Handle only the oc() calls from the ts-optchain import 153 | if (!t.isIdentifier(path.node.callee, {name: name})) { 154 | return; 155 | } 156 | 157 | transformOptchainCall(t, path); 158 | }, 159 | }, 160 | }; 161 | } 162 | -------------------------------------------------------------------------------- /packages/babel-plugin-ts-optchain/__tests__/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import dedent from "dedent"; 2 | import {transform} from "@babel/core"; 3 | 4 | function runPlugin(code: string) { 5 | const res = transform(code, { 6 | babelrc: false, 7 | filename: "test.ts", 8 | plugins: [__dirname + "/../src/plugin.ts"], 9 | }); 10 | 11 | if (!res) { 12 | throw new Error("plugin failed"); 13 | } 14 | 15 | return res; 16 | } 17 | 18 | test("can transform property access to oc call", () => { 19 | const code = dedent` 20 | import { oc } from "ts-optchain"; 21 | oc(data).foo.bar.baz.last(); 22 | `; 23 | 24 | const res = runPlugin(code); 25 | expect(res.code).toEqual(dedent` 26 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 27 | oc(data, ["foo", "bar", "baz", "last"]); 28 | `); 29 | }); 30 | 31 | test("can handle undefined literal", () => { 32 | const code = dedent` 33 | import { oc } from "ts-optchain"; 34 | oc(undefined).foo.bar.baz.last(); 35 | `; 36 | 37 | const res = runPlugin(code); 38 | expect(res.code).toEqual(dedent` 39 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 40 | oc(undefined, ["foo", "bar", "baz", "last"]); 41 | `); 42 | }); 43 | 44 | test("can pass the default value", () => { 45 | const code = dedent` 46 | import { oc } from "ts-optchain"; 47 | oc(data).foo.bar.baz.last("default"); 48 | `; 49 | 50 | const res = runPlugin(code); 51 | expect(res.code).toEqual(dedent` 52 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 53 | oc(data, ["foo", "bar", "baz", "last"], "default"); 54 | `); 55 | }); 56 | 57 | test("can handle complicated expressions in default values", () => { 58 | const code = dedent` 59 | import { oc } from "ts-optchain"; 60 | oc(data).foo.bar.baz.last(something ? getDefault() : other()); 61 | `; 62 | 63 | const res = runPlugin(code); 64 | expect(res.code).toEqual(dedent` 65 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 66 | oc(data, ["foo", "bar", "baz", "last"], something ? getDefault() : other()); 67 | `); 68 | }); 69 | 70 | test("can use local import alias", () => { 71 | const code = dedent` 72 | import { oc as custom } from "ts-optchain"; 73 | custom(data).foo.bar.baz.last(); 74 | `; 75 | 76 | const res = runPlugin(code); 77 | expect(res.code).toEqual(dedent` 78 | import { oc as custom } from "babel-plugin-ts-optchain/lib/runtime"; 79 | custom(data, ["foo", "bar", "baz", "last"]); 80 | `); 81 | }); 82 | 83 | test("does not touch oc() calls if they are not imported from ts-optchain", () => { 84 | const code = dedent` 85 | oc(data).foo.bar.baz.last(); 86 | `; 87 | 88 | const res = runPlugin(code); 89 | expect(res.code).toEqual(dedent` 90 | oc(data).foo.bar.baz.last(); 91 | `); 92 | }); 93 | 94 | test("can handle array access", () => { 95 | const code = dedent` 96 | import { oc } from "ts-optchain"; 97 | oc(data).foo.bar[0].baz.last(); 98 | `; 99 | 100 | const res = runPlugin(code); 101 | expect(res.code).toEqual(dedent` 102 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 103 | oc(data, ["foo", "bar", 0, "baz", "last"]); 104 | `); 105 | }); 106 | 107 | test("can handle string literal access", () => { 108 | const code = dedent` 109 | import { oc } from "ts-optchain"; 110 | oc(data).foo.bar["ding"].baz.last(); 111 | `; 112 | 113 | const res = runPlugin(code); 114 | expect(res.code).toEqual(dedent` 115 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 116 | oc(data, ["foo", "bar", "ding", "baz", "last"]); 117 | `); 118 | }); 119 | 120 | test("can handle variable accessor", () => { 121 | const code = dedent` 122 | import { oc } from "ts-optchain"; 123 | oc(data).foo.bar[dong].baz.last(); 124 | `; 125 | 126 | const res = runPlugin(code); 127 | expect(res.code).toEqual(dedent` 128 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 129 | oc(data, ["foo", "bar", dong, "baz", "last"]); 130 | `); 131 | }); 132 | 133 | test("can handle function in accessor", () => { 134 | const code = dedent` 135 | import { oc } from "ts-optchain"; 136 | oc(data).foo.bar[fun(1)].baz.last(); 137 | `; 138 | 139 | const res = runPlugin(code); 140 | expect(res.code).toEqual(dedent` 141 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 142 | oc(data, ["foo", "bar", fun(1), "baz", "last"]); 143 | `); 144 | }); 145 | 146 | test("can handle function in accessor in the leaf getter", () => { 147 | const code = dedent` 148 | import { oc } from "ts-optchain"; 149 | oc(data).foo.bar.baz[last(1)](); 150 | `; 151 | 152 | const res = runPlugin(code); 153 | expect(res.code).toEqual(dedent` 154 | import { oc } from "babel-plugin-ts-optchain/lib/runtime"; 155 | oc(data, ["foo", "bar", "baz", last(1)]); 156 | `); 157 | }); 158 | 159 | test("has good error message when chain does not end with function call", () => { 160 | const code = dedent` 161 | import { oc } from "ts-optchain"; 162 | const foo = oc(data).foo; 163 | const bar = foo.bar(); 164 | `; 165 | 166 | expect(() => { 167 | runPlugin(code); 168 | }).toThrow("Last property accessor in ts-optchain must be a function call"); 169 | }); 170 | 171 | test("has good error message when there are no property accessors at all", () => { 172 | const code = dedent` 173 | import { oc } from "ts-optchain"; 174 | const x = oc(data); 175 | const bar = x.bar(); 176 | `; 177 | 178 | expect(() => { 179 | runPlugin(code); 180 | }).toThrow("You must add at least one property accessor to oc() calls"); 181 | }); 182 | 183 | test("properly resets the state between source files", () => { 184 | const file1 = dedent` 185 | import { oc } from "ts-optchain"; 186 | oc(data).foo.bar.baz.last(); 187 | `; 188 | 189 | const file2 = dedent` 190 | import { oc } from "notoptchain"; 191 | oc().foo(); 192 | `; 193 | 194 | transform(file1, { 195 | babelrc: false, 196 | filename: "file1.ts", 197 | plugins: [__dirname + "/../src/plugin.ts"], 198 | }); 199 | 200 | const res = transform(file2, { 201 | babelrc: false, 202 | filename: "file1.ts", 203 | plugins: [__dirname + "/../src/plugin.ts"], 204 | }); 205 | 206 | if (!res) { 207 | throw new Error("plugin failed"); 208 | } 209 | 210 | expect(res.code).toEqual(dedent` 211 | import { oc } from "notoptchain"; 212 | oc().foo(); 213 | `); 214 | }); 215 | --------------------------------------------------------------------------------