├── .eslintignore ├── .eslintrc.json ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── assets │ └── intro_gif.gif └── workflows │ ├── docs.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── LICENSE ├── README.md ├── docs ├── In-Depth │ ├── builtins.md │ ├── chaining.md │ ├── decorators.md │ ├── expanding.md │ ├── literals.md │ ├── macro_labels.md │ ├── markers.md │ ├── overview.md │ ├── parameters.md │ └── repetitions.md └── Links │ └── playground.md ├── package-lock.json ├── package.json ├── playground ├── README.md ├── components │ ├── Editor.tsx │ └── Runnable.tsx ├── css │ ├── App.module.css │ └── global.css ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages │ ├── _app.tsx │ └── index.tsx ├── tsconfig.json └── utils │ └── transpile.ts ├── src ├── actions.ts ├── cli │ ├── formatter.ts │ ├── index.ts │ └── transform.ts ├── index.ts ├── nativeMacros.ts ├── transformer.ts ├── type-resolve │ ├── chainingTypes.ts │ ├── declarations.ts │ └── index.ts ├── utils.ts └── watcher │ └── index.ts ├── tests ├── integrated │ ├── builtins │ │ ├── decompose.test.ts │ │ ├── define.test.ts │ │ ├── i.test.ts │ │ ├── ident.test.ts │ │ ├── includes.test.ts │ │ ├── inline.test.ts │ │ ├── kindof.test.ts │ │ ├── length.test.ts │ │ ├── map.test.ts │ │ ├── propsOfType.test.ts │ │ ├── raw.test.ts │ │ ├── slice.test.ts │ │ ├── ts.test.ts │ │ └── typeToString.test.ts │ ├── expand.test.ts │ ├── labels │ │ ├── block.test.ts │ │ ├── for.test.ts │ │ ├── foriter.test.ts │ │ ├── if.test.ts │ │ └── while.test.ts │ └── markers │ │ ├── accumulator.test.ts │ │ └── save.test.ts ├── snapshots │ ├── artifacts │ │ ├── builtins_const.test.js │ │ ├── builtins_decompose.test.js │ │ ├── builtins_define.test.js │ │ ├── builtins_i.test.js │ │ ├── builtins_ident.test.js │ │ ├── builtins_includes.test.js │ │ ├── builtins_inline.test.js │ │ ├── builtins_inlineFunc.test.js │ │ ├── builtins_kindof.test.js │ │ ├── builtins_length.test.js │ │ ├── builtins_map.test.js │ │ ├── builtins_propsOfType.test.js │ │ ├── builtins_raw.test.js │ │ ├── builtins_slice.test.js │ │ ├── builtins_stores.test.js │ │ ├── builtins_ts.test.js │ │ ├── builtins_typeToString.test.js │ │ ├── expand.test.js │ │ ├── labels_block.test.js │ │ ├── labels_for.test.js │ │ ├── labels_foriter.test.js │ │ ├── labels_if.test.js │ │ ├── labels_while.test.js │ │ ├── markers_accumulator.test.js │ │ ├── markers_save.test.js │ │ └── markers_var.test.js │ └── index.ts └── tsconfig.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | dist/ 3 | test/ 4 | playground/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "parser": "@typescript-eslint/parser", 11 | "parserOptions": { 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "plugins": [ 16 | "@typescript-eslint" 17 | ], 18 | "rules": { 19 | "indent": [ 20 | "error", 21 | 4 22 | ], 23 | "linebreak-style": [ 24 | "error", 25 | "windows" 26 | ], 27 | "quotes": [ 28 | "error", 29 | "double" 30 | ], 31 | "semi": [ 32 | "error", 33 | "always" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | volen.sl666@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | Thank you for contributing to ts-macros! Your help is appreciated by the author of this library and everyone using it! 5 | 6 | ## Table of Contents 7 | 8 | - [How can I contribute?](#how-can-i-contribute) 9 | - [Bug Reports](#bug-reports) 10 | - [Feature Requests](#feature-requests) 11 | - [Pull Requests](#pull-requests) 12 | - [Setup](#setup) 13 | - [Testing](#testing) 14 | - [Finishing up](#finishing-up) 15 | 16 | ## How can I contribute? 17 | 18 | ### Bug Reports 19 | 20 | Before reporting a bug, plese [search for issues with similar keywords to yours](https://github.com/GoogleFeud/ts-macros/issues?q=is%3Aissue+is%3Aopen). If an issue already exists for the bug then you can "bump" it by commenting on it. If it doesn't, then you can create one. 21 | 22 | When writing a bug report: 23 | 24 | - Use a clear and descriptive title for the issue. 25 | - Explain what you expected to see instead and why. 26 | 27 | ### Feature Requests 28 | 29 | Suggestions are always welcome! Before writing a feature request, please [search for issues with similar keywords to yours](https://github.com/GoogleFeud/ts-macros/issues?q=is%3Aissue+is%3Aopen). If an issue already exists for the request then you can "bump" it by commenting on it. If it doesn't, then you can create one. 30 | 31 | When writing a feature request: 32 | 33 | - Use a clear and descriptive title for the issue. 34 | - Provide examples of how the feature will be useful. 35 | 36 | ### Pull Requests 37 | 38 | Want to go straight into writing code? To get some inspiration you can look through the issues with the `bug` tag and find one you think you can tackle. If you are implementing a feature, please make sure an issue already exists for it before directly making a PR. If it doesn't, feel free to create one! 39 | 40 | All future changes are made in the `dev` branch, so make sure to work in that branch! 41 | 42 | #### Setup 43 | 44 | - Fork this repository 45 | - Clone your fork 46 | - Install all dependencies: `npm i` 47 | - Build the project: `npm run build` 48 | - Run the tests to see if everything is running smoothly: `npm test` 49 | 50 | #### Testing 51 | 52 | ts-macros has integrated and snapshot testing implemented. To make sure any changes you've made have not changed the transformer for worse, run `npm test`. This will first run all integrated tests, which test the **transpiled code**, and then ask you to continue with the snapshot testing. 53 | 54 | During snapshot testing, ts-macros compares the **trusted** transpiled integrated tests with the ones on your machine that have just been transpiled in the previous step. If any changes have been detected, it will ask you if you approve of these changes. If you notice some of the generated code is wrong or not up to standards, disprove the changes, make your fixes and run `npm test` again until the latest transpiled code matches the trusted version, or until you're satisfied with the generated code. 55 | 56 | #### Finishing up 57 | 58 | Once you're done working on an issue, you can submit a pull request to have your changes merged! Before submitting the request, make sure there are no linting errors (`npm lint`), all tests pass (`npm test`), and your branch is up to date (`git pull`). -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Code to reproduce** 14 | Paste the relevant code which reproduces the error, or give detailed instructions on how to reproduce it. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/assets/intro_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleFeud/ts-macros/bbd8be1035900b1a0e822bf2c09fff84c9e04d9e/.github/assets/intro_gif.gif -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs to Github Pages 2 | on: 3 | workflow_dispatch: 4 | release: 5 | type: [published] 6 | branches: 7 | - dev 8 | jobs: 9 | deploy-docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v2 14 | - run: npm i --force 15 | - run: tsc 16 | - run: touch ./docs/.nojekyll 17 | - run: | 18 | cd ./playground 19 | npm i --force 20 | npx next build 21 | npx next export -o ../docs 22 | - name: Deploy to GitHub Pages 23 | uses: JamesIves/github-pages-deploy-action@4.1.3 24 | with: 25 | branch: gh-pages 26 | folder: . -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to GitHub Packages 2 | on: 3 | workflow_dispatch: 4 | release: 5 | branches: 6 | - dev 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | packages: write 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | with: 17 | registry-url: 'https://registry.npmjs.org' 18 | - run: npm i --force 19 | - run: npm publish 20 | env: 21 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | test/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | test/ 3 | tests/ 4 | .github/ 5 | src/ 6 | docs/ 7 | playground/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 GoogleFeud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts-macros 2 | 3 | ts-macros is a typescript transformer which allows you to create function macros that expand to javascript code during the transpilation phase of your program. 4 | 5 | 📖 **[Documentation](https://github.com/GoogleFeud/ts-macros/wiki)** 6 | 🎮 **[Playground](https://googlefeud.github.io/ts-macros/)** 7 | ✍️ **[Examples](https://github.com/GoogleFeud/ts-macros/wiki/Practical-Macro-Examples)** 8 | 9 | ## The Basics 10 | 11 | All macro names must start with a dollar sign (`$`) and must be declared using the function keyword. Macros can then be called just like a normal function, but with a `!` after it's name: `$macro!(params)`. All the code inside of the macro is going to "expand" where the macro is called. 12 | 13 | ![showcase](https://github.com/GoogleFeud/ts-macros/blob/dev/.github/assets/intro_gif.gif) 14 | 15 | **What you can do with ts-macros**: 16 | - Generate repetitive code 17 | - Generate code conditionally, based on enviourment variables or other configuration files 18 | - Generate types which you can use in your code (read more [here](https://github.com/GoogleFeud/ts-macros/wiki/Type-Resolver-Transformer)) 19 | - Create abstractions without the runtime cost 20 | 21 | ## Usage 22 | 23 | ``` 24 | npm i --save-dev ts-macros 25 | ``` 26 | 27 |
28 | Usage with ts-patch 29 | 30 | ``` 31 | npm i --save-dev ts-patch 32 | ``` 33 | 34 | and add the ts-macros transformer to your tsconfig.json: 35 | 36 | ```json 37 | "compilerOptions": { 38 | //... other options 39 | "plugins": [ 40 | { "transform": "ts-macros" } 41 | ] 42 | } 43 | ``` 44 | 45 | Afterwards you can either: 46 | - Transpile your code using the `tspc` command that ts-patch provides. 47 | - Patch the instance of typescript that's in your `node_modules` folder with the `ts-patch install` command and then use the `tsc` command to transpile your code. 48 |
49 | 50 |
51 | Usage with ts-loader 52 | 53 | ```js 54 | const TsMacros = require("ts-macros").default; 55 | 56 | options: { 57 | getCustomTransformers: program => { 58 | before: [TsMacros(program)] 59 | } 60 | } 61 | ``` 62 |
63 | 64 |
65 | Usage with ts-node 66 | 67 | To use transformers with ts-node, you'll have to change the compiler in the `tsconfig.json`: 68 | 69 | ``` 70 | npm i --save-dev ts-node 71 | ``` 72 | 73 | ```json 74 | "ts-node": { 75 | "compiler": "ts-patch/compiler" 76 | }, 77 | "compilerOptions": { 78 | "plugins": [ 79 | { "transform": "ts-macros" } 80 | ] 81 | } 82 | ``` 83 |
84 | 85 |
86 | CLI Usage (esbuild, vite, watchers) 87 | 88 | If you want to use ts-macros with: 89 | - tools that don't support typescript 90 | - tools that aren't written in javascript and therefore cannot run typescript transformers (tools that use swc, for example) 91 | - any tools' watch mode (webpack, vite, esbuild, etc) 92 | 93 | you can use the CLI - [read more about the CLI and example here](https://github.com/GoogleFeud/ts-macros/wiki/CLI-usage) 94 | 95 |
96 | 97 | ## Security 98 | 99 | This library has 2 built-in macros (`$raw` and `$comptime`) which execute arbitrary code during transpile time. The code is **not** sandboxed in any way and has access to your file system and all node modules. 100 | 101 | If you're transpiling an untrusted codebase which uses this library, make sure to set the `noComptime` option to `true`. Enabling it will replace all calls to these macros with `null` without executing the code inside them. It's always best to review all call sites to `$$raw` and `$$comptime` yourself before transpiling any untrusted codebases. 102 | 103 | **ttypescript/ts-patch:** 104 | ```json 105 | "plugins": [ 106 | { "transform": "ts-macros", "noComptime": true } 107 | ] 108 | ``` 109 | 110 | **manually creating the factory:** 111 | ```js 112 | TsMacros(program, { noComptime: true }); 113 | ``` 114 | 115 | ## Contributing 116 | 117 | `ts-macros` is being maintained by a single person. Contributions are welcome and appreciated. Feel free to open an issue or create a pull request at https://github.com/GoogleFeud/ts-macros. -------------------------------------------------------------------------------- /docs/In-Depth/builtins.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Built-in macros 3 | order: 8 4 | --- 5 | 6 | # Built-in macros 7 | 8 | ts-macros provides you with a lot of useful built-in macros which you can use inside macros. All the exported functions from this library that start with two dollar signs (`$$`) are built-in macros! 9 | 10 | |> Important: You cannot chain built-in macros! 11 | 12 | [[$$loadEnv]] - Loads an env file from the provided path. 13 | [[$$readFile]] - Reads from the provided file and expands to the file's contents. 14 | [[$$kindof]] - Expands to the `kind` of the AST node. 15 | [[$$inlineFunc]], [[$$inline]] - Inlines the provided arrow function. 16 | [[$$define]] - Creates a const variable with the provided name and initializer. 17 | [[$$i]] - Gives you the repetition count. 18 | [[$$length]] - Gets the length of an array / string literal. 19 | [[$$ident]] - Turns a string literal into an identifier. 20 | [[$$err]] - Throws an error during transpilation. 21 | [[$$includes]] - Checks if an item is included in an array / string literal. 22 | [[$$slice]] - Slices an array / string literal. 23 | [[$$ts]] - Turns a string literal into code. 24 | [[$$escape]] - Places a block of code in the parent block. 25 | [[$$propsOfType]] - Expands to an array with all properties of a type. 26 | [[$$typeToString]] - Turns a type to a string literal. 27 | [[$$typeAssignableTo]] - Compares two types. 28 | [[$$text]] - Turns an expression into a string literal. 29 | [[$$decompose]] - Expands to an array literal containing the nodes that make up an expression. 30 | [[$$map]] - Takes a function that acts as a macro and goes over all the nodes of an expression with it, replacing each node with the expanded result of the macro function. 31 | [[$$comptime]] - Allows you to run code during transpilation. 32 | [[$$raw]] - Allows you to interact with the raw typescript APIs. 33 | [[$$getStore]], [[$$setStore]] - Allow you to store variables in a macro call. -------------------------------------------------------------------------------- /docs/In-Depth/chaining.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Chaining macros 3 | order: 3 4 | --- 5 | 6 | # Chaining macros 7 | 8 | Let's create a simple macro which takes any possible value and compares it with other values of the same type: 9 | 10 | ```ts 11 | function $contains(value: T, ...possible: Array) { 12 | return +["||", [possible], (item: T) => value === item]; 13 | } 14 | ``` 15 | 16 | We can call the above macro like a normal funcion, except with an exclamation mark (`!`) after it's name: 17 | 18 | ```ts 19 | const searchItem = "google"; 20 | $contains!(searchItem, "erwin", "tj"); 21 | ``` 22 | 23 | This is one way to call a macro, however, the ts-macros transformer allows you to also call the macro as if it's a property of a value - this way the value becomes the first argument to the macro: 24 | 25 | ```ts 26 | searchItem.$contains!("erwin", "tg"); 27 | // Same as: $contains!(searchItem, "erwin", "tj"); 28 | ``` 29 | 30 | If we try to transpile this code, typescript is going to give us a `TypeError` - `$contains` is not a member of type `String`. When you're calling the macro like a normal function, typescript and the transformer are able to trace it to it's definition thanks to it's `symbol`. They're internally connected, so the transpiler is always going to be able to find the macro. 31 | 32 | When we try to chain a macro to a value, neither the transpiler or the transformer are able to trace back the identifier `$contains` to a function definiton. The transformer fixes this by going through each macro it knows with the name `$contains` and checking if the types of the parameters of the macro match the types of the passed arguments. To fix the typescript error we can either put a `//@ts-expect-error` comment above the macro call or modify the `String` type via ambient declarations: 33 | 34 | ```ts 35 | declare global { 36 | interface String { 37 | $contains(...possible: Array) : boolean; 38 | } 39 | } 40 | ``` 41 | 42 | Now if we run the code above in the [playground](https://googlefeud.github.io/ts-macros/playground/?code=KYDwDg9gTgLgBAbwL4G4BQaAmwDGAbAQymDgHM8IAjAvRNOBuASwDsZgoAzAnEgZRhRWpOozFwAJDghsCrAM4AKAHSrI8+U0p5gALjgBBKFAIBPADzzBwgHwBKOPsoQIOgi3Rikab2k4BXFhwYJhlJaVkFcwAVG0UANxp-PThogBo4VWV1TW0UoxMLWIcEekZiGH8oFjgAagBtACIAH2bGjPqcrR0AXQzFJnYAW31ohwBeGzhEvGS4cYXmYZ70XwirOHlgIhwACwBJYfm4RtIXcmBG9C2dg+HlKRkYORZ5AEJFRo4Ad1Z2k5gpEadhQQA) we aren't going to get any errors and the code will transpile correctly! 43 | 44 | ## Transparent types 45 | 46 | On paper this sounds like a nice quality of life feature, but you can use it for something quite powerful - transparent types. You are able to completely hide away a data source behind a type which in reality doesn't represent anything, and use macros to access the data source. Below are some ideas on how these transparent types could be used. 47 | 48 | ### Vector type 49 | 50 | A a `Vector` type which in reality is just an array with two elements inside of it (`[x, y]`): 51 | 52 | ```ts --Macros 53 | // This represent s our data source - the array. 54 | interface Vector { 55 | $x(): number; 56 | $y(): number; 57 | $data(): [number, number]; 58 | $add(x?: number, y?: number): Vector; 59 | } 60 | 61 | // Namespaces allow us to store macros 62 | namespace Vector { 63 | // Macro for creating a new Vector 64 | export function $new(): Vector { 65 | return [0, 0] as unknown as Vector; 66 | } 67 | 68 | // Macro which transforms the transparent type to the real type 69 | export function $data(v: Vector) : [number, number] { 70 | return v as unknown as [number, number]; 71 | } 72 | 73 | export function $x(v: Vector) : number { 74 | return $data!(v)[0]; 75 | } 76 | 77 | export function $y(v: Vector) : number { 78 | return $data!(v)[1]; 79 | } 80 | 81 | export function $add(v: Vector, x?: number, y?: number) : Vector { 82 | const $realData = $data!(v); 83 | return [$realData[0] + (x || 0), $realData[1] + (y || 0)] as unknown as Vector; 84 | } 85 | } 86 | ``` 87 | ```ts --Call 88 | const myVector = Vector.$new!().$add!(1).$add!(undefined, 10); 89 | console.log(myVector.$x!(), myVector.$y!()); 90 | ``` 91 | ```ts --Result 92 | const myVector = [1, 10]; 93 | console.log(myVector[0], myVector[1]); 94 | ``` 95 | 96 | ### Iterator type 97 | 98 | An iterator transparent type which allows us to use chaining for methods like `$map` and `$filter`, which expand to a single for loop when the iterator is collected with `$collect`. Here the `Iter` type isn't actually going to be used as a value in the code, instead it's just going to get passed to the `$map`, `$filter` and `$collect` macros. 99 | 100 | `$next` is not actually a macro but an arrow function which is going to contain all the code inside the for loop. `$map` and `$filter` modify this arrow function by adding their own logic inside of it after the old body of the function, and the `$collect` macro inlines the body function in the for loop. 101 | 102 | ```ts --Macros 103 | interface Iter { 104 | _arr: T[], 105 | $next(item: any) : T, 106 | $map(mapper: (item: T) => K) : Iter, 107 | $filter(fn: (item: T) => boolean) : Iter, 108 | $collect() : T[] 109 | } 110 | 111 | namespace Iter { 112 | 113 | export function $new(array: T[]) : Iter { 114 | return { 115 | _arr: array, 116 | $next: (item) => {} 117 | } as Iter; 118 | } 119 | 120 | export function $map(iter: Iter, mapper: (item: T) => K) : Iter { 121 | return { 122 | _arr: iter._arr, 123 | $next: (item) => { 124 | $$inline!(iter.$next, [item]); 125 | item = $$escape!($$inline!(mapper, [item], true)); 126 | } 127 | } as unknown as Iter; 128 | } 129 | 130 | export function $filter(iter: Iter, func: (item: T) => boolean) : Iter { 131 | return { 132 | _arr: iter._arr, 133 | $next: (item) => { 134 | $$inline!(iter.$next, [item]); 135 | if (!$$escape!($$inline!(func, [item], true))) $$ts!("continue"); 136 | } 137 | } as Iter; 138 | } 139 | 140 | export function $collect(iter: Iter) : T[] { 141 | return $$escape!(() => { 142 | const array = iter._arr; 143 | const result = []; 144 | for (let i=0; i < array.length; i++) { 145 | let item = array[i]; 146 | $$inline!(iter.$next, [item]); 147 | result.push(item); 148 | } 149 | return result; 150 | }); 151 | } 152 | } 153 | ``` 154 | ```ts --Call 155 | const arr = Iter.$new!([1, 2, 3]).$map!(m => m * 2).$filter!(el => el % 2 === 0).$collect!(); 156 | ``` 157 | ```ts --Result 158 | const array_1 = [1, 2, 3]; 159 | const result_1 = []; 160 | for (let i_1 = 1; i_1 < array_1.length; i_1++) { 161 | let item_1 = array_1[i_1]; 162 | item_1 = item_1 * 2; 163 | if (!(item_1 % 2 === 0)) 164 | continue; 165 | result_1.push(item_1); 166 | } 167 | const myIter = arr; 168 | ``` 169 | 170 | ## Details on macro resolution 171 | 172 | The ts-macros transformer keeps tracks of macros using their unique **symbol**. Since you must declare the type for the macros yourself via ambient declarations, the macro function declaration and the type declaration do not share a symbol, so the transformer needs another way to see which macro you're really trying to call. 173 | 174 | This is why the transformer compares the types of the parameters from the macro call site to all macros of the same name. Two types are considered equal if the type of the argument is **assignable** to the macro parameter type. For example: 175 | 176 | ```ts 177 | // ./A 178 | function $create(name: string, age: number) { ... } 179 | // ./B 180 | function $create(id: string, createdAt: number) { ... } 181 | ``` 182 | 183 | These two macros are perfectly fine, it's ok that they're sharing a name, the transformer can still differenciate them when they're used like this: 184 | 185 | ```ts 186 | import { $create } from "./A"; 187 | import { $create as $create2 } from "./B"; 188 | 189 | $create!("Google", 44); // Valid 190 | $create2!("123", Date.now()) // Valid 191 | ``` 192 | 193 | **However**, when either of the macros get used in chaining, the transformer is going to raise an error, because both macros have the exact same parameter types, in the exact same order - `string`, `number`. 194 | 195 | The only ways to fix this are to either: 196 | 197 | - Rename one of the macros 198 | - Switch the order of the parameters 199 | - Possibly brand one of the types -------------------------------------------------------------------------------- /docs/In-Depth/decorators.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Macro decorators 3 | order: 9 4 | --- 5 | 6 | # Macro decorators 7 | 8 | Macro functions can also be used as decorators! Here is a basic macro which adds two numbers, let's try using it as a decorator: 9 | 10 | ```ts --Macro 11 | function $add(numA: number, numB: number) : EmptyDecorator { 12 | return (numA + numB) as unknown as EmptyDecorator; 13 | } 14 | ``` 15 | ```ts --Call 16 | @$add!(1, 2) 17 | class Test {} 18 | ``` 19 | ```ts --Result 20 | (3) 21 | ``` 22 | 23 | The macro expands and replaces the entire class declaration. Since macros are just plain functions, they cannot get access to the class itself and manipulate it. This is why for decorator macros to work, we need to use the [[$$raw]] built-in macro, which allows us to manipulate the typescript AST directly! 24 | 25 | Let's write a macro which creates a copy of the class, except with a name of our choosing. With the `$$raw` macro, we get access to the class AST node thanks to the `ctx` object: 26 | 27 | ```ts 28 | function $renameClass(newName: string) : EmptyDecorator { 29 | return $$raw!((ctx, newNameNode: ts.StringLiteral) => { 30 | const target = ctx.thisMacro.target as ts.ClassDeclaration; 31 | }); 32 | } 33 | ``` 34 | 35 | To copy the class, we can use the `ctx.factory.updateClassDeclaration` method: 36 | 37 | ```ts 38 | ctx.factory.updateClassDeclaration( 39 | target, 40 | target.modifiers?.filter(m => m.kind !== ctx.ts.SyntaxKind.Decorator), 41 | ctx.factory.createIdentifier(newNameNode.text), 42 | target.typeParameters, 43 | target.heritageClauses, 44 | target.members 45 | ) 46 | ``` 47 | 48 | It's important to remove the decorators from the declaration so the macro decorators don't get to the compiled code. Let's put it all together: 49 | 50 | ```ts --Macro 51 | function $renameClass(newName: string) : EmptyDecorator { 52 | return $$raw!((ctx, newNameNode: ts.StringLiteral) => { 53 | const target = ctx.thisMacro.target as ts.ClassDeclaration; 54 | return ctx.factory.updateClassDeclaration( 55 | target, 56 | target.modifiers?.filter(m => m.kind !== ctx.ts.SyntaxKind.Decorator), 57 | ctx.factory.createIdentifier(newNameNode.text), 58 | target.typeParameters, 59 | target.heritageClauses, 60 | target.members 61 | ) 62 | }); 63 | } 64 | ``` 65 | ```ts --Call 66 | @$renameClass!("NewTest") 67 | class Test { 68 | propA: number 69 | propB: string 70 | constructor(a: number, b: string) { 71 | this.propA = a; 72 | this.propB = b; 73 | } 74 | } 75 | ``` 76 | ```ts --Result 77 | class NewTest { 78 | constructor(a, b) { 79 | this.propA = a; 80 | this.propB = b; 81 | } 82 | } 83 | ``` 84 | 85 | Multiple decorators can be applied to a declaration, so let's create another macro which adds a method which desplays all the properties of the class. I know this looks like a lot of code, but over 50% of the lines are just updating and creating the AST declarations: 86 | 87 | ```ts --Macro 88 | function $addDebugMethod() : EmptyDecorator { 89 | return $$raw!((ctx) => { 90 | const target = ctx.thisMacro.target as ts.ClassDeclaration; 91 | return ctx.factory.updateClassDeclaration( 92 | target, 93 | target.modifiers?.filter(m => m.kind !== ctx.ts.SyntaxKind.Decorator), 94 | target.name, 95 | target.typeParameters, 96 | target.heritageClauses, 97 | [ 98 | ...target.members, 99 | ctx.factory.createMethodDeclaration( 100 | undefined, 101 | undefined, 102 | "debug", 103 | undefined, 104 | undefined, 105 | [], 106 | undefined, 107 | ctx.factory.createBlock(ctx.transformer.strToAST(` 108 | console.log( 109 | "${target.name?.getText()} ", "{\\n", 110 | ${target.members.filter(m => ctx.ts.isPropertyDeclaration(m) && ctx.ts.isIdentifier(m.name)).map(m => `"${(m.name as ts.Identifier).text}: ", this.${(m.name as ts.Identifier).text}}`).join(",\"\\n\",")}, 111 | "\\n}" 112 | ) 113 | `)) 114 | ) 115 | ] 116 | ) 117 | }); 118 | } 119 | ``` 120 | ```ts --Call 121 | @$renameClass!("NewTest") 122 | @$addDebugMethod!() 123 | class Test { 124 | propA: number 125 | propB: string 126 | constructor(a: number, b: string) { 127 | this.propA = a; 128 | this.propB = b; 129 | } 130 | } 131 | ``` 132 | ```ts --Result 133 | class NewTest { 134 | constructor(a, b) { 135 | this.propA = a; 136 | this.propB = b; 137 | } 138 | debug() { 139 | console.log("Test ", "{\n", "propA: ", this.propA, "\n", "propB: ", this.propB, "\n}"); 140 | } 141 | } 142 | ``` 143 | 144 | Here we use the [[strToAST]] method to make writing the AST easier - the method transforms a string to an array of statements. We can also use it to create the entire class AST, but then you'll have to stringify the class' type parameters, constructor, other members, etc. so it becomes even more messy. 145 | 146 | To allow flexibility, decorator macros can return **an array of declarations** so they not only edit declarations, but also create new ones as well. Here's a macro which copies a method, but logs it's arguments in the body: 147 | 148 | ```ts --Macro 149 | function copyMethod(ctx: RawContext, original: ts.MethodDeclaration, name?: string, body?: ts.Block): ts.MethodDeclaration { 150 | return ctx.factory.updateMethodDeclaration( 151 | original, 152 | original.modifiers?.filter(m => m.kind !== ctx.ts.SyntaxKind.Decorator), 153 | original.asteriskToken, 154 | name ? ctx.factory.createIdentifier(name) : original.name, 155 | original.questionToken, 156 | original.typeParameters, 157 | original.parameters, 158 | original.type, 159 | body || original.body 160 | ) 161 | } 162 | 163 | function $logArgs(): EmptyDecorator { 164 | return $$raw!(ctx => { 165 | const target = ctx.thisMacro.target as ts.MethodDeclaration; 166 | return [ 167 | // Same method, we just remove the decorators 168 | copyMethod(ctx, target), 169 | // Test method which logs the arguments 170 | copyMethod(ctx, target, 171 | (target.name as ts.Identifier).text + "Test", 172 | ctx.factory.createBlock([ 173 | ...ctx.transformer.strToAST( 174 | `console.log(${target.parameters.filter(p => ctx.ts.isIdentifier(p.name)).map(p => (p.name as ts.Identifier).text).join(",")})` 175 | ), 176 | ...(target.body?.statements || []) 177 | ]) 178 | ) 179 | ] 180 | }); 181 | } 182 | ``` 183 | ```ts --Call 184 | @$renameClass!("NewTest") 185 | @$addDebugMethod!() 186 | class Test { 187 | propA: number 188 | propB: string 189 | constructor(a: number, b: string) { 190 | this.propA = a; 191 | this.propB = b; 192 | } 193 | 194 | @$logArgs!() 195 | add(a: number, b: string) { 196 | return a + b; 197 | } 198 | } 199 | ``` 200 | ```ts --Result 201 | class NewTest { 202 | constructor(a, b) { 203 | this.propA = a; 204 | this.propB = b; 205 | } 206 | add(a, b) { 207 | return a + b; 208 | } 209 | addTest(a, b) { console.log(a, b); return a + b; } 210 | debug() { console.log("Test ", "{\n", "propA: ", this.propA, "\n", "propB: ", this.propB, "\n}"); } 211 | } 212 | ``` 213 | 214 | ## Decorator composition 215 | 216 | Decorators are called bottom-to-top, so in the example above, `$addDebugMethod` is called first, then `$renameClass`. However, what happens when a decorator macro returns an array of declarations? Let's try it out by creating another decorator which renames a method: 217 | 218 | ```ts --Macro 219 | function $renameMethod(newName: string) : EmptyDecorator { 220 | return $$raw!((ctx, newNameNode: ts.StringLiteral) => { 221 | const target = ctx.thisMacro.target as ts.MethodDeclaration; 222 | return copyMethod(ctx, target, newNameNode.text); 223 | }); 224 | } 225 | ``` 226 | ```ts --Call 227 | @$renameClass!("NewTest") 228 | @$addDebugMethod!() 229 | class Test { 230 | propA: number 231 | propB: string 232 | constructor(a: number, b: string) { 233 | this.propA = a; 234 | this.propB = b; 235 | } 236 | 237 | @$renameMethod!("addNums") 238 | @$logArgs!() 239 | add(a: number, b: string) { 240 | return a + b; 241 | } 242 | } 243 | ``` 244 | ```ts --Result 245 | class NewTest { 246 | constructor(a, b) { 247 | this.propA = a; 248 | this.propB = b; 249 | } 250 | add(a, b) { 251 | return a + b; 252 | } 253 | addNums(a, b) { console.log(a, b); return a + b; } 254 | debug() { console.log("Test ", "{\n", "propA: ", this.propA, "\n", "propB: ", this.propB, "\n}"); } 255 | } 256 | ``` 257 | 258 | First `logArgs` gets the declaration and instead of it returns two new ones: `add` (which happens to be the old declaration) and `addTest`. Then `renameMethod` gets it's hands on **only the last returned method** from the previous decorator, which is `addTest`, so it renames it to `addNums`. 259 | 260 | To make this work, we'll have to switch the orders of the decorators: 261 | 262 | ```ts 263 | @$renameClass!("NewTest") 264 | @$addDebugMethod!() 265 | class Test { 266 | propA: number 267 | propB: string 268 | constructor(a: number, b: string) { 269 | this.propA = a; 270 | this.propB = b; 271 | } 272 | 273 | @$logArgs!() 274 | @$renameMethod!("addNums") 275 | add(a: number, b: string) { 276 | return a + b; 277 | } 278 | } 279 | ``` 280 | 281 | ## More info and tips 282 | 283 | - You can also use decorator macros on methods, accessors, properties and parameters. 284 | - Returning `undefined` in the [[$$raw]] macro callback will erase the decorator target. 285 | - The declaration returned by the [[$$raw]] callback goes through the transformer, so macros can be called inside it! 286 | - Always use the methods from `ctx.ts` and `ctx.factory`, **not** from `ts` and `ts.factory`. 287 | - If you get an `Invalid Arguments` error, that means that some node does not match the expected one by typescript, for example, you cannot give a call expression node to a method name. 288 | - Do **not** use the `getText` method if you're going to have multiple decorator macros on the same declaration. All but the bottom macros are going to receive synthetic nodes, not real nodes, and the `getText` method does not work for synthetic nodes. It's best to avoid it if you want to be able to reuse macros. -------------------------------------------------------------------------------- /docs/In-Depth/expanding.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Expanding macros 3 | order: 2 4 | --- 5 | 6 | # Expanding 7 | 8 | ## Expanding macros 9 | 10 | Every macro **expands** into the code that it contains. How it'll expand depends entirely on how the macro is used. Javascript has 3 main constructs: `Expression`, `ExpressionStatement` and `Statement`. Since macro calls are plain function calls, macros can never be used as a statement. 11 | 12 | |> Expanded macros are **always** hygienic! 13 | 14 | ### ExpressionStatement 15 | 16 | If a macro is an `ExpressionStatement`, then it's going to be "flattened" - the macro call will literally be replaced by the macro body, but all the new declared variables will have their names changed to a unique name. 17 | 18 | ```ts --Macro 19 | function $map(arr: Array, cb: (el: T) => R) : Array { 20 | const array = arr; 21 | const res = []; 22 | for (let i=0; i < array.length; i++) { 23 | res.push(cb(array[i])); 24 | } 25 | return res; 26 | } 27 | ``` 28 | ```ts --Call 29 | $map!([1, 2, 3], (num) => num * 2); // This is an ExpressionStatement 30 | ``` 31 | ```js --Result 32 | const array_1 = [1, 2, 3]; 33 | const res_1 = []; 34 | for (let i_1 = 0; i_1 < array_1.length; i_1++) { 35 | res_1.push(((num) => num * 2)(array_1[i_1])); 36 | } 37 | array_1; 38 | ``` 39 | 40 | You may have noticed that the return statement got omitted from the final result. `return` will be removed **only** if the macro is ran in the global scope. Anywhere else and `return` will be there. 41 | 42 | ### Expression 43 | 44 | Expanding inside an expression can do two different things depending on the what the macro expands to. 45 | 46 | #### Single expression 47 | 48 | If the macro expands to a single expression, then the macro call is directly replaced with the expression. 49 | 50 | ```ts --Macro 51 | function $push(array: Array, ...nums: Array) : number { 52 | return +["()", (nums: number) => array.push(nums)]; 53 | } 54 | ``` 55 | ```ts --Call 56 | const arr: Array = []; 57 | const newSize = $push!(arr, 1, 2, 3); 58 | ``` 59 | ```js --Result 60 | const arr = []; 61 | const newSize = (arr.push(1), arr.push(2), arr.push(3)); 62 | ``` 63 | 64 | `return` gets removed if the macro is used as an expression. 65 | 66 | #### Multiple expressions 67 | 68 | If the macro expands to multiple expressions, or has a statement inside it's body, then the body is wrapped inside an IIFE (Immediately Invoked function expression) and the last expression gets returned automatically. 69 | 70 | ```ts --Macro 71 | function $push(array: Array, ...nums: Array) : number { 72 | +[(nums: number) => array.push(nums)]; 73 | } 74 | ``` 75 | ```ts --Call 76 | const arr: Array = []; 77 | const newSize = $push!(arr, 1, 2, 3); 78 | ``` 79 | ```js --Result 80 | const arr = []; 81 | const newSize = (() => { 82 | arr.push(1) 83 | arr.push(2) 84 | return arr.push(3); 85 | })(); 86 | ``` 87 | 88 | ##### Escaping the IIFE 89 | 90 | If you want part of the code to be ran **outside** of the IIFE (for example you want to `return`, or `yield`, etc.) you can use the [[$$escape]] built-in macro. For example, here's a fully working macro which expands to a completely normal if statement, but it can be used as an expression: 91 | 92 | ```ts --Macro 93 | function $if(comparison: any, then: () => T, _else?: () => T) { 94 | return $$escape!(() => { 95 | var val; 96 | if ($$kindof!(_else) === ts.SyntaxKind.ArrowFunction) { 97 | if (comparison) { 98 | val = $$escape!(then); 99 | } else { 100 | val = $$escape!(_else!); 101 | } 102 | } else { 103 | if (comparison) { 104 | val = $$escape!(then); 105 | } 106 | } 107 | return val; 108 | }); 109 | } 110 | ``` 111 | ```ts --Call 112 | const variable: number = 54; 113 | console.log($if!(1 === variable, () => { 114 | console.log("variable is 1"); 115 | return "A"; 116 | }, () => { 117 | console.log("variable is not 1"); 118 | return "B"; 119 | })); 120 | ``` 121 | ```ts --Result 122 | const variable = 54; 123 | var val_1; 124 | if (1 === variable) { 125 | // Do something... 126 | console.log("variable is 1"); 127 | val_1 = "A"; 128 | } 129 | else { 130 | console.log("variable is not 1"); 131 | val_1 = "B"; 132 | } 133 | console.log(val_1); 134 | ``` 135 | 136 | ## Macro variables 137 | 138 | You can define **macro variables** inside macros, which save an expression and expand to that same expression when they are referenced. They can be used to make your macros more readable: 139 | 140 | ```ts --Macro 141 | function $test(value: T) { 142 | const $type = $$typeToString!(); 143 | if ($type === "string") return "Value is a string."; 144 | else if ($type === "number") return "Value is a number."; 145 | else if ($type === "symbol") return "Value is a symbol."; 146 | else if ($type === "undefined" || $type === "null") return "Value is undefined / null."; 147 | else return "Value is an object."; 148 | } 149 | ``` 150 | ```ts --Call 151 | const a = $test!(null); 152 | const c = $test!(123); 153 | const f = $test!({value: 123}); 154 | ``` 155 | ```ts --Result 156 | const a = "Value is undefined / null."; 157 | const c = "Value is a number."; 158 | const f = "Value is an object."; 159 | ``` -------------------------------------------------------------------------------- /docs/In-Depth/literals.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Literals 3 | order: 6 4 | --- 5 | 6 | # Literals 7 | 8 | Literals in ts-macros are **very** powerful. When you use literals in macros, ts-macros is able to completely remove those literls and give you the final result. For example, adding two numeric literals: 9 | 10 | ```ts --Macro 11 | function $add(numA: number, numB: number) : number { 12 | return numA + numB; 13 | } 14 | ``` 15 | ```ts --Call 16 | $add!(5, 10); 17 | ``` 18 | ```ts --Result 19 | 15 20 | ``` 21 | 22 | This works for almost all binary and unary operators. 23 | 24 | ## Logic 25 | 26 | If the condition of an if statement / ternary expression is a literal, then the entire condition will be removed and only the resulting code will be expanded. 27 | 28 | ```ts --Macro 29 | function $log(multiply: boolean, number: number) { 30 | console.log(multiply ? number * 2 : number); 31 | } 32 | 33 | // If version 34 | function $log(multiply: boolean, number: number) { 35 | if (multiply) console.log(number * 2); 36 | else console.log(number); 37 | } 38 | ``` 39 | ```ts --Call 40 | $log!(false, 10); 41 | $log!(true, 15); 42 | ``` 43 | ```ts --Result 44 | console.log(10); 45 | console.log(30); 46 | ``` 47 | 48 | ## Object / Array access 49 | 50 | Accessing object / array literals also get replaced with the literal. You can prevent this by wrapping the object / array in paranthesis. 51 | 52 | ```ts --Macro 53 | function $add(param1: { 54 | user: { name: string } 55 | }, arr: [number, string]) { 56 | return param1.user.name + arr[0] + arr[1]; 57 | } 58 | ``` 59 | ```ts --Call 60 | $add!({ 61 | user: { name: "Google" } 62 | }, [22, "Feud"]); 63 | ``` 64 | ```js --Result 65 | "Google22Feud"; 66 | ``` 67 | 68 | ## String parameters as identifiers 69 | 70 | If a **string literal** parameter is used as a class / function / enum declaration, then the parameter name will be repalced with the contents inside the literal. 71 | 72 | ```ts --Macro 73 | function $createClasses(values: Array, ...names: Array) { 74 | +[[values, names], (val, name) => { 75 | class name { 76 | static value = val 77 | } 78 | }] 79 | } 80 | ``` 81 | ```ts --Call 82 | $createClasses!(["A", "B", "C"], "A", "B", "C") 83 | ``` 84 | ```js --Result 85 | class A { 86 | } 87 | A.value = "A"; 88 | class B { 89 | } 90 | B.value = "B"; 91 | class C { 92 | } 93 | C.value = "C"; 94 | ``` 95 | 96 | ## Spread expression 97 | 98 | You can concat array literals with the spread syntax, like you do in regular javascript: 99 | 100 | ```ts --Macro 101 | function $concatArrayLiterals(a: Array, b: Array) : Array { 102 | return [...a, ...b]; 103 | } 104 | ``` 105 | ```ts --Call 106 | $concatArrayLiterals!([1, 2, 3], [4, 5, 6]); 107 | ``` 108 | ```ts --Result 109 | [1, 2, 3, 4, 5, 6]; 110 | ``` -------------------------------------------------------------------------------- /docs/In-Depth/macro_labels.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Macro labels 3 | order: 10 4 | --- 5 | 6 | # Macro labels 7 | 8 | Macros can also be used on statements with labels: 9 | 10 | ```ts --Macro 11 | // Macro for turning a for...of loop to a regular for loop 12 | function $NormalizeFor(info: ForIterLabel) : void { 13 | if ($$kindof!(info.initializer) === ts.SyntaxKind.Identifier) { 14 | const iter = (info.iterator).length; 15 | for ($$define!(info.initializer, 0, true); info.initializer < iter; info.initializer++) { 16 | $$inline!(info.statement); 17 | } 18 | } 19 | } 20 | ``` 21 | ```ts --Call 22 | const arr = [1, 2, 3, 4, 5]; 23 | 24 | $NormalizeFor: 25 | for (const item of arr) { 26 | console.log(item + 1); 27 | } 28 | ``` 29 | ```ts --Result 30 | const iter = (arr).length; 31 | for (let item = 0; item < iter; item++) { 32 | console.log(item + 1); 33 | } 34 | ``` 35 | 36 | Only catch is that these macros cannot accept any other parameters - their first parameter will **always** be an object with information about the statement. Even though you cannot provide parameters yourself, you can still use the `Var` and `Accumulator` markers. All statements are wrapped in an arrow function, you can either call it or inline it with `$$inline`. 37 | 38 | ## Usable statements 39 | 40 | ### If 41 | 42 | If statements. Check out the [[IfLabel]] interface to see all information exposed to the macro. 43 | 44 | ```ts --Macro 45 | // Macro for turning an if statement to a ternary expression 46 | function $ToTernary(label: IfLabel) : void { 47 | label.condition ? $$inline!(label.then) : $$inline!(label.else); 48 | } 49 | ``` 50 | ```ts --Call 51 | let num: number = 123; 52 | $ToTernary: 53 | if (num === 124) { 54 | console.log("Number is valid."); 55 | } else { 56 | console.log("Number is not valid."); 57 | } 58 | ``` 59 | ```ts --Result 60 | num === 124 ? console.log("Number is valid.") : console.log("Number is not valid."); 61 | ``` 62 | 63 | ### Variable declaration 64 | 65 | Variable declarations. Check out the [[VariableDeclarationLabel]] interface to see all information exposed to the macro. **Currently, typescript throws an error if you use a label on a variable declaration, but the error can be ignored via `ts-expect-error` or a vscode plugin**. Does not support deconstructing. 66 | 67 | ```ts --Macro 68 | function $addOneToVars(info: VariableDeclarationLabel) { 69 | +[[info.identifiers, info.initializers], (name: any, decl: any) => { 70 | $$define!(name, decl + 1, info.declarationType === "let"); 71 | }] 72 | } 73 | ``` 74 | ```ts --Call 75 | //@ts-expect-error 76 | $addOneToVars: const a = 2, b = 4, c = 10; 77 | ``` 78 | ```ts --Result 79 | const a = 3; 80 | const b = 5; 81 | const c = 11; 82 | ``` 83 | 84 | ### ForIter 85 | 86 | A `for...of` or a `for...in` loop. Check out the [[ForIterLabel]] interface for all the properties. 87 | 88 | ```ts --Macro 89 | // A macro which turns a for...of loop to a forEach 90 | function $ToForEach(info: ForIterLabel) : void { 91 | const $initializerName = info.initializer; 92 | if ($$kindof!($initializerName) === ts.SyntaxKind.Identifier) { 93 | info.iterator.forEach(($initializerName: any) => { 94 | $$escape!(info.statement); 95 | }) 96 | } 97 | } 98 | ``` 99 | ```ts --Call 100 | const arr = [1, 3, 4, 5, 6]; 101 | 102 | $ToForEach: 103 | for (const item of arr) { 104 | console.log(item); 105 | console.log(item + 1); 106 | } 107 | ``` 108 | ```ts --Result 109 | (arr).forEach((item) => { 110 | console.log(item); 111 | console.log(item + 1); 112 | }); 113 | ``` 114 | 115 | ### For 116 | 117 | A general C-like for loop. Check out the [[ForLabel]] interface for all the properties. 118 | 119 | ```ts --Macro 120 | // Macro for turning a regular for loop into a while loop 121 | function $ForToWhile(info: ForLabel) { 122 | if (info.initializer.variables) { 123 | +[[info.initializer.variables], (variable: [string, any]) => { 124 | $$define!(variable[0], variable[1], true) 125 | }]; 126 | } 127 | else info.initializer.expression; 128 | while(info.condition) { 129 | $$inline!(info.statement); 130 | info.increment; 131 | } 132 | } 133 | ``` 134 | ```ts --Call 135 | const arr = [1, 3, 4, 5, 6]; 136 | 137 | $ForToWhile: 138 | for (let i=2, j; i < arr.length; i++) { 139 | console.log(i); 140 | console.log(i + 1); 141 | } 142 | ``` 143 | ```ts --Result 144 | let i = 2; 145 | let j = undefined; 146 | while (i < arr.length) { 147 | console.log(i); 148 | console.log(i + 1); 149 | i++; 150 | } 151 | ``` 152 | 153 | ### While 154 | 155 | A `do...while` or a `while` loop. Check out the [[WhileLabel]] interface for all the properties. 156 | 157 | ```ts --Macro 158 | // Slows down a while loop by using an interval 159 | function $ToInterval(info: WhileLabel, intervalTimer = 1000) { 160 | const interval = setInterval(() => { 161 | if (info.condition) { 162 | $$inline!(info.statement); 163 | } else { 164 | clearInterval(interval); 165 | } 166 | }, intervalTimer); 167 | } 168 | ``` 169 | ```ts --Call 170 | const arr = [1, 3, 4, 5, 6]; 171 | 172 | $ToInterval: 173 | while (arr.length !== 0) { 174 | console.log(arr.pop()); 175 | } 176 | ``` 177 | ```ts --Result 178 | const interval = setInterval(() => { 179 | if (arr.length !== 0) { 180 | console.log(arr.pop()); 181 | } 182 | else { 183 | clearInterval(interval); 184 | } 185 | }, 1000); 186 | ``` 187 | 188 | ### Block 189 | 190 | A block, or a collection of statements, wrapped in an arrow function. See [[BlockLabel]] for all the properties. 191 | 192 | ```ts --Macro 193 | // Wraps a block in a try/catch, ignoring the error 194 | function $TrySilence(info: BlockLabel) { 195 | try { 196 | $$inline!(info.statement); 197 | } catch(err) {}; 198 | } 199 | ``` 200 | ```ts --Call 201 | const arr = [1, 3, 4, 5, 6]; 202 | 203 | if (arr.includes(5)) $TrySilence: { 204 | throw "Errorr..." 205 | // Some async actions... 206 | } else $TrySilence: { 207 | // Some async actions... 208 | console.log(arr); 209 | } 210 | ``` 211 | ```ts --Result 212 | if (arr.includes(5)) { 213 | try { 214 | throw "Errorr..."; 215 | } 216 | catch (err) { } 217 | ; 218 | } 219 | else { 220 | try { 221 | console.log(arr); 222 | } 223 | catch (err) { } 224 | ; 225 | } 226 | ``` 227 | 228 | ### Generic Label type 229 | 230 | A [[Label]] type is also provided, which allows you to be able to run a single macro for multiple statements. Just compare the `kind` property with any value of the [[LabelKinds]] enum. 231 | 232 | ## Calling label macros 233 | 234 | You can also call label macros just like regular macros! 235 | 236 | ```ts --Macro 237 | // Let's use the ToInterval macro, and let's make it so we can provide 238 | // a custom interval when we're calling the macro explicitly: 239 | 240 | function $ToInterval(info: WhileLabel, intervalTimer = 1000) { 241 | const interval = setInterval(() => { 242 | if (info.condition) { 243 | $$inline!(info.statement); 244 | } else { 245 | clearInterval(interval); 246 | } 247 | }, intervalTimer); 248 | } 249 | ``` 250 | ```ts --Call 251 | const arr = [1, 2, 3, 4, 5]; 252 | $ToInterval!({ 253 | condition: arr.length !== 0, 254 | do: false, 255 | kind: LabelKinds.While, 256 | statement: () => { 257 | console.log(arr.pop()); 258 | } 259 | }, 5000); 260 | ``` 261 | ```ts --Result 262 | const interval_1 = setInterval(() => { 263 | if (arr.length !== 0) { 264 | console.log(arr.pop()); 265 | } 266 | else { 267 | clearInterval(interval_1); 268 | } 269 | }, 5000); 270 | ``` 271 | 272 | ## Nesting macro labels 273 | 274 | Macro labels can be nested. Let's use both the `ForToWhile` and the `ToInterval` macros we created earlier on the same statement: 275 | 276 | ```ts --Call 277 | $ToInterval: 278 | $ForToWhile: 279 | for (let i=0; i < 100; i++) { 280 | console.log(i); 281 | } 282 | ``` 283 | ```ts --Result 284 | let i = 0; 285 | const interval = setInterval(() => { 286 | if (i < 100) { 287 | console.log(i); 288 | i++; 289 | } 290 | else { 291 | clearInterval(interval); 292 | } 293 | }, 1000); 294 | ``` 295 | 296 | If a nested label macro expands to two or more statements that can be used with macro labels, then only the first statement will be used in the upper macro label, while all other statements will be placed **above** that statement. -------------------------------------------------------------------------------- /docs/In-Depth/markers.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Markers 3 | order: 7 4 | --- 5 | 6 | # Markers 7 | 8 | `Markers` make macro parameters behave differently. They don't alter the parameter's type, but it's behaviour. 9 | 10 | ## Accumulator 11 | 12 | A parameter which increments every time the macro is called. You can only have one accumulator parameter per macro. 13 | 14 | ```ts --Macro 15 | import { Accumulator } from "ts-macros" 16 | 17 | function $num(acc: Accumulator = 0) : Array { 18 | acc; 19 | } 20 | ``` 21 | ```ts --Call 22 | $num!(); 23 | $num!(); 24 | $num!(); 25 | ``` 26 | ```ts --Result 27 | 0 28 | 1 29 | 2 30 | ``` 31 | 32 | ## Save 33 | 34 | Saves the provided expression in a hygienic variable. This guarantees that the parameter will expand to an identifier. The declaration statement is also not considered part of the expanded code. 35 | 36 | ```ts --Macro 37 | function $map(arr: Save>, cb: Function) : Array { 38 | const res = []; 39 | for (let i=0; i < arr.length; i++) { 40 | res.push($$inline!(cb, [arr[i]])); 41 | } 42 | return $$ident!("res"); 43 | } 44 | ``` 45 | ```ts --Call 46 | { 47 | const mapResult = $map!([1, 2, 3, 4, 5], (n) => console.log(n)); 48 | } 49 | ``` 50 | ```ts --Result 51 | { 52 | let arr_1 = [1, 2, 3, 4, 5]; 53 | const mapResult = (() => { 54 | const res = []; 55 | for (let i = 0; i < arr_1.length; i++) { 56 | res.push(console.log(arr_1[i])); 57 | } 58 | return res; 59 | })(); 60 | } 61 | ``` -------------------------------------------------------------------------------- /docs/In-Depth/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Overview 3 | order: 1 4 | --- 5 | 6 | # Overview 7 | 8 | ts-macros is a custom typescript **transformer** which implements function macros. This library is heavily inspired by Rust's `macro_rules!` macro. Since it's a custom transformer, it can be plugged in into any tool which uses the `typescript` npm package. 9 | 10 | 11 | ## Basic usage 12 | 13 | All macro names must start with a dollar sign (`$`) and must be declared using the function keyword. Macros can then be called just like a normal function, but with a `!` after it's name: `$macro!(params)`. 14 | 15 | ```ts --Macro 16 | function $contains(value: T, ...possible: Array) { 17 | return +["||", [possible], (item: T) => value === item]; 18 | } 19 | ``` 20 | ```ts --Call 21 | console.log($contains!(searchItem, "erwin", "tj")); 22 | ``` 23 | ```ts --Result 24 | console.log(searchItem === "erwin" || searchItem === "tj"); 25 | ``` 26 | 27 | ## Install 28 | 29 | ``` 30 | npm i --save-dev ts-macros 31 | ``` 32 | 33 | ### Usage with ttypescript 34 | 35 | By default, typescript doesn't allow you to add custom transformers, so you must use a tool which adds them. `ttypescript` does just that! Make sure to install it: 36 | 37 | ``` 38 | npm i --save-dev ttypescript 39 | ``` 40 | 41 | and add the `ts-macros` transformer to your `tsconfig.json`: 42 | 43 | ```json 44 | "compilerOptions": { 45 | //... other options 46 | "plugins": [ 47 | { "transform": "ts-macros" } 48 | ] 49 | } 50 | ``` 51 | 52 | then transpile your code with `ttsc`. 53 | 54 | ### Usage with ts-loader 55 | 56 | ```js 57 | const TsMacros = require("ts-macros").default; 58 | 59 | options: { 60 | getCustomTransformers: program => { 61 | before: [TsMacros(program)] 62 | } 63 | } 64 | ``` 65 | 66 | ### Usage with vite 67 | 68 | If you want to use ts-macros with vite, you'll have to use the `...` plguin. [Here](https://github.com/GoogleFeud/ts-macros-vite-example) is an 69 | example repository which sets up a basic vite project which includes ts-macros. 70 | 71 | **Note**: Macros and dev mode do not work well together. If your macro is in one file, and you're using it in a different file, and you want to change some code inside the macro, you'll also have to change some code in the file the macro's used in so you can see the change. It could be adding an empty line or a space somewhere, the change doesn't matter, the file just needs to be transpiled again for the changes in the macro to happen. 72 | 73 | ## Security 74 | 75 | This library has 2 built-in macros (`$raw` and `$comptime`) which **can** execute arbitrary code during transpile time. The code is **not** sandboxed in any way and has access to your file system and all node modules. 76 | 77 | If you're transpiling an untrusted codebase which uses this library, make sure to turn the `noComptime` option to `true`. Enabling it will replace all calls to these macros with `null` without executing the code inside them. 78 | 79 | **ttypescript:** 80 | ```json 81 | "plugins": [ 82 | { "transform": "ts-macros", "noComptime": true } 83 | ] 84 | ``` 85 | 86 | **manually creating the factory:** 87 | ```js 88 | TsMacros(program, { noComptime: true }); 89 | ``` 90 | 91 | ## Contributing 92 | 93 | `ts-macros` is being maintained by a single person. Contributions are welcome and appreciated. Feel free to open an issue or create a pull request at https://github.com/GoogleFeud/ts-macros. -------------------------------------------------------------------------------- /docs/In-Depth/parameters.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Parameters 3 | order: 3 4 | --- 5 | 6 | # Macro parameters 7 | 8 | By default, all parameters are replaced **literally** when the macro is expanding. For examle, if you pass an array literal to a macro, all uses of that parameter will be replaced with the EXACT array literal: 9 | 10 | ```ts --Macro 11 | function $loop(arr: Array, cb: (element: number) => void) { 12 | for (let i=0; i < arr.length; i++) { 13 | cb(arr[i]); 14 | } 15 | } 16 | ``` 17 | ```ts --Call 18 | $loop!([1, 2, 3, 4, 5], (el) => console.log(el)); 19 | ``` 20 | ```ts --Result 21 | for (let i = 0; i < [1, 2, 3, 4, 5].length; i++) { 22 | ((el) => console.log(el))([1, 2, 3, 4, 5][i]); 23 | } 24 | ``` 25 | 26 | To avoid this, you can assign the literal to a variable, or use the [[Save]] marker. 27 | 28 | ```ts --Macro 29 | function $loop(arr: Array, cb: (element: number) => void) { 30 | const array = arr; 31 | for (let i=0; i < array.length; i++) { 32 | cb(array[i]); 33 | } 34 | } 35 | ``` 36 | ```ts --Call 37 | $loop!([1, 2, 3, 4, 5], (el) => console.log(el)); 38 | ``` 39 | ```ts --Result 40 | const array_1 = [1, 2, 3, 4, 5]; 41 | for (let i_1 = 0; i_1 < array_1.length; i_1++) { 42 | ((el) => console.log(el))(array_1[i_1]); 43 | } 44 | ``` -------------------------------------------------------------------------------- /docs/In-Depth/repetitions.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Repetitions 3 | order: 4 4 | --- 5 | 6 | # Repetitions 7 | 8 | ts-macro has **repetitions** which are heavily inspired by Rust. They allow you to repeat code for every element of an array. Since ts-macros is limited by the typescript compiler, this is the syntax for repetitions: 9 | 10 | ``` 11 | +[separator?, [arrays], (...params) => codeToRepeat] 12 | ``` 13 | 14 | The `separator` is an optional string which will separate all the expressions generated by the repetition. If a separator is omitted, then every expression will be an `ExpressionStatement`. 15 | 16 | `[arrays]` is an array of array literals. The elements in the arrays are the things the repetition will go through. This is the simplest repetition: 17 | 18 | ```ts --Macro 19 | function $test(...numbers: Array) { 20 | +[[numbers, ["a", "b", "c"]], (num: number|string) => console.log(num)] 21 | } 22 | 23 | $test!(1, 2, 3); 24 | ``` 25 | ```ts --Result 26 | console.log(1) 27 | console.log(2) 28 | console.log(3) 29 | console.log("a") 30 | console.log("b") 31 | console.log("c") 32 | ``` 33 | 34 | The repetition goes through all the numbers and strings, and creates a `console.log` expression for each of them. Easy! 35 | 36 | ## Multiple elements in repetition 37 | 38 | Let's say you want to go through 2 or more arrays **at the same time**, to create combinations like `1a`, `2b`, etc. You can accomplish this by adding another parameter: 39 | 40 | ```ts --Macro 41 | function $test(...numbers: Array) { 42 | +[[numbers, ["a", "b", "c"]], (firstArr: number, secondArr: string) => console.log(firstArr + secondArr)] 43 | } 44 | 45 | $test!(1, 2, 3); 46 | ``` 47 | ```ts --Result 48 | console.log("1a") 49 | console.log("2b") 50 | console.log("3c") 51 | ``` 52 | 53 | The second parameter tells the transformer to separate **the second array** from the rest. So `firstArr` goes through all arrays **except** the second array (`["a", "b", "c"]`), and in this case the two arrays just get separated. But what if we add a third array? 54 | 55 | ```ts --Macro 56 | function $test(...numbers: Array) { 57 | +[[numbers, ["a", "b", "c"], ["e", "d", "f"]], (all: number, secondArr: string) => console.log(firstArr + secondArr)] 58 | } 59 | 60 | $test!(1, 2, 3); 61 | ``` 62 | ```ts --Result 63 | console.log("1a") 64 | console.log("2b") 65 | console.log("3c") 66 | console.log("e" + null) 67 | console.log("d" + null) 68 | console.log("f" + null) 69 | ``` 70 | 71 | Here `firstArr` goes through the first array and the third array, and `secondArr` goes through the second. The second array only has 3 elements, and so it's `null` for the elements of the third array. 72 | 73 | ## Separators 74 | 75 | You can use the following separators: 76 | 77 | - `+` - Adds all the values. 78 | - `-` - Subtracts all the values. 79 | - `*` - Multiplies all the values. 80 | - `.` - Creates a property / element access chain from the values. 81 | - `[]` - Puts all the values in an array. 82 | - `{}` - Creates an object literal from the values. For this separator to work, the result of the repetition callback must be an array literal with 2 elements, the key and the value (`[key, value]`). 83 | - `()` - Creates a comma list expression from all expressions. 84 | - `||` - Creates an OR chain with the expressions. 85 | - `&&` - Creates an AND chain with the expressions. 86 | 87 | ## Repetitions as function arguments 88 | 89 | If a repetition is placed inside of a function call, and a separator is **not** provided, then all results will be passed as arguments. 90 | 91 | ```ts --Macro 92 | function $log(...items: Array) { 93 | console.log(+[[items], (item: number) => item + 1]); 94 | } 95 | ``` 96 | ```ts --Call 97 | $log!(1, 2, 3, 4, 5); 98 | ``` 99 | ```ts --Result 100 | console.log(2, 3, 4, 5, 6); 101 | ``` 102 | 103 | ## $$i built-in macro 104 | 105 | ts-macros provides a built-in macro, `$$i`, if used inside a repetition, it'll return the number of the current iteration, if it's used outside, `-1`. 106 | 107 | ```ts --Macro 108 | import { $$i } from "../../dist"; 109 | 110 | function $arr(...els: Array) : Array { 111 | return +["[]", [els], (el: number) => el + $$i!()] as unknown as Array; 112 | } 113 | ``` 114 | ```ts --Call 115 | $arr!(1, 2, 3); 116 | ``` 117 | ```ts --Result 118 | [1, 3, 5] 119 | ``` -------------------------------------------------------------------------------- /docs/Links/playground.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Playground 3 | redirect: https://googlefeud.github.io/ts-macros/playground/ 4 | --- -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-macros", 3 | "version": "2.6.2", 4 | "description": "A typescript transformer / plugin which allows you to write macros for typescript!", 5 | "main": "dist/index.js", 6 | "bin": "dist/cli/index.js", 7 | "dependencies": { 8 | "yargs-parser": "^21.1.1" 9 | }, 10 | "devDependencies": { 11 | "@types/chai": "^4.3.4", 12 | "@types/mocha": "^9.1.1", 13 | "@types/node": "^16.18.103", 14 | "@types/ts-expose-internals": "npm:ts-expose-internals@^5.6.2", 15 | "@types/yargs-parser": "^21.0.0", 16 | "@typescript-eslint/eslint-plugin": "^6.7.2", 17 | "@typescript-eslint/parser": "^6.7.2", 18 | "chai": "^4.3.8", 19 | "diff": "^5.1.0", 20 | "eslint": "^7.32.0", 21 | "mocha": "^9.2.2", 22 | "ts-patch": "^3.2.1", 23 | "typescript": "^5.6.2" 24 | }, 25 | "peerDependencies": { 26 | "typescript": "5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x" 27 | }, 28 | "scripts": { 29 | "build": "tsc", 30 | "lint": "npx eslint", 31 | "test": "tsc && cd ./tests && tspc && mocha dist/integrated/**/*.js && node ./dist/snapshots/index", 32 | "playground": "tsc && cd ./playground && npm run dev", 33 | "manual": "tsc && cd ./test && tspc", 34 | "prepublishOnly": "tsc" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/GoogleFeud/ts-macros.git" 39 | }, 40 | "keywords": [ 41 | "typescript", 42 | "macros" 43 | ], 44 | "author": "GoogleFeud", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/GoogleFeud/ts-macros/issues" 48 | }, 49 | "homepage": "https://googlefeud.github.io/ts-macros/" 50 | } 51 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /playground/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | 2 | import Editor, { useMonaco } from "@monaco-editor/react"; 3 | import { languages, editor } from "monaco-editor"; 4 | import { useEffect, useState } from "react"; 5 | import { CompilerOptions, GeneratedTypes, Markers } from "../utils/transpile"; 6 | import { MacroError } from "../../dist"; 7 | 8 | 9 | export function TextEditor(props: { 10 | onChange: (code: string|undefined) => void, 11 | code: string|undefined, 12 | libCode?: GeneratedTypes, 13 | errors: MacroError[] 14 | }) { 15 | const monaco = useMonaco(); 16 | const [editor, setEditor] = useState(); 17 | const [macroTypeModel, setMacroTypeModel] = useState(); 18 | const [chainTypeModel, setChainTypeModel] = useState(); 19 | 20 | const macroTypesLib = "ts:ts-macros/generated_types.d.ts"; 21 | const chainTypesLib = "ts:ts-macros/chain_types.d.ts" 22 | 23 | useEffect(() => { 24 | if (!monaco) return; 25 | monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ 26 | ...CompilerOptions as unknown as languages.typescript.CompilerOptions 27 | }); 28 | monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ 29 | diagnosticCodesToIgnore: [1219] 30 | }); 31 | 32 | const markersLibName = "ts:ts-macros/markers.d.ts"; 33 | monaco.languages.typescript.javascriptDefaults.addExtraLib(Markers, markersLibName); 34 | monaco.editor.createModel(Markers, "typescript", monaco.Uri.parse(markersLibName)); 35 | 36 | const macroTypesContent = props.libCode?.fromMacros || ""; 37 | monaco.languages.typescript.javascriptDefaults.addExtraLib(macroTypesContent, macroTypesLib); 38 | setMacroTypeModel(monaco.editor.createModel(macroTypesContent, "typescript", monaco.Uri.parse(macroTypesLib))); 39 | 40 | const chainTypesContent = `export {};\n\n${props.libCode?.chainTypes || ""}`; 41 | monaco.languages.typescript.javascriptDefaults.addExtraLib(chainTypesContent, chainTypesLib); 42 | setChainTypeModel(monaco.editor.createModel(chainTypesContent, "typescript", monaco.Uri.parse(chainTypesLib))); 43 | }, [monaco]); 44 | 45 | useEffect(() => { 46 | if (!monaco) return; 47 | macroTypeModel?.setValue(props.libCode?.fromMacros || ""); 48 | chainTypeModel?.setValue(`export {};\n\n${props.libCode?.chainTypes || ""}`); 49 | }, [props.libCode]); 50 | 51 | useEffect(() => { 52 | if (!monaco || !editor) return; 53 | const model = editor.getModel(); 54 | if (!model) return; 55 | monaco.editor.setModelMarkers(model, "_", props.errors.map(error => { 56 | const startPos = model.getPositionAt(error.start); 57 | const endPos = model.getPositionAt(error.start + error.length); 58 | return { 59 | message: error.rawMsg, 60 | severity: 8, 61 | startColumn: startPos.column, 62 | startLineNumber: startPos.lineNumber, 63 | endColumn: endPos.column, 64 | endLineNumber: endPos.lineNumber 65 | } 66 | })); 67 | }, [props.errors]); 68 | 69 | return setEditor(editor)}> 70 | 71 | ; 72 | } -------------------------------------------------------------------------------- /playground/components/Runnable.tsx: -------------------------------------------------------------------------------- 1 | import SplitPane from "react-split-pane"; 2 | import { useEffect, useRef, useState } from "react"; 3 | import Editor from "@monaco-editor/react"; 4 | import style from "../css/App.module.css"; 5 | 6 | export enum LogKind { 7 | Log, 8 | Error, 9 | Warn 10 | } 11 | 12 | export interface Log { 13 | kind: LogKind, 14 | message: unknown 15 | } 16 | 17 | function resolveLogKind(kind: LogKind) : JSX.Element { 18 | switch (kind) { 19 | case LogKind.Log: return [LOG]:; 20 | case LogKind.Error: return [ERR]:; 21 | case LogKind.Warn: return [WARN]:; 22 | } 23 | } 24 | 25 | function formatObjectLike(obj: [string|number|symbol, any][], original: any, nestIdent?: number, extraCode?: string) : JSX.Element { 26 | return <> 27 | {(original.constructor && original.constructor.name && original.constructor.name !== "Object" && original.constructor.name + " ") || ""}{extraCode || ""}{"{"} 28 |
29 | 30 | {obj.map(([key, val], index) => 31 | {!!index && <>,
} 32 | {" ".repeat(nestIdent || 2)}{key}: {formatValue(val, (nestIdent || 2) + 1)} 33 |
)} 34 |
35 |
36 | {" ".repeat(nestIdent ? nestIdent - 1 : 1) + "}"}
37 | 38 | } 39 | 40 | function formatValue(obj: unknown, nestIdent = 0) : JSX.Element { 41 | if (typeof obj === "string") return "{obj.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """)}"; 42 | else if (typeof obj === "number") return {obj}; 43 | else if (typeof obj === "function") return [Function] 44 | else if (obj === undefined) return undefined; 45 | else if (obj === null) return null; 46 | else if (obj === true) return true; 47 | else if (obj === false) return false; 48 | else if (Array.isArray(obj)) return [{obj.map((element, index) => 49 | {!!index && , } 50 | {formatValue(element, nestIdent + 1)} 51 | )}] 52 | else if (obj instanceof Map) return formatObjectLike([...obj.entries()], obj, nestIdent, `(${obj.size}) `); 53 | else if (obj instanceof Set) return Set ({obj.size}){" {"}{[...obj.values()].map((element, index) => 54 | {!!index && , } 55 | {formatValue(element, nestIdent + 1)} 56 | )}{"}"} 57 | else { 58 | const entries = Object.entries(obj); 59 | if (entries.length === 0) return <>{"{}"}; 60 | else return formatObjectLike(entries, obj, nestIdent); 61 | } 62 | } 63 | 64 | 65 | export function Runnable(props: { code: string }) { 66 | const [logs, setLogs] = useState([]); 67 | const [newHeight, setNewHeight] = useState("100%"); 68 | const topPaneRef = useRef(null); 69 | const bottomPaneRef = useRef(null); 70 | 71 | const recalcHeight = () => { 72 | const current = topPaneRef.current; 73 | if (!current) return; 74 | setNewHeight(`${window.innerHeight - topPaneRef.current.clientHeight - (55 * 3)}px`); 75 | } 76 | 77 | const scrollToBottom = () => { 78 | const el = bottomPaneRef.current; 79 | if (!el) return; 80 | el.scrollTop = el.scrollHeight; 81 | } 82 | 83 | useEffect(() => { 84 | recalcHeight(); 85 | scrollToBottom(); 86 | }, [logs]); 87 | 88 | const specialConsole = { 89 | log: (...messages: any[]) => { 90 | setLogs([...logs, ...messages.map(msg => ({ kind: LogKind.Log, message: msg }))]); 91 | }, 92 | warn: (...messages: any[]) => { 93 | setLogs([...logs, ...messages.map(msg => ({ kind: LogKind.Warn, message: msg }))]); 94 | }, 95 | error: (...messages: any[]) => { 96 | setLogs([...logs, ...messages.map(msg => ({ kind: LogKind.Error, message: msg }))]); 97 | }, 98 | } 99 | 100 | return 101 |
102 | ; 103 |
104 |
105 | 113 | 114 |
115 |
116 | {logs.map((log, index) =>
117 | {!!index &&
} 118 | {resolveLogKind(log.kind)}{" "} 119 | {formatValue(log.message)} 120 |
)} 121 |
122 |
123 |
; 124 | } -------------------------------------------------------------------------------- /playground/css/App.module.css: -------------------------------------------------------------------------------- 1 | .app { 2 | display: flex; 3 | flex-direction: column; 4 | width: 100%; 5 | height: 100vh; 6 | } 7 | 8 | .header { 9 | background-color: #222; 10 | color: white; 11 | display: flex; 12 | height: 55px; 13 | justify-content: space-between; 14 | align-items: center; 15 | padding-left: 30px; 16 | padding-right: 30px; 17 | } 18 | 19 | .mainContent { 20 | height: calc(100% - 55px); 21 | width: 100%; 22 | } 23 | 24 | .footer { 25 | background-color: #222; 26 | color: white; 27 | display: flex; 28 | height: 55px; 29 | justify-content: center; 30 | align-items: center; 31 | width: 100%; 32 | z-index: 10; 33 | } 34 | 35 | .button { 36 | margin: 15px; 37 | background-color: #1e1e1e; 38 | color: white; 39 | border: solid 1px white; 40 | cursor: pointer; 41 | } 42 | 43 | .runSection { 44 | background-color: #1e1e1e; 45 | color: white; 46 | height: 100%; 47 | } 48 | 49 | .runSectionResult { 50 | margin-left: 30px; 51 | overflow-y: auto; 52 | height: calc(100% - 55px); 53 | } 54 | 55 | .header a, .header a:visited, .footer a, .footer a:visited { 56 | color: white !important; 57 | } 58 | 59 | .code { 60 | font-family: Consolas, "Courier New", monospace; 61 | font-size: 14px; 62 | white-space: pre; 63 | } 64 | 65 | .number { 66 | color: #b5cea8; 67 | } 68 | 69 | .string { 70 | color: #ce9178; 71 | } 72 | 73 | .keyword { 74 | color: #569cd6; 75 | } 76 | 77 | .comma { 78 | color: #777; 79 | } 80 | 81 | .classNameIdent { 82 | color: #3dc9b0; 83 | } 84 | 85 | .logSeparator { 86 | color: #777; 87 | width: 100%; 88 | border-bottom-style: dotted; 89 | border-bottom-width: 3px; 90 | margin-top: 10px; 91 | margin-bottom: 10px; 92 | } 93 | 94 | .Pane { 95 | overflow: auto; 96 | } -------------------------------------------------------------------------------- /playground/css/global.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | overflow: hidden; 4 | margin: 0; 5 | font-family: Arial, sans-serif; 6 | background-color: #1e1e1e; 7 | } 8 | 9 | .Resizer { 10 | background: rgb(255, 255, 255); 11 | opacity: 0.2; 12 | z-index: 1; 13 | -moz-box-sizing: border-box; 14 | -webkit-box-sizing: border-box; 15 | box-sizing: border-box; 16 | -moz-background-clip: padding; 17 | -webkit-background-clip: padding; 18 | background-clip: padding-box; 19 | } 20 | 21 | .Resizer:hover { 22 | -webkit-transition: all 2s ease; 23 | transition: all 2s ease; 24 | } 25 | 26 | .Resizer.horizontal { 27 | height: 11px; 28 | margin: -5px 0; 29 | border-top: 5px solid rgba(255, 255, 255, 0); 30 | border-bottom: 5px solid rgba(255, 255, 255, 0); 31 | cursor: row-resize; 32 | width: 100%; 33 | } 34 | 35 | .Resizer.horizontal:hover { 36 | border-top: 5px solid rgba(255, 255, 255, 0.5); 37 | border-bottom: 5px solid rgba(255, 255, 255, 0.5); 38 | } 39 | 40 | .Resizer.vertical { 41 | width: 11px; 42 | margin: 0 -5px; 43 | border-left: 5px solid rgba(255, 255, 255, 0); 44 | border-right: 5px solid rgba(255, 255, 255, 0); 45 | cursor: col-resize; 46 | } 47 | 48 | .Resizer.vertical:hover { 49 | border-left: 5px solid rgba(255, 255, 255, 0.5); 50 | border-right: 5px solid rgba(255, 255, 255, 0.5); 51 | } 52 | .Resizer.disabled { 53 | cursor: not-allowed; 54 | } 55 | .Resizer.disabled:hover { 56 | border-color: transparent; 57 | } 58 | -------------------------------------------------------------------------------- /playground/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /playground/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | assetPrefix: "./", 5 | eslint: { 6 | ignoreDuringBuilds: true 7 | }, 8 | webpack: (config, {isServer}) => { 9 | if (!isServer) { 10 | config.resolve.fallback = { 11 | fs: false, 12 | path: false, 13 | process: false, 14 | module: false 15 | } 16 | } 17 | return config; 18 | } 19 | }; 20 | 21 | module.exports = nextConfig; 22 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "export": "next export" 9 | }, 10 | "dependencies": { 11 | "@monaco-editor/react": "^4.4.6", 12 | "lz-string": "^1.4.4", 13 | "monaco-editor": "^0.33.0", 14 | "next": "12.1.1", 15 | "react": "17.0.2", 16 | "react-dom": "17.0.2", 17 | "react-split-pane": "^0.1.92" 18 | }, 19 | "devDependencies": { 20 | "@types/lz-string": "^1.3.34", 21 | "@types/node": "17.0.23", 22 | "@types/react": "17.0.43", 23 | "typescript": "^5.6.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../css/global.css"; 2 | import type { AppProps } from "next/app"; 3 | import Head from "next/head"; 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return <> 7 | 8 | Typescript Macros 9 | 10 | 11 | ; 12 | } 13 | 14 | export default MyApp; 15 | -------------------------------------------------------------------------------- /playground/pages/index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { GeneratedTypes, transpile } from "../utils/transpile"; 3 | import { useEffect, useState } from "react"; 4 | import { TextEditor } from "../components/Editor"; 5 | import { Runnable } from "../components/Runnable"; 6 | import SplitPane from "react-split-pane"; 7 | import { compressToEncodedURIComponent, decompressFromEncodedURIComponent } from "lz-string"; 8 | import styles from "../css/App.module.css"; 9 | import { MacroError } from "../../dist"; 10 | 11 | const SetupCodes = [ 12 | `function $contains(value: T, possible: Array) { 13 | return +["||", [possible], (val: T) => value === val]; 14 | } 15 | 16 | const searchItem = "google"; 17 | console.log($contains!(searchItem, ["erwin", "tj"]));`, 18 | `function $try(resultObj: Save<{ value?: number, is_err: () => boolean}>) { 19 | $$escape!(() => { 20 | if (resultObj.is_err()) { 21 | return resultObj; 22 | } 23 | }); 24 | return resultObj.value; 25 | } 26 | 27 | 28 | const a = $try!({ value: 123, is_err: () => false }); 29 | console.log(val);`, 30 | `type ClassInfo = { name: string, value: string }; 31 | 32 | function $makeClasses(...info: Array) { 33 | +[[info], (classInfo: ClassInfo) => { 34 | $$ts!(\` 35 | class \${classInfo.name} { 36 | constructor() { 37 | this.value = \${classInfo.value} 38 | } 39 | } 40 | \`); 41 | }]; 42 | } 43 | 44 | $makeClasses!({name: "A", value: "123"}, {name: "B", value: "345"});`, 45 | `function $map(arr: Save>, cb: (item: T) => R) : Array { 46 | $$escape!(() => { 47 | const res = []; 48 | for (let i=0; i < arr.length; i++) { 49 | res.push($$inline!(cb, [arr[i]])); 50 | } 51 | }); 52 | return $$ident!("res"); 53 | } 54 | 55 | console.log($map!([1, 2, 3, 4, 5, 6, 7, 8, 9], (num) => num * 2));`, 56 | `function $ToInterval(info: WhileLabel, intervalTimer = 1000) { 57 | const interval = setInterval(() => { 58 | if (info.condition) { 59 | $$inline!(info.statement, []); 60 | } else { 61 | clearInterval(interval); 62 | } 63 | }, intervalTimer); 64 | } 65 | 66 | const arr = [1, 3, 4, 5, 6]; 67 | 68 | $ToInterval: 69 | while (arr.length !== 0) { 70 | console.log(arr.pop()); 71 | }`, 72 | ` 73 | function $renameClass(newName: string) : EmptyDecorator { 74 | return $$raw!((ctx, newNameNode) => { 75 | const target = ctx.thisMacro.target; 76 | return ctx.factory.createClassDeclaration( 77 | target.modifiers?.filter(m => m.kind !== ctx.ts.SyntaxKind.Decorator), 78 | ctx.factory.createIdentifier(newNameNode.text), 79 | target.typeParameters, 80 | target.heritageClauses, 81 | target.members 82 | ) 83 | }); 84 | } 85 | 86 | @$renameClass!("NewTest") 87 | class Test { 88 | propA: number 89 | propB: string 90 | constructor(a: number, b: string) { 91 | this.propA = a; 92 | this.propB = b; 93 | } 94 | } 95 | 96 | console.log(new NewTest(1, "hello!")); 97 | ` 98 | ] 99 | 100 | const SetupCode = ` 101 | // Interactive playground! 102 | // Write your code here and see the transpiled result. 103 | // All types and functions from the library are already imported! 104 | 105 | ${SetupCodes[Math.floor(Math.random() * SetupCodes.length)]} 106 | `; 107 | 108 | function Main() { 109 | const [code, setCode] = useState(); 110 | const [errors, setErrors] = useState([]); 111 | const [libCode, setLibCode] = useState(); 112 | const [compiledCode, setCompiled] = useState(); 113 | 114 | const transpileCode = (source: string) => { 115 | setCode(source); 116 | const {generatedTypes, errors, transpiledSourceCode} = transpile(source); 117 | setCompiled(transpiledSourceCode); 118 | setLibCode(generatedTypes); 119 | setErrors(errors); 120 | } 121 | 122 | useEffect(() => { 123 | const params = Object.fromEntries(new URLSearchParams(window.location.search).entries()); 124 | if (params.code) { 125 | const normalized = decompressFromEncodedURIComponent(params.code); 126 | if (!normalized) return; 127 | transpileCode(normalized); 128 | } else { 129 | transpileCode(SetupCode); 130 | } 131 | }, []); 132 | 133 | return ( 134 |
135 |
136 |
137 |

Typescript Macros

138 | 146 |
147 | 148 | 149 | 150 | 151 | 152 |
153 |
154 | 155 | { 156 | transpileCode(code || ""); 157 | }} /> 158 | 159 | 160 |
161 | 164 |
165 | ); 166 | } 167 | 168 | export default () => { 169 | return
; 170 | }; -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "downlevelIteration": true, 16 | "jsx": "preserve", 17 | "incremental": true 18 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /playground/utils/transpile.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as ts from "typescript"; 3 | import { macros, MacroError } from "../../dist"; 4 | import { extractGeneratedTypes } from "../../dist/type-resolve"; 5 | import { MacroTransformer } from "../../dist/transformer"; 6 | 7 | export let Markers = ` 8 | declare function $$loadEnv(path?: string) : void; 9 | declare function $$readFile(path: string, parseJSON?: false) : string; 10 | declare function $$inline any>(func: F, params: Parameters, doNotCall: any) : () => ReturnType; 11 | declare function $$inline any>(func: F, params: Parameters) : ReturnType; 12 | declare function $$kindof(ast: unknown) : number; 13 | declare function $$define(varname: string, initializer: unknown, let?: boolean, exportDecl?: boolean) : void; 14 | declare function $$i() : number; 15 | declare function $$length(arr: Array|string) : number; 16 | declare function $$ident(str: string) : any; 17 | declare function $$err(str: string) : void; 18 | declare function $$includes(arr: Array, val: T) : boolean; 19 | declare function $$includes(arr: string, val: string) : boolean; 20 | declare function $$slice(str: Array, start?: number, end?: number) : Array; 21 | declare function $$slice(str: string, start?: number, end?: number) : string; 22 | declare function $$ts(code: string) : T; 23 | declare function $$escape(code: () => T) : T; 24 | declare function $$typeToString(simplify?: boolean, nonNull?: boolean, fullExpand?: boolean) : string; 25 | declare function $$propsOfType() : Array; 26 | declare function $$typeAssignableTo() : boolean; 27 | declare function $$comptime(fn: () => void) : void; 28 | interface RawContext { 29 | ts: any, 30 | factory: any, 31 | transformer: any, 32 | checker: any, 33 | thisMacro: any, 34 | error: (node: any, message: string) => void 35 | } 36 | declare function $$raw(fn: (ctx: RawContext, ...args: any[]) => ts.Node | ts.Node[] | undefined) : T; 37 | declare function $$text(exp: any) : string; 38 | declare function $$decompose(exp: any) : any[]; 39 | declare function $$map(exp: T, mapper: (value: any, parent: number) => any) : T; 40 | type TypeMetadataJSDocTagCollection = Record; 41 | interface TypeMetadataProperty { 42 | name: string, 43 | tags: TypeMetadataJSDocTagCollection, 44 | type: string, 45 | optional: boolean 46 | } 47 | interface TypeMetadataMethod { 48 | name: string, 49 | tags: TypeMetadataJSDocTagCollection, 50 | parameters: Array<{name: string, type: string, optional: boolean}>, 51 | returnType: string 52 | } 53 | interface TypeMedatada { 54 | name: string, 55 | properties: TypeMetadataProperty[], 56 | methods: TypeMetadataMethod[] 57 | } 58 | declare function $$typeMetadata(collectProps?: boolean, collectMethods?: boolean) : TypeMedatada; 59 | type Accumulator = number & { __marker?: "Accumulator" }; 60 | type Save = T & { __marker?: "Save" }; 61 | type EmptyDecorator = (...props: any) => void; 62 | const enum LabelKinds { 63 | If, 64 | ForIter, 65 | For, 66 | While, 67 | Block 68 | } 69 | interface IfLabel { 70 | kind: LabelKinds.If 71 | condition: any, 72 | then: any, 73 | else: any 74 | } 75 | interface ForIterLabel { 76 | kind: LabelKinds.ForIter, 77 | type: "in" | "of", 78 | initializer: any, 79 | iterator: any, 80 | statement: any 81 | } 82 | interface ForLabel { 83 | kind: LabelKinds.For, 84 | initializer: { 85 | expression?: any, 86 | variables?: Array<[variableName: string, initializer: any]> 87 | }, 88 | condition: any, 89 | increment: any, 90 | statement: any 91 | } 92 | interface WhileLabel { 93 | kind: LabelKinds.While, 94 | do: boolean, 95 | condition: any, 96 | statement: any 97 | } 98 | interface BlockLabel { 99 | kind: LabelKinds.Block, 100 | statement: any 101 | } 102 | type Label = IfLabel | ForIterLabel | ForLabel | WhileLabel | BlockLabel; 103 | `; 104 | 105 | Markers += "const enum SyntaxKind {\n"; 106 | for (const kind in Object.keys(ts.SyntaxKind)) { 107 | if (ts.SyntaxKind[kind]) Markers += `${ts.SyntaxKind[kind]} = ${kind},\n`; 108 | } 109 | Markers += "\n}\n"; 110 | 111 | export const CompilerOptions: ts.CompilerOptions = { 112 | //...ts.getDefaultCompilerOptions(), 113 | noImplicitAny: true, 114 | strictNullChecks: true, 115 | target: ts.ScriptTarget.ESNext, 116 | experimentalDecorators: true, 117 | lib: ["ES5"] 118 | }; 119 | 120 | export interface GeneratedTypes { 121 | fromMacros: string, 122 | chainTypes: string 123 | } 124 | 125 | export function transpile(str: string) : { 126 | generatedTypes: GeneratedTypes, 127 | errors: MacroError[], 128 | transpiledSourceCode?: string 129 | } { 130 | macros.clear(); 131 | 132 | const sourceFile = ts.createSourceFile("module.ts", Markers + str, CompilerOptions.target || ts.ScriptTarget.ESNext, true); 133 | const errors = []; 134 | 135 | const CompilerHost: ts.CompilerHost = { 136 | getSourceFile: (fileName) => { 137 | if (fileName === "module.ts") return sourceFile; 138 | }, 139 | getDefaultLibFileName: () => "lib.d.ts", 140 | useCaseSensitiveFileNames: () => false, 141 | getCanonicalFileName: fileName => fileName, 142 | writeFile: () => {}, 143 | getCurrentDirectory: () => "", 144 | getNewLine: () => "\n", 145 | fileExists: () => true, 146 | readFile: () => "", 147 | directoryExists: () => true, 148 | getDirectories: () => [] 149 | }; 150 | 151 | const program = ts.createProgram(["module.ts"], CompilerOptions, CompilerHost); 152 | 153 | let genResult: ReturnType | undefined, transpiledSourceCode; 154 | try { 155 | program.emit(undefined, (_, text) => transpiledSourceCode = text, undefined, undefined, { 156 | before: [(ctx: ts.TransformationContext) => { 157 | const transformer = new MacroTransformer(ctx, program.getTypeChecker(), macros); 158 | return (node: ts.SourceFile) => { 159 | const modified = transformer.run(node); 160 | genResult = extractGeneratedTypes(program.getTypeChecker(), modified); 161 | return modified; 162 | } 163 | }] 164 | }); 165 | } catch (err: unknown) { 166 | if (err instanceof MacroError) errors.push(err); 167 | } 168 | 169 | return { 170 | transpiledSourceCode, 171 | generatedTypes: { 172 | fromMacros: genResult ? genResult.print(genResult.typeNodes) : "", 173 | chainTypes: genResult ? genResult.print(genResult.chainTypes) : "" 174 | }, 175 | errors 176 | } 177 | } -------------------------------------------------------------------------------- /src/actions.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { LabelKinds } from "."; 3 | import { NO_LIT_FOUND, createNumberNode, createObjectLiteral, hasBit } from "./utils"; 4 | 5 | export const binaryNumberActions: Record ts.Expression> = { 6 | [ts.SyntaxKind.MinusToken]: (left: number, right: number) => createNumberNode(left - right), 7 | [ts.SyntaxKind.AsteriskToken]: (left: number, right: number) => createNumberNode(left * right), 8 | [ts.SyntaxKind.SlashToken]: (left: number, right: number) => createNumberNode(left / right), 9 | [ts.SyntaxKind.LessThanToken]: (left: number, right: number) => left < right ? ts.factory.createTrue() : ts.factory.createFalse(), 10 | [ts.SyntaxKind.LessThanEqualsToken]: (left: number, right: number) => left <= right ? ts.factory.createTrue() : ts.factory.createFalse(), 11 | [ts.SyntaxKind.GreaterThanToken]: (left: number, right: number) => left > right ? ts.factory.createTrue() : ts.factory.createFalse(), 12 | [ts.SyntaxKind.GreaterThanEqualsToken]: (left: number, right: number) => left >= right ? ts.factory.createTrue() : ts.factory.createFalse(), 13 | [ts.SyntaxKind.AmpersandToken]: (left: number, right: number) => createNumberNode(left & right), 14 | [ts.SyntaxKind.BarToken]: (left: number, right: number) => createNumberNode(left | right), 15 | [ts.SyntaxKind.CaretToken]: (left: number, right: number) => createNumberNode(left ^ right), 16 | [ts.SyntaxKind.PercentToken]: (left: number, right: number) => createNumberNode(left % right) 17 | }; 18 | 19 | export const binaryActions: Record ts.Expression|undefined> = { 20 | [ts.SyntaxKind.PlusToken]: (_origLeft: ts.Expression, _origRight: ts.Expression, left: unknown, right: unknown) => { 21 | if (typeof left === "string" || typeof right === "string") return ts.factory.createStringLiteral(left as string + right); 22 | else if (typeof left === "number" || typeof right === "number") return createNumberNode(left as number + (right as number)); 23 | }, 24 | [ts.SyntaxKind.EqualsEqualsEqualsToken]: (_origLeft: ts.Expression, _origRight: ts.Expression, left: unknown, right: unknown) => left === right ? ts.factory.createTrue() : ts.factory.createFalse(), 25 | [ts.SyntaxKind.EqualsEqualsToken]: (_origLeft: ts.Expression, _origRight: ts.Expression, left: unknown, right: unknown) => left == right ? ts.factory.createTrue() : ts.factory.createFalse(), 26 | [ts.SyntaxKind.ExclamationEqualsEqualsToken]: (_origLeft: ts.Expression, _origRight: ts.Expression, left: unknown, right: unknown) => left !== right ? ts.factory.createTrue() : ts.factory.createFalse(), 27 | [ts.SyntaxKind.ExclamationEqualsToken]: (_origLeft: ts.Expression, _origRight: ts.Expression, left: unknown, right: unknown) => left != right ? ts.factory.createTrue() : ts.factory.createFalse(), 28 | [ts.SyntaxKind.AmpersandAmpersandToken]: (origLeft: ts.Expression, origRight: ts.Expression, left: unknown, right: unknown) => { 29 | if (left && right) return origRight; 30 | if (!left) return origLeft; 31 | if (!right) return origRight; 32 | }, 33 | [ts.SyntaxKind.BarBarToken]: (origLeft: ts.Expression, origRight: ts.Expression, left: unknown, right: unknown) => { 34 | if (left) return origLeft; 35 | else if (right) return origRight; 36 | else return origRight; 37 | } 38 | }; 39 | 40 | 41 | export const possiblyUnknownValueBinaryActions: Record ts.Expression|undefined> = { 42 | [ts.SyntaxKind.AmpersandAmpersandToken]: (origLeft: ts.Expression, origRight: ts.Expression, left: unknown) => { 43 | if (left !== NO_LIT_FOUND) { 44 | if (left) return origRight; 45 | else return origLeft; 46 | } 47 | }, 48 | [ts.SyntaxKind.BarBarToken]: (origLeft: ts.Expression, origRight: ts.Expression, left: unknown) => { 49 | if (left !== NO_LIT_FOUND) { 50 | if (left) return origLeft; 51 | else return origRight; 52 | } 53 | } 54 | }; 55 | 56 | 57 | export const unaryActions: Record ts.Expression|undefined> = { 58 | [ts.SyntaxKind.ExclamationToken]: (val: unknown) => !val ? ts.factory.createTrue() : ts.factory.createFalse(), 59 | [ts.SyntaxKind.MinusToken]: (val: unknown) => { 60 | if (typeof val !== "number") return; 61 | return createNumberNode(-val); 62 | }, 63 | [ts.SyntaxKind.TildeToken]: (val: unknown) => { 64 | if (typeof val !== "number") return; 65 | return createNumberNode(~val); 66 | }, 67 | [ts.SyntaxKind.PlusToken]: (val: unknown) => { 68 | if (typeof val !== "number" && typeof val !== "string") return; 69 | return createNumberNode(+val); 70 | } 71 | }; 72 | 73 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 74 | export const labelActions: Record ts.Expression> = { 75 | [ts.SyntaxKind.IfStatement]: (node: ts.IfStatement) => { 76 | return createObjectLiteral({ 77 | kind: ts.factory.createNumericLiteral(LabelKinds.If), 78 | condition: node.expression, 79 | then: node.thenStatement, 80 | else: node.elseStatement 81 | }); 82 | }, 83 | [ts.SyntaxKind.ForOfStatement]: (node: ts.ForOfStatement) => { 84 | let initializer; 85 | if (ts.isVariableDeclarationList(node.initializer)) { 86 | const firstDecl = node.initializer.declarations[0]; 87 | if (firstDecl && ts.isIdentifier(firstDecl.name)) initializer = firstDecl.name; 88 | } else { 89 | initializer = node.initializer; 90 | } 91 | return createObjectLiteral({ 92 | kind: ts.factory.createNumericLiteral(LabelKinds.ForIter), 93 | type: ts.factory.createStringLiteral("of"), 94 | initializer: initializer, 95 | iterator: node.expression, 96 | statement: node.statement 97 | }); 98 | }, 99 | [ts.SyntaxKind.ForInStatement]: (node: ts.ForInStatement) => { 100 | let initializer; 101 | if (ts.isVariableDeclarationList(node.initializer)) { 102 | const firstDecl = node.initializer.declarations[0]; 103 | if (firstDecl && ts.isIdentifier(firstDecl.name)) initializer = firstDecl.name; 104 | } else { 105 | initializer = node.initializer; 106 | } 107 | return createObjectLiteral({ 108 | kind: ts.factory.createNumericLiteral(LabelKinds.ForIter), 109 | type: ts.factory.createStringLiteral("in"), 110 | initializer: initializer, 111 | iterator: node.expression, 112 | statement: node.statement 113 | }); 114 | }, 115 | [ts.SyntaxKind.WhileStatement]: (node: ts.WhileStatement) => { 116 | return createObjectLiteral({ 117 | kind: ts.factory.createNumericLiteral(LabelKinds.While), 118 | do: ts.factory.createFalse(), 119 | condition: node.expression, 120 | statement: node.statement 121 | }); 122 | }, 123 | [ts.SyntaxKind.DoStatement]: (node: ts.WhileStatement) => { 124 | return createObjectLiteral({ 125 | kind: ts.factory.createNumericLiteral(LabelKinds.While), 126 | do: ts.factory.createTrue(), 127 | condition: node.expression, 128 | statement: node.statement 129 | }); 130 | }, 131 | [ts.SyntaxKind.ForStatement]: (node: ts.ForStatement) => { 132 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 133 | let variables, expression; 134 | if (node.initializer) { 135 | if (ts.isVariableDeclarationList(node.initializer)) { 136 | variables = []; 137 | for (const decl of node.initializer.declarations) { 138 | if (ts.isIdentifier(decl.name)) variables.push(ts.factory.createArrayLiteralExpression([ts.factory.createIdentifier(decl.name.text), decl.initializer || ts.factory.createIdentifier("undefined")])); 139 | } 140 | } else expression = node.initializer; 141 | } 142 | return createObjectLiteral({ 143 | kind: ts.factory.createNumericLiteral(LabelKinds.For), 144 | initializer: createObjectLiteral({ 145 | variables: variables && ts.factory.createArrayLiteralExpression(variables), 146 | expression 147 | }), 148 | condition: node.condition, 149 | increment: node.incrementor, 150 | statement: node.statement 151 | }); 152 | }, 153 | [ts.SyntaxKind.Block]: (node: ts.Block) => { 154 | return createObjectLiteral({ 155 | kind: ts.factory.createNumericLiteral(LabelKinds.Block), 156 | statement: node 157 | }); 158 | }, 159 | [ts.SyntaxKind.VariableStatement]: (node: ts.VariableStatement) => { 160 | const idents: Array = [], inits: Array = []; 161 | for (const decl of node.declarationList.declarations) { 162 | if (!ts.isIdentifier(decl.name)) continue; 163 | idents.push(decl.name); 164 | inits.push(decl.initializer || ts.factory.createIdentifier("undefined")); 165 | } 166 | return createObjectLiteral({ 167 | kind: ts.factory.createNumericLiteral(LabelKinds.VariableDeclaration), 168 | identifiers: ts.factory.createArrayLiteralExpression(idents), 169 | initializers: ts.factory.createArrayLiteralExpression(inits), 170 | declarationType: hasBit(node.declarationList.flags, ts.NodeFlags.Const) ? ts.factory.createStringLiteral("const") : 171 | hasBit(node.declarationList.flags, ts.NodeFlags.Let) ? ts.factory.createStringLiteral("let") : ts.factory.createStringLiteral("var") 172 | }); 173 | } 174 | }; -------------------------------------------------------------------------------- /src/cli/formatter.ts: -------------------------------------------------------------------------------- 1 | export const red = (text: string): string => `\x1b[31m${text}\x1b[0m`; 2 | export const cyan = (text: string): string => `\x1b[36m${text}\x1b[0m`; 3 | 4 | function getColoredMessage(pre: string, text: TemplateStringsArray, ...exps: Array) : string { 5 | let i = 0; 6 | let final = ""; 7 | for (const str of text) { 8 | final += `${str}${exps[i] ? cyan(exps[i++]) : ""}`; 9 | } 10 | return `${pre}: ${final}`; 11 | } 12 | 13 | export function emitError(text: TemplateStringsArray, ...exps: Array) : void { 14 | console.error(getColoredMessage(red("[Error]"), text, ...exps)); 15 | process.exit(1); 16 | } 17 | 18 | 19 | export function emitNotification(text: TemplateStringsArray, ...exps: Array) : void { 20 | console.log(getColoredMessage(cyan("[Notification]"), text, ...exps)); 21 | } -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as parseArgs from "yargs-parser"; 4 | import * as ts from "typescript"; 5 | import { PretranspileSettings, pretranspile, validateSettings } from "./transform"; 6 | import { cyan, emitError, emitNotification } from "./formatter"; 7 | 8 | type CLIArgs = { 9 | _: string[], 10 | } & Omit; 11 | 12 | (() => { 13 | 14 | const args = parseArgs(process.argv.slice(2)) as CLIArgs; 15 | const command = args._[0]; 16 | 17 | if (command === "transform") { 18 | const dist = args._[1]; 19 | if (!dist || typeof dist !== "string") return emitError`Please provide an out folder path.\n\nUsage: ts-macros transform [PATH]`; 20 | const validatedSettings = validateSettings(args); 21 | if (validatedSettings.length) return emitError`Setting errors:\n${validatedSettings.join(", ")}`; 22 | const errors = pretranspile({ 23 | dist, 24 | ...args 25 | }); 26 | 27 | if (errors) console.log(ts.formatDiagnosticsWithColorAndContext(errors, { 28 | getNewLine: () => "\r\n", 29 | getCurrentDirectory: () => "unknown directory", 30 | getCanonicalFileName: (fileName) => fileName 31 | })); 32 | } 33 | else if (command === "help") emitHelp(); 34 | else { 35 | emitNotification`Unknown command ${command}.`; 36 | emitHelp(); 37 | } 38 | })(); 39 | 40 | 41 | function emitHelp() : void { 42 | emitNotification`ts-macros CLI args 43 | 44 | Commands: 45 | * transform [OUT] - Expand all macros and write transformed files to the selected OUT directory. 46 | ${cyan("Example")}: ts-macros transform ./transformed --nocomptime 47 | -- nocomptime - Disable usage of $$raw and $$comptime macros. 48 | -- emitjs - Emits javascript instead of typescript. 49 | -- exec=[CMD] - Execute a command after writing the transformed typescript files to disk. 50 | -- cleanup - Delete the OUT directory after executing CMD. 51 | -- tsconfig - Point the transformer to a different tsconfig.json file. 52 | -- watch - Transformer will transform your files on changes. If the exec option is also provided, it will be run only after the first transform. 53 | `; 54 | } -------------------------------------------------------------------------------- /src/cli/transform.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as path from "path"; 3 | import * as childProcess from "child_process"; 4 | import * as fs from "fs"; 5 | import { MacroTransformer } from "../transformer"; 6 | import { TsMacrosConfig, macros } from ".."; 7 | import { createMacroTransformerWatcher } from "../watcher"; 8 | 9 | export interface PretranspileSettings { 10 | dist: string, 11 | exec?: string, 12 | tsconfig?: string, 13 | cleanup?: boolean, 14 | watch?: boolean, 15 | nocomptime?: boolean, 16 | emitjs?: boolean 17 | } 18 | 19 | export function transformFile(sourceFile: ts.SourceFile, printer: ts.Printer, transformer: MacroTransformer) : string { 20 | const newSourceFile = transformer.run(sourceFile); 21 | return printer.printFile(newSourceFile); 22 | } 23 | 24 | export function createFile(providedPath: string, content: string, jsExtension?: boolean) : void { 25 | const withoutFilename = providedPath.slice(0, providedPath.lastIndexOf(path.sep)); 26 | if (!fs.existsSync(withoutFilename)) fs.mkdirSync(withoutFilename, { recursive: true }); 27 | fs.writeFileSync(jsExtension ? providedPath.slice(0, -3) + ".js" : providedPath, content); 28 | } 29 | 30 | export function createAnonDiagnostic(message: string) : ts.Diagnostic { 31 | return ts.createCompilerDiagnostic({ 32 | key: "Errror", 33 | code: 8000, 34 | message, 35 | category: ts.DiagnosticCategory.Error 36 | }); 37 | } 38 | 39 | export function pretranspile(settings: PretranspileSettings) : ts.Diagnostic[] | undefined { 40 | const config = settings.tsconfig || ts.findConfigFile(process.cwd(), ts.sys.fileExists, "tsconfig.json"); 41 | if (!config) return [createAnonDiagnostic( "Couldn't find tsconfig.json file.")]; 42 | 43 | const distPath = path.join(process.cwd(), settings.dist); 44 | if (!fs.existsSync(distPath)) fs.mkdirSync(distPath, { recursive: true }); 45 | 46 | const transformerConfig: TsMacrosConfig = { noComptime: settings.nocomptime, keepImports: true }; 47 | const printer = ts.createPrinter(); 48 | 49 | if (settings.watch) { 50 | createMacroTransformerWatcher(config, { 51 | updateFile: (fileName, content) => createFile(path.join(process.cwd(), settings.dist, fileName.slice(process.cwd().length)), content, settings.emitjs), 52 | afterUpdate: settings.exec ? (isInitial) => isInitial && childProcess.exec(settings.exec as string) : undefined 53 | }, settings.emitjs, transformerConfig, printer); 54 | } else { 55 | const readConfig = ts.parseConfigFileWithSystem(config, {}, undefined, undefined, ts.sys, () => undefined); 56 | if (!readConfig) return [createAnonDiagnostic("Couldn't read tsconfig.json file.")]; 57 | if (readConfig.errors.length) return readConfig.errors; 58 | const program = ts.createProgram({ 59 | rootNames: readConfig.fileNames, 60 | options: readConfig.options 61 | }); 62 | const transformer = new MacroTransformer(ts.nullTransformationContext, program.getTypeChecker(), macros, transformerConfig); 63 | for (const file of program.getSourceFiles()) { 64 | if (file.isDeclarationFile) continue; 65 | const transformed = transformFile(file, printer, transformer); 66 | createFile(path.join(process.cwd(), settings.dist, file.fileName.slice(process.cwd().length)), settings.emitjs ? ts.transpile(transformed, program.getCompilerOptions()) : transformed, settings.emitjs); 67 | } 68 | 69 | if (settings.exec) childProcess.execSync(settings.exec); 70 | if (settings.cleanup) fs.rmSync(settings.dist, { recursive: true, force: true }); 71 | } 72 | } 73 | 74 | export function validateSettings(settings: Record) : string[] { 75 | const errors = []; 76 | if (settings.exec && typeof settings.exec !== "string") errors.push("Expected exec to be a string"); 77 | if (settings.tsconfig && typeof settings.tsconfig !== "string") errors.push("Expected tsconfig to be a string"); 78 | return errors; 79 | } -------------------------------------------------------------------------------- /src/nativeMacros.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import * as fs from "fs"; 3 | import { MacroTransformer } from "./transformer"; 4 | import * as path from "path"; 5 | import { createNumberNode, expressionToStringLiteral, fnBodyToString, getGeneralType, hasBit, MacroError, macroParamsToArray, normalizeFunctionNode, primitiveToNode, tryRun } from "./utils"; 6 | 7 | const jsonFileCache: Record = {}; 8 | const regFileCache: Record = {}; 9 | 10 | export interface NativeMacro { 11 | call: (args: ts.NodeArray, transformer: MacroTransformer, callSite: ts.CallExpression) => ts.VisitResult, 12 | preserveParams?: boolean 13 | } 14 | 15 | export default { 16 | "$$loadEnv": { 17 | call: (args, transformer, callSite) => { 18 | const extraPath = args.length && ts.isStringLiteral(args[0]) ? args[0].text:""; 19 | let dotenv; 20 | try { 21 | dotenv = require("dotenv"); 22 | } catch { 23 | throw new MacroError(callSite, "`loadEnv` macro called but `dotenv` module is not installed."); 24 | } 25 | if (extraPath) dotenv.config({path: path.join(ts.sys.getCurrentDirectory(), extraPath)}); 26 | else dotenv.config(); 27 | transformer.props.optimizeEnv = true; 28 | return transformer.context.factory.createCallExpression( 29 | transformer.context.factory.createPropertyAccessExpression( 30 | transformer.context.factory.createCallExpression( 31 | transformer.context.factory.createIdentifier("require"), 32 | undefined, 33 | [transformer.context.factory.createStringLiteral("dotenv")] 34 | ), 35 | transformer.context.factory.createIdentifier("config") 36 | ), 37 | undefined, 38 | extraPath ? [transformer.context.factory.createObjectLiteralExpression( 39 | [transformer.context.factory.createPropertyAssignment( 40 | transformer.context.factory.createIdentifier("path"), 41 | transformer.context.factory.createStringLiteral(extraPath) 42 | )])]:[] 43 | ); 44 | } 45 | }, 46 | "$$readFile": { 47 | call: ([file, parseJSON], transformer, callSite) => { 48 | const filePath = file && transformer.getStringFromNode(file, false, true); 49 | if (!filePath) throw new MacroError(callSite, "`readFile` macro expects a path to the JSON file as the first parameter."); 50 | const shouldParse = parseJSON && transformer.getBoolFromNode(parseJSON); 51 | if (shouldParse) { 52 | if (jsonFileCache[filePath]) return jsonFileCache[filePath]; 53 | } 54 | else if (regFileCache[filePath]) return ts.factory.createStringLiteral(regFileCache[filePath]); 55 | const fileContents = fs.readFileSync(filePath, "utf-8"); 56 | if (shouldParse) { 57 | const value = primitiveToNode(JSON.parse(fileContents)); 58 | jsonFileCache[filePath] = value; 59 | return value; 60 | } else { 61 | regFileCache[filePath] = fileContents; 62 | return ts.factory.createStringLiteral(fileContents); 63 | } 64 | } 65 | }, 66 | "$$inline": { 67 | call: ([func, params, doNotCall], transformer, callSite) => { 68 | if (!func) throw new MacroError(callSite, "`inline` macro expects a function as the first argument."); 69 | if (!params || !ts.isArrayLiteralExpression(params)) throw new MacroError(callSite, "`inline` macro expects an array of expressions as the second argument."); 70 | const fn = normalizeFunctionNode(transformer.checker, func); 71 | if (!fn || !fn.body) throw new MacroError(callSite, "`inline` macro expects a function as the first argument."); 72 | let newBody: ts.ConciseBody; 73 | if (!fn.parameters.length) newBody = fn.body; 74 | else { 75 | const replacements = new Map(); 76 | for (let i=0; i < fn.parameters.length; i++) { 77 | const param = fn.parameters[i]; 78 | if (ts.isIdentifier(param.name)) replacements.set(param.name.text, params.elements[i]); 79 | } 80 | const visitor = (node: ts.Node): ts.Node|undefined => { 81 | if (ts.isIdentifier(node) && replacements.has(node.text)) return replacements.get(node.text); 82 | return ts.visitEachChild(node, visitor, transformer.context); 83 | }; 84 | transformer.context.suspendLexicalEnvironment(); 85 | newBody = ts.visitFunctionBody(fn.body, visitor, transformer.context); 86 | } 87 | if (doNotCall) return ts.factory.createArrowFunction(undefined, undefined, [], undefined, undefined, newBody); 88 | else { 89 | if (ts.isBlock(newBody)) return newBody.statements; 90 | else return newBody; 91 | } 92 | } 93 | }, 94 | "$$kindof": { 95 | call: (args, transformer, callSite) => { 96 | if (!args.length) throw new MacroError(callSite, "`kindof` macro expects a single argument."); 97 | return transformer.context.factory.createNumericLiteral(args[0].kind); 98 | } 99 | }, 100 | "$$define": { 101 | call: ([name, value, useLet, exportDecl], transformer, callSite) => { 102 | const strContent = transformer.getStringFromNode(name, true, true); 103 | if (!strContent) throw new MacroError(callSite, "`define` macro expects a string literal as the first argument."); 104 | const list = transformer.context.factory.createVariableDeclarationList([ 105 | transformer.context.factory.createVariableDeclaration(strContent, undefined, undefined, value) 106 | ], transformer.getBoolFromNode(useLet) ? ts.NodeFlags.Let : ts.NodeFlags.Const); 107 | if (ts.isForStatement(callSite.parent)) return list; 108 | else return ts.factory.createVariableStatement(transformer.getBoolFromNode(exportDecl) ? [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)] : undefined, list); 109 | } 110 | }, 111 | "$$i": { 112 | call: (_, transformer) => { 113 | if (transformer.repeat.length) return transformer.context.factory.createNumericLiteral(transformer.repeat[transformer.repeat.length - 1].index); 114 | else return createNumberNode(-1); 115 | } 116 | }, 117 | "$$length": { 118 | call: ([arrLit], transformer, callSite) => { 119 | if (!arrLit) throw new MacroError(callSite, "`length` macro expects an array / string literal as the first argument."); 120 | if (ts.isArrayLiteralExpression(arrLit)) return transformer.context.factory.createNumericLiteral(arrLit.elements.length); 121 | const str = transformer.getStringFromNode(arrLit, true, true); 122 | if (str) return transformer.context.factory.createNumericLiteral(str.length); 123 | throw new MacroError(callSite, "`length` macro expects an array / string literal as the first argument."); 124 | } 125 | }, 126 | "$$ident": { 127 | call: ([thing], transformer, callSite) => { 128 | if (!thing) throw new MacroError(callSite, "`ident` macro expects a string literal as the first parameter."); 129 | const strVal = transformer.getStringFromNode(thing, true, true); 130 | if (strVal) return transformer.getLastMacro()?.defined?.get(strVal) || ts.factory.createIdentifier(strVal); 131 | else return thing; 132 | } 133 | }, 134 | "$$err": { 135 | call: ([msg], transformer, callSite) => { 136 | const strVal = transformer.getStringFromNode(msg, false, true); 137 | if (!strVal) throw new MacroError(callSite, "`err` macro expects a string literal as the first argument."); 138 | const lastMacro = transformer.macroStack.pop(); 139 | throw new MacroError(callSite, `${lastMacro ? `In macro ${lastMacro.macro.name}: ` : ""}${strVal}`); 140 | } 141 | }, 142 | "$$includes": { 143 | call: ([array, item], transformer, callSite) => { 144 | if (!array) throw new MacroError(callSite, "`includes` macro expects an array/string literal as the first argument."); 145 | if (!item) throw new MacroError(callSite, "`includes` macro expects a second argument."); 146 | const strContent = transformer.getStringFromNode(array, false, true); 147 | if (strContent) { 148 | const valItem = transformer.getLiteralFromNode(item); 149 | if (typeof valItem !== "string") throw new MacroError(callSite, "`includes` macro expects a string literal as the second argument."); 150 | return strContent.includes(valItem) ? ts.factory.createTrue() : ts.factory.createFalse(); 151 | } else if (ts.isArrayLiteralExpression(array)) { 152 | const normalArr = array.elements.map(el => transformer.getLiteralFromNode(transformer.expectExpression(el))); 153 | return normalArr.includes(transformer.getLiteralFromNode(item)) ? ts.factory.createTrue() : ts.factory.createFalse(); 154 | } else throw new MacroError(callSite, "`includes` macro expects an array/string literal as the first argument."); 155 | } 156 | }, 157 | "$$ts": { 158 | call: ([code], transformer, callSite) => { 159 | const str = transformer.getStringFromNode(transformer.expectExpression(code), true, true); 160 | if (!str) throw new MacroError(callSite, "`ts` macro expects a string as it's first argument."); 161 | const result = ts.createSourceFile("expr", str, ts.ScriptTarget.ESNext, false, ts.ScriptKind.JS); 162 | const visitor = (node: ts.Node): ts.Node => ts.factory.cloneNode(ts.visitEachChild(node, visitor, transformer.context)); 163 | return ts.visitNodes(result.statements, visitor) as unknown as Array; 164 | } 165 | }, 166 | "$$escape": { 167 | call: ([code], transformer, callSite) => { 168 | if (!code) throw new MacroError(callSite, "`escape` macro expects a function as it's first argument."); 169 | const maybeFn = normalizeFunctionNode(transformer.checker, transformer.expectExpression(code)); 170 | if (!maybeFn || !maybeFn.body) throw new MacroError(callSite, "`escape` macro expects a function as it's first argument."); 171 | if (ts.isBlock(maybeFn.body)) { 172 | const hygienicBody = [...transformer.makeHygienic(maybeFn.body.statements as unknown as ts.Statement[])]; 173 | const lastStatement = hygienicBody.pop(); 174 | transformer.escapeStatement(...hygienicBody); 175 | if (lastStatement) { 176 | if (ts.isReturnStatement(lastStatement)) { 177 | return lastStatement.expression; 178 | } else { 179 | if (!hygienicBody.length && ts.isExpression(lastStatement)) return lastStatement; 180 | transformer.escapeStatement(lastStatement); 181 | } 182 | } 183 | } else return maybeFn.body; 184 | } 185 | }, 186 | "$$slice": { 187 | call: ([thing, start, end], transformer, callSite) => { 188 | if (!thing) throw new MacroError(callSite, "`slice` macro expects an array/string literal as the first argument."); 189 | const startNum = (start && transformer.getNumberFromNode(start)) ?? -Infinity; 190 | const endNum = (end && transformer.getNumberFromNode(end)) ?? Infinity; 191 | const strVal = transformer.getStringFromNode(thing, false, true); 192 | if (strVal) return ts.factory.createStringLiteral(strVal.slice(startNum, endNum)); 193 | else if (ts.isArrayLiteralExpression(thing)) return ts.factory.createArrayLiteralExpression(thing.elements.slice(startNum, endNum)); 194 | else throw new MacroError(callSite, "`slice` macro expects an array/string literal as the first argument."); 195 | } 196 | }, 197 | "$$propsOfType": { 198 | call: (_args, transformer, callSite) => { 199 | const type = transformer.resolveTypeArgumentOfCall(callSite, 0); 200 | if (!type) throw new MacroError(callSite, "`propsOfType` macro expects one type parameter."); 201 | return ts.factory.createArrayLiteralExpression(type.getProperties().map(sym => ts.factory.createStringLiteral(sym.name))); 202 | } 203 | }, 204 | "$$typeToString": { 205 | call: ([simplifyType, nonNullType, fullExpand], transformer, callSite) => { 206 | let type = transformer.resolveTypeArgumentOfCall(callSite, 0); 207 | if (!type) throw new MacroError(callSite, "`typeToString` macro expects one type parameter."); 208 | if (transformer.getBoolFromNode(simplifyType)) type = getGeneralType(transformer.checker, type); 209 | if (transformer.getBoolFromNode(nonNullType)) type = transformer.checker.getNonNullableType(type); 210 | return ts.factory.createStringLiteral(transformer.checker.typeToString(type, undefined, transformer.getBoolFromNode(fullExpand) ? ts.TypeFormatFlags.NoTruncation : undefined)); 211 | } 212 | }, 213 | "$$typeAssignableTo": { 214 | call: (_args, transformer, callSite) => { 215 | const type = transformer.resolveTypeArgumentOfCall(callSite, 0); 216 | const compareTo = transformer.resolveTypeArgumentOfCall(callSite, 1); 217 | if (!type || !compareTo) throw new MacroError(callSite, "`typeAssignableTo` macro expects two type parameters."); 218 | return transformer.checker.isTypeAssignableTo(type, compareTo) ? ts.factory.createTrue() : ts.factory.createFalse(); 219 | } 220 | }, 221 | "$$typeMetadata": { 222 | call: ([collectProps, collectMethods], transformer, callSite) => { 223 | const type = transformer.resolveTypeArgumentOfCall(callSite, 0); 224 | if (!type) throw new MacroError(callSite, "`typeMetadata` macro expects a type parameter."); 225 | const shouldCollectProps = transformer.getBoolFromNode(collectProps); 226 | const shouldCollectMethods = transformer.getBoolFromNode(collectMethods); 227 | 228 | const methods: ts.ObjectLiteralExpression[] = []; 229 | const properties: ts.ObjectLiteralExpression[] = []; 230 | 231 | const stringifyType = (type: ts.Type) => ts.factory.createStringLiteral(transformer.checker.typeToString(transformer.checker.getNonNullableType(type), undefined, ts.TypeFormatFlags.NoTruncation)); 232 | 233 | for (const property of type.getProperties()) { 234 | const valueDecl = property.valueDeclaration; 235 | if (!valueDecl) continue; 236 | const propType = transformer.checker.getTypeOfSymbolAtLocation(property, valueDecl); 237 | const callSig = propType.getCallSignatures()[0]; 238 | 239 | if (callSig && shouldCollectMethods) { 240 | methods.push(ts.factory.createObjectLiteralExpression([ 241 | ts.factory.createPropertyAssignment("name", ts.factory.createStringLiteral(property.name)), 242 | ts.factory.createPropertyAssignment("tags", ts.factory.createObjectLiteralExpression(ts.getJSDocTags(valueDecl).map(tag => ts.factory.createPropertyAssignment(tag.tagName.text, typeof tag.comment === "string" ? ts.factory.createStringLiteral(tag.comment) : ts.factory.createTrue())))), 243 | ts.factory.createPropertyAssignment("parameters", ts.factory.createArrayLiteralExpression(callSig.getParameters().map(method => { 244 | const paramType = transformer.checker.getTypeOfSymbol(method); 245 | return ts.factory.createObjectLiteralExpression([ 246 | ts.factory.createPropertyAssignment("name", ts.factory.createStringLiteral(method.name)), 247 | ts.factory.createPropertyAssignment("type", stringifyType(paramType)), 248 | ts.factory.createPropertyAssignment("optional", hasBit(method.flags, ts.SymbolFlags.Optional) ? ts.factory.createTrue() : ts.factory.createFalse()) 249 | ]); 250 | }))), 251 | ts.factory.createPropertyAssignment("returnType", stringifyType(callSig.getReturnType())) 252 | ])); 253 | } 254 | else if (!callSig && shouldCollectProps) { 255 | properties.push(ts.factory.createObjectLiteralExpression([ 256 | ts.factory.createPropertyAssignment("name", ts.factory.createStringLiteral(property.name)), 257 | ts.factory.createPropertyAssignment("tags", ts.factory.createObjectLiteralExpression(ts.getJSDocTags(valueDecl).map(tag => ts.factory.createPropertyAssignment(tag.tagName.text, typeof tag.comment === "string" ? ts.factory.createStringLiteral(tag.comment) : ts.factory.createTrue())))), 258 | ts.factory.createPropertyAssignment("type", stringifyType(propType)), 259 | ts.factory.createPropertyAssignment("optional", hasBit(property.flags, ts.SymbolFlags.Optional) ? ts.factory.createTrue() : ts.factory.createFalse()) 260 | ])); 261 | } 262 | } 263 | 264 | return ts.factory.createObjectLiteralExpression([ 265 | ts.factory.createPropertyAssignment("name", ts.factory.createStringLiteral(type.symbol?.name || "anonymous")), 266 | ts.factory.createPropertyAssignment("properties", ts.factory.createArrayLiteralExpression(properties)), 267 | ts.factory.createPropertyAssignment("methods", ts.factory.createArrayLiteralExpression(methods)) 268 | ]); 269 | } 270 | }, 271 | "$$text": { 272 | call: ([exp], transformer, callSite) => { 273 | if (!exp) throw new MacroError(callSite, "`text` macro expects an expression."); 274 | return expressionToStringLiteral(exp); 275 | } 276 | }, 277 | "$$decompose": { 278 | call: ([exp], transformer) => { 279 | if (!exp) return ts.factory.createArrayLiteralExpression([]); 280 | const elements: Array = []; 281 | const visitor = (node: ts.Node) => { 282 | if (ts.isExpression(node)) elements.push(node); 283 | return node; 284 | }; 285 | ts.visitEachChild(exp, visitor, transformer.context); 286 | return ts.factory.createArrayLiteralExpression(elements); 287 | } 288 | }, 289 | "$$map": { 290 | call: ([exp, visitor], transformer, callSite) => { 291 | const lastMacro = transformer.getLastMacro(); 292 | if (!lastMacro) throw new MacroError(callSite, "`$$map` macro can only be used inside other macros."); 293 | if (!exp) throw new MacroError(callSite, "`$$map` macro expects an expression as it's first argument."); 294 | if (!visitor) throw new MacroError(callSite, "`$$map` macro expects a function expression as it's second argument."); 295 | const fn = normalizeFunctionNode(transformer.checker, visitor); 296 | if (!fn || !fn.body) throw new MacroError(callSite, "`$$map` macro expects a function as it's second argument."); 297 | if (!fn.parameters.length || !ts.isIdentifier(fn.parameters[0].name)) throw new MacroError(callSite, "`$$map` macro expects the function to have a parameter."); 298 | const paramName = fn.parameters[0].name.text; 299 | const kindParamName = fn.parameters[1] && ts.isIdentifier(fn.parameters[1].name) && fn.parameters[1].name.text; 300 | const visitorFn = (node: ts.Node) : ts.Node|Array => { 301 | const visitedNode = ts.visitNode(node, transformer.boundVisitor); 302 | if (!visitedNode) return node; 303 | if (!ts.isExpression(visitedNode)) return ts.visitEachChild(visitedNode, visitorFn, transformer.context); 304 | lastMacro.store.set(paramName, visitedNode); 305 | if (kindParamName) lastMacro.store.set(kindParamName, ts.factory.createNumericLiteral(visitedNode.kind)); 306 | const newNodes = transformer.transformFunction(fn, true); 307 | if (newNodes.length === 1 && newNodes[0].kind === ts.SyntaxKind.NullKeyword) return ts.visitEachChild(visitedNode, visitorFn, transformer.context); 308 | return newNodes; 309 | }; 310 | return ts.visitNode(exp, visitorFn); 311 | }, 312 | preserveParams: true 313 | }, 314 | "$$comptime": { 315 | call: ([fn], transformer, callSite) => { 316 | if (transformer.config.noComptime) return; 317 | if (transformer.macroStack.length) throw new MacroError(callSite, "`comptime` macro cannot be called inside macros."); 318 | if (!fn) throw new MacroError(callSite, "`comptime` macro expects a function as the first parameter."); 319 | const callableFn = normalizeFunctionNode(transformer.checker, fn); 320 | if (!callableFn || !callableFn.body) throw new MacroError(callSite, "`comptime` macro expects a function as the first parameter."); 321 | let parent = callSite.parent; 322 | if (ts.isExpressionStatement(parent)) { 323 | parent = parent.parent; 324 | if (ts.isBlock(parent)) parent = parent.parent; 325 | if ("body" in parent) { 326 | const signature = transformer.checker.getSignatureFromDeclaration(parent as ts.SignatureDeclaration); 327 | if (!signature || !signature.declaration) return; 328 | transformer.addComptimeSignature(signature.declaration, fnBodyToString(transformer.checker, callableFn, transformer.context.getCompilerOptions()), signature.parameters.map(p => p.name)); 329 | return; 330 | } 331 | } 332 | }, 333 | preserveParams: true 334 | }, 335 | "$$raw": { 336 | call: ([fn], transformer, callSite) => { 337 | if (transformer.config.noComptime) return; 338 | const lastMacro = transformer.getLastMacro(); 339 | if (!lastMacro) throw new MacroError(callSite, "`raw` macro must be called inside another macro."); 340 | if (!fn) throw new MacroError(callSite, "`raw` macro expects a function as the first parameter."); 341 | const callableFn = normalizeFunctionNode(transformer.checker, fn); 342 | if (!callableFn || !callableFn.body) throw new MacroError(callSite, "`raw` macro expects a function as the first parameter."); 343 | const renamedParameters = []; 344 | for (const param of callableFn.parameters.slice(1)) { 345 | if (!ts.isIdentifier(param.name)) throw new MacroError(callSite, "`raw` macro parameters cannot be deconstructors."); 346 | renamedParameters.push(param.name.text); 347 | } 348 | const stringified = transformer.addComptimeSignature(callableFn, fnBodyToString(transformer.checker, callableFn, transformer.context.getCompilerOptions()), ["ctx", ...renamedParameters]); 349 | return tryRun(fn, stringified, [{ 350 | ts, 351 | factory: ts.factory, 352 | transformer, 353 | checker: transformer.checker, 354 | thisMacro: lastMacro, 355 | require, 356 | error: (node: ts.Node, message: string) => { 357 | throw new MacroError(node, message); 358 | } 359 | }, ...macroParamsToArray(lastMacro.macro.params, [...lastMacro.args])], `$$raw in ${lastMacro.macro.name}: `); 360 | }, 361 | preserveParams: true 362 | } 363 | } as Record; -------------------------------------------------------------------------------- /src/type-resolve/chainingTypes.ts: -------------------------------------------------------------------------------- 1 | import ts = require("typescript"); 2 | import { Macro } from "../transformer"; 3 | import { MapArray, hasBit } from "../utils"; 4 | import { UNKNOWN_TOKEN } from "./declarations"; 5 | 6 | function resolveTypeName(checker: ts.TypeChecker, type: ts.Type) : string | undefined { 7 | if (hasBit(type.flags, ts.TypeFlags.String)) return "String"; 8 | else if (hasBit(type.flags, ts.TypeFlags.Number)) return "Number"; 9 | else if (hasBit(type.flags, ts.TypeFlags.Boolean)) return "Boolean"; 10 | //else if (type.isClassOrInterface()) return type.symbol.name; 11 | else if (checker.isArrayType(type) || checker.isTupleType(type)) return "Array"; 12 | else return; 13 | } 14 | 15 | export function generateChainingTypings(checker: ts.TypeChecker, macros: Map) : ts.Statement[] { 16 | const ambientDecls = new MapArray(); 17 | for (const [, macro] of macros) { 18 | const macroParamNode = macro.params[0]?.node; 19 | if (!macroParamNode) continue; 20 | const macroParamType = checker.getTypeAtLocation(macroParamNode); 21 | if (!macroParamType) continue; 22 | const decl = ts.factory.createMethodSignature([], macro.name, macro.node.questionToken, macro.node.typeParameters, macro.node.parameters.slice(1), macro.node.type || UNKNOWN_TOKEN); 23 | if (macroParamType.isUnion()) { 24 | for (const type of macroParamType.types) ambientDecls.push(type, decl); 25 | } 26 | else ambientDecls.push(macroParamType, decl); 27 | } 28 | 29 | const decls: ts.Statement[] = []; 30 | for (const [type, chainFunctions] of ambientDecls) { 31 | const typeName = resolveTypeName(checker, type); 32 | if (!typeName) continue; 33 | //@ts-expect-error Err 34 | decls.push(ts.factory.createInterfaceDeclaration(undefined, typeName, type.target?.typeParameters?.map((p: ts.TypeParameter) => ts.factory.createTypeReferenceNode( 35 | ts.factory.createIdentifier(p.symbol.name), 36 | undefined 37 | )), undefined, chainFunctions)); 38 | } 39 | 40 | return [ 41 | ts.factory.createModuleDeclaration( 42 | [ts.factory.createToken(ts.SyntaxKind.DeclareKeyword)], 43 | ts.factory.createIdentifier("global"), 44 | ts.factory.createModuleBlock(decls), 45 | ts.NodeFlags.ExportContext | ts.NodeFlags.GlobalAugmentation | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags 46 | ) 47 | ]; 48 | } -------------------------------------------------------------------------------- /src/type-resolve/declarations.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | 3 | export const UNKNOWN_TOKEN = ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); 4 | 5 | export function transformDeclaration(checker: ts.TypeChecker, decl: ts.Statement) : ts.Statement | undefined { 6 | if (ts.isInterfaceDeclaration(decl) || ts.isTypeAliasDeclaration(decl) || ts.isEnumDeclaration(decl)) return decl; 7 | else if (ts.isClassDeclaration(decl)) { 8 | return ts.factory.createClassDeclaration([ 9 | ...(decl.modifiers || []), 10 | ts.factory.createToken(ts.SyntaxKind.DeclareKeyword) 11 | ], 12 | decl.name, 13 | decl.typeParameters, 14 | decl.heritageClauses, 15 | decl.members.map(m => { 16 | if (ts.isMethodDeclaration(m)) return ts.factory.createMethodDeclaration(m.modifiers, m.asteriskToken, m.name, m.questionToken, m.typeParameters, m.parameters, m.type || UNKNOWN_TOKEN, undefined); 17 | else if (ts.isPropertyDeclaration(m)) return ts.factory.createPropertyDeclaration(m.modifiers, m.name, m.questionToken || m.exclamationToken, m.type || UNKNOWN_TOKEN, undefined); 18 | else if (ts.isGetAccessorDeclaration(m)) return ts.factory.createGetAccessorDeclaration(m.modifiers, m.name, m.parameters, m.type || UNKNOWN_TOKEN, undefined); 19 | else if (ts.isSetAccessorDeclaration(m)) return ts.factory.createSetAccessorDeclaration(m.modifiers, m.name, m.parameters, undefined); 20 | else if (ts.isConstructorDeclaration(m)) return ts.factory.createConstructorDeclaration(m.modifiers, m.parameters, undefined); 21 | else return m; 22 | }) 23 | ); 24 | } 25 | else if (ts.isExpressionStatement(decl) && ts.isClassExpression(decl.expression)) { 26 | return ts.factory.createClassDeclaration([ 27 | ...(decl.expression.modifiers || []), 28 | ts.factory.createToken(ts.SyntaxKind.DeclareKeyword) 29 | ], 30 | decl.expression.name, 31 | decl.expression.typeParameters, 32 | decl.expression.heritageClauses, 33 | decl.expression.members.map(m => { 34 | if (ts.isMethodDeclaration(m)) return ts.factory.createMethodDeclaration(m.modifiers, m.asteriskToken, m.name, m.questionToken, m.typeParameters, m.parameters, m.type || UNKNOWN_TOKEN, undefined); 35 | else if (ts.isPropertyDeclaration(m)) return ts.factory.createPropertyDeclaration(m.modifiers, m.name, m.questionToken || m.exclamationToken, m.type || UNKNOWN_TOKEN, undefined); 36 | else if (ts.isGetAccessorDeclaration(m)) return ts.factory.createGetAccessorDeclaration(m.modifiers, m.name, m.parameters, m.type || UNKNOWN_TOKEN, undefined); 37 | else if (ts.isSetAccessorDeclaration(m)) return ts.factory.createSetAccessorDeclaration(m.modifiers, m.name, m.parameters, undefined); 38 | else if (ts.isConstructorDeclaration(m)) return ts.factory.createConstructorDeclaration(m.modifiers, m.parameters, undefined); 39 | else return m; 40 | }) 41 | ); 42 | } 43 | else if (ts.isVariableStatement(decl)) { 44 | const decls = []; 45 | for (const declaration of decl.declarationList.declarations) { 46 | let initializerType; 47 | if (declaration.initializer) { 48 | const type = checker.getTypeAtLocation(declaration.initializer); 49 | const typeNode = checker.typeToTypeNode(type, undefined, undefined); 50 | if (typeNode) initializerType = typeNode; 51 | } 52 | decls.push(ts.factory.createVariableDeclaration(declaration.name, declaration.exclamationToken, initializerType)); 53 | } 54 | return ts.factory.createVariableStatement([ 55 | ...(decl.modifiers || []), 56 | ts.factory.createToken(ts.SyntaxKind.DeclareKeyword) 57 | ], decls); 58 | } 59 | else if (ts.isFunctionDeclaration(decl)) { 60 | return ts.factory.createFunctionDeclaration( 61 | [ 62 | ...(decl.modifiers || []), 63 | ts.factory.createToken(ts.SyntaxKind.DeclareKeyword) 64 | ], 65 | decl.asteriskToken, 66 | decl.name, 67 | decl.typeParameters, 68 | decl.parameters, 69 | decl.type || UNKNOWN_TOKEN, 70 | undefined 71 | ); 72 | } 73 | } -------------------------------------------------------------------------------- /src/type-resolve/index.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import type { ProgramTransformerExtras, PluginConfig } from "ts-patch"; 3 | import { MacroTransformer } from "../transformer"; 4 | import { TsMacrosConfig, macros } from "../index"; 5 | import { transformDeclaration } from "./declarations"; 6 | import { MacroError, genDiagnosticFromMacroError } from "../utils"; 7 | import { generateChainingTypings } from "./chainingTypes"; 8 | 9 | function printAsTS(printer: ts.Printer, statements: ts.Statement[], source: ts.SourceFile) : string { 10 | let fileText = ""; 11 | for (const fileItem of statements) { 12 | fileText += printer.printNode(ts.EmitHint.Unspecified, fileItem, source); 13 | } 14 | return fileText; 15 | } 16 | 17 | export function patchCompilerHost(host: ts.CompilerHost | undefined, config: ts.CompilerOptions | undefined, newSourceFiles: Map, instance: typeof ts) : ts.CompilerHost { 18 | const compilerHost = host || instance.createCompilerHost(config || instance.getDefaultCompilerOptions(), true); 19 | const ogGetSourceFile = compilerHost.getSourceFile; 20 | return { 21 | ...compilerHost, 22 | getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) { 23 | if (newSourceFiles.has(fileName)) return newSourceFiles.get(fileName) as ts.SourceFile; 24 | else return ogGetSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile); 25 | } 26 | }; 27 | } 28 | 29 | export function extractGeneratedTypes(typeChecker: ts.TypeChecker, parsedSourceFile: ts.SourceFile) : { 30 | typeNodes: ts.Statement[], 31 | chainTypes: ts.Statement[], 32 | print: (statements: ts.Statement[]) => string 33 | } { 34 | const newNodes = []; 35 | for (const statement of parsedSourceFile.statements) { 36 | if (statement.pos === -1) { 37 | const transformed = transformDeclaration(typeChecker, statement); 38 | if (transformed) newNodes.push(transformed); 39 | } 40 | } 41 | 42 | const printer = ts.createPrinter(); 43 | 44 | return { 45 | typeNodes: newNodes, 46 | chainTypes: generateChainingTypings(typeChecker, macros), 47 | print: (statements: ts.Statement[]) => printAsTS(printer, statements, parsedSourceFile) 48 | }; 49 | } 50 | 51 | export default function ( 52 | program: ts.Program, 53 | host: ts.CompilerHost | undefined, 54 | options: PluginConfig & TsMacrosConfig, 55 | extras: ProgramTransformerExtras 56 | ) : ts.Program { 57 | const isTSC = process.argv[1]?.endsWith("tsc"); 58 | 59 | const instance = extras.ts as typeof ts; 60 | const transformer = new MacroTransformer(instance.nullTransformationContext, program.getTypeChecker(), macros, {...options as TsMacrosConfig, keepImports: true}, { 61 | beforeRegisterMacro: (transformer, _sym, macro) => transformer.cleanupMacros(macro) 62 | }); 63 | const newSourceFiles: Map = new Map(); 64 | const diagnostics: ts.Diagnostic[] = []; 65 | const compilerOptions = program.getCompilerOptions(); 66 | const typeChecker = program.getTypeChecker(); 67 | const printer = instance.createPrinter(); 68 | 69 | const sourceFiles = program.getSourceFiles(); 70 | 71 | for (let i=0; i < sourceFiles.length; i++) { 72 | const sourceFile = sourceFiles[i]; 73 | if (sourceFile.isDeclarationFile) continue; 74 | let localDiagnostic: ts.Diagnostic|undefined; 75 | 76 | let parsed; 77 | try { 78 | parsed = transformer.run(sourceFile); 79 | } catch(err) { 80 | parsed = sourceFile; 81 | if (err instanceof MacroError) { 82 | localDiagnostic = genDiagnosticFromMacroError(sourceFile, err); 83 | diagnostics.push(localDiagnostic); 84 | } 85 | } 86 | if (isTSC) newSourceFiles.set(sourceFile.fileName, instance.createSourceFile(sourceFile.fileName, printer.printFile(parsed), sourceFile.languageVersion, true, ts.ScriptKind.TS)); 87 | else { 88 | const newNodes = []; 89 | for (const statement of parsed.statements) { 90 | if (statement.pos === -1) { 91 | const transformed = transformDeclaration(typeChecker, statement); 92 | if (transformed) newNodes.push(transformed); 93 | } 94 | } 95 | 96 | if (i === sourceFiles.length - 1) { 97 | newNodes.push(...generateChainingTypings(typeChecker, macros)); 98 | } 99 | 100 | const newNodesOnly = printAsTS(printer, newNodes, parsed); 101 | const newNodesSource = instance.createSourceFile(sourceFile.fileName, sourceFile.text + "\n" + newNodesOnly, sourceFile.languageVersion, true, ts.ScriptKind.TS); 102 | if (localDiagnostic) newNodesSource.parseDiagnostics.push(localDiagnostic as ts.DiagnosticWithLocation); 103 | if (options.logFileData) ts.sys.writeFile(`${sourceFile.fileName}_log.txt`, `Generated at: ${new Date()}\nMacros: ${macros.size}\nNew node kinds: ${newNodes.map(n => ts.SyntaxKind[n.kind]).join(", ")}\nFull source:\n\n${newNodesSource.text}`); 104 | newSourceFiles.set(sourceFile.fileName, newNodesSource); 105 | } 106 | } 107 | 108 | return instance.createProgram( 109 | program.getRootFileNames(), 110 | compilerOptions, 111 | patchCompilerHost(host, compilerOptions, newSourceFiles, instance), 112 | undefined, 113 | diagnostics 114 | ); 115 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 3 | import * as ts from "typescript"; 4 | import { ComptimeFunction, MacroParam, MacroTransformer } from "./transformer"; 5 | 6 | export const NO_LIT_FOUND = Symbol("NO_LIT_FOUND"); 7 | 8 | export function flattenBody(body: ts.ConciseBody) : Array { 9 | if ("statements" in body) { 10 | return [...body.statements]; 11 | } 12 | return [ts.factory.createExpressionStatement(body)]; 13 | } 14 | 15 | export function isMacroIdent(ident: ts.MemberName) : boolean { 16 | return ident.text[0] === "$"; 17 | } 18 | 19 | export function hasBit(flags: number, bit: number) : boolean { 20 | return (flags & bit) !== 0; 21 | } 22 | 23 | export function wrapExpressions(exprs: Array) : ts.Expression { 24 | let last = exprs.pop()!; 25 | if (!last) return ts.factory.createNull(); 26 | if (exprs.length === 0 && ts.isReturnStatement(last)) return last.expression || ts.factory.createIdentifier("undefined"); 27 | if (ts.isExpressionStatement(last)) last = ts.factory.createReturnStatement(last.expression); 28 | else if (!(last.kind > ts.SyntaxKind.EmptyStatement && last.kind < ts.SyntaxKind.DebuggerStatement)) last = ts.factory.createReturnStatement(last as unknown as ts.Expression); 29 | return ts.factory.createImmediatelyInvokedArrowFunction([...exprs, last as ts.Statement]); 30 | } 31 | 32 | export function toBinaryExp(transformer: MacroTransformer, body: Array, id: number) : ts.Expression { 33 | let last; 34 | for (const element of body.map(m => ts.isExpressionStatement(m) ? m.expression : (m as ts.Expression))) { 35 | if (!last) last = element; 36 | else last = transformer.context.factory.createBinaryExpression(last, id, element); 37 | } 38 | return ts.visitNode(last, transformer.boundVisitor) as ts.Expression; 39 | } 40 | 41 | export interface RepetitionData { 42 | separator?: string, 43 | literals: Array, 44 | fn: ts.ArrowFunction, 45 | indexTypes: ts.Type[] 46 | } 47 | 48 | export function getRepetitionParams(checker: ts.TypeChecker, rep: ts.ArrayLiteralExpression) : RepetitionData { 49 | const res: Partial = { literals: [] }; 50 | const firstElement = rep.elements[0]; 51 | if (ts.isStringLiteral(firstElement)) res.separator = firstElement.text; 52 | else if (ts.isArrayLiteralExpression(firstElement)) res.literals!.push(...firstElement.elements); 53 | else if (ts.isArrowFunction(firstElement)) res.fn = firstElement; 54 | 55 | const secondElement = rep.elements[1]; 56 | if (secondElement) { 57 | if (ts.isArrayLiteralExpression(secondElement)) res.literals!.push(...secondElement.elements); 58 | else if (ts.isArrowFunction(secondElement)) res.fn = secondElement; 59 | } 60 | 61 | const thirdElement = rep.elements[2]; 62 | if (thirdElement && ts.isArrowFunction(thirdElement)) res.fn = thirdElement; 63 | if (!res.fn) throw new MacroError(rep, "Repetition must include arrow function."); 64 | 65 | res.indexTypes = (res.fn.typeParameters || []).map(arg => checker.getTypeAtLocation(arg)); 66 | 67 | return res as RepetitionData; 68 | } 69 | 70 | export class MacroError extends Error { 71 | start: number; 72 | length: number; 73 | rawMsg: string; 74 | constructor(callSite: ts.Node, msg: string) { 75 | const start = callSite.pos; 76 | const length = callSite.end - callSite.pos; 77 | super(ts.formatDiagnosticsWithColorAndContext([{ 78 | category: ts.DiagnosticCategory.Error, 79 | code: 8000, 80 | file: callSite.getSourceFile(), 81 | start, 82 | length, 83 | messageText: msg 84 | }], { 85 | getNewLine: () => "\r\n", 86 | getCurrentDirectory: () => "unknown directory", 87 | getCanonicalFileName: (fileName) => fileName 88 | })); 89 | this.start = start; 90 | this.length = length; 91 | this.rawMsg = msg; 92 | } 93 | } 94 | 95 | export function genDiagnosticFromMacroError(sourceFile: ts.SourceFile, err: MacroError) : ts.Diagnostic { 96 | return { 97 | code: 8000, 98 | start: err.start, 99 | length: err.length, 100 | messageText: err.rawMsg, 101 | file: sourceFile, 102 | category: ts.DiagnosticCategory.Error 103 | }; 104 | } 105 | 106 | export function getNameFromProperty(obj: ts.PropertyName) : string|undefined { 107 | if (ts.isIdentifier(obj) || ts.isStringLiteral(obj) || ts.isPrivateIdentifier(obj) || ts.isNumericLiteral(obj)) return obj.text; 108 | else return undefined; 109 | } 110 | 111 | export function createObjectLiteral(record: Record) : ts.ObjectLiteralExpression { 112 | const assignments = []; 113 | for (const key in record) { 114 | const obj = record[key]; 115 | assignments.push(ts.factory.createPropertyAssignment(key, 116 | obj ? ts.isStatement(obj) ? ts.factory.createArrowFunction(undefined, undefined, [], undefined, undefined, ts.isBlock(obj) ? obj : ts.factory.createBlock([obj])) : obj : ts.factory.createIdentifier("undefined") 117 | )); 118 | } 119 | return ts.factory.createObjectLiteralExpression(assignments); 120 | } 121 | 122 | export function primitiveToNode(primitive: unknown) : ts.Expression { 123 | if (primitive === null) return ts.factory.createNull(); 124 | else if (primitive === undefined) return ts.factory.createIdentifier("undefined"); 125 | else if (typeof primitive === "string") return ts.factory.createStringLiteral(primitive); 126 | else if (typeof primitive === "number") return ts.factory.createNumericLiteral(primitive); 127 | else if (typeof primitive === "boolean") return primitive ? ts.factory.createTrue() : ts.factory.createFalse(); 128 | else if (Array.isArray(primitive)) return ts.factory.createArrayLiteralExpression(primitive.map(p => primitiveToNode(p))); 129 | else { 130 | const assignments: Array = []; 131 | for (const key in (primitive as Record)) { 132 | assignments.push(ts.factory.createPropertyAssignment(ts.factory.createStringLiteral(key), primitiveToNode((primitive as Record)[key]))); 133 | } 134 | return ts.factory.createObjectLiteralExpression(assignments); 135 | } 136 | } 137 | 138 | export function resolveAliasedSymbol(checker: ts.TypeChecker, sym?: ts.Symbol) : ts.Symbol | undefined { 139 | if (!sym) return; 140 | while ((sym.flags & ts.SymbolFlags.Alias) !== 0) { 141 | const newSym = checker.getAliasedSymbol(sym); 142 | if (newSym.name === "unknown") return sym; 143 | sym = newSym; 144 | } 145 | return sym; 146 | } 147 | 148 | export function fnBodyToString(checker: ts.TypeChecker, fn: { body?: ts.ConciseBody | undefined }, compilerOptions?: ts.CompilerOptions) : string { 149 | if (!fn.body) return ""; 150 | const includedFns = new Set(); 151 | let code = ""; 152 | const visitor = (node: ts.Node) => { 153 | if (ts.isCallExpression(node)) { 154 | const signature = checker.getResolvedSignature(node); 155 | if (signature && 156 | signature.declaration && 157 | signature.declaration !== fn && 158 | signature.declaration.parent.parent !== fn && 159 | (ts.isFunctionDeclaration(signature.declaration) || 160 | ts.isArrowFunction(signature.declaration) || 161 | ts.isFunctionExpression(signature.declaration) 162 | )) { 163 | const name = signature.declaration.name ? signature.declaration.name.text : ts.isIdentifier(node.expression) ? node.expression.text : undefined; 164 | if (!name || includedFns.has(name)) return; 165 | includedFns.add(name); 166 | code += `function ${name}(${signature.parameters.map(p => p.name).join(",")}){${fnBodyToString(checker, signature.declaration, compilerOptions)}}`; 167 | } 168 | ts.forEachChild(node, visitor); 169 | } 170 | else ts.forEachChild(node, visitor); 171 | }; 172 | ts.forEachChild(fn.body, visitor); 173 | return code + ts.transpile((fn.body.original || fn.body).getText(), compilerOptions); 174 | } 175 | 176 | export function tryRun(contentStartNode: ts.Node, comptime: ComptimeFunction, args: Array = [], additionalMessage?: string) : any { 177 | try { 178 | return comptime(...args); 179 | } catch(err: unknown) { 180 | if (err instanceof Error) { 181 | throw new MacroError(contentStartNode, (additionalMessage || "") + err.message); 182 | } else throw err; 183 | } 184 | } 185 | 186 | export function macroParamsToArray(params: Array, values: Array) : Array> { 187 | const result = []; 188 | for (let i=0; i < params.length; i++) { 189 | if (params[i].spread) result.push(values.slice(i)); 190 | else if (!values[i] && params[i].defaultVal) result.push(params[i].defaultVal as T); 191 | else result.push(values[i]); 192 | } 193 | return result; 194 | } 195 | 196 | export function resolveTypeWithTypeParams(providedType: ts.Type, typeParams: ts.TypeParameter[], replacementTypes: ts.Type[]) : ts.Type { 197 | const checker = providedType.checker; 198 | // Access type 199 | if ("indexType" in providedType && "objectType" in providedType) { 200 | const indexType = resolveTypeWithTypeParams((providedType as any).indexType as ts.Type, typeParams, replacementTypes); 201 | const objectType = resolveTypeWithTypeParams((providedType as any).objectType as ts.Type, typeParams, replacementTypes); 202 | const foundType = indexType.isTypeParameter() ? replacementTypes[typeParams.findIndex(t => t === indexType)] : indexType; 203 | if (!foundType || !foundType.isLiteral()) return providedType; 204 | const realType = objectType.getProperty(foundType.value.toString()); 205 | if (!realType) return providedType; 206 | return checker.getTypeOfSymbol(realType); 207 | } 208 | // Conditional type 209 | else if ("checkType" in providedType && "extendsType" in providedType && "resolvedTrueType" in providedType && "resolvedFalseType" in providedType) { 210 | const checkType = resolveTypeWithTypeParams((providedType as any).checkType as ts.Type, typeParams, replacementTypes); 211 | const extendsType = resolveTypeWithTypeParams((providedType as any).extendsType as ts.Type, typeParams, replacementTypes); 212 | const trueType = resolveTypeWithTypeParams((providedType as any).resolvedTrueType as ts.Type, typeParams, replacementTypes); 213 | const falseType = resolveTypeWithTypeParams((providedType as any).resolvedFalseType as ts.Type, typeParams, replacementTypes); 214 | if (checker.isTypeAssignableTo(checkType, extendsType)) return trueType; 215 | else return falseType; 216 | } 217 | else if (providedType.isIntersection()) { 218 | const symTable = new Map(); 219 | for (const unresolvedType of providedType.types) { 220 | const resolved = resolveTypeWithTypeParams(unresolvedType, typeParams, replacementTypes); 221 | for (const prop of resolved.getProperties()) { 222 | symTable.set(prop.name, prop); 223 | } 224 | } 225 | return checker.createAnonymousType(undefined, symTable, [], [], []); 226 | } 227 | else if (providedType.isUnion()) { 228 | const newType = {...providedType}; 229 | newType.types = newType.types.map(t => resolveTypeWithTypeParams(t, typeParams, replacementTypes)); 230 | return newType; 231 | } 232 | else if (providedType.isTypeParameter()) return replacementTypes[typeParams.findIndex(t => t === providedType)] || providedType; 233 | //@ts-expect-error Private API 234 | else if (providedType.resolvedTypeArguments) { 235 | const newType = {...providedType}; 236 | //@ts-expect-error Private API 237 | newType.resolvedTypeArguments = providedType.resolvedTypeArguments.map(arg => resolveTypeWithTypeParams(arg, typeParams, replacementTypes)); 238 | return newType; 239 | } 240 | else if (providedType.getCallSignatures().length) { 241 | const newType = {...providedType}; 242 | const originalCallSignature = providedType.getCallSignatures()[0]; 243 | const callSignature = {...originalCallSignature}; 244 | callSignature.resolvedReturnType = resolveTypeWithTypeParams(originalCallSignature.getReturnType(), typeParams, replacementTypes); 245 | callSignature.parameters = callSignature.parameters.map(p => { 246 | if (!p.valueDeclaration || !(p.valueDeclaration as ts.ParameterDeclaration).type) return p; 247 | const newParam = checker.createSymbol(p.flags, p.escapedName); 248 | //@ts-expect-error Private API 249 | newParam.type = resolveTypeWithTypeParams(checker.getTypeAtLocation((p.valueDeclaration as ts.ParameterDeclaration).type as ts.Node), typeParams, replacementTypes); 250 | return newParam; 251 | }); 252 | //@ts-expect-error Private API 253 | newType.callSignatures = [callSignature]; 254 | return newType; 255 | } 256 | return providedType; 257 | } 258 | 259 | export function resolveTypeArguments(checker: ts.TypeChecker, call: ts.CallExpression) : ts.Type[] { 260 | const sig = checker.getResolvedSignature(call); 261 | if (!sig || !sig.mapper) return []; 262 | switch (sig.mapper.kind) { 263 | case ts.TypeMapKind.Simple: 264 | return [sig.mapper.target]; 265 | case ts.TypeMapKind.Array: 266 | return sig.mapper.targets?.filter(t => t) || []; 267 | default: 268 | return []; 269 | } 270 | } 271 | 272 | /** 273 | * When a macro gets called, no matter if it's built-in or not, it must expand to a valid expression. 274 | * If the macro expands to multiple statements, it gets wrapped in an IIFE. 275 | * This helper function does the opposite, it de-expands the expanded valid expression to an array 276 | * of statements. 277 | */ 278 | export function deExpandMacroResults(nodes: Array) : [Array, ts.Node?] { 279 | const cloned = [...nodes]; 280 | const lastNode = cloned[nodes.length - 1]; 281 | if (!lastNode) return [nodes]; 282 | if (ts.isReturnStatement(lastNode)) { 283 | const expression = (cloned.pop() as ts.ReturnStatement).expression; 284 | if (!expression) return [nodes]; 285 | if (ts.isCallExpression(expression) && ts.isParenthesizedExpression(expression.expression) && ts.isArrowFunction(expression.expression.expression)) { 286 | const flattened = flattenBody(expression.expression.expression.body); 287 | let last: ts.Node|undefined = flattened.pop(); 288 | if (last && ts.isReturnStatement(last) && last.expression) last = last.expression; 289 | return [[...cloned, ...flattened], last]; 290 | } 291 | else return [cloned, expression]; 292 | } 293 | return [cloned, cloned[cloned.length - 1]]; 294 | } 295 | 296 | export function normalizeFunctionNode(checker: ts.TypeChecker, fnNode: ts.Expression) : ts.FunctionLikeDeclaration | undefined { 297 | if (ts.isArrowFunction(fnNode) || ts.isFunctionExpression(fnNode) || ts.isFunctionDeclaration(fnNode)) return fnNode; 298 | const origin = checker.getSymbolAtLocation(fnNode); 299 | if (origin && origin.declarations?.length) { 300 | const originDecl = origin.declarations[0]; 301 | if (ts.isFunctionLikeDeclaration(originDecl)) return originDecl; 302 | else if (ts.isVariableDeclaration(originDecl) && originDecl.initializer && ts.isFunctionLikeDeclaration(originDecl.initializer)) return originDecl.initializer; 303 | } 304 | } 305 | 306 | export function expressionToStringLiteral(exp: ts.Expression) : ts.Expression { 307 | if (ts.isParenthesizedExpression(exp)) return expressionToStringLiteral(exp.expression); 308 | else if (ts.isStringLiteral(exp)) return exp; 309 | else if (ts.isIdentifier(exp)) return ts.factory.createStringLiteral(exp.text); 310 | else if (ts.isNumericLiteral(exp)) return ts.factory.createStringLiteral(exp.text); 311 | else if (exp.kind === ts.SyntaxKind.TrueKeyword) return ts.factory.createStringLiteral("true"); 312 | else if (exp.kind === ts.SyntaxKind.FalseKeyword) return ts.factory.createStringLiteral("false"); 313 | else return ts.factory.createStringLiteral("null"); 314 | } 315 | 316 | /** 317 | * If you attempt to get the type of a synthetic node literal (string literals like "abc", numeric literals like 3.14, etc.), 318 | * the default `checker.getTypeAtLocation` method will return the `never` type. This fixes that issue. 319 | */ 320 | export function getTypeAtLocation(checker: ts.TypeChecker, node: ts.Node) : ts.Type { 321 | if (node.pos === -1) { 322 | if (ts.isStringLiteral(node)) return checker.getStringLiteralType(node.text); 323 | else if (ts.isNumericLiteral(node)) return checker.getNumberLiteralType(+node.text); 324 | else if (ts.isTemplateExpression(node)) return checker.getStringType(); 325 | else return checker.getTypeAtLocation(node); 326 | } 327 | return checker.getTypeAtLocation(node); 328 | } 329 | 330 | export function getGeneralType(checker: ts.TypeChecker, type: ts.Type) : ts.Type { 331 | if (type.isStringLiteral()) return checker.getStringType(); 332 | else if (type.isNumberLiteral()) return checker.getNumberType(); 333 | else if (hasBit(type.flags, ts.TypeFlags.BooleanLiteral)) return checker.getBooleanType(); 334 | else return type; 335 | } 336 | 337 | export function createNumberNode(num: number) : ts.Expression { 338 | if (num < 0) return ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, ts.factory.createNumericLiteral(Math.abs(num))); 339 | else return ts.factory.createNumericLiteral(num); 340 | } 341 | 342 | export class MapArray extends Map { 343 | constructor() { 344 | super(); 345 | } 346 | 347 | push(key: K, value: V) : void { 348 | const arr = this.get(key); 349 | if (!arr) this.set(key, [value]); 350 | else arr.push(value); 351 | } 352 | 353 | transferKey(oldKey: K, newKey: K) : void { 354 | const returned = this.deleteAndReturn(oldKey); 355 | if (!returned) return; 356 | this.set(newKey, returned); 357 | } 358 | 359 | deleteAndReturn(key: K) : V[] | undefined { 360 | const returned = this.get(key); 361 | this.delete(key); 362 | return returned; 363 | } 364 | 365 | deleteEntry(toBeDeleted: V) : void { 366 | for (const [, arr] of this) { 367 | const ind = arr.indexOf(toBeDeleted); 368 | if (ind === -1) continue; 369 | arr.splice(ind, 1); 370 | } 371 | } 372 | 373 | clearArray(key: K) : void { 374 | const arr = this.get(key); 375 | if (arr) arr.length = 0; 376 | } 377 | 378 | } -------------------------------------------------------------------------------- /src/watcher/index.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { Macro, MacroTransformer } from "../transformer"; 3 | import { TsMacrosConfig, macros } from ".."; 4 | import { MacroError, MapArray, genDiagnosticFromMacroError } from "../utils"; 5 | 6 | export enum FileUpdateCause { 7 | ContentChange, 8 | MacroChange 9 | } 10 | 11 | export interface MacroTransformerWatcherActions { 12 | updateFile: (fileName: string, content: string, cause: FileUpdateCause, isJS?: boolean) => void, 13 | afterUpdate?: (isInitial: boolean) => void 14 | } 15 | 16 | export function transpileFile(sourceFile: ts.SourceFile, printer: ts.Printer, transformer: MacroTransformer) : ts.Diagnostic | string { 17 | try { 18 | const transformed = transformer.run(sourceFile); 19 | return printer.printFile(transformed); 20 | } catch(err) { 21 | if (err instanceof MacroError) return genDiagnosticFromMacroError(sourceFile, err); 22 | else throw err; 23 | } 24 | } 25 | 26 | export function createMacroTransformerWatcher(configFileName: string, actions: MacroTransformerWatcherActions, jsOut?: boolean, transformerConfig?: TsMacrosConfig, inPrinter?: ts.Printer) : ts.WatchOfConfigFile { 27 | const printer = inPrinter || ts.createPrinter(), 28 | host = ts.createWatchCompilerHost(configFileName, { noEmit: true }, ts.sys, ts.createSemanticDiagnosticsBuilderProgram, undefined, undefined, undefined, undefined), 29 | oldCreateProgram = host.createProgram, 30 | macrosCreatedInFile = new MapArray(), 31 | macrosReferencedInFiles = new MapArray(), 32 | transformer = new MacroTransformer(ts.nullTransformationContext, (undefined as unknown as ts.TypeChecker), macros, transformerConfig, { 33 | beforeRegisterMacro(transformer, _symbol, macro) { 34 | transformer.cleanupMacros(macro, (oldMacro) => macrosReferencedInFiles.transferKey(oldMacro, macro)); 35 | macrosCreatedInFile.push(macro.node.getSourceFile().fileName, macro); 36 | }, 37 | beforeCallMacro(_transformer, macro, expand) { 38 | if (!expand.call) return; 39 | macrosReferencedInFiles.push(macro, expand.call.getSourceFile().fileName); 40 | }, 41 | beforeFileTransform(_transformer, sourceFile) { 42 | macrosCreatedInFile.clearArray(sourceFile.fileName); 43 | macrosReferencedInFiles.deleteEntry(sourceFile.fileName); 44 | }, 45 | }), 46 | getFilesThatNeedChanges = (origin: string) : string[] => { 47 | const ownedMacros = macrosCreatedInFile.get(origin); 48 | if (!ownedMacros) return []; 49 | const files = []; 50 | for (const macro of ownedMacros) { 51 | const macroIsReferencedIn = macrosReferencedInFiles.get(macro); 52 | if (!macroIsReferencedIn) continue; 53 | files.push(...macroIsReferencedIn); 54 | } 55 | return files; 56 | }; 57 | 58 | host.createProgram = (rootNames, options, host, oldProgram) => { 59 | const errors: ts.Diagnostic[] = []; 60 | const newProgram = oldCreateProgram(rootNames, options, host, oldProgram, errors); 61 | transformer.checker = newProgram.getProgram().getTypeChecker(); 62 | 63 | const forcedFilesToGetTranspiled: string[] = []; 64 | 65 | for (const source of newProgram.getProgram().getSourceFiles()) { 66 | if (source.isDeclarationFile) continue; 67 | //@ts-expect-error Bypass 68 | newProgram.getSemanticDiagnostics(source).length = 0; 69 | const oldSource = oldProgram?.getSourceFile(source.fileName); 70 | 71 | const isForced = forcedFilesToGetTranspiled.includes(source.fileName); 72 | 73 | if (!oldSource || oldSource.version !== source.version || isForced) { 74 | const transpiled = transpileFile(source, printer, transformer); 75 | if (typeof transpiled === "string") { 76 | forcedFilesToGetTranspiled.push(...getFilesThatNeedChanges(source.fileName)); 77 | actions.updateFile(source.fileName, jsOut ? ts.transpile(transpiled, newProgram.getCompilerOptions()) : transpiled, isForced ? FileUpdateCause.MacroChange : FileUpdateCause.ContentChange, jsOut); 78 | } else errors.push(transpiled); 79 | } 80 | } 81 | actions.afterUpdate?.(!!oldProgram); 82 | return newProgram; 83 | }; 84 | return ts.createWatchProgram(host); 85 | } -------------------------------------------------------------------------------- /tests/integrated/builtins/decompose.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import ts from "typescript"; 3 | const { $$decompose, $$kindof, $$text, $$length, $$i, $$slice } = require("../../../../dist/index"); 4 | 5 | describe("$$decompose", () => { 6 | function $stringify(value: any): string { 7 | const $decomposed = $$decompose!(value); 8 | if ($$kindof!(value) === ts.SyntaxKind.PropertyAccessExpression) return $stringify!($decomposed[0]) + "." + $stringify!($decomposed[1]); 9 | else if ($$kindof!(value) === ts.SyntaxKind.CallExpression) return $stringify!($decomposed[0]) + "(" + (+["+", [$$slice!($decomposed, 1)], (part: any) => { 10 | const $len = $$length!($decomposed) - 2; 11 | return $stringify!(part) + ($$i!() === $len ? "" : ", "); 12 | }] || "") + ")"; 13 | else if ($$kindof!(value) === ts.SyntaxKind.StringLiteral) return "\"" + value + "\""; 14 | else return $$text!(value); 15 | } 16 | 17 | it("To stringify the expression", () => { 18 | expect($stringify!(console.log(123))).to.be.equal("console.log(123)"); 19 | expect($stringify!(console.log(1, true, console.log("Hello")))).to.be.equal("console.log(1, true, console.log(\"Hello\"))"); 20 | }); 21 | 22 | }); -------------------------------------------------------------------------------- /tests/integrated/builtins/define.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | const { $$define } = require("../../../../dist/index"); 3 | 4 | describe("$$define", () => { 5 | 6 | it("Define a constant", () => { 7 | $$define!("testVar", 123); 8 | //@ts-expect-error Should be correct 9 | expect(testVar).to.be.equal(123); 10 | $$define!("testVar1", (a, b) => a + b); 11 | //@ts-expect-error Should be correct 12 | expect(testVar1(1, 10)).to.be.equal(11); 13 | }); 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /tests/integrated/builtins/i.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | const { $$i } = require("../../../../dist/index"); 3 | 4 | describe("$$i", () => { 5 | 6 | it("To be -1 when outside of repetition", () => { 7 | expect($$i!()).to.be.equal(-1); 8 | }); 9 | 10 | it("To be the index of repetitions", () => { 11 | function $test(array: Array) { 12 | +["+", [array], (el: string) => el + $$i!()]; 13 | } 14 | 15 | expect($test!(["a", "b", "c"])).to.be.equal("a0b1c2"); 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /tests/integrated/builtins/ident.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | const { $$ident } = require("../../../../dist/index"); 3 | 4 | describe("$$ident", () => { 5 | 6 | const Hello = 123; 7 | 8 | it("To turn the string into the right identifier", () => { 9 | expect($$ident!("Hello")).to.be.equal(123); 10 | }); 11 | 12 | }); -------------------------------------------------------------------------------- /tests/integrated/builtins/includes.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | const { $$includes } = require("../../../../dist/index"); 3 | 4 | describe("$$includes", () => { 5 | 6 | it("To return true when the substring is there", () => { 7 | expect($$includes!("Hello World", "World")).to.be.equal(true); 8 | }); 9 | 10 | it("To return false when the substring is not there", () => { 11 | expect($$includes!("Hello World", "Google")).to.be.equal(false); 12 | }); 13 | 14 | it("To return true when the item is there", () => { 15 | expect($$includes!([1, 2, 3, 4, "wow"], "wow")).to.be.equal(true); 16 | }); 17 | 18 | it("To return false when the item is not there", () => { 19 | expect($$includes!([1, 2, 3, 4, 5], 6)).to.be.equal(false); 20 | }); 21 | 22 | }); -------------------------------------------------------------------------------- /tests/integrated/builtins/inline.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | const { $$inline } = require("../../../../dist/index"); 4 | 5 | describe("$$inlineFunc", () => { 6 | 7 | it("Inline the function and replace the arguments", () => { 8 | expect($$inline!((a, b) => a + b, [1, 5])).to.be.equal(6); 9 | expect($$inline!((a: Array, b: string) => a.push(b), [["a", "b", "c"], "d"])).to.be.deep.equal(4); 10 | }); 11 | 12 | it("Wrap the function in an IIFE", () => { 13 | expect($$inline!((a, b) => { 14 | let acc = 0; 15 | for (let i=a; i < b; i++) { 16 | acc += i; 17 | } 18 | return acc; 19 | }, [1, 10])).to.be.equal(45); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /tests/integrated/builtins/kindof.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import ts from "typescript"; 3 | import { expect } from "chai"; 4 | 5 | const { $$kindof } = require("../../../../dist/index"); 6 | 7 | describe("$$kindof", () => { 8 | 9 | it("Expand to the correct node kind", () => { 10 | expect($$kindof!([1, 2, 3])).to.be.equal(ts.SyntaxKind.ArrayLiteralExpression); 11 | expect($$kindof!(() => 1)).to.be.equal(ts.SyntaxKind.ArrowFunction); 12 | expect($$kindof!(123)).to.be.equal(ts.SyntaxKind.NumericLiteral); 13 | expect($$kindof!(expect)).to.be.equal(ts.SyntaxKind.Identifier); 14 | }); 15 | 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /tests/integrated/builtins/length.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | const { $$length } = require("../../../../dist/index"); 3 | 4 | describe("$$length", () => { 5 | 6 | function $test(arr: Array) { 7 | return $$length!(arr); 8 | } 9 | 10 | it("To return the length of the array literal", () => { 11 | expect($test!([1, 2, 3])).to.be.equal(3); 12 | expect($test!([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])).to.be.equal(10); 13 | }); 14 | 15 | }); -------------------------------------------------------------------------------- /tests/integrated/builtins/map.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import ts from "typescript"; 3 | const { $$map, $$kindof, $$text, $$ident } = require("../../../../dist/index"); 4 | 5 | describe("$$map", () => { 6 | 7 | function log() { 8 | return 123; 9 | } 10 | 11 | function debug() { 12 | return "abc"; 13 | } 14 | 15 | function $replace(exp: any, identifier: any, replaceWith: any) { 16 | return $$map!(exp, (value) => { 17 | if ($$kindof!(value) === ts.SyntaxKind.Identifier && $$text!(value) === identifier) return $$ident!(replaceWith); 18 | }); 19 | } 20 | 21 | it("To correctly replace the identifiers", () => { 22 | expect($replace!(log(), "log", "debug")).to.be.equal("abc"); 23 | expect($replace!(() => { 24 | return debug() + 1; 25 | }, "debug", "log")()).to.be.equal(124); 26 | }); 27 | 28 | }); -------------------------------------------------------------------------------- /tests/integrated/builtins/propsOfType.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | const { $$slice } = require("../../../../dist/index"); 3 | 4 | declare function $$propsOfType() : Array; 5 | 6 | describe("$$propsOfType", () => { 7 | 8 | function $test(param: T) { 9 | const parameter = param; 10 | +[[$$propsOfType!()], (name: string) => { 11 | if ($$slice!(name, 0, 2) === "__") delete parameter[name]; 12 | }] 13 | return parameter; 14 | } 15 | 16 | it("To return the properties", () => { 17 | expect($$propsOfType!<{a: string, b: number}>()).to.be.deep.equal(["a", "b"]); 18 | expect($test!<{a: number, __b: string}>({a: 123, __b: "Hello"})).to.be.deep.equal({a: 123}); 19 | }); 20 | 21 | type Complex = { 22 | foo: { 23 | bar1: { a: number, b: string }, 24 | bar2: { c: number, d: string }, 25 | bar3: { e: number, f: string } 26 | } 27 | } 28 | 29 | function $test2(key1: K, key2: T, element: number = 0) { 30 | $$propsOfType!()[element] 31 | } 32 | 33 | it("Should work with complex type", () => { 34 | expect($test2!("foo", "bar1"), "a"); 35 | expect($test2!("foo", "bar1", 1), "b"); 36 | expect($test2!("foo", "bar3"), "e"); 37 | expect($test2!("foo", "bar2", 1), "d"); 38 | }); 39 | 40 | }); -------------------------------------------------------------------------------- /tests/integrated/builtins/raw.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | const { $$raw } = require("../../../../dist/index"); 3 | 4 | describe("$$raw", () => { 5 | 6 | function $test(a: string|Array) : string|Array { 7 | return $$raw!((ctx, a) => { 8 | if (ctx.ts.isStringLiteral(a)) return a; 9 | else if (ctx.ts.isArrayLiteralExpression(a)) return ctx.factory.createStringLiteral(a.elements.filter(el => ctx.ts.isNumericLiteral(el)).map(n => n.text).join("")); 10 | else return ctx.factory.createNull(); 11 | }); 12 | } 13 | 14 | it("To run the raw code", () => { 15 | expect($test!("hello")).to.be.equal("hello"); 16 | expect($test!([1, 2, 3, 4, 5])).to.be.deep.equal("12345"); 17 | const str = "abc"; 18 | expect($test!(str)).to.be.deep.equal(null); 19 | }); 20 | 21 | }); -------------------------------------------------------------------------------- /tests/integrated/builtins/slice.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import ts from "typescript"; 3 | const { $$slice, $$kindof } = require("../../../../dist/index"); 4 | 5 | describe("$$slice", () => { 6 | 7 | function $test(a: string|Array) : string|Array { 8 | if ($$kindof!(a) === ts.SyntaxKind.StringLiteral) return $$slice!(a, 0, 4); 9 | else if ($$kindof!(a) === ts.SyntaxKind.ArrayLiteralExpression) return $$slice!(a, -2); 10 | else return ""; 11 | } 12 | 13 | it("To return the slice", () => { 14 | expect($test!("hello")).to.be.equal("hell"); 15 | expect($test!([1, 2, 3, 4, 5])).to.be.deep.equal([4, 5]); 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /tests/integrated/builtins/ts.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | const { $$ts } = require("../../../../dist/index"); 3 | 4 | describe("$$ts", () => { 5 | 6 | it("To turn the string into code", () => { 7 | expect($$ts!("() => 123")()).to.be.equal(123); 8 | }); 9 | 10 | }); -------------------------------------------------------------------------------- /tests/integrated/builtins/typeToString.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | const { $$slice } = require("../../../../dist/index"); 3 | 4 | declare function $$typeToString() : string; 5 | 6 | describe("$$typeToString", () => { 7 | 8 | function $test(a: unknown) { 9 | if (typeof a !== $$typeToString!()) return false; 10 | else return true; 11 | } 12 | 13 | it("Should stringify the type", () => { 14 | expect($$typeToString!()).to.be.equal("string"); 15 | expect($test!(123)).to.be.equal(false); 16 | expect($test!(true)).to.be.equal(true); 17 | }); 18 | 19 | type Foo = { 20 | foo: boolean 21 | bar: Bar 22 | } 23 | 24 | type Mar = { 25 | a: "foo", 26 | b: "bar" 27 | } 28 | 29 | type Bar = number 30 | 31 | function $test2(key: K) { 32 | return $$typeToString!() 33 | } 34 | 35 | it("Should work with complex type", () => { 36 | expect($test2!("a")).to.equal("number"); 37 | expect($test2!("b")).to.equal("boolean") 38 | }) 39 | 40 | }); -------------------------------------------------------------------------------- /tests/integrated/expand.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { expect } from "chai"; 3 | 4 | describe("Macro expand", () => { 5 | 6 | function $push(array: T[], ...elements: Array) { 7 | +[[elements], (el: T) => { 8 | array.push(el); 9 | }]; 10 | } 11 | 12 | it("IIFE in expressions", () => { 13 | const arr = [1, 2, 3]; 14 | expect($push!(arr, 4, 5, 6)).to.be.equal(6); 15 | }); 16 | 17 | it("Inlined in expression statements", () => { 18 | const arr = [1, 2, 3]; 19 | $push!(arr, 1, 2, 3); 20 | expect(arr).to.be.deep.equal([1, 2, 3, 1, 2, 3]); 21 | }); 22 | 23 | function $push2(array: T[], ...elements: Array) { 24 | +["()", [elements], (el: T) => { 25 | array.push(el); 26 | }]; 27 | } 28 | 29 | it("Inlined in expressions", () => { 30 | const arr = [1, 2, 3]; 31 | expect($push2!(arr, 4, 5, 6)).to.be.equal(6); 32 | }); 33 | 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /tests/integrated/labels/block.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | const { $$inline } = require("../../../../dist/index"); 3 | 4 | function $TrySilence(info: any) { 5 | try { 6 | $$inline!(info.statement, []); 7 | } catch(err) {}; 8 | } 9 | 10 | describe("Block label marker", () => { 11 | 12 | it("To transpile to the correct statement", () => { 13 | expect(() => { 14 | $TrySilence: 15 | { 16 | throw new Error("This shouldn't throw!"); 17 | } 18 | }).to.not.throw(); 19 | }); 20 | 21 | }); -------------------------------------------------------------------------------- /tests/integrated/labels/for.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { expect } from "chai"; 3 | const { $$inline, $$define } = require("../../../../dist/index"); 4 | 5 | 6 | function $ForToWhile(info: any) { 7 | if (info.initializer.variables) { 8 | +[[info.initializer.variables], (variable: [string, any]) => { 9 | $$define!(variable[0], variable[1], true) 10 | }]; 11 | } 12 | else info.initializer.expression; 13 | while(info.condition) { 14 | $$inline!(info.statement, []); 15 | info.increment; 16 | } 17 | } 18 | 19 | describe("For label marker", () => { 20 | 21 | it("To transpile to the correct statement", () => { 22 | const arr = [1, 3, 4, 5, 6]; 23 | const arr2: Array = []; 24 | 25 | $ForToWhile: 26 | for (let i=2, j=10; i < arr.length; i++) { 27 | arr2.push(i); 28 | } 29 | expect(arr2).to.be.deep.equal([2, 3, 4]); 30 | //@ts-expect-error 31 | expect(j).to.be.equal(10); 32 | }); 33 | 34 | }); -------------------------------------------------------------------------------- /tests/integrated/labels/foriter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import ts from "typescript"; 3 | const { $$inline, $$kindof, $$define } = require("../../../../dist/index"); 4 | 5 | function $NormalizeFor(info: any) : void { 6 | if ($$kindof!(info.initializer) === ts.SyntaxKind.Identifier) { 7 | for (let i=0; i < info.iterator.length; i++) { 8 | $$define!(info.initializer, info.iterator[i]); 9 | $$inline!(info.statement, []); 10 | } 11 | } 12 | } 13 | 14 | describe("ForIter label marker", () => { 15 | 16 | it("To transpile to the correct statement", () => { 17 | const arr = [1, 3, 4, 5, 6]; 18 | 19 | let sum = 0; 20 | $NormalizeFor: 21 | for (const el of arr) { 22 | sum += el; 23 | } 24 | expect(sum).to.be.deep.equal(19); 25 | }); 26 | 27 | }); -------------------------------------------------------------------------------- /tests/integrated/labels/if.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { expect } from "chai"; 3 | const { $$inline } = require("../../../../dist/index"); 4 | 5 | 6 | function $ToTernary(label: any) : void { 7 | label.condition ? $$inline!(label.then, []) : $$inline!(label.else, []); 8 | } 9 | 10 | describe("If label marker", () => { 11 | 12 | it("To transpile to the correct statement", () => { 13 | let value: string = "test"; 14 | $ToTernary: 15 | if (value === "test") value = "other"; 16 | else value = "other2"; 17 | expect(value).to.be.equal("other"); 18 | }); 19 | 20 | }); -------------------------------------------------------------------------------- /tests/integrated/labels/while.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | const { $$inline } = require("../../../../dist/index"); 3 | 4 | 5 | function $DeWhile(info: any) { 6 | if (info.condition) { 7 | $$inline!(info.statement, []); 8 | } 9 | } 10 | 11 | describe("While label marker", () => { 12 | 13 | it("To transpile to the correct statement", () => { 14 | let val: string = "123"; 15 | $DeWhile: 16 | while (val === "123") { 17 | val = "124"; 18 | } 19 | expect(val).to.be.equal("124"); 20 | }); 21 | 22 | }); -------------------------------------------------------------------------------- /tests/integrated/markers/accumulator.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | type Accumulator = number & { __marker?: "Accumulator" }; 4 | 5 | describe("Accumulator marker", () => { 6 | 7 | function $test(acc: Accumulator = 4) { 8 | return acc; 9 | } 10 | 11 | it("Return the right amount", () => { 12 | expect($test!()).to.be.equal(4); 13 | expect($test!()).to.be.equal(5); 14 | expect($test!()).to.be.equal(6); 15 | }); 16 | 17 | }); -------------------------------------------------------------------------------- /tests/integrated/markers/save.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | export type Save = T & { __marker?: "Save" } 4 | 5 | describe("Save marker", () => { 6 | 7 | function $test(value: string, thing: Save) { 8 | if (value === "yes") thing = 1; 9 | else if (value === "no") thing = 0; 10 | return thing; 11 | } 12 | 13 | it("Return the right amount", () => { 14 | expect($test!("yes", 343)).to.be.equal(1); 15 | expect($test!("no", 11)).to.be.equal(0); 16 | expect($test!("maybe", 11)).to.be.equal(11); 17 | }); 18 | 19 | }); -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_const.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$const } = require("../../../../dist/index"); 5 | describe("$$const", () => { 6 | it("Define a constant", () => { 7 | const testVar = 123; 8 | (0, chai_1.expect)(testVar).to.be.equal(123); 9 | const testVar1 = (a, b) => a + b; 10 | (0, chai_1.expect)(testVar1(1, 10)).to.be.equal(11); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_decompose.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const chai_1 = require("chai"); 7 | const typescript_1 = __importDefault(require("typescript")); 8 | const { $$decompose, $$kindof, $$text, $$length, $$i, $$slice } = require("../../../../dist/index"); 9 | describe("$$decompose", () => { 10 | it("To stringify the expression", () => { 11 | (0, chai_1.expect)("console.log(123)").to.be.equal("console.log(123)"); 12 | (0, chai_1.expect)("console.log(1, true, console.log(\"Hello\"))").to.be.equal("console.log(1, true, console.log(\"Hello\"))"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_define.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$define } = require("../../../../dist/index"); 5 | describe("$$define", () => { 6 | it("Define a constant", () => { 7 | const testVar = 123; 8 | (0, chai_1.expect)(testVar).to.be.equal(123); 9 | const testVar1 = (a, b) => a + b; 10 | (0, chai_1.expect)(testVar1(1, 10)).to.be.equal(11); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_i.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$i } = require("../../../../dist/index"); 5 | describe("$$i", () => { 6 | it("To be -1 when outside of repetition", () => { 7 | (0, chai_1.expect)(-1).to.be.equal(-1); 8 | }); 9 | it("To be the index of repetitions", () => { 10 | (0, chai_1.expect)("a0b1c2").to.be.equal("a0b1c2"); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_ident.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$ident } = require("../../../../dist/index"); 5 | describe("$$ident", () => { 6 | const Hello = 123; 7 | it("To turn the string into the right identifier", () => { 8 | (0, chai_1.expect)(Hello).to.be.equal(123); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_includes.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$includes } = require("../../../../dist/index"); 5 | describe("$$includes", () => { 6 | it("To return true when the substring is there", () => { 7 | (0, chai_1.expect)(true).to.be.equal(true); 8 | }); 9 | it("To return false when the substring is not there", () => { 10 | (0, chai_1.expect)(false).to.be.equal(false); 11 | }); 12 | it("To return true when the item is there", () => { 13 | (0, chai_1.expect)(true).to.be.equal(true); 14 | }); 15 | it("To return false when the item is not there", () => { 16 | (0, chai_1.expect)(false).to.be.equal(false); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_inline.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$inline } = require("../../../../dist/index"); 5 | describe("$$inlineFunc", () => { 6 | it("Inline the function and replace the arguments", () => { 7 | (0, chai_1.expect)(1 + 5).to.be.equal(6); 8 | (0, chai_1.expect)(["a", "b", "c"].push("d")).to.be.deep.equal(4); 9 | }); 10 | it("Wrap the function in an IIFE", () => { 11 | (0, chai_1.expect)((() => { 12 | let acc = 0; 13 | for (let i = 1; i < 14 | 10; i++) { 15 | acc += i; 16 | } 17 | return acc; 18 | })()).to.be.equal(45); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_inlineFunc.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$inlineFunc } = require("../../../../dist/index"); 5 | describe("$$inlineFunc", () => { 6 | it("Inline the function and replace the arguments", () => { 7 | (0, chai_1.expect)(1 + 5).to.be.equal(6); 8 | (0, chai_1.expect)(["a", "b", "c"].push("d")).to.be.deep.equal(4); 9 | }); 10 | it("Wrap the function in an IIFE", () => { 11 | (0, chai_1.expect)((() => { 12 | let acc = 0; 13 | for (let i = 1; i < 14 | 10; i++) { 15 | acc += i; 16 | } 17 | return acc; 18 | })()).to.be.equal(45); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_kindof.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const typescript_1 = __importDefault(require("typescript")); 7 | const chai_1 = require("chai"); 8 | const { $$kindof } = require("../../../../dist/index"); 9 | describe("$$kindof", () => { 10 | it("Expand to the correct node kind", () => { 11 | (0, chai_1.expect)(209).to.be.equal(typescript_1.default.SyntaxKind.ArrayLiteralExpression); 12 | (0, chai_1.expect)(219).to.be.equal(typescript_1.default.SyntaxKind.ArrowFunction); 13 | (0, chai_1.expect)(9).to.be.equal(typescript_1.default.SyntaxKind.NumericLiteral); 14 | (0, chai_1.expect)(80).to.be.equal(typescript_1.default.SyntaxKind.Identifier); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_length.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$length } = require("../../../../dist/index"); 5 | describe("$$length", () => { 6 | it("To return the length of the array literal", () => { 7 | (0, chai_1.expect)(3).to.be.equal(3); 8 | (0, chai_1.expect)(10).to.be.equal(10); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_map.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const chai_1 = require("chai"); 7 | const typescript_1 = __importDefault(require("typescript")); 8 | const { $$map, $$kindof, $$text, $$ident } = require("../../../../dist/index"); 9 | describe("$$map", () => { 10 | function log() { 11 | return 123; 12 | } 13 | function debug() { 14 | return "abc"; 15 | } 16 | it("To correctly replace the identifiers", () => { 17 | (0, chai_1.expect)(debug()).to.be.equal("abc"); 18 | (0, chai_1.expect)((() => { 19 | return log() + 1; 20 | })()).to.be.equal(124); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_propsOfType.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$slice } = require("../../../../dist/index"); 5 | describe("$$propsOfType", () => { 6 | it("To return the properties", () => { 7 | (0, chai_1.expect)(["a", "b"]).to.be.deep.equal(["a", "b"]); 8 | (0, chai_1.expect)((() => { 9 | const parameter = { a: 123, __b: "Hello" }; 10 | delete parameter["__b"]; 11 | return parameter; 12 | })()).to.be.deep.equal({ a: 123 }); 13 | }); 14 | it("Should work with complex type", () => { 15 | (0, chai_1.expect)("a", "a"); 16 | (0, chai_1.expect)("b", "b"); 17 | (0, chai_1.expect)("e", "e"); 18 | (0, chai_1.expect)("d", "d"); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_raw.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$raw } = require("../../../../dist/index"); 5 | describe("$$raw", () => { 6 | it("To run the raw code", () => { 7 | (0, chai_1.expect)("hello").to.be.equal("hello"); 8 | (0, chai_1.expect)("12345").to.be.deep.equal("12345"); 9 | const str = "abc"; 10 | (0, chai_1.expect)(null).to.be.deep.equal(null); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_slice.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const chai_1 = require("chai"); 7 | const typescript_1 = __importDefault(require("typescript")); 8 | const { $$slice, $$kindof } = require("../../../../dist/index"); 9 | describe("$$slice", () => { 10 | it("To return the slice", () => { 11 | (0, chai_1.expect)("hell").to.be.equal("hell"); 12 | (0, chai_1.expect)([4, 5]).to.be.deep.equal([4, 5]); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_stores.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$getStore, $$setStore } = require("../../../../dist/index"); 5 | describe("$$getStore and $$setStore", () => { 6 | it("Save and retrieve", () => { 7 | (0, chai_1.expect)([]).to.be.instanceOf(Array); 8 | (0, chai_1.expect)(null).to.be.equal(null); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_ts.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$ts } = require("../../../../dist/index"); 5 | describe("$$ts", () => { 6 | it("To turn the string into code", () => { 7 | (0, chai_1.expect)((() => 123)()).to.be.equal(123); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/builtins_typeToString.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$slice } = require("../../../../dist/index"); 5 | describe("$$typeToString", () => { 6 | it("Should stringify the type", () => { 7 | (0, chai_1.expect)("string").to.be.equal("string"); 8 | (0, chai_1.expect)(false).to.be.equal(false); 9 | (0, chai_1.expect)(true).to.be.equal(true); 10 | }); 11 | it("Should work with complex type", () => { 12 | (0, chai_1.expect)("number").to.equal("number"); 13 | (0, chai_1.expect)("boolean").to.equal("boolean"); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/expand.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | describe("Macro expand", () => { 5 | it("IIFE in expressions", () => { 6 | const arr = [1, 2, 3]; 7 | (0, chai_1.expect)((() => { 8 | arr.push(4); 9 | arr.push(5); 10 | return arr.push(6); 11 | })()).to.be.equal(6); 12 | }); 13 | it("Inlined in expression statements", () => { 14 | const arr = [1, 2, 3]; 15 | arr.push(1); 16 | arr.push(2); 17 | arr.push(3); 18 | (0, chai_1.expect)(arr).to.be.deep.equal([1, 2, 3, 1, 2, 3]); 19 | }); 20 | it("Inlined in expressions", () => { 21 | const arr = [1, 2, 3]; 22 | (0, chai_1.expect)((arr.push(4), arr.push(5), arr.push(6))).to.be.equal(6); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/labels_block.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$inline } = require("../../../../dist/index"); 5 | describe("Block label marker", () => { 6 | it("To transpile to the correct statement", () => { 7 | (0, chai_1.expect)(() => { 8 | try { 9 | throw new Error("This shouldn't throw!"); 10 | } 11 | catch (err) { } 12 | ; 13 | }).to.not.throw(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/labels_for.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$inline, $$define } = require("../../../../dist/index"); 5 | describe("For label marker", () => { 6 | it("To transpile to the correct statement", () => { 7 | const arr = [1, 3, 4, 5, 6]; 8 | const arr2 = []; 9 | let i = 2; 10 | let j = 10; 11 | while (i < arr.length) { 12 | arr2.push(i); 13 | i++; 14 | } 15 | (0, chai_1.expect)(arr2).to.be.deep.equal([2, 3, 4]); 16 | (0, chai_1.expect)(j).to.be.equal(10); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/labels_foriter.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const chai_1 = require("chai"); 7 | const typescript_1 = __importDefault(require("typescript")); 8 | const { $$inline, $$kindof, $$define } = require("../../../../dist/index"); 9 | describe("ForIter label marker", () => { 10 | it("To transpile to the correct statement", () => { 11 | const arr = [1, 3, 4, 5, 6]; 12 | let sum = 0; 13 | for (let i = 0; i < arr.length; i++) { 14 | const el = arr[i]; 15 | sum += el; 16 | } 17 | (0, chai_1.expect)(sum).to.be.deep.equal(19); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/labels_if.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$inline } = require("../../../../dist/index"); 5 | describe("If label marker", () => { 6 | it("To transpile to the correct statement", () => { 7 | let value = "test"; 8 | value === "test" ? value = "other" : value = "other2"; 9 | (0, chai_1.expect)(value).to.be.equal("other"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/labels_while.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | const { $$inline } = require("../../../../dist/index"); 5 | describe("While label marker", () => { 6 | it("To transpile to the correct statement", () => { 7 | let val = "123"; 8 | if (val === "123") { 9 | val = "124"; 10 | } 11 | (0, chai_1.expect)(val).to.be.equal("124"); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/markers_accumulator.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | describe("Accumulator marker", () => { 5 | it("Return the right amount", () => { 6 | (0, chai_1.expect)(4).to.be.equal(4); 7 | (0, chai_1.expect)(5).to.be.equal(5); 8 | (0, chai_1.expect)(6).to.be.equal(6); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/markers_save.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const chai_1 = require("chai"); 4 | describe("Save marker", () => { 5 | it("Return the right amount", () => { 6 | let thing_1 = 343; 7 | (0, chai_1.expect)((() => { 8 | thing_1 = 1; 9 | return thing_1; 10 | })()).to.be.equal(1); 11 | let thing_2 = 11; 12 | (0, chai_1.expect)((() => { 13 | thing_2 = 0; 14 | return thing_2; 15 | })()).to.be.equal(0); 16 | let thing_3 = 11; 17 | (0, chai_1.expect)(thing_3).to.be.equal(11); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/snapshots/artifacts/markers_var.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const chai_1 = require("chai"); 7 | const typescript_1 = __importDefault(require("typescript")); 8 | const { $$kindof } = require("../../../../dist/index"); 9 | ; 10 | describe("Var marker", () => { 11 | it("Return the right expression", () => { 12 | (0, chai_1.expect)("number").to.be.equal("number"); 13 | (0, chai_1.expect)("string").to.be.equal("string"); 14 | (0, chai_1.expect)("array").to.be.equal("array"); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/snapshots/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import readline from "readline"; 4 | import { diffLines } from "diff"; 5 | 6 | const rl = readline.createInterface(process.stdin, process.stdout); 7 | 8 | /** 9 | * If "force" is enabled, the script won't ask you to continue, and if it notices 10 | * any differences in the code, it'll automatically error, not ask you if anything is 11 | * valid. 12 | */ 13 | const NO_PROMPT = process.argv[2]?.toLowerCase() === "force"; 14 | 15 | export const red = (text: string): string => `\x1b[31m${text}\x1b[0m`; 16 | export const gray = (text: string): string => `\x1b[90m${text}\x1b[0m`; 17 | export const cyan = (text: string): string => `\x1b[36m${text}\x1b[0m`; 18 | export const green = (text: string): string => `\x1b[32m${text}\x1b[0m`; 19 | 20 | const artifactsPath = path.join(process.cwd(), "../tests/snapshots/artifacts"); 21 | const integrated = path.join(process.cwd(), "../tests/dist/integrated"); 22 | 23 | if (!fs.existsSync(artifactsPath)) fs.mkdirSync(artifactsPath); 24 | 25 | (async () => { 26 | if (!NO_PROMPT && !(await askYesOrNo("Run snapshot tests? (y/n): "))) return process.exit(); 27 | const wrongful: Array = []; 28 | for (const [fileName, dirName, passedDirs] of eachFile(integrated, "")) { 29 | const newFilePath = path.join(dirName, fileName); 30 | const newFile = fs.readFileSync(path.join(dirName, fileName), "utf-8"); 31 | const targetFilePath = path.join(artifactsPath, passedDirs.replace("/", "_") + fileName); 32 | if (!fs.existsSync(targetFilePath)) fs.writeFileSync(targetFilePath, newFile); 33 | else { 34 | const oldFile = fs.readFileSync(targetFilePath, "utf-8"); 35 | if (oldFile === newFile) continue; 36 | const diffs = diffLines(oldFile, newFile); 37 | 38 | console.log(`[${cyan("FILE CHANGED")}]: ${red(passedDirs + fileName)}`); 39 | let final = ""; 40 | for (const change of diffs) { 41 | if (change.added) final += green(change.value); 42 | else if (change.removed) final += red(change.value); 43 | else final += gray(change.value); 44 | } 45 | console.log(final); 46 | if (!NO_PROMPT && await askYesOrNo("Do you agree with this change? (y/n): ")) { 47 | fs.writeFileSync(targetFilePath, newFile); 48 | console.clear(); 49 | } else { 50 | if (NO_PROMPT) { 51 | console.error(red("Make sure the following changes are valid before continuing.")); 52 | process.exit(); 53 | } else { 54 | wrongful.push(newFilePath); 55 | } 56 | } 57 | } 58 | } 59 | if (wrongful.length) console.error(`${red("The following files didn't match the snapshot")}:\n${wrongful.join("\n")}`); 60 | process.exit(); 61 | })(); 62 | 63 | 64 | function* eachFile(directory: string, passedDirs: string) : Generator<[fileName: string, directory: string, passedDirs: string]> { 65 | const files = fs.readdirSync(directory, { withFileTypes: true }); 66 | for (const file of files) { 67 | if (file.isDirectory()) yield* eachFile(path.join(directory, file.name), passedDirs + `${file.name}/`); 68 | else if (file.isFile()) yield [file.name, directory, passedDirs]; 69 | } 70 | } 71 | 72 | function ask(q: string) : Promise { 73 | return new Promise(res => rl.question(q, res)); 74 | } 75 | 76 | async function askYesOrNo(q: string) : Promise { 77 | // eslint-disable-next-line no-constant-condition 78 | while(true) { 79 | const answer = (await ask(q)).toLowerCase(); 80 | if (answer === "y") return true; 81 | else if (answer === "n") return false; 82 | } 83 | } -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "commonjs", 5 | "removeComments": true, 6 | "outDir": "./dist", 7 | "esModuleInterop": true, 8 | "strictNullChecks": true, 9 | "plugins": [ 10 | { "transform": "../dist" } 11 | ] 12 | }, 13 | "include": ["./integrated", "./snapshots/index.ts", "integrated/labels"] 14 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "skipLibCheck": true, 10 | "strict": true 11 | }, 12 | "exclude": ["./tests", "./test", "./dist", "./node_modules", "./playground"] 13 | } --------------------------------------------------------------------------------