├── .prettierignore
├── .eslintignore
├── .npmignore
├── .github
├── assets
│ └── intro_gif.gif
├── workflows
│ ├── publish.yml
│ └── docs.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── CONTRIBUTING.md
└── CODE_OF_CONDUCT.md
├── docs
├── Links
│ └── playground.md
└── In-Depth
│ ├── parameters.md
│ ├── markers.md
│ ├── builtins.md
│ ├── literals.md
│ ├── overview.md
│ ├── repetitions.md
│ ├── expanding.md
│ ├── macro_labels.md
│ ├── chaining.md
│ └── decorators.md
├── playground
├── next-env.d.ts
├── pages
│ ├── _app.tsx
│ └── index.tsx
├── next.config.js
├── tsconfig.json
├── package.json
├── css
│ ├── global.css
│ └── App.module.css
├── README.md
├── components
│ ├── Editor.tsx
│ └── Runnable.tsx
└── utils
│ └── transpile.ts
├── tests
├── integrated
│ ├── builtins
│ │ ├── ts.test.ts
│ │ ├── ident.test.ts
│ │ ├── length.test.ts
│ │ ├── define.test.ts
│ │ ├── i.test.ts
│ │ ├── kindof.test.ts
│ │ ├── slice.test.ts
│ │ ├── inline.test.ts
│ │ ├── includes.test.ts
│ │ ├── raw.test.ts
│ │ ├── map.test.ts
│ │ ├── typeToString.test.ts
│ │ ├── decompose.test.ts
│ │ └── propsOfType.test.ts
│ ├── markers
│ │ ├── accumulator.test.ts
│ │ └── save.test.ts
│ ├── labels
│ │ ├── block.test.ts
│ │ ├── while.test.ts
│ │ ├── if.test.ts
│ │ ├── foriter.test.ts
│ │ └── for.test.ts
│ └── expand.test.ts
├── snapshots
│ ├── artifacts
│ │ ├── builtins_ts.test.js
│ │ ├── builtins_ident.test.js
│ │ ├── markers_accumulator.test.js
│ │ ├── builtins_length.test.js
│ │ ├── builtins_stores.test.js
│ │ ├── labels_if.test.js
│ │ ├── builtins_i.test.js
│ │ ├── labels_while.test.js
│ │ ├── builtins_const.test.js
│ │ ├── builtins_define.test.js
│ │ ├── builtins_raw.test.js
│ │ ├── labels_block.test.js
│ │ ├── builtins_slice.test.js
│ │ ├── markers_save.test.js
│ │ ├── labels_for.test.js
│ │ ├── builtins_typeToString.test.js
│ │ ├── markers_var.test.js
│ │ ├── builtins_inline.test.js
│ │ ├── builtins_inlineFunc.test.js
│ │ ├── builtins_includes.test.js
│ │ ├── builtins_decompose.test.js
│ │ ├── labels_foriter.test.js
│ │ ├── builtins_propsOfType.test.js
│ │ ├── builtins_map.test.js
│ │ ├── expand.test.js
│ │ └── builtins_kindof.test.js
│ └── index.ts
└── tsconfig.json
├── tsconfig.json
├── .eslintrc.json
├── src
├── cli
│ ├── formatter.ts
│ ├── index.ts
│ └── transform.ts
├── type-resolve
│ ├── chainingTypes.ts
│ ├── declarations.ts
│ └── index.ts
├── watcher
│ └── index.ts
├── actions.ts
├── utils.ts
└── nativeMacros.ts
├── LICENSE
├── package.json
├── .gitignore
└── README.md
/.prettierignore:
--------------------------------------------------------------------------------
1 | **
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | tests/
2 | dist/
3 | test/
4 | playground/
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 |
2 | test/
3 | tests/
4 | .github/
5 | src/
6 | docs/
7 | playground/
--------------------------------------------------------------------------------
/.github/assets/intro_gif.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleFeud/ts-macros/HEAD/.github/assets/intro_gif.gif
--------------------------------------------------------------------------------
/docs/Links/playground.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Playground
3 | redirect: https://googlefeud.github.io/ts-macros/playground/
4 | ---
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | });
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | });
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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/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/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_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/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/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/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/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/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/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_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/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 }}
--------------------------------------------------------------------------------
/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/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 | });
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 | });
--------------------------------------------------------------------------------
/.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/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: .
--------------------------------------------------------------------------------
/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/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/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_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_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/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 | });
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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 | });
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | });
--------------------------------------------------------------------------------
/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 | ```
--------------------------------------------------------------------------------
/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 | });
--------------------------------------------------------------------------------
/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 | ```
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/.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/
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 | ```
--------------------------------------------------------------------------------
/.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`).
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | ```
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | ```
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 |
151 |
152 |
153 |
154 |
155 | {
156 | transpileCode(code || "");
157 | }} />
158 |
159 |
160 |
161 |
164 |
165 | );
166 | }
167 |
168 | export default () => {
169 | return ;
170 | };
--------------------------------------------------------------------------------
/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/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
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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/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;
--------------------------------------------------------------------------------