├── .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 | 
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 |