Repository: dsherret/ts-nameof Branch: main Commit: 5e817b83998d Files: 129 Total size: 163.2 KB Directory structure: gitextract_ndkir86v/ ├── .gitignore ├── .travis.yml ├── .vscode/ │ └── launch.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── dprint.json ├── lib/ │ ├── global.d.ts │ └── global.tests.ts ├── package.json ├── packages/ │ ├── babel-plugin-ts-nameof/ │ │ ├── .npmignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── common/ │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── errors.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── scripts-common/ │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── ArgsChecker.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── tests-common/ │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── runCommonTests.ts │ │ └── tsconfig.json │ ├── transforms-babel/ │ │ ├── .mocharc.yml │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── VisitSourceFileContext.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── parse.ts │ │ │ ├── tests/ │ │ │ │ └── pluginTests.ts │ │ │ └── transform.ts │ │ └── tsconfig.json │ ├── transforms-common/ │ │ ├── .mocharc.yml │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── StringOrTemplateExpressionBuilder.ts │ │ │ ├── index.ts │ │ │ ├── nodeFactories.ts │ │ │ ├── nodeHelpers.ts │ │ │ ├── nodes.ts │ │ │ ├── printers.ts │ │ │ ├── tests/ │ │ │ │ └── printerTests.ts │ │ │ └── transformCallExpression.ts │ │ └── tsconfig.json │ ├── transforms-ts/ │ │ ├── .mocharc.yml │ │ ├── .npmignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── VisitSourceFileContext.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── parse.ts │ │ │ ├── tests/ │ │ │ │ └── transformerFactoryTests.ts │ │ │ ├── transform.ts │ │ │ └── transformerFactory.ts │ │ └── tsconfig.json │ ├── ts-nameof/ │ │ ├── .mocharc.yml │ │ ├── .npmignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── lib/ │ │ │ └── declarationFileTests.ts │ │ ├── package.json │ │ ├── scripts/ │ │ │ ├── common/ │ │ │ │ ├── createProject.ts │ │ │ │ └── index.ts │ │ │ ├── generation/ │ │ │ │ ├── createDeclarationFile.ts │ │ │ │ └── main.ts │ │ │ ├── tsconfig.json │ │ │ └── verification/ │ │ │ ├── main.ts │ │ │ └── verifyDeclarationFile.ts │ │ ├── setup/ │ │ │ ├── custom.md │ │ │ ├── fusebox.md │ │ │ ├── gulp.md │ │ │ ├── jest.md │ │ │ ├── tsc.md │ │ │ └── webpack.md │ │ ├── src/ │ │ │ ├── main.ts │ │ │ ├── tests/ │ │ │ │ ├── testFiles/ │ │ │ │ │ ├── GeneralTestFile.txt │ │ │ │ │ ├── StreamNoNameofTestFile.txt │ │ │ │ │ ├── StreamTestFile.txt │ │ │ │ │ ├── globFolder/ │ │ │ │ │ │ └── MyGlobTestFile.txt │ │ │ │ │ └── issues/ │ │ │ │ │ ├── 11-expected.txt │ │ │ │ │ ├── 11-source.txt │ │ │ │ │ ├── 8-expected.txt │ │ │ │ │ └── 8-source.txt │ │ │ │ └── text/ │ │ │ │ ├── helpers/ │ │ │ │ │ ├── fileHelpers.ts │ │ │ │ │ ├── getTestFilePath.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── issuesTests.ts │ │ │ │ ├── replaceInFilesTests.ts │ │ │ │ └── replaceInTextTests.ts │ │ │ └── text/ │ │ │ ├── getFileNamesFromGlobs.ts │ │ │ ├── index.ts │ │ │ ├── replaceInFiles.ts │ │ │ └── replaceInText.ts │ │ ├── ts-nameof.d.ts │ │ └── tsconfig.json │ └── ts-nameof.macro/ │ ├── .mocharc.yml │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── scripts/ │ │ ├── common/ │ │ │ ├── createProject.ts │ │ │ └── index.ts │ │ ├── generation/ │ │ │ ├── createDeclarationFile.ts │ │ │ └── main.ts │ │ └── tsconfig.json │ ├── src/ │ │ ├── index.js │ │ ├── references.d.ts │ │ └── tests/ │ │ ├── macroTests.ts │ │ └── ts-nameof.macro/ │ │ └── index.js │ ├── ts-nameof.macro.d.ts │ └── tsconfig.json └── tsconfig.common.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /node_modules /.vs /bin /dist /obj /temp /temp *.js.map *.suo *.csproj *.csproj.user *.sln /packages/ts-nameof/temp/ /packages/*/dist/ /packages/*/node_modules/ *.tsbuildinfo # Logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # next.js build output .next # OS X temporary files .DS_Store ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - '16' script: - set -e - yarn install - yarn build - yarn verify ================================================ FILE: .vscode/launch.json ================================================ { "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Run transforms-babel tests", "runtimeExecutable": "npm", "runtimeArgs": ["run", "--prefix", "packages/transforms-babel", "test:debug"], "port": 9229, "stopOnEntry": false }, { "type": "node", "request": "launch", "name": "Run transforms-common tests", "runtimeExecutable": "npm", "runtimeArgs": ["run", "--prefix", "packages/transforms-common", "test:debug"], "port": 9229, "stopOnEntry": false }, { "type": "node", "request": "launch", "name": "Run transforms-ts tests", "runtimeExecutable": "npm", "runtimeArgs": ["run", "--prefix", "packages/transforms-ts", "test:debug"], "port": 9229, "stopOnEntry": false }, { "type": "node", "request": "launch", "name": "Run ts-nameof.macro tests", "runtimeExecutable": "npm", "runtimeArgs": ["run", "--prefix", "packages/ts-nameof.macro", "test:debug"], "port": 9229, "stopOnEntry": false }, { "type": "node", "request": "launch", "name": "Run ts-nameof tests", "runtimeExecutable": "npm", "runtimeArgs": ["run", "--prefix", "packages/ts-nameof", "test:debug"], "port": 9229, "stopOnEntry": false } ] } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team on twitter via direct message at https://twitter.com/DavidSherret (DMs open). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Logging Bugs 1. Start logging an issue in the [issue tracker](https://github.com/dsherret/ts-nameof/issues). 2. Clearly identify the problem and submit some reproduction code. - Prune the reproduction to remove needless details. 3. State the current and expected behaviour. 4. State the version of ts-nameof (always show a reproduction of the bug on the latest version). # Contributing Bug Fixes 1. Follow the instructions above about logging a bug. In addition: 1. State that you are going to work on the bug. 2. Discuss major structural changes in the issue before doing the work to ensure it makes sense and work isn't wasted. 2. Start working on the fix in a branch and submit a PR when done. 3. Ensure `yarn verify` passes when run in the root directory. # Contributing Features 1. Log an issue in the [issue tracker](https://github.com/dsherret/ts-nameof/issues). In the issue: 1. Propose the change. - Outline all changes that will be made to the public API. - Discuss any structural changes to the code if necessary. 2. Wait for discussion and green light from [@dsherret](https://github.com/dsherret) (who will try to reply as soon as possible, but it might take a few days). - Note: If the change is small and you think it wouldn't take you too much time, then feel free to start working on it and even submit a PR. Just beware that you're taking the risk that it could be denied. 2. After approval, start working on the change in a branch and submit a PR. 3. Read [DEVELOPMENT.md](DEVELOPMENT.md) for some useful information. 4. Ensure `yarn verify` passes when run in the root directory. ================================================ FILE: DEVELOPMENT.md ================================================ # Development ## Building Open the root directory of the repo and run: ```bash # install dependencies yarn install # build yarn build ``` ## Packages - [packages/babel-plugin-ts-nameof](packages/babel-plugin-ts-nameof) - Transform plugin for Babel. - [packages/common](packages/common) - Common code used by almost everything. - [packages/scripts-common](packages/scripts-common) - Common scripts used by other packages. - [packages/tests-common](packages/tests-common) - Tests used by some packages. Write all your transform tests here. - [packages/transforms-babel](packages/transforms-babel) - Transforms from the Babel AST to the Common AST. - [packages/transforms-common](packages/transforms-common) - Nameof transforms done in the Common AST. - [packages/transforms-ts](packages/transforms-ts) - Transforms from the TypeScript AST to the Common AST. - [packages/ts-nameof](packages/ts-nameof) - ts-nameof library for the TypeScript compiler. - [packages/ts-nameof.macro](packages/ts-nameof) - ts-nameof.macro library for Babel macros. ## Standard Commands ```bash # build (run in root dir) yarn build # run tests (run in root dir) yarn test # format the code (download dprint from dprint.dev) dprint fmt ``` ### Clean Rebuild ``` yarn clean && yarn build ``` ## Declaration File ### Global Definitions The global definitions are stored in [lib/global.d.ts](lib/global.d.ts). To make changes: 1. Add a failing test in [lib/global.tests.ts](lib/global.tests.ts) (failing test means you get a compile error) 2. Update [lib/global.d.ts](lib/global.d.ts). 3. Run `yarn create-declaration-file` in the root directory ### ts-nameof - Updating API 1. Update [packages/ts-nameof/lib/declarationFileTests.ts](packages/ts-nameof/lib/declarationFileTests.ts) with a failing test. 2. Update the API in [packages/ts-nameof/src/main.ts](packages/ts-nameof/src/main.ts). 3. Run `yarn create-declaration-file` in the root directory ## After Development Run the following command in the root directory, which will check that everything is good: ```bash yarn verify ``` ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2019 David Sherret Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ts-nameof [![Build Status](https://travis-ci.org/dsherret/ts-nameof.svg)](https://travis-ci.org/dsherret/ts-nameof) [`nameof`](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/nameof) in TypeScript. Monorepo for ts-nameof projects: - [ts-nameof](packages/ts-nameof) (TypeScript compiler) - [babel-plugin-ts-nameof](packages/babel-plugin-ts-nameof) (Babel compiler) - [ts-nameof.macro](packages/ts-nameof.macro) (Babel compiler) ## Recommend: Don't use this package See [here](https://github.com/dsherret/ts-nameof/issues/121). ## Setup ts-nameof is a _compile time transform_ so it requires some setup. For setup instructions, see the packages above for the compiler you use. ## nameof transform ### `nameof(...)` ```ts nameof(console); nameof(console.log); nameof(console["warn"]); ``` Transforms to: ```ts "console"; "log"; "warn"; ``` ### `nameof()` ```ts nameof(); nameof>(); nameof(); ``` Transforms to: ```ts "MyInterface"; "Array"; "MyInnerInterface"; ``` This is useful when working in the type domain. ### `nameof(o => ...)` ```ts nameof(o => o.prop); ``` Transforms to: ```ts "prop"; ``` ## nameof.full transform ### `nameof.full(...)` ```ts nameof.full(console.log); nameof.full(window.alert.length, 1); nameof.full(window.alert.length, 2); nameof.full(window.alert.length, -1); nameof.full(window.alert.length, -2); nameof.full(window.alert.length, -3); ``` Transforms to: ```ts "console.log"; "alert.length"; "length"; "length"; "alert.length"; "window.alert.length"; ``` ### `nameof.full()` ```ts nameof.full(); nameof.full(1); nameof.full>(); ``` Transforms to: ```ts "MyNamespace.MyInnerInterface"; "MyInnerInterface"; "Array"; ``` ### `nameof.full(o => ...)` ```ts nameof.full(o => o.prop.prop2); nameof.full(o => o.prop.prop2.prop3, 1); ``` Transforms to: ```ts "prop.prop2"; "prop2.prop3"; ``` ### `nameof.interpolate(value)` Writing the following: ```ts nameof.full(myObj.prop[i]); ``` ...does not interpolate the node in the computed property. ```ts "myObj.prop[i]"; ``` If you want to interpolate the value then you can specify that explicitly with a `nameof.interpolate` function. ```ts nameof.full(myObj.prop[nameof.interpolate(i)]); ``` Transforms to: ```ts `myObj.prop[${i}]`; ``` ## nameof.toArray transform Contributed by: [@cecilyth](https://github.com/cecilyth) ### `nameof.toArray(...)` ```ts nameof.toArray(myObject, otherObject); nameof.toArray(obj.firstProp, obj.secondProp, otherObject, nameof.full(obj.other)); ``` Transforms to: ```ts ["myObject", "otherObject"]; ["firstProp", "secondProp", "otherObject", "obj.other"]; ``` ### `nameof.toArray(o => [...])` ```ts nameof.toArray(o => [o.firstProp, o.otherProp.secondProp, o.other]); nameof.toArray(o => [o.prop, nameof.full(o.myProp.otherProp, 1)]); ``` Transforms to: ```ts ["firstProp", "secondProp", "other"]; ["prop", "myProp.otherProp"]; ``` ## nameof.split transform Contributed by: [@cecilyth](https://github.com/cecilyth) ### `nameof.split(...)` ```ts nameof.split(myObj.prop.prop2); nameof.split(myObj.prop.prop2, 1); nameof.split(myObj.prop.prop2, -1); nameof.split(myObj.prop.prop2).join("/"); ``` Transforms to: ```ts ["myObj", "prop", "prop2"]; ["prop", "prop2"]; ["prop2"]; ["myObj", "prop", "prop2"].join("/"); // "myObj/prop/prop2" ``` ### `nameof.split(o => ...)` ```ts nameof.split(o => o.prop.prop2.prop3); nameof.split(o => o.prop.prop2.prop3, 1); nameof.split(o => o.prop.prop2.prop3, -1); nameof.split(s => s.a.b.c).join("/"); ``` Transforms to: ```ts ["prop", "prop2", "prop3"]; ["prop2", "prop3"]; ["prop3"]; ["a", "b", "c"].join("/"); // "a/b/c" ``` ## Other - [Contributing](CONTRIBUTING.md) - [Development](DEVELOPMENT.md) ================================================ FILE: dprint.json ================================================ { "incremental": true, "lineWidth": 160, "indentWidth": 2, "includes": ["**/*.{ts,tsx,js,jsx,json,md}"], "excludes": [ "common", "**/node_modules", "**/*-lock.json" ], "plugins": [ "https://plugins.dprint.dev/typescript-0.60.0.wasm", "https://plugins.dprint.dev/json-0.7.0.wasm", "https://plugins.dprint.dev/markdown-0.11.3.wasm" ] } ================================================ FILE: lib/global.d.ts ================================================ /** * Gets a string representation of the final identifier of the given expression. * * @example nameof() -> "MyInterface" * @example nameof>() -> "Array" * @example nameof() -> "MyInnerInterface" * @example nameof(o => o.prop) -> "prop" * * @param func An optional function for which the last identifier of the expression will be parsed. */ declare function nameof(func?: (obj: T) => any): string; /** * Gets a string representation of the last identifier of the given expression. * * @example nameof(console) -> "console" * @example nameof(console.log) -> "log" * @example nameof(console["warn"]) -> "warn" * * @param obj An expression for which the last identifier will be parsed. */ declare function nameof(obj: any): string; declare namespace nameof { /** * Gets the string representation of the entire type parameter expression. * * @example nameof.full() -> "MyNamespace.MyInnerInterface" * @example nameof.full(1) -> "MyInnerInterface" * @example nameof.full>() -> "Array" * @example nameof.full>(-1) -> "MyInnerInterface" * * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function full(periodIndex?: number): string; /** * Gets the string representation of the entire resultant expression. * * @example nameof.full(o => o.prop.prop2) -> "prop.prop2" * @example nameof.full(o => o.prop.prop2.prop3, 1) -> "prop2.prop3" * @example nameof.full(o => o.prop.prop2.prop3, -1) -> `"prop3" * * @param func A function for which the result will be parsed, excluding the parameter's identifier. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function full(func: (obj: T) => any, periodIndex?: number): string; /** * Gets the string representation of the entire given expression. * * @example nameof.full(console.log) -> "console.log" * @example nameof.full(window.alert.length, -1) -> "length" * @example nameof.full(window.alert.length, 2) -> "length" * * @param obj The expression which will be parsed. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function full(obj: any, periodIndex?: number): string; /** * Gets an array containing the string representation of the final identifier of each expression in the array returned by the provided function. * * @example nameof.toArray(o => [o.firstProp, o.otherProp.secondProp, o.other]) -> ["firstProp", "secondProp", "other"] * @example nameof.toArray(o => [o.prop, nameof.full(o.myProp.otherProp, 1)]) -> ["prop", "myProp.otherProp"] * * @param func A function returning an array of expressions to be parsed, excluding the parameter's identifier. */ function toArray(func: (obj: T) => any[]): string[]; /** * Gets an array containing the string representation of each expression in the arguments. * * @example nameof.toArray(myObject, otherObject) -> ["myObject", "otherObject"] * @example nameof.toArray(obj.firstProp, obj.secondProp, otherObject, nameof.full(obj.other)) -> ["firstProp", "secondProp", "otherObject", "obj.other"] * * @param args An array of expressions to be parsed. */ function toArray(...args: any[]): string[]; /** * Embeds an expression into the string representation of the result of nameof.full. * * @example nameof.full(myObj.prop[nameof.interpolate(i)]) -> `myObj.prop[${i}]` * * @param value The value to interpolate. */ function interpolate(value: T): T; /** * Gets an array of strings where each element is a subsequent part of the expression provided. * * @example nameof.split(o => o.prop.prop2.prop3) -> ["prop", "prop2", "prop3"] * @example nameof.split(o => o.prop.prop2.prop3, 1) -> ["prop2", "prop3"] * @example nameof.split(o => o.prop.prop2.prop3, -1) -> ["prop", "prop2"] * * @param func A function for which the resultant parts will be parsed, excluding the parameter's identifier. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function split(func: (obj: T) => any, periodIndex?: number): string[]; /** * Gets an array of strings where each element is a subsequent part of the expression provided. * * @example nameof.split(myObj.prop.prop2.prop3) -> ["myObj", "prop", "prop2", "prop3"] * @example nameof.split(myObj.prop.prop2.prop3, -3);`, `["prop", "prop2", "prop3"]; * @example nameof.split(myObj.prop.prop2.prop3, 2);`, `["prop2", "prop3"] * * @param obj An expression for which the parts will be parsed. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function split(obj: any, periodIndex?: number): string[]; } ================================================ FILE: lib/global.tests.ts ================================================ /// namespace TestNamespace { export interface TestType { prop: string; } } class TestClass { prop1 = ""; prop2 = ""; } // nameof tests nameof(TestClass); // $ExpectType string nameof(); // $ExpectType string nameof(t => t.prop1); // $ExpectType string // nameof.full tests const testInstance = new TestClass(); nameof.full(testInstance.prop1); // $ExpectType string nameof.full(testInstance.prop1, 1); // $ExpectType string nameof.full(); // $ExpectType string nameof.full(1); // $ExpectType string nameof.full(t => t.prop1); // $ExpectType string nameof.full(t => t.prop1, 1); // $ExpectType string // nameof.toArray tests nameof.toArray(testInstance.prop1); // $ExpectType string[] nameof.toArray(testInstance.prop1, testInstance.prop2); // $ExpectType string[] nameof.toArray(t => [t.prop1]); // $ExpectType string[] // nameof.split tests nameof.split(testInstance.prop1); // $ExpectType string[] nameof.split(testInstance.prop1, 1); // $ExpectType string[] nameof.split(obj => obj.prop1); // $ExpectType string[] nameof.split(obj => obj.prop1, 1); // $ExpectType string[] // nameof.interpolate tests nameof.interpolate(""); // $ExpectType "" // reference type test const myObj = { test: "" }; nameof(myObj); // $ExpectType string nameof.full(myObj); // $ExpectType string nameof.toArray(myObj); // $ExpectType string[] // primitive type test const myStr = ""; nameof(myStr); // $ExpectType string nameof.full(myStr); // $ExpectType string nameof.toArray(myStr); // $ExpectType string[] // null test const nullTypedVar = null; nameof(nullTypedVar); // $ExpectType string nameof.full(nullTypedVar); // $ExpectType string nameof.toArray(nullTypedVar); // $ExpectType string[] // undefined test const undefinedTypedVar = undefined; nameof(undefinedTypedVar); // $ExpectType string nameof.full(undefinedTypedVar); // $ExpectType string nameof.toArray(undefinedTypedVar); // $ExpectType string[] ================================================ FILE: package.json ================================================ { "name": "ts-nameof-workspace", "private": true, "workspaces": [ "packages/common", "packages/scripts-common", "packages/tests-common", "packages/transforms-common", "packages/transforms-babel", "packages/transforms-ts", "packages/ts-nameof", "packages/ts-nameof.macro", "packages/babel-plugin-ts-nameof" ], "scripts": { "clean": "yarn workspaces run clean", "build": "yarn workspaces run build", "test": "yarn workspaces run test", "verify": "yarn test && yarn verify-declaration-file", "create-declaration-file": "yarn workspace ts-nameof build:declarations && yarn workspace ts-nameof.macro build:declarations", "verify-declaration-file": "yarn workspace ts-nameof verify-declaration-file" } } ================================================ FILE: packages/babel-plugin-ts-nameof/.npmignore ================================================ /node_modules /.vscode /.vs /.git /obj /temp /bin /src /dist/tests /setup /scripts /lib *.js.map *.v12.suo *.csproj *.csproj.user *.sln *.log .travis.yml .gitignore CHANGELOG.md .mocharc.yml tsconfig.json ================================================ FILE: packages/babel-plugin-ts-nameof/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2019 David Sherret Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/babel-plugin-ts-nameof/README.md ================================================ # babel-plugin-ts-nameof [![npm version](https://badge.fury.io/js/babel-plugin-ts-nameof.svg)](https://badge.fury.io/js/babel-plugin-ts-nameof) [![Build Status](https://travis-ci.org/dsherret/ts-nameof.svg)](https://travis-ci.org/dsherret/ts-nameof) [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) [`nameof`](https://msdn.microsoft.com/en-us/library/dn986596.aspx) in TypeScript. ## Setup 1. Run: ``` npm install babel-plugin-ts-nameof @types/ts-nameof --save-dev ``` 2. Add an entry to `.babelrc` for `babel-plugin-ts-nameof`: ``` { "plugins": ["babel-plugin-ts-nameof"] } ``` ## Transforms [Read here](https://github.com/dsherret/ts-nameof/blob/master/README.md) ## Other - [Contributing](https://github.com/dsherret/ts-nameof/blob/master/CONTRIBUTING.md) - [Development](https://github.com/dsherret/ts-nameof/blob/master/DEVELOPMENT.md) ================================================ FILE: packages/babel-plugin-ts-nameof/package.json ================================================ { "name": "babel-plugin-ts-nameof", "version": "4.2.1", "description": "nameof in TypeScript for babel.", "main": "dist/index.js", "scripts": { "clean": "rimraf dist && tsc --b --clean", "build": "tsc --b", "test": "echo 'ignore'", "dopublish": "npm run install && npm run build && echo \"Run: npm publish --otp\"" }, "keywords": [ "nameof", "typescript", "transforms", "babel" ], "repository": { "type": "git", "url": "git+https://github.com/dsherret/ts-nameof.git", "directory": "packages/babel-plugin-ts-nameof" }, "author": "David Sherret", "license": "MIT", "dependencies": { "@ts-nameof/transforms-babel": "^4.2.1" }, "devDependencies": { "@babel/core": "^7.16.5", "@babel/preset-typescript": "^7.16.5", "@ts-nameof/tests-common": "^4.2.0", "@types/babel__core": "^7.1.17", "@types/babel__generator": "^7.6.3", "@types/babel__template": "^7.4.1", "@types/babel__traverse": "^7.14.2", "@types/node": "^17.0.0", "mocha": "^9.1.3", "rimraf": "^3.0.2", "ts-node": "^10.4.0", "typescript": "^4.5.4" } } ================================================ FILE: packages/babel-plugin-ts-nameof/src/index.ts ================================================ export { plugin as default } from "@ts-nameof/transforms-babel"; ================================================ FILE: packages/babel-plugin-ts-nameof/tsconfig.json ================================================ { "compilerOptions": { "outDir": "./dist" }, "extends": "../../tsconfig.common.json", "include": ["./src"], "references": [ { "path": "../transforms-babel" }, { "path": "../tests-common" } ] } ================================================ FILE: packages/common/.npmignore ================================================ tsconfig.json tsconfig.tsbuildinfo *.log *.js.map src ================================================ FILE: packages/common/README.md ================================================ # ts-nameof - Common Contains common utilities functions. ================================================ FILE: packages/common/package.json ================================================ { "name": "@ts-nameof/common", "version": "4.2.1", "description": "ts-nameof - Common code across packages.", "main": "./dist/index.js", "author": "David Sherret", "license": "MIT", "publishConfig": { "access": "public" }, "scripts": { "clean": "rimraf dist && tsc --b --clean", "build": "tsc --b", "test": "echo 'ignore'" }, "dependencies": { }, "devDependencies": { "typescript": "^4.5.4", "rimraf": "^3.0.2" } } ================================================ FILE: packages/common/src/errors.ts ================================================ export function throwError(message: string): never { throw new Error(`[ts-nameof]: ${sanitizeMessage(message)}`); } export function throwErrorForSourceFile(message: string, sourceFilePath: string): never { throw new Error(`[ts-nameof:${sourceFilePath}]: ${sanitizeMessage(message)}`); } export function assertNever(value: never, message: string): never { return throwError(message); } function sanitizeMessage(message: string) { return message.replace(/^\[ts\-nameof[^\]]*\]: /, ""); } ================================================ FILE: packages/common/src/index.ts ================================================ export * from "./errors"; ================================================ FILE: packages/common/tsconfig.json ================================================ { "compilerOptions": { "composite": true, "declaration": true, "outDir": "./dist" }, "extends": "../../tsconfig.common.json", "include": ["./src/**/*.ts"] } ================================================ FILE: packages/scripts-common/.npmignore ================================================ tsconfig.json tsconfig.tsbuildinfo src *.log *.js.map ================================================ FILE: packages/scripts-common/README.md ================================================ # ts-nameof - Scripts Common Contains scripts for use across packages. ================================================ FILE: packages/scripts-common/package.json ================================================ { "name": "@ts-nameof/scripts-common", "version": "4.0.2", "description": "ts-nameof - Common scripts for ts-nameof packages.", "main": "./dist/index.js", "author": "David Sherret", "license": "MIT", "publishConfig": { "access": "public" }, "scripts": { "clean": "rimraf dist && tsc --b --clean", "build": "tsc --b", "test": "echo 'ignore'" }, "devDependencies": { "@types/mocha": "^9.0.0", "@types/node": "^17.0.0", "rimraf": "^3.0.2", "typescript": "^4.5.4" } } ================================================ FILE: packages/scripts-common/src/ArgsChecker.ts ================================================ export class ArgsChecker { private readonly originalArgs: ReadonlyArray; private readonly args: string[]; constructor(args?: string[]) { this.args = args || process.argv.slice(2); this.originalArgs = [...this.args]; } checkHasArg(argName: string) { if (this.originalArgs.length === 0) { return true; // run all } return this.checkHasExplicitArg(argName); } checkHasExplicitArg(argName: string) { const index = this.args.indexOf(argName); if (index === -1) { return false; } this.args.splice(index, 1); return true; } verifyArgsUsed() { if (this.args.length > 0) { console.error(`Unknown args: ${this.args.join(", ")}`); } } } ================================================ FILE: packages/scripts-common/src/index.ts ================================================ export * from "./ArgsChecker"; ================================================ FILE: packages/scripts-common/tsconfig.json ================================================ { "compilerOptions": { "composite": true, "declaration": true, "outDir": "./dist" }, "extends": "../../tsconfig.common.json", "include": ["./src/**/*.ts"] } ================================================ FILE: packages/tests-common/.npmignore ================================================ tsconfig.json tsconfig.tsbuildinfo src *.log *.js.map ================================================ FILE: packages/tests-common/README.md ================================================ # ts-nameof - Tests Common Contains tests that should work across any implementation of ts-nameof (whether babel or the typescript compiler). ================================================ FILE: packages/tests-common/package.json ================================================ { "name": "@ts-nameof/tests-common", "version": "4.2.0", "description": "Common tests for ts-nameof packages.", "main": "./dist/index.js", "author": "David Sherret", "license": "MIT", "publishConfig": { "access": "public" }, "scripts": { "clean": "rimraf dist && tsc --b --clean", "build": "tsc --b", "test": "echo 'ignore'" }, "devDependencies": { "@dprint/formatter": "^0.1.5", "@dprint/typescript": "^0.60.0", "@types/mocha": "^9.0.0", "@types/node": "^17.0.0", "rimraf": "^3.0.2", "typescript": "^4.5.4" } } ================================================ FILE: packages/tests-common/src/index.ts ================================================ export * from "./runCommonTests"; ================================================ FILE: packages/tests-common/src/runCommonTests.ts ================================================ import { createFromBuffer } from "@dprint/formatter"; // @ts-ignore import { getBuffer } from "@dprint/typescript"; import * as assert from "assert"; import * as path from "path"; const formatter = createFromBuffer(getBuffer()); formatter.setConfig({ indentWidth: 2 }, {}); /** * Runs tests across different compilers. * @param getTransformedText Function to get the transformed text. * @param options Options for running the tests. */ export function runCommonTests(getTransformedText: (text: string) => string, options: { commonPrefix?: string } = {}) { describe("nameof", () => { describe("bad call expressions", () => { it("should throw if someone does not provide arguments or type arguments", () => { runThrowTest("nameof();", "Call expression must have one argument or type argument: nameof()"); }); }); describe("argument", () => { it("should get the result of an identifier", () => { runTest(`nameof(myObj);`, `"myObj";`); }); it("should get the result of the this keyword", () => { runTest(`nameof(this);`, `"this";`); }); it("should get the result of a property access expression", () => { runTest(`nameof(myObj.prop);`, `"prop";`); }); it("should get the result of an expression with a parenthesized expression", () => { runTest(`nameof((myObj).prop);`, `"prop";`); }); it("should get the result of an expression with a type assertion", () => { runTest(`nameof((myObj as any).prop);`, `"prop";`); }); it("should get the result of a property access expression with null assertion operators", () => { runTest(`nameof(myObj!.prop!);`, `"prop";`); }); it("should get the result of an identifier with a dollar sign", () => { runTest(`nameof(myObj.$prop);`, `"$prop";`); }); it("should resolve to string when nesting nameofs", () => { runTest(`nameof(nameof(testing));`, `"testing";`); }); }); describe("type parameter", () => { it("should get the result of an identifier", () => { runTest(`nameof();`, `"Test";`); }); it("should get the result of a fully qualified name", () => { runTest(`nameof();`, `"Test";`); }); it("should get an identifier with a dollar sign", () => { runTest(`nameof();`, `"Test$";`); }); it("should handle when someone uses an import type as not the last node", () => { runTest(`nameof();`, `"prop";`); }); it("should throw when someone only uses an import type", () => { runThrowTest(`nameof();`, getNotSupportedErrorText("import(\"test\")")); }); it("should throw when someone only uses an import type with typeof", () => { runThrowTest(`nameof();`, getNotSupportedErrorText("typeof import(\"test\")")); }); }); describe("computed properties", () => { it("should not allow a computed property to be at the end with a number", () => { runThrowTest(`nameof(anyProp[0]);`, getFirstAccessedPropertyMustNotBeComputedErrorText(`anyProp[0]`)); }); it("should get after the period", () => { runTest(`nameof(anyProp[0].prop);`, `"prop";`); }); it("should get the string inside the computed property", () => { runTest(`nameof(obj["prop"]);`, `"prop";`); }); it("should get the string inside the computed property for a function", () => { runTest(`nameof(i => i["prop"]);`, `"prop";`); }); it("should not allow a computed property to be at the end with a number when using a function", () => { runThrowTest(`nameof(i => i.prop[0]);`, getFirstAccessedPropertyMustNotBeComputedErrorText("(i) => i.prop[0]")); }); it("should not allow an identifier nested in a computed property", () => { runThrowTest(`nameof(i => i.prop[prop[0]]);`, getFirstAccessedPropertyMustNotBeComputedErrorText("(i) => i.prop[prop[0]]")); }); }); describe("array", () => { it("should not allow only an array", () => { runThrowTest(`nameof([0]);`, getNotSupportedErrorText("[0]")); }); it("should allow getting an array's property", () => { runTest(`nameof([].length);`, `"length";`); }); }); describe("with function", () => { it("should get the last string", () => { runTest(`nameof(i => i.prop1.prop2);`, `"prop2";`); }); it("should get from the return statement", () => { // no reason for people to do this, but don't bother complaining runTest(`nameof(i => { console.log('test'); return i.prop1.prop2; });`, `"prop2";`); }); it("should handle when someone uses an import type", () => { runTest(`nameof(x => x.Foo);`, `"Foo";`); }); it("should get when using an element access expression directly on the object", () => { runTest(`nameof(i => i["prop1"]);`, `"prop1";`); }); it("should throw when using an element access expression directly on the object and it is not a string", () => { runThrowTest(`nameof(i => i[0]);`, getFirstAccessedPropertyMustNotBeComputedErrorText(`(i) => i[0]`)); }); it("should throw when the function doesn't have a period", () => { runThrowTest(`nameof(i => i);`, "A property must be accessed on the object: (i) => i"); }); it("should throw when the function doesn't have a return statement", () => { const errorPrefix = "Cound not find return statement with an expression in function expression: "; const possibleMessages = [ errorPrefix + "{ i; }", // babel errorPrefix + "{\n i;\n}", // typescript ]; runThrowTest(`nameof(i => { i; });`, possibleMessages); }); }); describe("literals", () => { it("should leave the string literal as-is", () => { // this allows for nested nameofs runTest(`nameof("test");`, `"test";`); }); it("should transform a numeric literal as a string", () => { runTest(`nameof(5);`, `"5";`); }); }); describe("interpolate", () => { it("should throw when providing nameof.interpolate to nameof", () => { runThrowTest(`nameof(nameof.interpolate(5));`, [ getNotSupportedErrorText("nameof.interpolate(5)"), // it will be this for babel because it checks the parent nodes getUnusedNameofInterpolateErrorText("5"), ]); }); }); describe("template expression", () => { it("should return a no substitution template literal", () => { runTest("nameof(`testing`);", "`testing`;"); }); it("should return the template expression when it has only a template tail", () => { runTest("nameof(`testing${test}final`);", "`testing${test}final`;"); }); it("should return the template expression when it has a template middle", () => { runTest("nameof(`testing${other}asdf${test}${asdf}final`);", "`testing${other}asdf${test}${asdf}final`;"); }); it("should return the template expression when it starts and ends with one", () => { runTest("nameof(`${other}`);", "`${other}`;"); }); it("should return the template expression when it starts and ends with multiple", () => { runTest("nameof(`${other}${asdf}${test}`);", "`${other}${asdf}${test}`;"); }); it("should throw when a nameof.interpolate is not used", () => { runThrowTest("nameof(`${nameof.interpolate(other)}`);", getUnusedNameofInterpolateErrorText("other")); }); }); describe("other", () => { it("should ignore spread syntax", () => { runTest(`nameof(...test);`, `"test";`); }); }); }); describe("nameof.full", () => { describe("bad call expressions", () => { it("should throw if someone does not provide arguments or type arguments", () => { runThrowTest("nameof.full();", "Unsupported use of nameof.full: nameof.full()"); }); }); describe("argument", () => { it("should include everything when no count arg is provided", () => { runTest(`nameof.full(obj.prop.other);`, `"obj.prop.other";`); }); it("should not include null assertion operators", () => { runTest(`nameof.full(obj!.prop!.other!);`, `"obj.prop.other";`); }); it("should not include null assertion operators when also using element access expressions", () => { runTest(`nameof.full(obj!.prop![0].other!);`, `"obj.prop[0].other";`); }); it("should escape string literals in element access expressions", () => { runTest(`nameof.full(obj.prop["other"]);`, `"obj.prop[\\"other\\"]";`); }); it("should allow using a period index", () => { runTest("nameof.full(MyTest.Test.This, 1);", `"Test.This";`); }); it("should allow using a period index of 0", () => { runTest("nameof.full(MyTest.Test.This, 0);", `"MyTest.Test.This";`); }); it("should allow using a period index up to its max value", () => { runTest("nameof.full(MyTest.Test.This, 2);", `"This";`); }); it("should allow using a negative period index", () => { runTest("nameof.full(MyTest.Test.This, -1);", `"This";`); }); it("should allow using a negative period index to its max value", () => { runTest("nameof.full(MyTest.Test.This, -3);", `"MyTest.Test.This";`); }); it("should throw when the periodIndex is not a number literal", () => { runThrowTest("nameof.full(MyTest.Test, 'test')", `Expected count to be a number, but was: "test"`); }); it("should throw when the periodIndex is greater than the number of periods", () => { runThrowTest("nameof.full(MyTest.Test, 2)", "Count of 2 was larger than max count of 1: nameof.full(MyTest.Test, 2)"); }); it("should throw when the absolute value of the negative periodIndex is greater than the number of periods + 1", () => { runThrowTest("nameof.full(MyTest.Test, -3)", "Count of -3 was larger than max count of -2: nameof.full(MyTest.Test, -3)"); }); it("should resolve to string when nesting nameofs", () => { runTest(`nameof.full(nameof(testing));`, `"testing";`); }); it("should get the result of the super keyword", () => { runTest( `class Test {\n constructor() {\n nameof.full(super.test);\n }\n}`, `class Test {\n constructor() {\n "super.test";\n }\n}`, ); }); }); describe("type parameter", () => { it("should include everything when no count arg is provided", () => { runTest(`nameof.full();`, `"Some.Test.Name";`); }); it("should allow using a period index", () => { runTest("nameof.full(1);", `"Test.This";`); }); it("should allow using a period index of 0", () => { runTest("nameof.full(0);", `"MyTest.Test.This";`); }); it("should allow using a period index up to its max value", () => { runTest("nameof.full(2);", `"This";`); }); it("should allow using a negative period index", () => { runTest("nameof.full(-1);", `"This";`); }); it("should allow using a negative period index to its max value", () => { runTest("nameof.full(-3);", `"MyTest.Test.This";`); }); it("should throw when the periodIndex is not a number literal", () => { runThrowTest("nameof.full('test')", `Expected count to be a number, but was: "test"`); }); it("should throw when the periodIndex is greater than the number of periods", () => { runThrowTest("nameof.full(2)", "Count of 2 was larger than max count of 1: nameof.full(2)"); }); it("should throw when the absolute value of the negative periodIndex is greater than the number of periods + 1", () => { runThrowTest("nameof.full(-3)", "Count of -3 was larger than max count of -2: nameof.full(-3)"); }); it("should throw when someone uses an import type", () => { runThrowTest(`nameof.full();`, getNotSupportedErrorText("import(\"test\").other.test")); }); }); describe("arrays", () => { it("should include the brackets", () => { runTest(`nameof.full(anyProp[0].myProp);`, `"anyProp[0].myProp";`); }); }); describe("with function", () => { it("should get the text", () => { runTest(`nameof.full(i => i.prop1.prop2);`, `"prop1.prop2";`); }); it("should get the text without the null assertion operator", () => { runTest(`nameof.full(i => i.prop1!.prop2!);`, `"prop1.prop2";`); }); it("should get the text when there's a trailing comma with whitespace", () => { runTest("nameof.full(state => state.field.dates, );", `"field.dates";`); }); it("should get the text when using a function", () => { runTest(`nameof.full(function(i) { return i.prop1.prop2; });`, `"prop1.prop2";`); }); it("should get the text when providing a period", () => { runTest(`nameof.full(i => i.prop1.prop2, 0);`, `"prop1.prop2";`); runTest(`nameof.full(i => i.prop1.prop2, 1);`, `"prop2";`); runTest(`nameof.full(i => i.prop1.prop2.prop3, -1);`, `"prop3";`); }); it("should throw when the function doesn't have a period", () => { runThrowTest(`nameof.full(i => i);`, "A property must be accessed on the object: (i) => i"); }); it("should throw when someone nests a function within a function", () => { runThrowTest(`nameof.full(i => () => 5);`, "A property must be accessed on the object: (i) => () => 5"); }); }); describe("interpolate", () => { const singleArgumentErrorMessage = "Unexpected scenario where a nameof.interpolate function did not have a single argument."; it("should interpolate the provided expression", () => { runTest(`nameof.full(Test.Other[nameof.interpolate(other)]);`, "`Test.Other[${other}]`;"); }); it("should interpolate when using a function", () => { runTest(`nameof.full(a => a.b.c[nameof.interpolate(index)].d);`, "`b.c[${index}].d`;"); }); it("should throw when the interpolate function has zero arguments", () => { runThrowTest(`nameof.full(Test.Other[nameof.interpolate()]);`, singleArgumentErrorMessage); }); it("should throw when the interpolate function has multiple arguments", () => { runThrowTest(`nameof.full(Test.Other[nameof.interpolate(test, test)]);`, singleArgumentErrorMessage); }); it("should throw when a nameof.interpolate is not used inside a nameof.full", () => { runThrowTest("nameof.interpolate(some.expression);", getUnusedNameofInterpolateErrorText("some.expression")); }); it("should handle the scenarios in issue #104", () => { runTest("nameof.full(m.Data[nameof.interpolate(i)].Title);", "`m.Data[${i}].Title`;"); runTest("nameof.full(m.Data[i].Title);", `"m.Data[i].Title";`); }); }); }); describe("toArray", () => { it("should return an array of values when given a function that returns an array as input", () => { runTest(`nameof.toArray(o => [o.Prop1, o.Prop2, o.Prop3]);`, `["Prop1", "Prop2", "Prop3"];`); }); it("should return an array of values when given multiple arguments", () => { runTest(`nameof.toArray(myObject.Prop1, otherObject.Prop2);`, `["Prop1", "Prop2"];`); }); it("should return an array with a single element if a non-function argument is passed", () => { runTest(`nameof.toArray(myObject.Prop1);`, `["Prop1"];`); }); it("should support nested nameof calls", () => { runTest(`nameof.toArray(nameof.full(Some.Qualified.Name), Some.Qualified.Name);`, `["Some.Qualified.Name", "Name"];`); }); it("should support a non-arrow function expression", () => { runTest(`nameof.toArray(function(o) { return [o.Prop1, o.Prop2]; });`, `["Prop1", "Prop2"];`); }); it("should throw when the function argument does not return an array", () => { runThrowTest( `nameof.toArray(o => o.Prop1);`, "Unsupported toArray call expression. An array must be returned by the provided function: nameof.toArray((o) => o.Prop1)", ); }); it("should throw when no arguments are provided", () => { runThrowTest(`nameof.toArray();`, "Unable to parse call expression. No arguments provided: nameof.toArray()"); }); }); describe("split", () => { it("should return an array of values where each element is a subsequent part of the path provided", () => { runTest(`nameof.split(o => o.Prop1.Prop2.Prop3);`, `["Prop1", "Prop2", "Prop3"];`); }); it("should return an array of values where each element is a subsequent part of the path provided", () => { runTest(`nameof.split(o.Prop1.Prop2.Prop3);`, `["o", "Prop1", "Prop2", "Prop3"];`); }); it("should allow using a period index", () => { runTest(`nameof.split(MyTest.Test.This, 1);`, `["Test", "This"];`); }); it("should allow using a period index of 0", () => { runTest(`nameof.split(MyTest.Test.This, 0);`, `["MyTest", "Test", "This"];`); }); it("should allow using a period index up to its max value", () => { runTest(`nameof.split(MyTest.Test.This, 2);`, `["This"];`); }); it("should allow using a negative period index", () => { runTest(`nameof.split(MyTest.Test.This, -1);`, `["This"];`); }); it("should allow using a negative period index to its max value", () => { runTest(`nameof.split(MyTest.Test.This, -3);`, `["MyTest", "Test", "This"];`); }); it("should throw when the periodIndex is not a number literal", () => { runThrowTest(`nameof.split(MyTest.Test, 'test')`, `Expected count to be a number, but was: "test"`); }); it("should throw when the periodIndex is greater than the number of periods", () => { runThrowTest(`nameof.split(MyTest.Test, 2)`, "Count of 2 was larger than max count of 1: nameof.split(MyTest.Test, 2)"); }); it("should throw when the absolute value of the negative periodIndex is greater than the number of periods + 1", () => { runThrowTest(`nameof.split(MyTest.Test, -3)`, "Count of -3 was larger than max count of -2: nameof.split(MyTest.Test, -3)"); }); }); describe("general", () => { it("should error when specifying a different nameof property", () => { runThrowTest(`nameof.nonExistent()`, "Unsupported nameof call expression with property 'nonExistent': nameof.nonExistent()"); }); it("should replace handling comments", () => { const input = `nameof(window); // nameof(window); nameof(window); /* nameof(window); nameof(window); */ nameof(window); `; const expected = `"window"; // nameof(window); "window"; /* nameof(window); nameof(window); */ "window"; `; runTest(input, expected); }); it("should replace handling strings", () => { const input = `nameof(window); const t = /\`/g; \`nameof(window); / \${nameof(window)} \${nameof(alert)} nameof(window); \`; // test "nameof(window);"; "\\"nameof(window);"; 'nameof(window);'; '\\'\\"nameof(window);'; "C:\\\\"; nameof(window); \`\${() => { nameof(console); }}\`; `; const expected = `"window"; const t = /\`/g; \`nameof(window); / $\{"window"\} $\{"alert"\} nameof(window); \`; // test "nameof(window);"; "\\"nameof(window);"; "nameof(window);"; "'\\"nameof(window);"; "C:\\\\"; "window"; \`\${() => { "console"; }}\`; `; runTest(input, expected); }); it("should handle division operators", () => { const input = `const t = 2 / 1;\nnameof(testing);`; const expected = `const t = 2 / 1;\n"testing";`; runTest(input, expected); }); }); function runTest(text: string, expected: string) { if (options.commonPrefix != null) { text = options.commonPrefix + text; } const result = getTransformedText(text); if (!expected.endsWith("\n")) { expected += "\n"; } assert.strictEqual(formatter.formatText("file.ts", result), expected); } function runThrowTest(text: string, possibleExpectedMessages: string | string[]) { if (options.commonPrefix != null) { text = options.commonPrefix + text; } let transformedText: string | undefined; // for some reason, assert.throws was not working try { transformedText = getTransformedText(text); } catch (ex: any) { possibleExpectedMessages = getPossibleExpectedMessages(); const actualMessage = (ex as any).message; for (const message of possibleExpectedMessages) { if (message === actualMessage) { return; } } throw new Error( `Expected the error message of ${JSON.stringify(actualMessage)} to equal ` + `one of the following messages: ${JSON.stringify(possibleExpectedMessages)}`, ); } throw new Error(`Expected to throw, but returned: ${transformedText}`); function getPossibleExpectedMessages() { const result = getAsArray(); for (let i = result.length - 1; i >= 0; i--) { const originalText = result[i]; result[i] = "[ts-nameof]: " + originalText; // ts result.push("[ts-nameof:/file.ts]: " + originalText); // babel const babelPath = path.resolve(__dirname, "../../transforms-babel/src/tests/test.ts"); // todo: temporary... switch to one path in the year 2021 (old versions of babel won't have the prefixed path) result.push(`${babelPath}: [ts-nameof:${babelPath}]: ${originalText}`); // babel macro (not ideal, but whatever) result.push(`${path.resolve(__dirname, "../../ts-nameof.macro/src/tests/test.ts")}: ./ts-nameof.macro: [ts-nameof]: ${originalText}`); } return result; function getAsArray() { if (typeof possibleExpectedMessages === "string") { return [possibleExpectedMessages]; } return possibleExpectedMessages; } } } } function getFirstAccessedPropertyMustNotBeComputedErrorText(nodeText: string) { return `First accessed property must not be computed except if providing a string: ${nodeText}`; } function getNotSupportedErrorText(nodeText: string) { return `The node \`${nodeText}\` is not supported in this scenario.`; } function getUnusedNameofInterpolateErrorText(nodeText: string) { return `Found a nameof.interpolate that did not exist within a nameof.full call expression: nameof.interpolate(${nodeText})`; } ================================================ FILE: packages/tests-common/tsconfig.json ================================================ { "compilerOptions": { "composite": true, "declaration": true, "outDir": "./dist" }, "extends": "../../tsconfig.common.json", "include": ["./src/**/*.ts"] } ================================================ FILE: packages/transforms-babel/.mocharc.yml ================================================ require: ts-node/register recursive: true reporter: progress watch-extensions: ts timeout: 10000 spec: src/tests/**/*.ts ================================================ FILE: packages/transforms-babel/.npmignore ================================================ tsconfig.json tsconfig.tsbuildinfo src *.log *.js.map .mocharc.yml ================================================ FILE: packages/transforms-babel/README.md ================================================ # ts-nameof - Babel transforms Contains the babel transforms used in ts-nameof. ## Development Commands ``` npm run test ``` ================================================ FILE: packages/transforms-babel/package.json ================================================ { "name": "@ts-nameof/transforms-babel", "version": "4.2.1", "description": "ts-nameof - Babel transforms for ts-nameof packages.", "main": "./dist/index.js", "author": "David Sherret", "license": "MIT", "publishConfig": { "access": "public" }, "scripts": { "clean": "rimraf dist && tsc --b --clean", "build": "tsc --b", "test": "npm run build && mocha", "test:debug": "npm run build && mocha --inspect-brk" }, "dependencies": { "@ts-nameof/common": "^4.2.1", "@ts-nameof/transforms-common": "^4.2.1" }, "devDependencies": { "@babel/core": "^7.16.5", "@babel/preset-typescript": "^7.16.5", "@babel/types": "^7.16.0", "@ts-nameof/tests-common": "^4.2.0", "@types/babel__core": "^7.1.17", "@types/babel__generator": "^7.6.3", "@types/babel__template": "^7.4.1", "@types/babel__traverse": "^7.14.2", "@types/node": "^17.0.0", "mocha": "^9.1.3", "rimraf": "^3.0.2", "ts-node": "^10.4.0", "typescript": "^4.5.4" } } ================================================ FILE: packages/transforms-babel/src/VisitSourceFileContext.ts ================================================ import { Node } from "@babel/types"; export interface VisitSourceFileContext { interpolateExpressions: Set; } ================================================ FILE: packages/transforms-babel/src/helpers.ts ================================================ import * as babelTypes from "@babel/types"; import { BlockStatement, Node, UnaryExpression } from "@babel/types"; import { throwError } from "@ts-nameof/common"; export function isNegativeNumericLiteral(t: typeof babelTypes, node: Node): node is UnaryExpression { if (!t.isUnaryExpression(node)) { return false; } return node.operator === "-" && t.isNumericLiteral(node.argument); } export function getNegativeNumericLiteralValue(t: typeof babelTypes, node: UnaryExpression) { if (node.operator !== "-" || !t.isNumericLiteral(node.argument)) { return throwError("The passed in UnaryExpression must be for a negative numeric literal."); } return node.argument.value * -1; } export function getReturnStatementArgumentFromBlock(t: typeof babelTypes, block: BlockStatement) { for (const statement of block.body) { if (t.isReturnStatement(statement) && statement.argument != null) { return statement.argument; } } return undefined; } ================================================ FILE: packages/transforms-babel/src/index.ts ================================================ import * as babel from "@babel/core"; import { Node, NodePath } from "@babel/traverse"; import * as babelTypes from "@babel/types"; import { throwErrorForSourceFile } from "@ts-nameof/common"; import { transformCallExpression } from "@ts-nameof/transforms-common"; import { parse, ParseOptions } from "./parse"; import { transform } from "./transform"; export interface TransformOptions extends ParseOptions { } export function plugin({ types: t }: { types: typeof babelTypes }): babel.PluginItem { const visitor = { CallExpression(path: NodePath, state: unknown) { const filePath = (state as any).file.opts.filename as string; try { transformNode(t, path, { // temp assertion because I'm too lazy to investigate what's going on here traverseChildren: () => path.traverse(visitor as any, state as any), }); } catch (err: any) { return throwErrorForSourceFile(err.message, filePath); } }, }; return { visitor }; } export function transformNode(t: typeof babelTypes, path: NodePath, options: TransformOptions = {}) { const parseResult = parse(t, path, options); if (parseResult == null) { return; } const transformResult = transform(t, transformCallExpression(parseResult)); // temporary assertion due to conflicting type declaration versions path.replaceWith(transformResult as Node); } ================================================ FILE: packages/transforms-babel/src/parse.ts ================================================ import { NodePath } from "@babel/traverse"; import * as babelTypes from "@babel/types"; import { ArrayExpression, ArrowFunctionExpression, BlockStatement, CallExpression, Expression, FunctionExpression, MemberExpression, Node, NumericLiteral, StringLiteral, TemplateLiteral, TSImportType, TSQualifiedName, TSTypeParameterInstantiation, UnaryExpression, V8IntrinsicIdentifier, } from "@babel/types"; import { throwError } from "@ts-nameof/common"; import * as common from "@ts-nameof/transforms-common"; import { getNegativeNumericLiteralValue, getReturnStatementArgumentFromBlock, isNegativeNumericLiteral } from "./helpers"; export interface ParseOptions { /** * Action to prompt the children to be traversed. This is to allow traversing the nodes in post order. */ traverseChildren?: () => void; /** * Expected identifier name at the start of the call expression. This could be different when using a macro. * @default Defaults to "nameof". */ nameofIdentifierName?: string; } /** * Parses a Babel AST node to a common NameofCallExpression or returns undefined if the current node * is not a nameof call expression. * @param t - Babel types namespace to use. * @param path - Path of the current Babel AST node. * @param options - Options for parsing. * @remarks Parsing to a common structure allows for the same code to be used to determine the final string. */ export function parse(t: typeof babelTypes, path: NodePath, options: ParseOptions = {}) { if (!isNameof(path.node)) { return undefined; } if (options.traverseChildren) { options.traverseChildren(); // tell the caller to go over the nodes in post order } const propertyName = parsePropertyName(path.node); // ignore nameof.interpolate function calls... they will be dealt with later if (isInterpolatePropertyName(propertyName)) { handleNameofInterpolate(path.node); return undefined; } return parseNameof(path.node); function parseNameof(callExpr: CallExpression): common.NameofCallExpression { return { property: propertyName, typeArguments: parseTypeArguments(callExpr), arguments: parseArguments(callExpr), }; } function parsePropertyName(callExpr: CallExpression) { const { callee } = callExpr; if (!t.isMemberExpression(callee) || !t.isIdentifier(callee.property)) { return undefined; } return callee.property.name; } function parseTypeArguments(callExpr: CallExpression) { // babel uses incorrect naming. these are type arguments const typeArguments = (callExpr as any).typeParameters as TSTypeParameterInstantiation | undefined; if (typeArguments == null) { return []; } return typeArguments.params.map(arg => parseCommonNode(arg)); } function parseArguments(callExpr: CallExpression) { return callExpr.arguments.map(arg => parseCommonNode(arg)); } function parseCommonNode(node: Node): common.Node { if (t.isMemberExpression(node)) { return parseMemberExpression(node); } if (t.isArrowFunctionExpression(node)) { return parseFunctionReturnExpression(node, getArrowFunctionReturnExpression(node)); } if (t.isFunctionExpression(node)) { return parseFunctionReturnExpression(node, getReturnStatementArgumentFromBlockOrThrow(node.body)); } if (t.isTSNonNullExpression(node) || t.isParenthesizedExpression(node) || t.isTSAsExpression(node)) { return parseCommonNode(node.expression); } if (t.isTSQualifiedName(node)) { return parseQualifiedName(node); } if (t.isTSTypeReference(node)) { return parseCommonNode(node.typeName); } if (t.isSpreadElement(node)) { return parseCommonNode(node.argument); } if (t.isNumericLiteral(node) || isNegativeNumericLiteral(t, node)) { return parseNumeric(node); } if (t.isStringLiteral(node)) { return parseStringLiteral(node); } if (t.isIdentifier(node)) { return parseIdentifier(node); } if (t.isArrayExpression(node)) { return parseArrayExpression(node); } if (t.isThisExpression(node)) { return common.createIdentifierNode("this"); } if (t.isSuper(node)) { return common.createIdentifierNode("super"); } if (t.isTSImportType(node)) { return parseImportType(node, false); } if (t.isTSTypeQuery(node) && t.isTSImportType(node.exprName)) { return parseImportType(node.exprName, true); } if (t.isTSLiteralType(node)) { return parseCommonNode(node.literal); // skip over and go straight to the literal } if (t.isTemplateLiteral(node)) { return parseTemplateExpression(node); } if (isNameof(node) && isInterpolatePropertyName(parsePropertyName(node))) { return parseInterpolateNode(node); } return throwError(`Unhandled node type (${node.type}) in text: ${getNodeText(node)} (Please open an issue if you believe this should be supported.)`); } function parseArrayExpression(node: ArrayExpression) { const result: common.Node[] = []; node.elements.forEach(element => { if (element == null) { return throwError(`Unsupported scenario with empty element encountered in array: ${getNodeText(node)}`); } result.push(parseCommonNode(element)); }); return common.createArrayLiteralNode(result); } function parseMemberExpression(node: MemberExpression) { const expressionCommonNode = parseCommonNode(node.object); const nameCommonNode = parseCommonNode(node.property); const computedCommonNode = node.computed ? common.createComputedNode(nameCommonNode) : undefined; getEndCommonNode(expressionCommonNode).next = computedCommonNode || nameCommonNode; return expressionCommonNode; } function parseQualifiedName(node: TSQualifiedName) { const leftCommonNode = parseCommonNode(node.left); const rightCommonNode = parseCommonNode(node.right); getEndCommonNode(leftCommonNode).next = rightCommonNode; return leftCommonNode; } function parseNumeric(node: NumericLiteral | UnaryExpression) { return common.createNumericLiteralNode(getNodeValue()); function getNodeValue() { if (t.isNumericLiteral(node)) { return node.value; } return getNegativeNumericLiteralValue(t, node); } } function parseStringLiteral(node: StringLiteral) { return common.createStringLiteralNode(node.value); } function parseIdentifier(node: Node) { const text = getIdentifierTextOrThrow(node); return common.createIdentifierNode(text); } function parseFunctionReturnExpression(functionNode: FunctionExpression | ArrowFunctionExpression, node: Expression) { const parameterNames = functionNode.params.map(p => { if (t.isIdentifier(p)) { return p.name; } return getNodeText(p); }); return common.createFunctionNode(parseCommonNode(node), parameterNames); } function parseImportType(node: TSImportType, isTypeOf: boolean) { const importTypeNode = common.createImportTypeNode(isTypeOf, parseCommonNode(node.argument)); const qualifier = node.qualifier == null ? undefined : parseCommonNode(node.qualifier); getEndCommonNode(importTypeNode).next = qualifier; return importTypeNode; } function parseTemplateExpression(node: TemplateLiteral) { return common.createTemplateExpressionNode(getParts()); function getParts() { const parts: (string | common.InterpolateNode)[] = []; // the number of quasis will always be greater than the number of expressions for (let i = 0; i < node.quasis.length; i++) { parts.push(node.quasis[i].value.raw); const expression = node.expressions[i]; if (expression != null) { parts.push(common.createInterpolateNode(expression, getNodeText(expression))); } } return parts; } } function parseInterpolateNode(node: CallExpression) { if (node.arguments.length !== 1) { return throwError(`Expected a single argument for the nameof.interpolate function call ${getNodeText(node.arguments[0])}.`); } return common.createInterpolateNode(node.arguments[0], getNodeText(node.arguments[0])); } function getEndCommonNode(commonNode: common.Node) { while (commonNode.next != null) { commonNode = commonNode.next; } return commonNode; } function getArrowFunctionReturnExpression(func: ArrowFunctionExpression) { if (t.isBlock(func.body)) { return getReturnStatementArgumentFromBlockOrThrow(func.body); } return func.body; } function getIdentifierTextOrThrow(node: Node) { if (!t.isIdentifier(node)) { return throwError(`Expected node to be an identifier: ${getNodeText(node)}`); } return node.name; } function getReturnStatementArgumentFromBlockOrThrow(block: BlockStatement) { return getReturnStatementArgumentFromBlock(t, block) || throwError(`Cound not find return statement with an expression in function expression: ${getNodeText(block)}`); } function getNodeText(node: Node) { const outerNodeStart = path.node.start!; const innerNodeStart = node.start!; const offset = innerNodeStart - outerNodeStart; return path.getSource().substr(offset, node.end! - node.start!); } function isNameof(node: Node): node is CallExpression { if (!t.isCallExpression(node)) { return false; } const identifier = getIdentifierToInspect(node.callee); return identifier != null && identifier.name === (options.nameofIdentifierName || "nameof"); function getIdentifierToInspect(expression: Expression | V8IntrinsicIdentifier) { if (t.isIdentifier(expression)) { return expression; } if (t.isMemberExpression(expression) && t.isIdentifier(expression.object)) { return expression.object; } return undefined; } } function handleNameofInterpolate(callExpr: CallExpression) { if (!hasAncestorNameofFull()) { return throwError( `Found a nameof.interpolate that did not exist within a ` + `nameof.full call expression: ${getNodeText(callExpr)}`, ); } if (callExpr.arguments.length !== 1) { return throwError("Unexpected scenario where a nameof.interpolate function did not have a single argument."); } function hasAncestorNameofFull() { let parentPath: NodePath | null | undefined = path.parentPath; while (parentPath != null) { if (isNameof(parentPath.node) && parsePropertyName(parentPath.node) === "full") { return true; } parentPath = parentPath.parentPath; } return false; } } function isInterpolatePropertyName(propertyName: string | undefined) { return propertyName === "interpolate"; } } ================================================ FILE: packages/transforms-babel/src/tests/pluginTests.ts ================================================ import * as babel from "@babel/core"; import "@babel/preset-typescript"; import { runCommonTests } from "@ts-nameof/tests-common"; import * as path from "path"; import { plugin } from "../index"; runCommonTests(run); function run(text: string) { return babel.transformSync(text, { presets: [ "@babel/preset-typescript", ], plugins: [ plugin, ], filename: path.resolve(__dirname, "test.ts"), ast: false, generatorOpts: { retainLines: true, }, })!.code!; } ================================================ FILE: packages/transforms-babel/src/transform.ts ================================================ import * as babelTypes from "@babel/types"; import { throwError } from "@ts-nameof/common"; import * as common from "@ts-nameof/transforms-common"; /** * Transforms a common node to a Babel node. * @param node Common node to be transformed. */ export function transform(t: typeof babelTypes, node: common.Node): babelTypes.StringLiteral | babelTypes.ArrayExpression | babelTypes.TemplateLiteral { switch (node.kind) { case "StringLiteral": return t.stringLiteral(node.value); case "ArrayLiteral": return t.arrayExpression(node.elements.map(element => transform(t, element))); case "TemplateExpression": return createTemplateLiteral(t, node); default: return throwError(`Unsupported node kind: ${node.kind}`); } } function createTemplateLiteral(t: typeof babelTypes, node: common.TemplateExpressionNode) { const quasis: babelTypes.TemplateElement[] = []; const expressions: babelTypes.Expression[] = []; for (const part of node.parts) { if (typeof part === "string") { quasis.push(t.templateElement({ // I believe for the use case of this library, both the raw and cooked can be the same, but adding this // just in case for the future... raw: getRawValue(part), // Need to add this for @babel/preset-env. cooked: part, })); } else { const expr = part.expression as babelTypes.Expression; expressions.push(expr); } } // set the last quasi as the tail quasis[quasis.length - 1].tail = true; return t.templateLiteral(quasis, expressions); function getRawValue(text: string) { // From // Adds a backslash before every `, \ and ${ return text.replace(/\\|`|\${/g, "\\$&"); } } ================================================ FILE: packages/transforms-babel/tsconfig.json ================================================ { "compilerOptions": { "composite": true, "declaration": true, "outDir": "./dist" }, "extends": "../../tsconfig.common.json", "include": ["./src"], "references": [ { "path": "../common" }, { "path": "../transforms-common" }, { "path": "../tests-common" } ] } ================================================ FILE: packages/transforms-common/.mocharc.yml ================================================ require: ts-node/register recursive: true reporter: progress watch-extensions: ts timeout: 10000 spec: src/tests/**/*.ts ================================================ FILE: packages/transforms-common/.npmignore ================================================ tsconfig.json tsconfig.tsbuildinfo src dist/tests .mocharc.yml *.log *.js.map ================================================ FILE: packages/transforms-common/README.md ================================================ # ts-nameof - Transforms Common Contains the code shared between babel and typescript transforms. ================================================ FILE: packages/transforms-common/package.json ================================================ { "name": "@ts-nameof/transforms-common", "version": "4.2.1", "description": "ts-nameof - Common code for transforms.", "main": "./dist/index.js", "author": "David Sherret", "license": "MIT", "publishConfig": { "access": "public" }, "scripts": { "clean": "rimraf dist && tsc --b --clean", "build": "tsc --b", "test": "npm run build && mocha", "test:debug": "npm run build && mocha --inspect-brk" }, "dependencies": { "@ts-nameof/common": "^4.2.1" }, "devDependencies": { "@types/mocha": "^9.0.0", "@types/node": "^17.0.0", "mocha": "^9.1.3", "rimraf": "^3.0.2", "ts-node": "^10.4.0", "typescript": "^4.5.4" } } ================================================ FILE: packages/transforms-common/src/StringOrTemplateExpressionBuilder.ts ================================================ import { createStringLiteralNode, createTemplateExpressionNode } from "./nodeFactories"; import { InterpolateNode, StringLiteralNode, TemplateExpressionNode } from "./nodes"; /** * Builds up a string that will be a string literal if able, but will change to a template * expression if necessary. */ export class StringOrTemplateExpressionNodeBuilder { private text: string | undefined = ""; private items: (string | InterpolateNode)[] = []; hasText() { return this.text != null && this.text.length > 0 || this.items.length > 0; } buildNode(): StringLiteralNode | TemplateExpressionNode { if (this.text != null) { return createStringLiteralNode(this.text); } return createTemplateExpressionNode(this.items); } addItem(item: string | InterpolateNode | StringLiteralNode | TemplateExpressionNode) { if (typeof item === "string") { this.addText(item); } else if (item.kind === "StringLiteral") { this.addText(item.value); } else if (item.kind === "TemplateExpression") { for (const part of item.parts) { this.addItem(part); } } else { this.addInterpolate(item); } } addText(newText: string) { if (this.text == null) { if (typeof this.items[this.items.length - 1] === "string") { this.items[this.items.length - 1] += newText; } else { this.items.push(newText); } } else { this.text += newText; } } private addInterpolate(interpolate: InterpolateNode) { if (this.text != null) { this.items.push(this.text); this.text = undefined; } this.items.push(interpolate); } } ================================================ FILE: packages/transforms-common/src/index.ts ================================================ export * from "./nodeFactories"; export * from "./nodes"; export * from "./transformCallExpression"; ================================================ FILE: packages/transforms-common/src/nodeFactories.ts ================================================ import { ArrayLiteralNode, ComputedNode, FunctionNode, IdentifierNode, ImportTypeNode, InterpolateNode, Node, NumericLiteralNode, StringLiteralNode, TemplateExpressionNode, } from "./nodes"; export function createIdentifierNode(value: string, next?: Node | undefined): IdentifierNode { return { kind: "Identifier", value, next, }; } export function createStringLiteralNode(value: string, next?: Node | undefined): StringLiteralNode { return { kind: "StringLiteral", value, next, }; } export function createNumericLiteralNode(value: number, next?: Node | undefined): NumericLiteralNode { return { kind: "NumericLiteral", value, next, }; } export function createArrayLiteralNode(elements: ArrayLiteralNode["elements"], next?: Node | undefined): ArrayLiteralNode { return { kind: "ArrayLiteral", elements, next, }; } export function createComputedNode(value: Node, next?: Node | undefined): ComputedNode { return { kind: "Computed", value, next, }; } export function createFunctionNode(value: Node, parameterNames: string[], next?: Node | undefined): FunctionNode { return { kind: "Function", parameterNames, value, next, }; } export function createImportTypeNode(isTypeOf: boolean, argument: Node | undefined, next?: Node | undefined): ImportTypeNode { return { kind: "ImportType", isTypeOf, argument, next, }; } export function createTemplateExpressionNode(parts: (string | InterpolateNode)[], next?: Node | undefined): TemplateExpressionNode { return { kind: "TemplateExpression", parts, next, }; } export function createInterpolateNode(expression: unknown, expressionText: string, next?: Node | undefined): InterpolateNode { return { kind: "Interpolate", expression, expressionText, next, }; } ================================================ FILE: packages/transforms-common/src/nodeHelpers.ts ================================================ import { Node } from "./nodes"; export function flattenNodeToArray(node: Node) { const flattenedNodes: Node[] = [node]; while (node.next != null) { flattenedNodes.push(node.next); node = node.next; } return flattenedNodes; } export function getLastNextNode(node: Node) { while (node.next != null) { node = node.next; } return node; } ================================================ FILE: packages/transforms-common/src/nodes.ts ================================================ // common AST to share between babel and typescript export interface NameofCallExpression { property: string | undefined; typeArguments: Node[]; arguments: Node[]; } export type Node = | IdentifierNode | StringLiteralNode | NumericLiteralNode | ArrayLiteralNode | ComputedNode | FunctionNode | ImportTypeNode | TemplateExpressionNode | InterpolateNode; export interface IdentifierNode { kind: "Identifier"; value: string; next: Node | undefined; } export interface StringLiteralNode { kind: "StringLiteral"; value: string; next: Node | undefined; } export interface NumericLiteralNode { kind: "NumericLiteral"; value: number; next: Node | undefined; } export interface ArrayLiteralNode { kind: "ArrayLiteral"; elements: Node[]; next: Node | undefined; } /** * Node surrounded in brackets. * Ex. `[4]` in `obj[4]` */ export interface ComputedNode { kind: "Computed"; value: Node; next: Node | undefined; } export interface FunctionNode { kind: "Function"; parameterNames: string[]; value: Node; next: Node | undefined; } export interface ImportTypeNode { kind: "ImportType"; isTypeOf: boolean; argument: Node | undefined; next: Node | undefined; } export interface TemplateExpressionNode { kind: "TemplateExpression"; parts: (string | InterpolateNode)[]; next: Node | undefined; } /** * An interpolate node. * Ex. Created from call expressions such as: `nameof.interpolate(expression)` */ export interface InterpolateNode { kind: "Interpolate"; /** The original AST node. */ expression: unknown; /** The expression text for printing purposes. */ expressionText: string; next: Node | undefined; } ================================================ FILE: packages/transforms-common/src/printers.ts ================================================ import { assertNever } from "@ts-nameof/common"; import { NameofCallExpression, Node, TemplateExpressionNode } from "./nodes"; /** * Prints the call expression to a string. Useful for displaying diagnostic information to the user. * @param callExpr `nameof` call expression to print. */ export function printCallExpression(callExpr: NameofCallExpression) { let result = "nameof"; writePropertyName(); if (callExpr.typeArguments.length > 0) { writeTypeArguments(); } writeArguments(); return result; function writePropertyName() { if (callExpr.property != null) { result += `.${callExpr.property}`; } } function writeTypeArguments() { result += "<"; for (let i = 0; i < callExpr.typeArguments.length; i++) { if (i > 0) { result += ", "; } result += printNode(callExpr.typeArguments[i]); } result += ">"; } function writeArguments() { result += "("; for (let i = 0; i < callExpr.arguments.length; i++) { if (i > 0) { result += ", "; } result += printNode(callExpr.arguments[i]); } result += ")"; } } /** * Prints a node to a string. Useful for displaying diagnostic information to the user. * @param node Node to print. */ export function printNode(node: Node): string { // todo: this should throw in more scenarios (ex. string literal after an identifier) let result = getCurrentText(); if (node.next != null) { if (node.next.kind === "Identifier") { result += "." + printNode(node.next); } else { result += printNode(node.next); } } return result; function getCurrentText() { switch (node.kind) { case "StringLiteral": return `\"${node.value}\"`; case "NumericLiteral": return node.value.toString(); case "Identifier": return node.value; case "Computed": return `[${printNode(node.value)}]`; case "Function": let functionResult = `(${node.parameterNames.join(", ")}) => ${printNode(node.value)}`; if (node.next != null) { functionResult = `(${functionResult})`; } return functionResult; case "ArrayLiteral": return `[${node.elements.map(e => printNode(e)).join(", ")}]`; case "ImportType": return (node.isTypeOf ? "typeof " : "") + `import(${node.argument == null ? "" : printNode(node.argument)})`; case "Interpolate": return `nameof.interpolate(${node.expressionText})`; case "TemplateExpression": return printTemplateExpression(node); default: return assertNever(node, `Unhandled kind: ${(node as Node).kind}`); } } function printTemplateExpression(TemplateExpression: TemplateExpressionNode) { let text = "`"; for (const part of TemplateExpression.parts) { if (typeof part === "string") { text += part; } else { text += "${" + printNode(part) + "}"; } } text += "`"; return text; } } ================================================ FILE: packages/transforms-common/src/tests/printerTests.ts ================================================ import * as assert from "assert"; import * as factories from "../nodeFactories"; import { NameofCallExpression, Node } from "../nodes"; import * as printers from "../printers"; describe("printCallExpression", () => { function doTest(callExpr: NameofCallExpression, expectedText: string) { const result = printers.printCallExpression(callExpr); assert.equal(result, expectedText); } it("should print a basic call expression", () => { doTest({ property: undefined, typeArguments: [], arguments: [], }, "nameof()"); }); it("should print with a property", () => { doTest({ property: "full", typeArguments: [], arguments: [], }, "nameof.full()"); }); it("should print with an argument", () => { doTest({ property: undefined, typeArguments: [], arguments: [factories.createIdentifierNode("test")], }, "nameof(test)"); }); it("should print with arguments", () => { doTest({ property: undefined, typeArguments: [], arguments: [ factories.createIdentifierNode("test1"), factories.createIdentifierNode("test2"), ], }, "nameof(test1, test2)"); }); it("should print with a type argument", () => { doTest({ property: undefined, typeArguments: [factories.createIdentifierNode("T")], arguments: [], }, "nameof()"); }); it("should print with type arguments", () => { doTest({ property: undefined, typeArguments: [ factories.createIdentifierNode("T"), factories.createIdentifierNode("U"), ], arguments: [], }, "nameof()"); }); it("should print with everything", () => { doTest({ property: "full", typeArguments: [ factories.createIdentifierNode("T"), factories.createIdentifierNode("U"), ], arguments: [ factories.createIdentifierNode("test1"), factories.createIdentifierNode("test2"), ], }, "nameof.full(test1, test2)"); }); }); describe("printNode", () => { function doTest(node: Node, expectedText: string) { const result = printers.printNode(node); assert.equal(result, expectedText); } describe("identifier", () => { it("should print an identifier", () => { doTest(factories.createIdentifierNode("Test"), "Test"); }); it("should print the next identifier separated by a period", () => { doTest(factories.createIdentifierNode("Test", factories.createIdentifierNode("Next")), "Test.Next"); }); it("should print the next computed value with no separation", () => { const node = factories.createIdentifierNode("Test", factories.createComputedNode(factories.createStringLiteralNode("prop"))); doTest(node, `Test["prop"]`); }); }); describe("string literal", () => { it("should print in quotes", () => { doTest(factories.createStringLiteralNode("test"), `"test"`); }); it("should print with a property after", () => { const node = factories.createStringLiteralNode("test", factories.createIdentifierNode("length")); doTest(node, `"test".length`); }); }); describe("numeric literal", () => { it("should print", () => { doTest(factories.createNumericLiteralNode(5), `5`); }); it("should print with a property after", () => { const node = factories.createNumericLiteralNode(5, factories.createIdentifierNode("length")); doTest(node, `5.length`); }); }); describe("computed", () => { it("should print inside brackets", () => { const node = factories.createComputedNode(factories.createStringLiteralNode("test")); doTest(node, `["test"]`); }); it("should print with a property after", () => { const node = factories.createComputedNode(factories.createNumericLiteralNode(5), factories.createIdentifierNode("length")); doTest(node, `[5].length`); }); }); describe("function", () => { it("should print with no arguments", () => { const node = factories.createFunctionNode(factories.createNumericLiteralNode(5), []); doTest(node, `() => 5`); }); it("should print with an argument", () => { const node = factories.createFunctionNode(factories.createNumericLiteralNode(5), ["p"]); doTest(node, `(p) => 5`); // keep it simple (don't bother removing parens) }); it("should print with arguments", () => { const node = factories.createFunctionNode(factories.createNumericLiteralNode(5), ["a", "b"]); doTest(node, `(a, b) => 5`); }); it("should print with a property after", () => { const node = factories.createFunctionNode(factories.createNumericLiteralNode(5), ["a", "b"], factories.createIdentifierNode("length")); doTest(node, `((a, b) => 5).length`); }); }); describe("array", () => { it("should print the array with no elements", () => { const node = factories.createArrayLiteralNode([]); doTest(node, "[]"); }); it("should print the array with one element", () => { const node = factories.createArrayLiteralNode([factories.createStringLiteralNode("test")]); doTest(node, `["test"]`); }); it("should print the array with multiple elements", () => { const node = factories.createArrayLiteralNode([factories.createStringLiteralNode("test"), factories.createStringLiteralNode("test2")]); doTest(node, `["test", "test2"]`); }); it("should print with a property after", () => { const node = factories.createArrayLiteralNode([], factories.createIdentifierNode("length")); doTest(node, `[].length`); }); }); describe("import type", () => { it("should print when it has no argument", () => { const node = factories.createImportTypeNode(false, undefined, factories.createIdentifierNode("length")); doTest(node, `import().length`); }); it("should print when it receives an identifier", () => { const node = factories.createImportTypeNode(false, factories.createIdentifierNode("test"), undefined); doTest(node, `import(test)`); }); it("should print when it receives a string literal", () => { const node = factories.createImportTypeNode(false, factories.createStringLiteralNode("test"), undefined); doTest(node, `import("test")`); }); it("should print when it has a typeof", () => { const node = factories.createImportTypeNode(true, factories.createIdentifierNode("test")); doTest(node, `typeof import(test)`); }); }); describe("template literal", () => { it("should print when only has a string", () => { const node = factories.createTemplateExpressionNode(["testing"], factories.createIdentifierNode("length")); doTest(node, "`testing`.length"); }); it("should print when also has an interpolate node", () => { const node = factories.createTemplateExpressionNode(["testing", factories.createInterpolateNode(undefined, "myVar"), "this"]); // in practice, the printer will never be printing a template literal doTest(node, "`testing${nameof.interpolate(myVar)}this`"); }); }); describe("interpolate node", () => { it("should print", () => { const node = factories.createInterpolateNode(undefined, "myVar", factories.createIdentifierNode("length")); doTest(node, "nameof.interpolate(myVar).length"); }); }); }); ================================================ FILE: packages/transforms-common/src/transformCallExpression.ts ================================================ import { assertNever, throwError } from "@ts-nameof/common"; import { createArrayLiteralNode, createStringLiteralNode, createTemplateExpressionNode } from "./nodeFactories"; import { flattenNodeToArray, getLastNextNode } from "./nodeHelpers"; import { FunctionNode, NameofCallExpression, Node, StringLiteralNode, TemplateExpressionNode } from "./nodes"; import { printCallExpression, printNode } from "./printers"; import { StringOrTemplateExpressionNodeBuilder } from "./StringOrTemplateExpressionBuilder"; export function transformCallExpression(callExpr: NameofCallExpression) { if (callExpr.property == null) { return handleNameof(callExpr); } if (callExpr.property === "full") { return handleNameofFull(callExpr); } if (callExpr.property === "toArray") { return handleNameofToArray(callExpr); } if (callExpr.property === "split") { return handleNameofSplit(callExpr); } return throwError(`Unsupported nameof call expression with property '${callExpr.property}': ${printCallExpression(callExpr)}`); } function handleNameof(callExpr: NameofCallExpression) { return parseNameofExpression(getExpression()); function getExpression() { if (callExpr.arguments.length === 1) { return callExpr.arguments[0]; } else if (callExpr.typeArguments.length === 1) { return callExpr.typeArguments[0]; } return throwError(`Call expression must have one argument or type argument: ${printCallExpression(callExpr)}`); } } function handleNameofFull(callExpr: NameofCallExpression) { return parseNameofFullExpression(getNodesFromCallExpression(callExpr)); } function handleNameofSplit(callExpr: NameofCallExpression) { const literalNodes = getNodesFromCallExpression(callExpr).map(node => parseNode(node)); return createArrayLiteralNode(literalNodes); } function handleNameofToArray(callExpr: NameofCallExpression) { const arrayArguments = getNodeArray(); return createArrayLiteralNode(arrayArguments.map(element => parseNameofExpression(element))); function getNodeArray() { if (callExpr.arguments.length === 0) { return throwError(`Unable to parse call expression. No arguments provided: ${printCallExpression(callExpr)}`); } const firstArgument = callExpr.arguments[0]; if (callExpr.arguments.length === 1 && firstArgument.kind === "Function") { return handleFunction(firstArgument); } else { return callExpr.arguments; } function handleFunction(func: FunctionNode) { const functionReturnValue = func.value; if (functionReturnValue == null || functionReturnValue.kind !== "ArrayLiteral") { return throwError(`Unsupported toArray call expression. An array must be returned by the provided function: ${printCallExpression(callExpr)}`); } return functionReturnValue.elements; } } } function getNodesFromCallExpression(callExpr: NameofCallExpression) { const { expression, count } = getExpressionAndCount(); return getNodesFromCount(flattenNodeToArray(expression), count); function getExpressionAndCount() { if (shouldUseArguments()) { return { expression: getArgumentExpression(), count: getCountFromNode(callExpr.arguments.length > 1 ? callExpr.arguments[1] : undefined), }; } if (callExpr.typeArguments.length > 0) { return { expression: callExpr.typeArguments[0], count: getCountFromNode(callExpr.arguments.length > 0 ? callExpr.arguments[0] : undefined), }; } return throwError(`Unsupported use of nameof.full: ${printCallExpression(callExpr)}`); function shouldUseArguments() { if (callExpr.arguments.length === 0) { return false; } if (callExpr.typeArguments.length === 0) { return true; } return callExpr.arguments[0].kind === "Function"; } function getArgumentExpression() { let expression = callExpr.arguments[0]; if (expression.kind === "Function") { expression = expression.value; // skip over the first identifier (ex. skip over `obj` in `obj => obj.test`) if (expression.next == null) { return throwError(`A property must be accessed on the object: ${printNode(callExpr.arguments[0])}`); } expression = expression.next; } return expression; } function getCountFromNode(countExpr: Node | undefined) { if (countExpr == null) { return 0; } if (countExpr.kind !== "NumericLiteral") { return throwError(`Expected count to be a number, but was: ${printNode(countExpr)}`); } return countExpr.value; } } function getNodesFromCount(nodes: Node[], count: number) { if (count > 0) { if (count > nodes.length - 1) { return throwError(`Count of ${count} was larger than max count of ${nodes.length - 1}: ${printCallExpression(callExpr)}`); } return nodes.slice(count); } if (count < 0) { if (Math.abs(count) > nodes.length) { return throwError(`Count of ${count} was larger than max count of ${nodes.length * -1}: ${printCallExpression(callExpr)}`); } return nodes.slice(nodes.length + count); } return nodes; } } function parseNameofExpression(expression: Node) { return parseNode(getNodeForNameOf(), expression); function getNodeForNameOf() { const node = getLastNextNode(expression); if (node.kind === "Function") { const argument = node.value; if (argument.next == null) { return throwError(`A property must be accessed on the object: ${printNode(expression)}`); } return getLastNextNode(argument.next); } return node; } } function parseNode(node: Node, parent?: Node) { switch (node.kind) { case "Identifier": return createStringLiteralNode(node.value); case "StringLiteral": // make a copy return createStringLiteralNode(node.value); case "TemplateExpression": // todo: test this return createTemplateExpressionNode(node.parts); case "NumericLiteral": // make a copy return createStringLiteralNode(node.value.toString()); case "Function": return throwError(`Nesting functions is not supported: ${printNode(parent || node)}`); case "Computed": if (node.value.kind === "StringLiteral" && node.value.next == null) { return createStringLiteralNode(node.value.value); } return throwError(`First accessed property must not be computed except if providing a string: ${printNode(parent || node)}`); case "Interpolate": case "ArrayLiteral": case "ImportType": return throwNotSupportedErrorForNode(node); default: return assertNever(node, `Not implemented node: ${JSON.stringify(node)}`); } } function parseNameofFullExpression(expressionNodes: Node[]): StringLiteralNode | TemplateExpressionNode { const nodeBuilder = new StringOrTemplateExpressionNodeBuilder(); for (let i = 0; i < expressionNodes.length; i++) { const node = expressionNodes[i]; if (i > 0 && node.kind === "Identifier") { nodeBuilder.addText("."); } addNodeToBuilder(node); } return nodeBuilder.buildNode(); function addNodeToBuilder(node: Node) { switch (node.kind) { case "Identifier": nodeBuilder.addText(node.value); break; case "Computed": nodeBuilder.addText("["); const computedNodes = flattenNodeToArray(node.value); for (let i = 0; i < computedNodes.length; i++) { const computedNode = computedNodes[i]; if (computedNode.kind === "StringLiteral") { nodeBuilder.addText(`"${computedNode.value}"`); } else { if (i > 0 && computedNode.kind === "Identifier") { nodeBuilder.addText("."); } addNodeToBuilder(computedNode); } } nodeBuilder.addText("]"); break; case "TemplateExpression": case "StringLiteral": nodeBuilder.addItem(node); break; case "NumericLiteral": nodeBuilder.addText(node.value.toString()); break; case "Interpolate": nodeBuilder.addItem(node); break; case "ArrayLiteral": case "ImportType": case "Function": return throwNotSupportedErrorForNode(node); default: return assertNever(node, `Not implemented node: ${JSON.stringify(node)}`); } } } function throwNotSupportedErrorForNode(node: Node) { return throwError(`The node \`${printNode(node)}\` is not supported in this scenario.`); } ================================================ FILE: packages/transforms-common/tsconfig.json ================================================ { "compilerOptions": { "composite": true, "declaration": true, "outDir": "./dist" }, "extends": "../../tsconfig.common.json", "include": ["./src/**/*.ts"], "references": [ { "path": "../common" } ] } ================================================ FILE: packages/transforms-ts/.mocharc.yml ================================================ require: ts-node/register recursive: true reporter: progress watch-extensions: ts timeout: 10000 spec: src/tests/**/*.ts ================================================ FILE: packages/transforms-ts/.npmignore ================================================ tsconfig.json tsconfig.tsbuildinfo src dist/tests .mocharc.yml *.log *.js.map ================================================ FILE: packages/transforms-ts/README.md ================================================ # ts-nameof - TypeScript transforms Contains the TypeScript Compiler API transforms used in ts-nameof. ## Development Commands ``` npm run test ``` ================================================ FILE: packages/transforms-ts/package.json ================================================ { "name": "@ts-nameof/transforms-ts", "version": "4.2.1", "description": "ts-nameof - TypeScript compiler transforms for ts-nameof packages.", "main": "./dist/index.js", "author": "David Sherret", "license": "MIT", "publishConfig": { "access": "public" }, "scripts": { "clean": "rimraf dist && tsc --b --clean", "build": "tsc --b", "test": "npm run build && mocha", "test:debug": "npm run build && mocha --inspect-brk" }, "dependencies": { "@ts-nameof/common": "^4.2.1", "@ts-nameof/transforms-common": "^4.2.1" }, "devDependencies": { "@ts-nameof/tests-common": "^4.2.0", "mocha": "^9.1.3", "rimraf": "^3.0.2", "ts-node": "^10.4.0", "typescript": "^4.5.4" } } ================================================ FILE: packages/transforms-ts/src/VisitSourceFileContext.ts ================================================ import * as ts from "typescript"; export interface VisitSourceFileContext { interpolateExpressions: Set; } ================================================ FILE: packages/transforms-ts/src/helpers.ts ================================================ import { throwError } from "@ts-nameof/common"; import * as ts from "typescript"; export function isNegativeNumericLiteral(node: ts.Node): node is ts.PrefixUnaryExpression { if (!ts.isPrefixUnaryExpression(node)) { return false; } return node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand); } export function getNegativeNumericLiteralValue(node: ts.PrefixUnaryExpression) { if (node.operator !== ts.SyntaxKind.MinusToken || !ts.isNumericLiteral(node.operand)) { return throwError("The passed in PrefixUnaryExpression must be for a negative numeric literal."); } const result = parseFloat(node.operand.text); if (isNaN(result)) { return throwError(`Unable to parse negative numeric literal: ${node.operand.text}`); } return result * -1; } export function getReturnStatementExpressionFromBlock(block: ts.Block) { for (const statement of block.statements) { if (ts.isReturnStatement(statement) && statement.expression != null) { return statement.expression; } } return undefined; } // todo: remove the use of the printer except for exceptions const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); export function getNodeText(node: ts.Node, sourceFile: ts.SourceFile) { return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile); } ================================================ FILE: packages/transforms-ts/src/index.ts ================================================ export { TransformResult } from "./transform"; export * from "./transformerFactory"; export { VisitSourceFileContext } from "./VisitSourceFileContext"; ================================================ FILE: packages/transforms-ts/src/parse.ts ================================================ import { assertNever, throwError } from "@ts-nameof/common"; import * as common from "@ts-nameof/transforms-common"; import { createInterpolateNode, InterpolateNode } from "@ts-nameof/transforms-common"; import * as ts from "typescript"; import { getNegativeNumericLiteralValue, getNodeText, getReturnStatementExpressionFromBlock, isNegativeNumericLiteral } from "./helpers"; import { VisitSourceFileContext } from "./VisitSourceFileContext"; /** * Parses a TypeScript AST node to a common NameofCallExpression or returns undefined if the current node * is not a nameof call expression. * @param parsingNode - Babel AST node to parse. * @param sourceFile - Containing source file. * @param context - Context for when visiting all the source file nodes */ export function parse(parsingNode: ts.Node, sourceFile: ts.SourceFile, context: VisitSourceFileContext | undefined) { if (!isNameof(parsingNode)) { return undefined; } const propertyName = parsePropertyName(parsingNode); // Ignore nameof.interpolate function calls... they will be dealt with later. if (isInterpolatePropertyName(propertyName)) { handleNameofInterpolate(parsingNode); return undefined; } return parseNameof(parsingNode); function parseNameof(callExpr: ts.CallExpression): common.NameofCallExpression { return { property: propertyName, typeArguments: parseTypeArguments(callExpr), arguments: parseArguments(callExpr), }; } function parsePropertyName(callExpr: ts.CallExpression) { const { expression } = callExpr; if (!ts.isPropertyAccessExpression(expression) || !ts.isIdentifier(expression.name)) { return undefined; } return expression.name.text; } function parseTypeArguments(callExpr: ts.CallExpression) { if (callExpr.typeArguments == null) { return []; } return callExpr.typeArguments.map(arg => parseCommonNode(arg)); } function parseArguments(callExpr: ts.CallExpression) { return callExpr.arguments.map(arg => parseCommonNode(arg)); } function parseCommonNode(node: ts.Expression | ts.TypeNode | ts.EntityName): common.Node { if (ts.isPropertyAccessExpression(node)) { return parsePropertyAccessExpression(node); } if (ts.isElementAccessExpression(node)) { return parseElementAccessExpression(node); } if (ts.isArrowFunction(node)) { return parseFunctionReturnExpression(node, getArrowFunctionReturnExpression(node)); } if (ts.isFunctionExpression(node)) { return parseFunctionReturnExpression(node, getReturnStatementExpressionFromBlockOrThrow(node.body)); } if (ts.isNonNullExpression(node) || ts.isParenthesizedExpression(node) || ts.isAsExpression(node)) { return parseCommonNode(node.expression); } if (ts.isQualifiedName(node)) { return parseQualifiedName(node); } if (ts.isTypeReferenceNode(node)) { return parseCommonNode(node.typeName); } if (ts.isSpreadElement(node)) { return parseCommonNode(node.expression); } if (ts.isNumericLiteral(node) || isNegativeNumericLiteral(node)) { return parseNumeric(node); } if (ts.isStringLiteral(node)) { return parseStringLiteral(node); } if (ts.isArrayLiteralExpression(node)) { return parseArrayLiteralExpression(node); } if (ts.isIdentifier(node)) { return parseIdentifier(node); } if (ts.isImportTypeNode(node)) { return parseImportType(node); } if (ts.isLiteralTypeNode(node)) { return parseCommonNode(node.literal); // skip over and go straight to the literal } if (node.kind === ts.SyntaxKind.ThisKeyword) { return common.createIdentifierNode("this"); } if (node.kind === ts.SyntaxKind.SuperKeyword) { return common.createIdentifierNode("super"); } if (ts.isNoSubstitutionTemplateLiteral(node)) { return common.createTemplateExpressionNode([node.text]); } if (ts.isTemplateExpression(node)) { return parseTemplateExpression(node); } if (isNameof(node) && isInterpolatePropertyName(parsePropertyName(node))) { return parseInterpolateNode(node); } return throwError( `Unhandled node kind (${node.kind}) in text: ${getNodeText(node, sourceFile)}` + ` (Please open an issue if you believe this should be supported.)`, ); } function parseArrayLiteralExpression(node: ts.ArrayLiteralExpression) { const elements = node.elements.map(element => parseCommonNode(element)); return common.createArrayLiteralNode(elements); } function parsePropertyAccessExpression(node: ts.PropertyAccessExpression) { const expressionCommonNode = parseCommonNode(node.expression); const nameCommonNode = parseIdentifier(node.name); getEndCommonNode(expressionCommonNode).next = nameCommonNode; return expressionCommonNode; } function parseElementAccessExpression(node: ts.ElementAccessExpression) { const expressionCommonNode = parseCommonNode(node.expression); const argumentExpressionCommonNode = parseCommonNode(node.argumentExpression); const computedCommonNode = common.createComputedNode(argumentExpressionCommonNode); getEndCommonNode(expressionCommonNode).next = computedCommonNode; return expressionCommonNode; } function parseQualifiedName(node: ts.QualifiedName) { const leftCommonNode = parseCommonNode(node.left); const rightCommonNode = parseCommonNode(node.right); getEndCommonNode(leftCommonNode).next = rightCommonNode; return leftCommonNode; } function parseNumeric(node: ts.NumericLiteral | ts.PrefixUnaryExpression) { return common.createNumericLiteralNode(getNodeValue()); function getNodeValue() { if (ts.isNumericLiteral(node)) { return parseFloat(node.text); } return getNegativeNumericLiteralValue(node); } } function parseStringLiteral(node: ts.StringLiteral) { return common.createStringLiteralNode(node.text); } function parseIdentifier(node: ts.Node) { const text = getIdentifierTextOrThrow(node); return common.createIdentifierNode(text); } function parseFunctionReturnExpression(functionLikeNode: ts.FunctionLike, node: ts.Expression) { const parameterNames = functionLikeNode.parameters.map(p => { const name = p.name; if (ts.isIdentifier(name)) { return name.text; } return getNodeText(name, sourceFile); }); return common.createFunctionNode(parseCommonNode(node), parameterNames); } function parseImportType(node: ts.ImportTypeNode) { const importType = common.createImportTypeNode(node.isTypeOf || false, node.argument && parseCommonNode(node.argument)); const qualifier = node.qualifier && parseCommonNode(node.qualifier); getEndCommonNode(importType).next = qualifier; return importType; } function parseTemplateExpression(node: ts.TemplateExpression) { return common.createTemplateExpressionNode(getParts()); function getParts() { const parts: (string | InterpolateNode)[] = []; if (node.head.text.length > 0) { parts.push(node.head.text); } for (const templateSpan of node.templateSpans) { parts.push(createInterpolateNode(templateSpan.expression, getNodeText(templateSpan.expression, sourceFile))); parts.push(templateSpan.literal.text); } return parts; } } function parseInterpolateNode(node: ts.CallExpression) { if (node.arguments.length !== 1) { return throwError(`Should never happen as this would have been tested for earlier.`); } return common.createInterpolateNode(node.arguments[0], getNodeText(node.arguments[0], sourceFile)); } function getEndCommonNode(commonNode: common.Node) { while (commonNode.next != null) { commonNode = commonNode.next; } return commonNode; } function getArrowFunctionReturnExpression(func: ts.ArrowFunction) { if (ts.isBlock(func.body)) { return getReturnStatementExpressionFromBlockOrThrow(func.body); } return func.body; } function getIdentifierTextOrThrow(node: ts.Node) { if (!ts.isIdentifier(node)) { return throwError(`Expected node to be an identifier: ${getNodeText(node, sourceFile)}`); } return node.text; } function getReturnStatementExpressionFromBlockOrThrow(block: ts.Block) { return getReturnStatementExpressionFromBlock(block) || throwError(`Cound not find return statement with an expression in function expression: ${getNodeText(block, sourceFile)}`); } function handleNameofInterpolate(callExpr: ts.CallExpression) { if (callExpr.arguments.length !== 1) { return throwError("Unexpected scenario where a nameof.interpolate function did not have a single argument."); } // Add the interpolate expression to the context so that it can be checked later to find // nameof.interpolate calls that were never resolved. if (context != null) { context.interpolateExpressions.add(callExpr.arguments[0]); } } function isNameof(node: ts.Node): node is ts.CallExpression { if (!ts.isCallExpression(node)) { return false; } const identifier = getIdentifierToInspect(node.expression); return identifier != null && identifier.text === "nameof"; function getIdentifierToInspect(expression: ts.LeftHandSideExpression) { if (ts.isIdentifier(expression)) { return expression; } if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression)) { return expression.expression; } } } function isInterpolatePropertyName(propertyName: string | undefined) { return propertyName === "interpolate"; } } ================================================ FILE: packages/transforms-ts/src/tests/transformerFactoryTests.ts ================================================ import { runCommonTests } from "@ts-nameof/tests-common"; import * as ts from "typescript"; import { transformerFactory } from "../transformerFactory"; runCommonTests(run); function run(text: string) { const results: { fileName: string; fileText: string }[] = []; const compilerOptions: ts.CompilerOptions = { strictNullChecks: true, target: ts.ScriptTarget.ES2017, }; const transformers: ts.CustomTransformers = { before: [transformerFactory], after: [], }; const testFileName = "/file.ts"; const host: ts.CompilerHost = { fileExists: (fileName: string) => fileName === testFileName, readFile: (fileName: string) => fileName === testFileName ? text : undefined, getSourceFile: (fileName, languageVersion) => { if (fileName !== testFileName) { return undefined; } return ts.createSourceFile(fileName, text, languageVersion, false, ts.ScriptKind.TS); }, getDefaultLibFileName: options => ts.getDefaultLibFileName(options), writeFile: () => { throw new Error("Not implemented"); }, getCurrentDirectory: () => "/", getDirectories: () => [], getCanonicalFileName: fileName => fileName, useCaseSensitiveFileNames: () => true, getNewLine: () => "\n", }; const program = ts.createProgram(["/file.ts"], compilerOptions, host); program.emit(undefined, (fileName, fileText) => results.push({ fileName, fileText }), undefined, false, transformers); return results[0].fileText; } ================================================ FILE: packages/transforms-ts/src/transform.ts ================================================ import { throwError } from "@ts-nameof/common"; import * as common from "@ts-nameof/transforms-common"; import * as ts from "typescript"; import { VisitSourceFileContext } from "./VisitSourceFileContext"; /** * Resulting node type of a nameof transform. */ export type TransformResult = ts.StringLiteral | ts.ArrayLiteralExpression | ts.NoSubstitutionTemplateLiteral | ts.TemplateExpression; /** * Transforms a common node to a TypeScript compiler node. * @param node Common node to be transformed. */ export function transform(node: common.Node, context: VisitSourceFileContext | undefined): TransformResult { switch (node.kind) { case "StringLiteral": return ts.createLiteral(node.value); case "ArrayLiteral": return ts.createArrayLiteral(node.elements.map(element => transform(element, context))); case "TemplateExpression": if (node.parts.length === 1 && typeof node.parts[0] === "string") { return ts.createNoSubstitutionTemplateLiteral(node.parts[0] as string); } return createTemplateExpression(node, context); default: return throwError(`Unsupported node kind: ${node.kind}`); } } function createTemplateExpression(node: common.TemplateExpressionNode, context: VisitSourceFileContext | undefined) { const firstPart = typeof node.parts[0] === "string" ? node.parts[0] as string : undefined; const parts = firstPart != null ? node.parts.slice(1) : [...node.parts]; return ts.createTemplateExpression(ts.createTemplateHead(firstPart || ""), getParts()); function getParts() { const templateSpans: ts.TemplateSpan[] = []; for (let i = 0; i < parts.length; i += 2) { const isLast = i + 2 === parts.length; const interpolatedNode = parts[i]; if (typeof interpolatedNode === "string") { return throwError("Unexpected scenario where an interpolated node was expected, but a string was found."); } const text = parts[i + 1]; if (typeof text !== "string") { return throwError("Unexpected scenario where a string was expected, but an interpolated node was found."); } const tsExpression = interpolatedNode.expression as ts.Expression; const tsText = !isLast ? ts.createTemplateMiddle(text) : ts.createTemplateTail(text); // mark this nameof.interpolate expression as being handled if (context != null) { context.interpolateExpressions.delete(tsExpression); } templateSpans.push(ts.createTemplateSpan(tsExpression, tsText)); } return templateSpans; } } ================================================ FILE: packages/transforms-ts/src/transformerFactory.ts ================================================ import { throwError, throwErrorForSourceFile } from "@ts-nameof/common"; import { transformCallExpression } from "@ts-nameof/transforms-common"; import * as ts from "typescript"; import { getNodeText } from "./helpers"; import { parse } from "./parse"; import { transform, TransformResult } from "./transform"; import { VisitSourceFileContext } from "./VisitSourceFileContext"; /** Transformer factory for performing nameof transformations. */ export const transformerFactory: ts.TransformerFactory = context => { return file => visitSourceFile(file, context) as ts.SourceFile; }; /** Visits all the nodes of the source file. */ export function visitSourceFile(sourceFile: ts.SourceFile, context: ts.TransformationContext) { const visitSourceFileContext: VisitSourceFileContext = { interpolateExpressions: new Set(), }; try { const result = visitNodeAndChildren(sourceFile); throwIfContextHasInterpolateExpressions(visitSourceFileContext, sourceFile); return result; } catch (err: any) { return throwErrorForSourceFile(err.message, sourceFile.fileName); } function visitNodeAndChildren(node: ts.Node): ts.Node { if (node == null) { return node; } // visit the children in post order node = ts.visitEachChild(node, childNode => visitNodeAndChildren(childNode), context); return visitNode(node, sourceFile, visitSourceFileContext); } } /** * Throws if the context contains any remaining interpolate expressions. * @param context - Context to check. * @param sourceFile - Source file being transformed. */ export function throwIfContextHasInterpolateExpressions(context: VisitSourceFileContext, sourceFile: ts.SourceFile) { if (context.interpolateExpressions.size > 0) { const firstResult = Array.from(context.interpolateExpressions.values())[0]; return throwError( `Found a nameof.interpolate that did not exist within a ` + `nameof.full call expression: nameof.interpolate(${getNodeText(firstResult, sourceFile)})`, ); } } /** Visit a node and do a nameof transformation on it if necessary. */ export function visitNode(visitingNode: ts.Node, sourceFile: ts.SourceFile): TransformResult; /** @internal */ export function visitNode(visitingNode: ts.Node, sourceFile: ts.SourceFile, context: VisitSourceFileContext | undefined): TransformResult; export function visitNode(visitingNode: ts.Node, sourceFile: ts.SourceFile, context?: VisitSourceFileContext) { const parseResult = parse(visitingNode, sourceFile, context); if (parseResult == null) { return visitingNode; } return transform(transformCallExpression(parseResult), context); } ================================================ FILE: packages/transforms-ts/tsconfig.json ================================================ { "compilerOptions": { "composite": true, "declaration": true, "outDir": "./dist" }, "extends": "../../tsconfig.common.json", "include": ["./src/**/*.ts"], "references": [ { "path": "../common" }, { "path": "../transforms-common" }, { "path": "../tests-common" } ] } ================================================ FILE: packages/ts-nameof/.mocharc.yml ================================================ require: ts-node/register recursive: true reporter: progress watch-extensions: ts timeout: 10000 spec: src/tests/**/*.ts ================================================ FILE: packages/ts-nameof/.npmignore ================================================ /node_modules /.vscode /.vs /.git /obj /temp /bin /src /dist/tests /setup /scripts /lib *.js.map *.v12.suo *.csproj *.csproj.user *.sln *.log .travis.yml .gitignore CHANGELOG.md .mocharc.yml tsconfig.json tsconfig.scripts.json tsconfig.tsbuildinfo ================================================ FILE: packages/ts-nameof/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2019 David Sherret Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/ts-nameof/README.md ================================================ # ts-nameof [![npm version](https://badge.fury.io/js/ts-nameof.svg)](https://badge.fury.io/js/ts-nameof) [![Build Status](https://travis-ci.org/dsherret/ts-nameof.svg)](https://travis-ci.org/dsherret/ts-nameof) [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) [`nameof`](https://msdn.microsoft.com/en-us/library/dn986596.aspx) in TypeScript. ``` npm install ts-nameof @types/ts-nameof --save-dev ``` ## Setup ### 1. Build Setup Follow any of these instructions: - [Webpack](https://github.com/dsherret/ts-nameof/blob/master/packages/ts-nameof/setup/webpack.md) - [Gulp](https://github.com/dsherret/ts-nameof/blob/master/packages/ts-nameof/setup/gulp.md) - [FuseBox](https://github.com/dsherret/ts-nameof/blob/master/packages/ts-nameof/setup/fusebox.md) - [tsc](https://github.com/dsherret/ts-nameof/blob/master/packages/ts-nameof/setup/tsc.md) - [Jest](https://github.com/dsherret/ts-nameof/blob/master/packages/ts-nameof/setup/jest.md) - [Custom](https://github.com/dsherret/ts-nameof/blob/master/packages/ts-nameof/setup/custom.md) - Others - Open an [issue](https://github.com/dsherret/ts-nameof/issues). These instructions need updating to use the transformation API, but will still work in the meantime: - Nuxt - Use [https://github.com/Kukks/nuxt-ts-nameof](https://github.com/Kukks/nuxt-ts-nameof#readme) ### 2. Declaring global `nameof` function Install `@types/ts-nameof`: ``` npm install @types/ts-nameof --save-dev ``` ## Transforms [Read here](https://github.com/dsherret/ts-nameof/blob/master/README.md) ## Other - [Contributing](https://github.com/dsherret/ts-nameof/blob/master/CONTRIBUTING.md) - [Development](https://github.com/dsherret/ts-nameof/blob/master/DEVELOPMENT.md) ================================================ FILE: packages/ts-nameof/lib/declarationFileTests.ts ================================================ /// /* istanbul ignore next */ import tsNameOf = require("ts-nameof"); import { assert, IsExact } from "conditional-type-checks"; import * as tsNameOfEs6 from "ts-nameof"; /* istanbul ignore next */ function testFunc() { tsNameOf.replaceInFiles(["test"]); tsNameOf.replaceInFiles(["test"]).then(() => {}); // replaceInText const replaceInTextResult = tsNameOf.replaceInText("fileName.ts", "const t = 5;"); console.log(replaceInTextResult); assert>(true); assert>(true); // es6 test const es6Result = tsNameOfEs6.replaceInText("file.ts", ""); console.log(es6Result.replaced); } ================================================ FILE: packages/ts-nameof/package.json ================================================ { "name": "ts-nameof", "version": "5.0.0", "description": "nameof in TypeScript", "main": "dist/main.js", "typings": "ts-nameof.d.ts", "scripts": { "clean": "rimraf dist && tsc --b --clean", "build": "tsc --b && npm run build:declarations", "build-test": "tsc --build && npm run --silent copy-test-files", "test": "npm run build-test && mocha", "test:debug": "npm run build-test && mocha --inspect-brk", "copy-test-files": "rimraf temp && copyfiles -u 2 \"./src/tests/testFiles/**/*{.js,.txt}\" \"./temp\"", "build:declarations": "ts-node --project scripts/tsconfig.json scripts/generation/main create-declaration-file && npm run --silent verify-declaration-file", "verify-declaration-file": "ts-node --project scripts/tsconfig.json scripts/verification/main verify-declaration-file", "dopublish": "npm run test && npm run build && npm run verify-declaration-file && echo \"run: npm publish --otp\"" }, "repository": { "type": "git", "url": "git+https://github.com/dsherret/ts-nameof.git", "directory": "packages/ts-nameof" }, "keywords": [ "nameof", "typescript", "transformer", "custom-transformer" ], "author": "David Sherret", "license": "MIT", "bugs": { "url": "https://github.com/dsherret/ts-nameof/issues" }, "homepage": "https://github.com/dsherret/ts-nameof#readme", "peerDependencies": { "typescript": "*" }, "dependencies": { "@ts-nameof/common": "^4.2.1", "@ts-nameof/transforms-ts": "^4.2.1", "glob": "^7.2.0" }, "devDependencies": { "@ts-nameof/scripts-common": "^4.0.2", "@ts-nameof/tests-common": "^4.2.0", "@types/glob": "^7.2.0", "@types/mocha": "^9.0.0", "@types/node": "^17.0.0", "conditional-type-checks": "^1.0.5", "copyfiles": "^2.4.1", "mocha": "^9.1.3", "rimraf": "^3.0.2", "ts-morph": "^13.0.2", "ts-node": "^10.4.0", "typescript": "^4.4.3" } } ================================================ FILE: packages/ts-nameof/scripts/common/createProject.ts ================================================ import { Project } from "ts-morph"; export function getProject() { return new Project({ tsConfigFilePath: "tsconfig.json", compilerOptions: { declaration: true } }); } ================================================ FILE: packages/ts-nameof/scripts/common/index.ts ================================================ export * from "./createProject"; ================================================ FILE: packages/ts-nameof/scripts/generation/createDeclarationFile.ts ================================================ import { ModuleDeclarationKind, Project } from "ts-morph"; export function createDeclarationFile(project: Project) { const mainFile = project.getSourceFileOrThrow("src/main.ts"); const outputFiles = mainFile.getEmitOutput({ emitOnlyDtsFiles: true }).getOutputFiles(); if (outputFiles.length !== 1) { throw new Error(`Expected 1 file when emitting, but had ${outputFiles.length}`); } const declarationFile = project.createSourceFile("ts-nameof.d.ts", outputFiles[0].getText(), { overwrite: true }); removePreceedingCommentReference(); commentExternalTypes(); removeTypeScriptImport(); wrapInGlobalModule(); addGlobalDeclarations(); function removePreceedingCommentReference() { const firstChild = declarationFile.getFirstChildOrThrow(); declarationFile.removeText(0, firstChild.getStart()); } function commentExternalTypes() { // these types are made to be any so that this library will work when included in // web projects and NodeJS does not exist. See issue #22. const typesToComment = [ "ts.TransformerFactory", "NodeJS.ErrnoException", ]; declarationFile.forEachDescendant(descendant => { if (typesToComment.indexOf(descendant.getText()) >= 0) { descendant.replaceWithText(`any /* ${descendant.getText()} */`); } }); } function removeTypeScriptImport() { declarationFile.getImportDeclarationOrThrow("typescript").remove(); } function wrapInGlobalModule() { const fileText = declarationFile.getText(); declarationFile.removeText(); const apiModule = declarationFile.addModule({ hasDeclareKeyword: true, declarationKind: ModuleDeclarationKind.Module, name: `"ts-nameof"`, }); apiModule.setBodyText(fileText); apiModule.getVariableStatementOrThrow(s => s.getDeclarations().some(d => d.getName() === "api")) .setHasDeclareKeyword(false); } function addGlobalDeclarations() { const globalFile = project.addSourceFileAtPath("../../lib/global.d.ts"); declarationFile.addStatements(writer => { writer.newLine(); writer.write(globalFile.getText().replace(/\r?\n$/, "")); }); } } ================================================ FILE: packages/ts-nameof/scripts/generation/main.ts ================================================ import { ArgsChecker } from "@ts-nameof/scripts-common"; import { getProject } from "../common"; import { createDeclarationFile } from "./createDeclarationFile"; const argsChecker = new ArgsChecker(); const project = getProject(); if (argsChecker.checkHasArg("create-declaration-file")) { console.log("Creating declaration file..."); createDeclarationFile(project); } argsChecker.verifyArgsUsed(); project.save(); ================================================ FILE: packages/ts-nameof/scripts/tsconfig.json ================================================ { "compilerOptions": { "target": "es6", "noEmit": true }, "extends": "../../../tsconfig.common.json", "exclude": [ "./lib", "./src" ] } ================================================ FILE: packages/ts-nameof/scripts/verification/main.ts ================================================ import { ArgsChecker } from "@ts-nameof/scripts-common"; import { verifyDeclarationFile } from "./verifyDeclarationFile"; const argsChecker = new ArgsChecker(); if (argsChecker.checkHasArg("verify-declaration-file")) { console.log("Verifying declaration file..."); verifyDeclarationFile(); } argsChecker.verifyArgsUsed(); ================================================ FILE: packages/ts-nameof/scripts/verification/verifyDeclarationFile.ts ================================================ import { Project } from "ts-morph"; export function verifyDeclarationFile() { const project = new Project(); const declarationFile = project.addSourceFileAtPath("ts-nameof.d.ts"); const declarationFileTests = project.addSourceFileAtPath("lib/declarationFileTests.ts"); const diagnostics = [...declarationFile.getPreEmitDiagnostics(), ...declarationFileTests.getPreEmitDiagnostics()]; if (diagnostics.length > 0) { console.error(project.formatDiagnosticsWithColorAndContext(diagnostics)); } } ================================================ FILE: packages/ts-nameof/setup/custom.md ================================================ # Using ts-nameof with a Custom Setup ## Transformation API The export from `ts-nameof` is a `ts.TransformerFactory` so that can be used anywhere custom transformers are accepted. For example, see the [webpack instructions](webpack.md). ```ts const tsNameof = require("ts-nameof"); // tsNameof is a ts.TransformerFactory ``` If you find this works for the library you're using to do a build then please consider submitting a PR with setup instructions for that library. ## Working with text Note that these solution require reparsing the source file text and that might make the build slow. The other solutions mostly all use the transformation api which will be much faster. ### Replacing in Files You can use `replaceInFiles` to replace in .ts files: ```javascript var replaceInFiles = require("ts-nameof").replaceInFiles; replaceInFiles(["./dist/**/*.ts"]); ``` 1. Copy your .ts files to a build folder. This is necessary so you don't overwrite your original source files. 2. Run `replaceInFiles` on these files. 3. Compile the final typescript files. ### Replacing in Text You can also use the `replaceInText` function to replace occurrences of `nameof` in any string: ```javascript var replaceInText = require("ts-nameof").replaceInText; var replacedText = replaceInText("filename.ts", "nameof(test);"); ``` ================================================ FILE: packages/ts-nameof/setup/fusebox.md ================================================ # Using ts-nameof with FuseBox To use ts-nameof with [FuseBox](https://github.com/fuse-box/fuse-box), specify it as a custom transformer: ```javascript const tsNameof = require("ts-nameof"); FuseBox.init({ transformers: { before: [tsNameof], }, }); ``` ================================================ FILE: packages/ts-nameof/setup/gulp.md ================================================ # Using ts-nameof with Gulp Specify it as a custom transformer with [gulp-typescript](https://github.com/ivogabe/gulp-typescript): ```javascript const gulp = require("gulp"); const ts = require("gulp-typescript"); const tsNameof = require("ts-nameof"); gulp.task("typescript", function() { gulp.src("src/**/*.ts") .pipe(ts({ getCustomTransformers: () => ({ before: [tsNameof] }), })) .pipe(gulp.dest("dist")); }); ``` ================================================ FILE: packages/ts-nameof/setup/jest.md ================================================ # Using ts-nameof with Jest 1. Setup jest with [ts-jest](https://github.com/kulshekhar/ts-jest) 2. `npm install --save-dev ts-nameof @types/ts-nameof` 3. In _package.json_ specify... ```jsonc { // ... "jest": { "globals": { "ts-jest": { "astTransformers": ["ts-nameof"] } } } } ``` ...or in _jest.config.js_... ```ts module.exports = { // ... globals: { "ts-jest": { "astTransformers": ["ts-nameof"], }, }, }; ``` ================================================ FILE: packages/ts-nameof/setup/tsc.md ================================================ # Using ts-nameof with tsc Transformation plugins are currently not supported by `tsc` alone. Please go and upvote [this issue](https://github.com/Microsoft/TypeScript/issues/14419) on TypeScript's issue tracker. In the meantime, this is possible using [ttypescript](https://github.com/cevek/ttypescript) thanks to [@cevek](https://github.com/cevek)! ## Setup 1. Install `ttypescript` and `ts-nameof`: ```bash npm install --save-dev ttypescript ts-nameof // or yarn add --dev ttypescript ts-nameof ``` 2. Add `ts-nameof` to `tsconfig.json` as a custom transformer: ```json { "compilerOptions": { "plugins": [{ "transform": "ts-nameof", "type": "raw" }] } } ``` 3. Compile with `ttsc` instead of `tsc`: ```bash npx ttsc ``` ### Swapping out TypeScript with TTypeScript Read the instructions at [`ttypescript`'s GitHub page](https://github.com/cevek/ttypescript) for how to use with tools like `ts-node` and visual studio code. Generally, most build tools have a way to swap out which version of the compiler you use (ex. using environment variables or command line arguments) so this should work across a lot of build tools. ================================================ FILE: packages/ts-nameof/setup/webpack.md ================================================ # Using ts-nameof with Webpack ## ts-loader / awesome-typescript-loader If using [ts-loader](https://github.com/TypeStrong/ts-loader) or [awesome-typescript-loader](https://github.com/s-panferov/awesome-typescript-loader), specify ts-nameof as a custom transformation like so in _webpack.config.js_: ```ts const tsNameof = require("ts-nameof"); module.exports = { // ...etc... module: { rules: [{ test: /\.tsx?$/, use: [{ loader: "ts-loader", // or awesome-typescript-loader options: { getCustomTransformers: () => ({ before: [tsNameof] }), }, }], }], }, }; ``` ================================================ FILE: packages/ts-nameof/src/main.ts ================================================ import { transformerFactory } from "@ts-nameof/transforms-ts"; import * as ts from "typescript"; import { replaceInFiles, replaceInText } from "./text"; interface Api { (): ts.TransformerFactory; replaceInFiles(fileNames: ReadonlyArray): Promise; replaceInText(fileName: string, fileText: string): { fileText?: string; replaced: boolean }; } const api: Api = transformerFactory as any as Api; api.replaceInFiles = replaceInFiles; api.replaceInText = replaceInText; // this is for ts-jest support... not ideal (api as any).factory = () => transformerFactory; export = api; ================================================ FILE: packages/ts-nameof/src/tests/testFiles/GeneralTestFile.txt ================================================ console.log(nameof(alert)); console.log(nameof(window.alert)); console.log(nameof.full(window.alert)); console.log(nameof.full(window .alert)); console.log(nameof.full(window.alert.length, -3)); console.log(nameof.full(window.alert.length, -2)); console.log(nameof.full(window.alert.length, -1)); console.log(nameof.full(window.alert.length, 0)); console.log(nameof.full(window.alert.length, 1)); console.log(nameof.full(window.alert.length, 2)); console.log( nameof( window ) ); console.log( nameof (window) ); console.log(nameof(nameof(nameof(clearTimeout)))); console.log(nameof>()); console.log(nameof.full>()); console.log(nameof(i => i.prop)); console.log(nameof(function(i) { return i.prop; })); console.log(nameof(function(i) { return i.prop })); console.log(nameof.full()); console.log(nameof.full(-2)); console.log(nameof.full(-1)); console.log(nameof.full(0)); console.log(nameof.full(1)); ================================================ FILE: packages/ts-nameof/src/tests/testFiles/StreamNoNameofTestFile.txt ================================================ console.log(""); ================================================ FILE: packages/ts-nameof/src/tests/testFiles/StreamTestFile.txt ================================================ nameof(window); ================================================ FILE: packages/ts-nameof/src/tests/testFiles/globFolder/MyGlobTestFile.txt ================================================ console.log(nameof(console)); ================================================ FILE: packages/ts-nameof/src/tests/testFiles/issues/11-expected.txt ================================================ class Test { private x: Test; public y: Test; public z: string; constructor() { "x"; "y"; "this.x.y.z"; "x.y.z"; } } ================================================ FILE: packages/ts-nameof/src/tests/testFiles/issues/11-source.txt ================================================ class Test { private x: Test; public y: Test; public z: string; constructor() { nameof(this.x); nameof(this.x.y); nameof.full(this.x.y.z); nameof.full(this.x.y.z, 1); } } ================================================ FILE: packages/ts-nameof/src/tests/testFiles/issues/8-expected.txt ================================================ import CodeBlockWriter from "code-block-writer"; import {expect} from "chai"; import {FunctionDefinition, InterfaceMethodDefinition, ClassMethodDefinition} from "./../../definitions"; import {FunctionBodyWriter} from "./../../writers"; import {WriteFlags} from "./../../WriteFlags"; import * as mocks from "./mocks"; describe("FunctionBodyWriter", () => { function createObjects() { const writer = new CodeBlockWriter(); const defWriter = new FunctionBodyWriter(writer); return {writer, defWriter}; } describe("write", () => { it(`should not write out the function body if ${"HideFunctionBodies"} is set`, () => { const def = new FunctionDefinition(); const {writer, defWriter} = createObjects(); defWriter.write(def, WriteFlags.HideFunctionBodies); expect(writer.toString()).to.equal(";"); }); it(`should not write out the function body if it's an ${"InterfaceMethodDefinition"}`, () => { const def = new InterfaceMethodDefinition(); const {writer, defWriter} = createObjects(); defWriter.write(def, WriteFlags.None); expect(writer.toString()).to.equal(";"); }); }); }); ================================================ FILE: packages/ts-nameof/src/tests/testFiles/issues/8-source.txt ================================================ import CodeBlockWriter from "code-block-writer"; import {expect} from "chai"; import {FunctionDefinition, InterfaceMethodDefinition, ClassMethodDefinition} from "./../../definitions"; import {FunctionBodyWriter} from "./../../writers"; import {WriteFlags} from "./../../WriteFlags"; import * as mocks from "./mocks"; describe(nameof(FunctionBodyWriter), () => { function createObjects() { const writer = new CodeBlockWriter(); const defWriter = new FunctionBodyWriter(writer); return {writer, defWriter}; } describe(nameof(w => w.write), () => { it(`should not write out the function body if ${nameof(WriteFlags.HideFunctionBodies)} is set`, () => { const def = new FunctionDefinition(); const {writer, defWriter} = createObjects(); defWriter.write(def, WriteFlags.HideFunctionBodies); expect(writer.toString()).to.equal(";"); }); it(`should not write out the function body if it's an ${nameof(InterfaceMethodDefinition)}`, () => { const def = new InterfaceMethodDefinition(); const {writer, defWriter} = createObjects(); defWriter.write(def, WriteFlags.None); expect(writer.toString()).to.equal(";"); }); }); }); ================================================ FILE: packages/ts-nameof/src/tests/text/helpers/fileHelpers.ts ================================================ import * as fs from "fs"; export function readFile(path: string) { return new Promise((resolve, reject) => { fs.readFile(path, { encoding: "utf-8" }, (err, data) => { if (err) { reject(err); return; } resolve(data); }); }); } export function writeFile(path: string, contents: string) { return new Promise((resolve, reject) => { fs.writeFile(path, contents, err => { if (err) { reject(err); return; } resolve(); }); }); } ================================================ FILE: packages/ts-nameof/src/tests/text/helpers/getTestFilePath.ts ================================================ import * as path from "path"; export function getTestFilePath(...paths: string[]) { return path.join("./temp/testFiles", ...paths); } ================================================ FILE: packages/ts-nameof/src/tests/text/helpers/index.ts ================================================ export * from "./fileHelpers"; export * from "./getTestFilePath"; ================================================ FILE: packages/ts-nameof/src/tests/text/issuesTests.ts ================================================ import * as assert from "assert"; import { replaceInFiles } from "../../text"; import { getTestFilePath, readFile, writeFile } from "./helpers"; describe("replaceInFiles()", () => { async function runTest(fileName: string, expectedFileName: string) { fileName = getTestFilePath(fileName); expectedFileName = getTestFilePath(expectedFileName); const originalFileText = await readFile(expectedFileName); try { await replaceInFiles([fileName]); const data = await readFile(fileName); const expectedContents = await readFile(expectedFileName); assert.equal(data.replace(/\r?\n/g, "\n"), expectedContents.replace(/\r?\n/g, "\n")); } finally { await writeFile(expectedFileName, originalFileText); } } function runIssueTest(issueNumber: number) { describe(`issue ${issueNumber}`, () => { it("should replace", async () => { await runTest(`issues/${issueNumber}-source.txt`, `issues/${issueNumber}-expected.txt`); }); }); } runIssueTest(8); runIssueTest(11); }); ================================================ FILE: packages/ts-nameof/src/tests/text/replaceInFilesTests.ts ================================================ import * as assert from "assert"; import { replaceInFiles } from "../../text"; import { getTestFilePath, readFile, writeFile } from "./helpers"; describe("replaceInFiles()", () => { interface FileInfo { filePath: string; contents: string; } async function runTest(paths: string[], expectedFiles: FileInfo[]) { paths = paths.map(p => getTestFilePath(p)); expectedFiles.forEach(f => f.filePath = getTestFilePath(f.filePath)); const initialFiles = await Promise.all(expectedFiles.map(f => readFile(f.filePath).then(data => ({ filePath: f.filePath, contents: data, } as FileInfo)) )); try { await replaceInFiles(paths); const readFilePromises = expectedFiles.map(f => readFile(f.filePath).then(data => ({ data, expectedContents: f.contents }))); for (const promise of readFilePromises) { const { data, expectedContents } = await promise; assert.equal(data.replace(/\r?\n/g, "\n"), expectedContents.replace(/\r?\n/g, "\n")); } } finally { await Promise.all(initialFiles.map(f => writeFile(f.filePath, f.contents))); } } describe("glob support", () => { it("should replace in MyGlobTestFile.txt", async () => { await runTest(["globFolder/**/*.txt"], [{ filePath: "globFolder/MyGlobTestFile.txt", contents: `console.log("console");\n`, }]); }); }); describe("general file", () => { it("should have the correct number of characters", async () => { // because an IDE might auto-format the code, this makes sure that hasn't happened assert.equal((await readFile(getTestFilePath("GeneralTestFile.txt"))).replace(/\r?\n/g, "\n").length, 1121); }); const expected = `console.log("alert"); console.log("alert"); console.log("window.alert"); console.log("window.alert"); console.log("window.alert.length"); console.log("alert.length"); console.log("length"); console.log("window.alert.length"); console.log("alert.length"); console.log("length"); console.log( "window" ); console.log( "window" ); console.log("clearTimeout"); console.log("Array"); console.log("Array"); console.log("prop"); console.log("prop"); console.log("prop"); console.log("MyNamespace.MyInnerInterface"); console.log("MyNamespace.MyInnerInterface"); console.log("MyInnerInterface"); console.log("MyNamespace.MyInnerInterface"); console.log("MyInnerInterface"); `; describe("file modifying test", () => { it("should modify the file", async () => { await runTest(["GeneralTestFile.txt"], [{ filePath: "GeneralTestFile.txt", contents: expected, }]); }); }); }); }); ================================================ FILE: packages/ts-nameof/src/tests/text/replaceInTextTests.ts ================================================ import { runCommonTests } from "@ts-nameof/tests-common"; import * as assert from "assert"; import { replaceInText } from "../../text"; describe("replaceInText", () => { it("should unofficially maintain backwards compatibility when providing one argument", () => { assert.equal((replaceInText as any)("nameof(window);").fileText, `"window";`); }); it("should not replace when no nameof", () => { const result = replaceInText("file.ts", "some random text with no nameof in it"); assert.equal(result.replaced, false); assert.equal(result.fileText, undefined); }); it("should replace when there was a nameof", () => { const result = replaceInText("file.ts", "describe(nameof(myTest), () => {});"); assert.equal(result.replaced, true); assert.equal(result.fileText, `describe("myTest", () => {});`); }); it("should replace when there was a nameof in tsx file", () => { const result = replaceInText("file.tsx", "const t =
;"); assert.equal(result.replaced, true); assert.equal(result.fileText, `const t =
;`); }); runCommonTests(text => { return replaceInText("file.ts", text).fileText || text; }); }); ================================================ FILE: packages/ts-nameof/src/text/getFileNamesFromGlobs.ts ================================================ import glob from "glob"; export function getFileNamesFromGlobs(globs: ReadonlyArray) { const promises = globs.map(g => getFileNamesFromGlob(g)); return Promise.all(promises).then(values => values.reduce((a, b) => a.concat(b), [])); } function getFileNamesFromGlob(globFileName: string) { return new Promise((resolve, reject) => { glob(globFileName, (err, files) => { /* istanbul ignore if */ if (err) { reject(err); return; } resolve(files); }); }); } ================================================ FILE: packages/ts-nameof/src/text/index.ts ================================================ export * from "./getFileNamesFromGlobs"; export * from "./replaceInFiles"; export * from "./replaceInText"; ================================================ FILE: packages/ts-nameof/src/text/replaceInFiles.ts ================================================ import * as fs from "fs"; import { getFileNamesFromGlobs } from "./getFileNamesFromGlobs"; import { replaceInText } from "./replaceInText"; export function replaceInFiles(fileNames: ReadonlyArray): Promise { return getFileNamesFromGlobs(fileNames).then(globbedFileNames => doReplaceInFiles(globbedFileNames)); } function doReplaceInFiles(fileNames: ReadonlyArray) { const promises: Promise[] = []; fileNames.forEach(fileName => { promises.push( new Promise((resolve, reject) => { fs.readFile(fileName, { encoding: "utf8" }, (err, fileText) => { /* istanbul ignore if */ if (err) { reject(err); return; } const result = replaceInText(fileName, fileText); if (result.replaced) { fs.writeFile(fileName, result.fileText!, writeErr => { /* istanbul ignore if */ if (writeErr) { reject(writeErr); return; } resolve(); }); } else { resolve(); } }); }), ); }); return Promise.all(promises); } ================================================ FILE: packages/ts-nameof/src/text/replaceInText.ts ================================================ import { throwIfContextHasInterpolateExpressions, visitNode, VisitSourceFileContext } from "@ts-nameof/transforms-ts"; import * as ts from "typescript"; const printer = ts.createPrinter(); export function replaceInText(fileName: string, fileText: string): { fileText?: string; replaced: boolean } { // unofficial pre-2.0 backwards compatibility for this method if (arguments.length === 1) { fileText = fileName; fileName = "/file.tsx"; // assume tsx } const visitSourceFileContext: VisitSourceFileContext = { interpolateExpressions: new Set(), }; const sourceFile = ts.createSourceFile(fileName, fileText, ts.ScriptTarget.Latest, false); const transformations: { start: number; end: number; text: string }[] = []; const transformerFactory: ts.TransformerFactory = context => { // this will always use the source file above return _ => visitSourceFile(context); }; ts.transform(sourceFile, [transformerFactory]); throwIfContextHasInterpolateExpressions(visitSourceFileContext, sourceFile); if (transformations.length === 0) { return { replaced: false }; } return { fileText: getTransformedText(), replaced: true }; function getTransformedText() { let finalText = ""; let lastPos = 0; for (const transform of transformations) { finalText += fileText.substring(lastPos, transform.start); finalText += transform.text; lastPos = transform.end; } finalText += fileText.substring(lastPos); return finalText; } function visitSourceFile(context: ts.TransformationContext) { return visitNodeAndChildren(sourceFile) as ts.SourceFile; function visitNodeAndChildren(node: ts.Node): ts.Node { if (node == null) { return node; } node = ts.visitEachChild(node, childNode => visitNodeAndChildren(childNode), context); const resultNode = visitNode(node, sourceFile, visitSourceFileContext); const wasTransformed = resultNode !== node; if (wasTransformed) { storeTransformation(); } return resultNode; function storeTransformation() { const nodeStart = node.getStart(sourceFile); const lastTransformation = transformations[transformations.length - 1]; // remove the last transformation if it's nested within this transformation if (lastTransformation != null && lastTransformation.start > nodeStart) { transformations.pop(); } transformations.push({ start: nodeStart, end: node.end, text: printer.printNode(ts.EmitHint.Unspecified, resultNode, sourceFile), }); } } } } ================================================ FILE: packages/ts-nameof/ts-nameof.d.ts ================================================ declare module "ts-nameof" { interface Api { (): any /* ts.TransformerFactory */; replaceInFiles(fileNames: ReadonlyArray): Promise; replaceInText(fileName: string, fileText: string): { fileText?: string; replaced: boolean; }; } const api: Api; export = api; } declare function nameof(func?: (obj: T) => any): string; /** * Gets a string representation of the last identifier of the given expression. * * @example nameof(console) -> "console" * @example nameof(console.log) -> "log" * @example nameof(console["warn"]) -> "warn" * * @param obj An expression for which the last identifier will be parsed. */ declare function nameof(obj: any): string; declare namespace nameof { /** * Gets the string representation of the entire type parameter expression. * * @example nameof.full() -> "MyNamespace.MyInnerInterface" * @example nameof.full(1) -> "MyInnerInterface" * @example nameof.full>() -> "Array" * @example nameof.full>(-1) -> "MyInnerInterface" * * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function full(periodIndex?: number): string; /** * Gets the string representation of the entire resultant expression. * * @example nameof.full(o => o.prop.prop2) -> "prop.prop2" * @example nameof.full(o => o.prop.prop2.prop3, 1) -> "prop2.prop3" * @example nameof.full(o => o.prop.prop2.prop3, -1) -> `"prop3" * * @param func A function for which the result will be parsed, excluding the parameter's identifier. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function full(func: (obj: T) => any, periodIndex?: number): string; /** * Gets the string representation of the entire given expression. * * @example nameof.full(console.log) -> "console.log" * @example nameof.full(window.alert.length, -1) -> "length" * @example nameof.full(window.alert.length, 2) -> "length" * * @param obj The expression which will be parsed. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function full(obj: any, periodIndex?: number): string; /** * Gets an array containing the string representation of the final identifier of each expression in the array returned by the provided function. * * @example nameof.toArray(o => [o.firstProp, o.otherProp.secondProp, o.other]) -> ["firstProp", "secondProp", "other"] * @example nameof.toArray(o => [o.prop, nameof.full(o.myProp.otherProp, 1)]) -> ["prop", "myProp.otherProp"] * * @param func A function returning an array of expressions to be parsed, excluding the parameter's identifier. */ function toArray(func: (obj: T) => any[]): string[]; /** * Gets an array containing the string representation of each expression in the arguments. * * @example nameof.toArray(myObject, otherObject) -> ["myObject", "otherObject"] * @example nameof.toArray(obj.firstProp, obj.secondProp, otherObject, nameof.full(obj.other)) -> ["firstProp", "secondProp", "otherObject", "obj.other"] * * @param args An array of expressions to be parsed. */ function toArray(...args: any[]): string[]; /** * Embeds an expression into the string representation of the result of nameof.full. * * @example nameof.full(myObj.prop[nameof.interpolate(i)]) -> `myObj.prop[${i}]` * * @param value The value to interpolate. */ function interpolate(value: T): T; /** * Gets an array of strings where each element is a subsequent part of the expression provided. * * @example nameof.split(o => o.prop.prop2.prop3) -> ["prop", "prop2", "prop3"] * @example nameof.split(o => o.prop.prop2.prop3, 1) -> ["prop2", "prop3"] * @example nameof.split(o => o.prop.prop2.prop3, -1) -> ["prop", "prop2"] * * @param func A function for which the resultant parts will be parsed, excluding the parameter's identifier. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function split(func: (obj: T) => any, periodIndex?: number): string[]; /** * Gets an array of strings where each element is a subsequent part of the expression provided. * * @example nameof.split(myObj.prop.prop2.prop3) -> ["myObj", "prop", "prop2", "prop3"] * @example nameof.split(myObj.prop.prop2.prop3, -3);`, `["prop", "prop2", "prop3"]; * @example nameof.split(myObj.prop.prop2.prop3, 2);`, `["prop2", "prop3"] * * @param obj An expression for which the parts will be parsed. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function split(obj: any, periodIndex?: number): string[]; } ================================================ FILE: packages/ts-nameof/tsconfig.json ================================================ { "compilerOptions": { "outDir": "./dist" }, "extends": "../../tsconfig.common.json", "include": ["./src"], "references": [ { "path": "../transforms-ts" }, { "path": "../tests-common" }, { "path": "../scripts-common" } ] } ================================================ FILE: packages/ts-nameof.macro/.mocharc.yml ================================================ require: ts-node/register recursive: true reporter: progress watch-extensions: ts timeout: 10000 spec: src/tests/**/*.ts ================================================ FILE: packages/ts-nameof.macro/.npmignore ================================================ /node_modules /.vscode /.vs /.git /obj /temp /bin /src /dist/tests /setup /scripts /lib *.js.map *.v12.suo *.csproj *.csproj.user *.sln *.log .travis.yml .gitignore CHANGELOG.md .mocharc.yml tsconfig.json tsconfig.tsbuildinfo ================================================ FILE: packages/ts-nameof.macro/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2019 David Sherret Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: packages/ts-nameof.macro/README.md ================================================ # ts-nameof.macro [![npm version](https://badge.fury.io/js/ts-nameof.macro.svg)](https://badge.fury.io/js/ts-nameof.macro) [![Build Status](https://travis-ci.org/dsherret/ts-nameof.svg)](https://travis-ci.org/dsherret/ts-nameof) [![Babel Macro](https://img.shields.io/badge/babel--macro-%F0%9F%8E%A3-f5da55.svg?style=flat-square)](https://github.com/kentcdodds/babel-plugin-macros) [![stable](http://badges.github.io/stability-badges/dist/stable.svg)](http://github.com/badges/stability-badges) [`nameof`](https://msdn.microsoft.com/en-us/library/dn986596.aspx) in TypeScript. This is a [babel macro](https://github.com/kentcdodds/babel-plugin-macros) of [ts-nameof](https://github.com/dsherret/ts-nameof). ## Setup 1. Install dependencies: ``` npm install --save-dev babel-plugin-macros ts-nameof.macro ``` 2. Ensure `babel-plugin-macros` is properly setup ([Instructions](https://github.com/kentcdodds/babel-plugin-macros/blob/master/other/docs/user.md)). 3. Import and use the default export. For example: ```ts import nameof from "ts-nameof.macro"; nameof(window.alert); ``` Transforms to: ```ts "alert"; ``` ## Transforms [Read here](https://github.com/dsherret/ts-nameof/blob/master/README.md) ## Other - [Contributing](https://github.com/dsherret/ts-nameof/blob/master/CONTRIBUTING.md) - [Development](https://github.com/dsherret/ts-nameof/blob/master/DEVELOPMENT.md) ================================================ FILE: packages/ts-nameof.macro/package.json ================================================ { "name": "ts-nameof.macro", "version": "4.2.2", "description": "Babel macro for nameof in TypeScript.", "main": "dist/index.js", "types": "ts-nameof.macro.d.ts", "scripts": { "clean": "rimraf dist && tsc --b --clean", "build": "tsc --b && npm run build:declarations", "build:declarations": "ts-node --project scripts/tsconfig.json scripts/generation/main create-declaration-file", "test": "tsc --build && mocha", "test:debug": "npm run build && mocha --inspect-brk", "dopublish": "npm run install && npm run build && echo \"Run: npm publish --otp\"" }, "keywords": [ "nameof", "typescript", "transforms", "babel", "babel-plugin-macros" ], "repository": { "type": "git", "url": "git+https://github.com/dsherret/ts-nameof.git", "directory": "packages/ts-nameof.macro" }, "author": "David Sherret", "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "dependencies": { "@ts-nameof/transforms-babel": "^4.2.1" }, "devDependencies": { "@babel/core": "^7.16.5", "@babel/preset-typescript": "^7.16.5", "@ts-nameof/scripts-common": "^4.0.2", "@ts-nameof/tests-common": "^4.2.0", "@types/babel__core": "^7.1.17", "@types/babel__generator": "^7.6.3", "@types/babel__template": "^7.4.1", "@types/babel__traverse": "^7.14.2", "@types/mocha": "^9.0.0", "@types/node": "^17.0.0", "babel-plugin-macros": "^3.1.0", "mocha": "^9.1.3", "rimraf": "^3.0.2", "ts-morph": "^13.0.2", "ts-node": "^10.4.0", "typescript": "^4.5.4" } } ================================================ FILE: packages/ts-nameof.macro/scripts/common/createProject.ts ================================================ import { Project } from "ts-morph"; export function getProject() { return new Project({ tsConfigFilePath: "tsconfig.json" }); } ================================================ FILE: packages/ts-nameof.macro/scripts/common/index.ts ================================================ export * from "./createProject"; ================================================ FILE: packages/ts-nameof.macro/scripts/generation/createDeclarationFile.ts ================================================ import { ModuleDeclarationKind, Node, Project } from "ts-morph"; export function createDeclarationFile(project: Project) { const globalFile = project.addSourceFileAtPath("../../lib/global.d.ts"); const declarationFile = project.createSourceFile("ts-nameof.macro.d.ts", "", { overwrite: true }); const namespaceDec = declarationFile.addModule({ name: `"ts-nameof.macro"`, declarationKind: ModuleDeclarationKind.Module, hasDeclareKeyword: true, }); namespaceDec.setBodyText(globalFile.getFullText()); for (const statement of namespaceDec.getStatements()) { if (Node.isAmbientable(statement)) { statement.setHasDeclareKeyword(false); } } namespaceDec.addExportAssignment({ expression: "nameof", isExportEquals: false, }); } ================================================ FILE: packages/ts-nameof.macro/scripts/generation/main.ts ================================================ import { ArgsChecker } from "@ts-nameof/scripts-common"; import { getProject } from "../common"; import { createDeclarationFile } from "./createDeclarationFile"; const argsChecker = new ArgsChecker(); const project = getProject(); if (argsChecker.checkHasArg("create-declaration-file")) { console.log("Creating declaration file..."); createDeclarationFile(project); } argsChecker.verifyArgsUsed(); project.save(); ================================================ FILE: packages/ts-nameof.macro/scripts/tsconfig.json ================================================ { "compilerOptions": { "target": "es6", "noEmit": true }, "extends": "../../../tsconfig.common.json", "exclude": [ "./lib", "./src" ] } ================================================ FILE: packages/ts-nameof.macro/src/index.js ================================================ /// @ts-check /// import { transformNode } from "@ts-nameof/transforms-babel"; import { createMacro, MacroError } from "babel-plugin-macros"; export default createMacro(nameofMacro); // @ts-ignore function nameofMacro({ references, state, babel }) { // go over in reverse as if traversing in post order const reverseDefault = references.default.slice().reverse(); // @ts-ignore reverseDefault.forEach(path => { const t = babel.types; transformNode(t, getPath(), { // tell the transformation to expect this identifier's name nameofIdentifierName: path.node.name, }); function getPath() { const parentPath = path.parentPath; // identifier; if (parentPath.type === "CallExpression") { return parentPath; } const grandParentPath = parentPath.parentPath; if (parentPath.type === "MemberExpression" && grandParentPath.type === "CallExpression") { return grandParentPath; } throw new MacroError("[ts-nameof]: Could not find a call expression at path: " + grandParentPath.getSource()); } }); } ================================================ FILE: packages/ts-nameof.macro/src/references.d.ts ================================================ declare module "babel-plugin-macros"; ================================================ FILE: packages/ts-nameof.macro/src/tests/macroTests.ts ================================================ /// import * as babel from "@babel/core"; import "@babel/preset-typescript"; import { runCommonTests } from "@ts-nameof/tests-common"; import * as assert from "assert"; import babelPluginMacros from "babel-plugin-macros"; import * as path from "path"; runCommonTests(run, { commonPrefix: "import nameof from './ts-nameof.macro';\n" }); describe("using a name other than nameof", () => { it("should work when using a different import name", () => { const text = "import other from './ts-nameof.macro';other(console.log);other.full(console.log);"; assert.equal(run(text), `"log";"console.log";`); }); }); function run(text: string) { return babel.transformSync(text, { presets: [ "@babel/preset-typescript", ], plugins: [ babelPluginMacros, ], filename: path.join(__dirname, "test.ts"), ast: false, generatorOpts: { retainLines: true, }, })!.code!; } ================================================ FILE: packages/ts-nameof.macro/src/tests/ts-nameof.macro/index.js ================================================ /// @ts-check // hack to get tests working export { default } from "../../index"; ================================================ FILE: packages/ts-nameof.macro/ts-nameof.macro.d.ts ================================================ declare module "ts-nameof.macro" { /** * Gets a string representation of the final identifier of the given expression. * * @example nameof() -> "MyInterface" * @example nameof>() -> "Array" * @example nameof() -> "MyInnerInterface" * @example nameof(o => o.prop) -> "prop" * * @param func An optional function for which the last identifier of the expression will be parsed. */ function nameof(func?: (obj: T) => any): string; /** * Gets a string representation of the last identifier of the given expression. * * @example nameof(console) -> "console" * @example nameof(console.log) -> "log" * @example nameof(console["warn"]) -> "warn" * * @param obj An expression for which the last identifier will be parsed. */ function nameof(obj: any): string; namespace nameof { /** * Gets the string representation of the entire type parameter expression. * * @example nameof.full() -> "MyNamespace.MyInnerInterface" * @example nameof.full(1) -> "MyInnerInterface" * @example nameof.full>() -> "Array" * @example nameof.full>(-1) -> "MyInnerInterface" * * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function full(periodIndex?: number): string; /** * Gets the string representation of the entire resultant expression. * * @example nameof.full(o => o.prop.prop2) -> "prop.prop2" * @example nameof.full(o => o.prop.prop2.prop3, 1) -> "prop2.prop3" * @example nameof.full(o => o.prop.prop2.prop3, -1) -> `"prop3" * * @param func A function for which the result will be parsed, excluding the parameter's identifier. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function full(func: (obj: T) => any, periodIndex?: number): string; /** * Gets the string representation of the entire given expression. * * @example nameof.full(console.log) -> "console.log" * @example nameof.full(window.alert.length, -1) -> "length" * @example nameof.full(window.alert.length, 2) -> "length" * * @param obj The expression which will be parsed. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function full(obj: any, periodIndex?: number): string; /** * Gets an array containing the string representation of the final identifier of each expression in the array returned by the provided function. * * @example nameof.toArray(o => [o.firstProp, o.otherProp.secondProp, o.other]) -> ["firstProp", "secondProp", "other"] * @example nameof.toArray(o => [o.prop, nameof.full(o.myProp.otherProp, 1)]) -> ["prop", "myProp.otherProp"] * * @param func A function returning an array of expressions to be parsed, excluding the parameter's identifier. */ function toArray(func: (obj: T) => any[]): string[]; /** * Gets an array containing the string representation of each expression in the arguments. * * @example nameof.toArray(myObject, otherObject) -> ["myObject", "otherObject"] * @example nameof.toArray(obj.firstProp, obj.secondProp, otherObject, nameof.full(obj.other)) -> ["firstProp", "secondProp", "otherObject", "obj.other"] * * @param args An array of expressions to be parsed. */ function toArray(...args: any[]): string[]; /** * Embeds an expression into the string representation of the result of nameof.full. * * @example nameof.full(myObj.prop[nameof.interpolate(i)]) -> `myObj.prop[${i}]` * * @param value The value to interpolate. */ function interpolate(value: T): T; /** * Gets an array of strings where each element is a subsequent part of the expression provided. * * @example nameof.split(o => o.prop.prop2.prop3) -> ["prop", "prop2", "prop3"] * @example nameof.split(o => o.prop.prop2.prop3, 1) -> ["prop2", "prop3"] * @example nameof.split(o => o.prop.prop2.prop3, -1) -> ["prop", "prop2"] * * @param func A function for which the resultant parts will be parsed, excluding the parameter's identifier. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function split(func: (obj: T) => any, periodIndex?: number): string[]; /** * Gets an array of strings where each element is a subsequent part of the expression provided. * * @example nameof.split(myObj.prop.prop2.prop3) -> ["myObj", "prop", "prop2", "prop3"] * @example nameof.split(myObj.prop.prop2.prop3, -3);`, `["prop", "prop2", "prop3"]; * @example nameof.split(myObj.prop.prop2.prop3, 2);`, `["prop2", "prop3"] * * @param obj An expression for which the parts will be parsed. * @param periodIndex Specifies the index of the part of the expression to parse. * When absent, the full expression will be parsed. * A negative index can be used, indicating an offset from the end of the sequence. */ function split(obj: any, periodIndex?: number): string[]; } export default nameof; } ================================================ FILE: packages/ts-nameof.macro/tsconfig.json ================================================ { "compilerOptions": { "allowJs": true, "outDir": "./dist" }, "extends": "../../tsconfig.common.json", "include": ["./src"], "references": [ { "path": "../transforms-babel" }, { "path": "../tests-common" }, { "path": "../scripts-common" } ] } ================================================ FILE: tsconfig.common.json ================================================ { "compilerOptions": { "target": "es2015", "module": "commonjs", "declaration": false, "sourceMap": true, "strict": true, "removeComments": true, "experimentalDecorators": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "lib": ["es2015", "DOM"], "esModuleInterop": true }, "compileOnSave": false }