├── .eslintrc.json ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .vim └── coc-settings.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── __tests__ ├── babel.js ├── fixtures │ ├── errors │ │ ├── invalid-syntax │ │ │ ├── input.ts │ │ │ └── macros.js │ │ ├── throws │ │ │ ├── input.ts │ │ │ └── macros.js │ │ ├── wrong-name │ │ │ ├── input.ts │ │ │ └── macros.js │ │ └── wrong-path │ │ │ └── input.ts │ └── transform │ │ ├── basic │ │ ├── babel.ts │ │ ├── input.ts │ │ ├── macros.js │ │ └── typescript.ts │ │ ├── keeps-imports │ │ ├── babel.ts │ │ ├── input.ts │ │ ├── macros.js │ │ └── typescript.ts │ │ └── nested-macros │ │ ├── babel.ts │ │ ├── input.ts │ │ ├── macros.js │ │ └── typescript.ts ├── mapping │ ├── position.js │ └── source-map.js ├── setup.js ├── transform │ └── diagnostics.js ├── typescript.js └── utils │ ├── babel.js │ ├── fixtures.js │ └── ts.js ├── babel ├── create.js └── index.js ├── bin ├── tscm └── tsserverm ├── docs ├── Bundler-setup.md ├── How-it-works.md └── Writing-macros.md ├── jest.config.json ├── macro.d.ts ├── package.json ├── src ├── lsp.js ├── mappers.js ├── methods.js └── utils.js ├── typescript ├── lib │ ├── mapping │ │ ├── position.js │ │ └── source-map.js │ ├── modify-ts.js │ ├── shared.js │ ├── transform │ │ ├── ast.js │ │ ├── diagnostics.js │ │ ├── host.js │ │ ├── program.js │ │ └── updated-source.js │ ├── tsc.js │ ├── tsserver.js │ ├── typescript.d.ts │ ├── typescript.js │ └── utils.js └── package.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "prettier", 5 | "plugin:import/recommended" 6 | ], 7 | "plugins": ["import"], 8 | "parserOptions": { 9 | "ecmaVersion": 2020, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "curly": ["error", "multi-line"], 14 | "linebreak-style": ["error", "unix"], 15 | "max-len": [ 16 | "error", 17 | { 18 | "code": 80, 19 | "ignoreComments": true, 20 | "ignoreStrings": true, 21 | "ignoreTemplateLiterals": true 22 | } 23 | ], 24 | "new-cap": "off", 25 | "no-case-declarations": "error", 26 | "no-var": "error", 27 | "prefer-const": "error", 28 | "no-unused-vars": ["error", { "ignoreRestSiblings": true }], 29 | "import/order": "error" 30 | }, 31 | "env": { 32 | "node": true, 33 | "es6": true 34 | }, 35 | "ignorePatterns": [ 36 | "node_modules", 37 | "build", 38 | "coverage", 39 | "__tests__/fixtures" 40 | ], 41 | "overrides": [ 42 | { 43 | "files": ["__tests__/**"], 44 | "env": { 45 | "jest": true 46 | } 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - '*' 5 | push: 6 | branches: 7 | - '*' 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Setup Node.js environment 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: 16.x 20 | 21 | - name: Install packages 22 | run: yarn --frozen-lockfile 23 | 24 | - name: Check linting 25 | run: yarn lint:check 26 | 27 | - name: Check formatting 28 | run: yarn format:check 29 | 30 | - name: Tests 31 | run: yarn test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | *.log* 4 | .DS_Store 5 | coverage 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | *.log* 4 | .vim 5 | coverage 6 | __tests__ 7 | demo 8 | .github 9 | .* 10 | jest.config.json 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.9.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | coverage 4 | __tests__/fixtures 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "always" 5 | } 6 | -------------------------------------------------------------------------------- /.vim/coc-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tsserver.ignoreLocalTsserver": true, 3 | "tsserver.tsdk": "./typescript/lib", 4 | "tsserver.debugPort": 9229, 5 | "tsserver.log": "verbose" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./typescript/lib", 3 | "typescript.tsserver.log": "verbose" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Johan Holmerin 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | Since TypeScript 5 this project is no longer supported 4 | 5 | --- 6 | 7 | # tscm 8 | 9 | Function-like macros for TypeScript, inspired by Rust 10 | 11 | - Fully type-checked 12 | - IDE support - correct messages, types and diagnostics while editing 13 | - easy to use, easy to write 14 | - Generates correct source-maps 15 | - [Compatible Babel plugin](docs/Bundler-setup.md#babel-plugin) 16 | 17 | For a collection of example macros, like [GraphQL][gql] or [SQL][sql], see the [tscm-examples][tscm-examples] repository. 18 | 19 | ## Installation 20 | 21 | Bring your own TypeScript, version 4.0 or higher 22 | 23 | ```sh 24 | # Yarn 25 | yarn add -D tscm typescript 26 | 27 | # npm 28 | npm install -D tscm typescript 29 | ``` 30 | 31 | ## Example 32 | 33 | #### Usage 34 | 35 | ```typescript 36 | // macros are imported like normal 37 | import { macro } from './macros'; 38 | 39 | // Two non-null-assertion operators are used as indicator for macro calls 40 | const val = macro!!('literal', identifier); 41 | ``` 42 | 43 | #### Macro definition 44 | 45 | Macros are normal functions that get the CallExpression Node as parameter and return a new Node. They can be local files or npm packages. For more information see the [Writing macros](docs/Writing-macros.md) guide. 46 | 47 | ```javascript 48 | const t = require('@babel/types'); 49 | 50 | /** 51 | * @type {import('tscm/macro').Macro} 52 | */ 53 | module.exports.macro = function ({ node }) { 54 | // Returns that arguments as an array 55 | return t.arrayExpression(node.arguments); 56 | }; 57 | ``` 58 | 59 | ## Compiling 60 | 61 | To compile, use tscm instead of tsc. If you are using a bundler, see [Bundler setup](docs/Bundler-setup.md). 62 | 63 | ```sh 64 | npx tscm 65 | ``` 66 | 67 | ## Editor config 68 | 69 | To get the correct types in your editor, make sure to point it to `tscm`. 70 | 71 | ### coc.nvim 72 | 73 | ```javascript 74 | // .vim/coc-settings.json 75 | { 76 | "tsserver.ignoreLocalTsserver": true, 77 | "tsserver.tsdk": "./node_modules/tscm/typescript/lib" 78 | } 79 | ``` 80 | 81 | ### VS Code 82 | 83 | Make sure to select `Use Workspace Version` under `TypeScript: Select TypeScript version` 84 | 85 | ```javascript 86 | // .vscode/settings.json 87 | { 88 | "typescript.tsdk": "./node_modules/tscm/typescript/lib" 89 | } 90 | ``` 91 | 92 | ## Documentation 93 | 94 | 1. [Bundler setup](docs/Bundler-setup.md) 95 | 1. [Writing macros](docs/Writing-macros.md) 96 | 1. [How it works](docs/How-it-works.md) 97 | 98 | [tscm-examples]: https://github.com/johanholmerin/tscm-examples 99 | [gql]: https://github.com/johanholmerin/tscm-examples/tree/master/macros/graphql 100 | [sql]: https://github.com/johanholmerin/tscm-examples/tree/master/macros/pgtyped 101 | -------------------------------------------------------------------------------- /__tests__/babel.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { compile, compileString } = require('./utils/babel'); 3 | const { fixtures } = require('./utils/fixtures'); 4 | 5 | describe('babel', () => { 6 | for (const file of fixtures) { 7 | it(path.parse(file).name, () => { 8 | const { code } = compile(path.join(file, 'input.ts')); 9 | expect(code).toMatchFile(path.join(file, 'babel.ts')); 10 | }); 11 | } 12 | 13 | it('reports error when macro throws', () => { 14 | expect(() => 15 | compile(path.join(__dirname, './fixtures/errors/throws/input.ts')) 16 | ).toThrowErrorMatchingInlineSnapshot(` 17 | "Macro error error: test_message 18 | 1 | import { error } from './macros'; 19 | 2 | 20 | > 3 | error!!(); 21 | | ^^^^^^^^^ 22 | 4 |" 23 | `); 24 | }); 25 | 26 | it("reports error when macro file doesn't exist", () => { 27 | expect(() => 28 | compile(path.join(__dirname, './fixtures/errors/wrong-path/input.ts')) 29 | ).toThrowErrorMatchingInlineSnapshot(` 30 | "Macro error error: Cannot find module './macros' 31 | > 1 | import { error } from './macros'; 32 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 33 | 2 | 34 | 3 | error!!(); 35 | 4 |" 36 | `); 37 | }); 38 | 39 | it("reports error when macro name doesn't exist", () => { 40 | expect(() => 41 | compile(path.join(__dirname, './fixtures/errors/wrong-name/input.ts')) 42 | ).toThrowErrorMatchingInlineSnapshot(` 43 | "Macro err error: Function err does not exist in './macros' 44 | > 1 | import { err } from './macros'; 45 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 46 | 2 | 47 | 3 | err!!(); 48 | 4 |" 49 | `); 50 | }); 51 | 52 | it("doesn't transform non-macro code", () => { 53 | const INPUT = `foo!();`; 54 | expect(compileString(INPUT).code).toEqual(INPUT); 55 | }); 56 | 57 | it("doesn't transform macro without import", () => { 58 | const INPUT = `foo!!();`; 59 | expect(compileString(INPUT).code).toEqual(INPUT); 60 | }); 61 | 62 | it("doesn't transform non-import reference", () => { 63 | const INPUT = `const foo = () => {}; 64 | 65 | foo!!();`; 66 | expect(compileString(INPUT).code).toEqual(INPUT); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /__tests__/fixtures/errors/invalid-syntax/input.ts: -------------------------------------------------------------------------------- 1 | import { error } from './macros 2 | 3 | error!!(); 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/errors/invalid-syntax/macros.js: -------------------------------------------------------------------------------- 1 | module.exports.error = function error() { 2 | throw new Error('test_message'); 3 | }; 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/errors/throws/input.ts: -------------------------------------------------------------------------------- 1 | import { error } from './macros'; 2 | 3 | error!!(); 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/errors/throws/macros.js: -------------------------------------------------------------------------------- 1 | module.exports.error = function error() { 2 | throw new Error('test_message'); 3 | }; 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/errors/wrong-name/input.ts: -------------------------------------------------------------------------------- 1 | import { err } from './macros'; 2 | 3 | err!!(); 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/errors/wrong-name/macros.js: -------------------------------------------------------------------------------- 1 | module.exports.error = function error() { 2 | throw new Error('test_message'); 3 | }; 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/errors/wrong-path/input.ts: -------------------------------------------------------------------------------- 1 | import { error } from './macros'; 2 | 3 | error!!(); 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/transform/basic/babel.ts: -------------------------------------------------------------------------------- 1 | (() => { 2 | return 1; 3 | })(); 4 | 5 | export const query = Promise.resolve<{ 6 | id: string; 7 | }[]>([]); 8 | export const cls = []; 9 | export const validator = Promise.resolve<{ 10 | foo: string; 11 | }[]>([]); -------------------------------------------------------------------------------- /__tests__/fixtures/transform/basic/input.ts: -------------------------------------------------------------------------------- 1 | import { sql, styles, validate } from './macros'; 2 | 3 | (() => { 4 | return 1; 5 | })(); 6 | 7 | export const query = sql!!('SELECT id from items;'); 8 | export const cls = styles!!({ 9 | color: 'red' 10 | }); 11 | export const validator = validate!!<{ foo: string }>(); 12 | -------------------------------------------------------------------------------- /__tests__/fixtures/transform/basic/macros.js: -------------------------------------------------------------------------------- 1 | const t = require('@babel/types'); 2 | 3 | module.exports.sql = function sql() { 4 | return t.expressionStatement( 5 | Object.assign( 6 | t.callExpression( 7 | t.memberExpression(t.identifier('Promise'), t.identifier('resolve')), 8 | [t.arrayExpression()] 9 | ), 10 | { 11 | typeParameters: t.tsTypeParameterInstantiation([ 12 | t.tsArrayType( 13 | t.tsTypeLiteral([ 14 | t.tsPropertySignature( 15 | t.identifier('id'), 16 | t.tsTypeAnnotation(t.tsStringKeyword()) 17 | ) 18 | ]) 19 | ) 20 | ]) 21 | } 22 | ) 23 | ); 24 | }; 25 | 26 | module.exports.styles = function styles() { 27 | return t.arrayExpression([]); 28 | }; 29 | 30 | module.exports.validate = function validate({ node }) { 31 | return t.expressionStatement( 32 | Object.assign( 33 | t.callExpression( 34 | t.memberExpression(t.identifier('Promise'), t.identifier('resolve')), 35 | [t.arrayExpression()] 36 | ), 37 | { 38 | typeParameters: t.tsTypeParameterInstantiation([ 39 | t.tsArrayType(node.typeParameters.params[0]) 40 | ]) 41 | } 42 | ) 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /__tests__/fixtures/transform/basic/typescript.ts: -------------------------------------------------------------------------------- 1 | (() => { 2 | return 1; 3 | })(); 4 | export const query = Promise.resolve<{ 5 | id: string; 6 | }[]>([]); 7 | export const cls = []; 8 | export const validator = Promise.resolve<{ 9 | foo: string; 10 | }[]>([]); 11 | -------------------------------------------------------------------------------- /__tests__/fixtures/transform/keeps-imports/babel.ts: -------------------------------------------------------------------------------- 1 | import { sql } from './macros'; 2 | export const value = []; 3 | export const value2 = {}; 4 | export { sql }; -------------------------------------------------------------------------------- /__tests__/fixtures/transform/keeps-imports/input.ts: -------------------------------------------------------------------------------- 1 | import { sql, other } from './macros'; 2 | 3 | export const value = sql!!(); 4 | export const value2 = other!!(); 5 | 6 | export { sql }; 7 | -------------------------------------------------------------------------------- /__tests__/fixtures/transform/keeps-imports/macros.js: -------------------------------------------------------------------------------- 1 | const t = require('@babel/types'); 2 | 3 | module.exports.sql = function sql() { 4 | return t.arrayExpression([]); 5 | }; 6 | 7 | module.exports.other = function other() { 8 | return t.objectExpression([]); 9 | }; 10 | -------------------------------------------------------------------------------- /__tests__/fixtures/transform/keeps-imports/typescript.ts: -------------------------------------------------------------------------------- 1 | import { sql, } from "./macros"; 2 | export const value = []; 3 | export const value2 = {}; 4 | export { sql }; 5 | -------------------------------------------------------------------------------- /__tests__/fixtures/transform/nested-macros/babel.ts: -------------------------------------------------------------------------------- 1 | export const value = [{ 2 | prop: "second: val" 3 | }]; -------------------------------------------------------------------------------- /__tests__/fixtures/transform/nested-macros/input.ts: -------------------------------------------------------------------------------- 1 | import { first, second } from './macros'; 2 | 3 | export const value = first!!({ 4 | prop: second!!('val'), 5 | }); 6 | -------------------------------------------------------------------------------- /__tests__/fixtures/transform/nested-macros/macros.js: -------------------------------------------------------------------------------- 1 | const t = require('@babel/types'); 2 | 3 | module.exports.first = function first({ node }) { 4 | return t.arrayExpression(node.arguments); 5 | }; 6 | 7 | module.exports.second = function second({ node }) { 8 | return t.stringLiteral(`second: ${node.arguments[0].value}`); 9 | }; 10 | -------------------------------------------------------------------------------- /__tests__/fixtures/transform/nested-macros/typescript.ts: -------------------------------------------------------------------------------- 1 | export const value = [{ 2 | prop: "second: val" 3 | }]; 4 | -------------------------------------------------------------------------------- /__tests__/mapping/position.js: -------------------------------------------------------------------------------- 1 | const { 2 | translatePosForward, 3 | translatePosBackward 4 | } = require('../../typescript/lib/mapping/position'); 5 | 6 | const MAPPING = [ 7 | [98, 128, 'Promise.resolve<{\n id: string;\n}[]>([]);'], 8 | [149, 177, '[]'], 9 | [204, 233, 'Promise.resolve<{\n foo: string;\n}[]>([]);'], 10 | [0, 47, ' '] 11 | ]; 12 | 13 | const NESTED_MAPPING = [ 14 | [82, 97, '"string value: val"'], 15 | [64, 101, '[{\n prop: "string value: val"\n}]'], 16 | [0, 41, ' '] 17 | ]; 18 | 19 | describe('translatePosForward', () => { 20 | it('returns input on no mappings', () => { 21 | expect(translatePosForward([], 10)).toEqual(10); 22 | }); 23 | 24 | it('returns input outside source', () => { 25 | expect(translatePosForward([], 117)).toEqual(117); 26 | }); 27 | 28 | it('maps after replacement of same length', () => { 29 | expect(translatePosForward(MAPPING, 91)).toEqual(91); 30 | }); 31 | 32 | it('maps after multi-line replacement', () => { 33 | expect(translatePosForward(MAPPING, 144)).toEqual(155); 34 | }); 35 | 36 | it('maps after multi-line replacement2', () => { 37 | expect(translatePosForward(MAPPING, 193)).toEqual(178); 38 | }); 39 | 40 | it('returns -1 in replacement', () => { 41 | expect(translatePosForward(MAPPING, 102)).toEqual(-1); 42 | }); 43 | 44 | it('supports nested macros', () => { 45 | expect(translatePosForward(NESTED_MAPPING, 118)).toEqual(114); 46 | }); 47 | }); 48 | 49 | describe('translatePosBackward', () => { 50 | it('returns input on no mappings', () => { 51 | expect(translatePosBackward([], 10)).toEqual(10); 52 | }); 53 | 54 | it('returns input outside source', () => { 55 | expect(translatePosBackward([], 117)).toEqual(117); 56 | }); 57 | 58 | it('maps after replacement of same length', () => { 59 | expect(translatePosBackward(MAPPING, 91)).toEqual(91); 60 | }); 61 | 62 | it('maps after multi-line replacement', () => { 63 | expect(translatePosBackward(MAPPING, 155)).toEqual(144); 64 | }); 65 | 66 | it('maps after multi-line replacement2', () => { 67 | expect(translatePosBackward(MAPPING, 178)).toEqual(193); 68 | }); 69 | 70 | it('supports nested macros', () => { 71 | expect(translatePosBackward(NESTED_MAPPING, 114)).toEqual(118); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /__tests__/mapping/source-map.js: -------------------------------------------------------------------------------- 1 | const { mapSourceMap } = require('../../typescript/lib/mapping/source-map'); 2 | const { sourcemaps } = require('../../typescript/lib/shared'); 3 | 4 | function toB64(string) { 5 | return Buffer.from(string, 'utf8').toString('base64'); 6 | } 7 | 8 | const SOURCE_INPUT = 9 | ' \n\n(() => {\n return 1;\n})();\n\nexport const query = Promise.resolve<{\n id: string;\n}[]>([]);;\nexport const cls = [];\nexport const validator = Promise.resolve<{\n foo: string;\n}[]>([]);;\n'; 10 | const SOURCE_OUTPUT = `(() => { 11 | return 1; 12 | })(); 13 | export const query = Promise.resolve([]); 14 | ; 15 | export const cls = []; 16 | export const validator = Promise.resolve([]); 17 | ;`; 18 | const INPUT_MAP = { 19 | version: 3, 20 | file: '/OUTPUT.js', 21 | sourceRoot: '', 22 | sources: ['/INPUT.ts'], 23 | names: [], 24 | mappings: 25 | 'AAEA,CAAC,GAAG,EAAE;IACJ,OAAO,CAAC,CAAC;AACX,CAAC,CAAC,EAAE,CAAC;AAEL,MAAM,CAAC,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAE/B,EAAE,CAAC,CAAC;AAAA,CAAC;AACV,MAAM,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;AACtB,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAEnC,EAAE,CAAC,CAAC;AAAA,CAAC', 26 | sourcesContent: [SOURCE_INPUT] 27 | }; 28 | const OUTPUT_MAP = { 29 | version: 3, 30 | sources: ['/INPUT.ts'], 31 | names: [], 32 | mappings: 33 | 'AAEA,CAAC,GAAG,EAAE;IACJ,OAAO,CAAC,CAAC;AACX,CAAC,CAAC,EAAE,CAAC;AAEL,MAAM,CAAC,MAAM,KAAK,GAAG,OAAA,CAAA,OAAA,CAEhB,EAAE,CAAC,CAF2C;AAAA,CAAC;AACpD,MAAM,CAAC,MAAM,GAAG,GAAG,EAEjB,CAAC;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,OAAA,CAAA,OAAA,CAEpB,EAAE,CAAC,CAF8C;AAAA,CAAC', 34 | file: '/OUTPUT.js', 35 | sourceRoot: '', 36 | sourcesContent: [SOURCE_INPUT] 37 | }; 38 | 39 | beforeEach(() => { 40 | sourcemaps.set('/INPUT.ts', { 41 | version: 3, 42 | file: null, 43 | sources: ['/INPUT.ts'], 44 | sourcesContent: [null], 45 | names: [], 46 | mappings: 47 | 'AAAA,+CAA+C;AAC/C;AACA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACR,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACX,CAAC,CAAC,CAAC,CAAC,CAAC;AACL;AACA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;SAA8B,CAAC;AACpD,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAEjB,CAAC;AACH,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;SAA6B,CAAC;' 48 | }); 49 | }); 50 | 51 | describe('mapSourceMap', () => { 52 | it('maps external source map', () => { 53 | const fileName = '/OUTPUT.js.map'; 54 | expect( 55 | JSON.parse(mapSourceMap(fileName, JSON.stringify(INPUT_MAP))) 56 | ).toEqual(OUTPUT_MAP); 57 | }); 58 | 59 | it('maps inline source map', () => { 60 | const fileName = '/OUTPUT.js'; 61 | const INPUT = [ 62 | SOURCE_OUTPUT, 63 | '//# sourceMappingURL=data:application/json;base64,' + 64 | toB64(JSON.stringify(INPUT_MAP)) 65 | ].join('\n'); 66 | const OUTPUT = [ 67 | SOURCE_OUTPUT, 68 | '//# sourceMappingURL=data:application/json;base64,' + 69 | toB64(JSON.stringify(OUTPUT_MAP)) 70 | ].join('\n'); 71 | expect(mapSourceMap(fileName, INPUT)).toEqual(OUTPUT); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /__tests__/setup.js: -------------------------------------------------------------------------------- 1 | const { toMatchFile } = require('jest-file-snapshot'); 2 | const { 3 | mappings, 4 | sourcemaps, 5 | sources, 6 | diagnostics, 7 | sequences 8 | } = require('../typescript/lib/shared'); 9 | 10 | beforeEach(() => { 11 | mappings.clear(); 12 | sourcemaps.clear(); 13 | sources.clear(); 14 | diagnostics.clear(); 15 | sequences.clear(); 16 | }); 17 | 18 | expect.extend({ toMatchFile }); 19 | -------------------------------------------------------------------------------- /__tests__/transform/diagnostics.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const { diagnostics, mappings } = require('../../typescript/lib/shared'); 3 | const { 4 | patchProgramDiagnostics 5 | } = require('../../typescript/lib/transform/diagnostics'); 6 | 7 | const MAPPING = [ 8 | [98, 128, 'Promise.resolve<{\n id: string;\n}[]>([]);'], 9 | [0, 47, ' '] 10 | ]; 11 | 12 | const SOURCE = `import { sql, styles, validate } from './mods'; 13 | 14 | (() => { 15 | return 1; 16 | })(); 17 | 18 | export const query = sql!!('SELECT id from items;');`; 19 | 20 | describe('patchProgramDiagnostics', () => { 21 | const programMocks = { 22 | getSuggestionDiagnostics: jest.fn(), 23 | getSyntacticDiagnostics: jest.fn(), 24 | getSemanticDiagnostics: jest.fn() 25 | }; 26 | const program = { ...programMocks }; 27 | patchProgramDiagnostics(program); 28 | const sourceFile = ts.createSourceFile( 29 | 'FAKE_FILE', 30 | SOURCE, 31 | ts.ScriptTarget.ESNext 32 | ); 33 | const diagInReplacement = { start: 100, length: 10, file: sourceFile }; 34 | const diagInSource = { start: 50, length: 5, file: sourceFile }; 35 | const diagFromMacro = { start: 101, length: 2, file: sourceFile }; 36 | 37 | beforeEach(() => { 38 | programMocks.getSuggestionDiagnostics.mockClear(); 39 | programMocks.getSyntacticDiagnostics.mockClear(); 40 | programMocks.getSemanticDiagnostics.mockClear(); 41 | }); 42 | 43 | function testDiagMethod(name, includeMacroDiag) { 44 | describe(name, () => { 45 | it('calls original function', () => { 46 | programMocks[name].mockReturnValue([]); 47 | 48 | program[name](sourceFile); 49 | 50 | expect(programMocks[name]).toHaveBeenCalledWith(sourceFile); 51 | }); 52 | 53 | if (includeMacroDiag) { 54 | it('adds diagnostics', () => { 55 | diagnostics.set('FAKE_FILE', [diagFromMacro]); 56 | programMocks[name].mockReturnValue([]); 57 | 58 | const result = program[name](sourceFile); 59 | 60 | expect(result).toEqual([diagFromMacro]); 61 | expect(programMocks[name]).toHaveBeenCalledWith(sourceFile); 62 | }); 63 | 64 | it('adds all diagnostics on no sourceFile input', () => { 65 | diagnostics.set('ANOTHER_FAKE_FILE', [diagFromMacro]); 66 | programMocks[name].mockReturnValue([]); 67 | 68 | const result = program[name](); 69 | 70 | expect(result).toEqual([diagFromMacro]); 71 | expect(programMocks[name]).toHaveBeenCalledWith(); 72 | }); 73 | } else { 74 | it("doesn't add diagnostics", () => { 75 | diagnostics.set('FAKE_FILE', [diagFromMacro]); 76 | programMocks[name].mockReturnValue([]); 77 | 78 | const result = program[name](sourceFile); 79 | 80 | expect(result).toEqual([]); 81 | expect(programMocks[name]).toHaveBeenCalledWith(sourceFile); 82 | }); 83 | } 84 | 85 | it('filters out diagnostics in macro replacement', () => { 86 | mappings.set('FAKE_FILE', MAPPING); 87 | programMocks[name].mockReturnValue([diagInReplacement]); 88 | 89 | const result = program[name](sourceFile); 90 | 91 | expect(result).toEqual([]); 92 | expect(programMocks[name]).toHaveBeenCalledWith(sourceFile); 93 | }); 94 | 95 | it('keeps diagnostics outside macro replacement', () => { 96 | mappings.set('FAKE_FILE', MAPPING); 97 | programMocks[name].mockReturnValue([diagInSource]); 98 | 99 | const result = program[name](sourceFile); 100 | 101 | expect(result).toEqual([diagInSource]); 102 | expect(programMocks[name]).toHaveBeenCalledWith(sourceFile); 103 | }); 104 | }); 105 | } 106 | 107 | testDiagMethod('getSuggestionDiagnostics', false); 108 | testDiagMethod('getSyntacticDiagnostics', true); 109 | testDiagMethod('getSemanticDiagnostics', true); 110 | }); 111 | -------------------------------------------------------------------------------- /__tests__/typescript.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ts = require('typescript'); 3 | 4 | jest.mock('../typescript/lib/mapping/source-map', () => ({ 5 | mapSourceMap: jest.fn(() => 'MAPPED_OUTPUT') 6 | })); 7 | const { mapSourceMap } = require('../typescript/lib/mapping/source-map'); 8 | beforeEach(() => { 9 | mapSourceMap.mockClear(); 10 | }); 11 | 12 | const { diagnostics } = require('../typescript/lib/shared'); 13 | const { compile, createHost, createProgram } = require('./utils/ts'); 14 | const { fixtures } = require('./utils/fixtures'); 15 | 16 | describe('typescript', () => { 17 | for (const file of fixtures) { 18 | it(path.parse(file).name, () => { 19 | const { code } = compile(path.join(file, 'input.ts')); 20 | expect(code).toMatchFile(path.join(file, 'typescript.ts')); 21 | }); 22 | } 23 | 24 | it('adds errors to diagnostics when macro throws', () => { 25 | const fileName = path.join(__dirname, './fixtures/errors/throws/input.ts'); 26 | compile(fileName); 27 | expect(diagnostics.get(fileName)).toEqual([ 28 | { 29 | file: { 30 | fileName, 31 | text: `import { error } from './macros'; 32 | 33 | error!!(); 34 | ` 35 | }, 36 | start: 35, 37 | length: 9, 38 | messageText: 'Macro error error: test_message', 39 | category: 1, 40 | code: -1, 41 | reportsUnnecessary: {}, 42 | reportsDeprecated: {} 43 | } 44 | ]); 45 | }); 46 | 47 | it('maps output sourcemap', () => { 48 | const fileName = path.join( 49 | __dirname, 50 | './fixtures/transform/basic/input.ts' 51 | ); 52 | 53 | const host = createHost(); 54 | const writeFile = jest.fn((_fileName, content) => { 55 | expect(content).toEqual('MAPPED_OUTPUT'); 56 | }); 57 | host.writeFile = writeFile; 58 | const program = createProgram(fileName, host); 59 | program.emit(); 60 | expect(writeFile).toHaveBeenCalled(); 61 | expect(mapSourceMap).toHaveBeenCalled(); 62 | expect(writeFile).toHaveBeenCalled(); 63 | }); 64 | 65 | it('adds errors to diagnostics when babel throws', () => { 66 | const fileName = path.join( 67 | __dirname, 68 | './fixtures/errors/invalid-syntax/input.ts' 69 | ); 70 | compile(fileName); 71 | const diagnosticsList = diagnostics.get(fileName); 72 | expect(diagnosticsList).toHaveLength(1); 73 | const { file, ...diagnostic } = diagnosticsList[0]; 74 | expect(file).toBeInstanceOf(ts.objectAllocator.getSourceFileConstructor()); 75 | expect(diagnostic).toEqual({ 76 | category: 1, 77 | code: -2, 78 | length: 0, 79 | messageText: 'Babel error: Unterminated string constant. (1:22)', 80 | reportsDeprecated: {}, 81 | reportsUnnecessary: {}, 82 | start: 22 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /__tests__/utils/babel.js: -------------------------------------------------------------------------------- 1 | const babel = require('@babel/core'); 2 | const plugin = require('../../babel'); 3 | 4 | function compileString(code) { 5 | return babel.transformSync(code, { 6 | filename: 'foo', 7 | sourceType: 'module', 8 | plugins: ['@babel/plugin-syntax-typescript', plugin()], 9 | highlightCode: false 10 | }); 11 | } 12 | 13 | function compileFile(fileName) { 14 | return babel.transformFileSync(fileName, { 15 | filename: 'foo', 16 | sourceType: 'module', 17 | plugins: ['@babel/plugin-syntax-typescript', plugin()], 18 | highlightCode: false 19 | }); 20 | } 21 | 22 | function compile(fileName) { 23 | try { 24 | return compileFile(fileName); 25 | } catch (error) { 26 | // Remove file name from error message for consistency 27 | error.message = error.message.replace(`${fileName}: `, ''); 28 | throw error; 29 | } 30 | } 31 | 32 | module.exports = { compile, compileString }; 33 | -------------------------------------------------------------------------------- /__tests__/utils/fixtures.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const DIR = path.join(__dirname, '../fixtures/transform'); 5 | const fixtures = fs.readdirSync(DIR).map((file) => path.join(DIR, file)); 6 | 7 | module.exports = { fixtures }; 8 | -------------------------------------------------------------------------------- /__tests__/utils/ts.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | require('../../typescript/lib/modify-ts')(ts); 3 | 4 | const options = { 5 | target: ts.ScriptTarget.ESNext, 6 | module: ts.ModuleKind.ESNext, 7 | strict: true 8 | }; 9 | 10 | function createHost() { 11 | return ts.createCompilerHost(options); 12 | } 13 | 14 | function createProgram(fileName, host) { 15 | return ts.createProgram([fileName], options, host); 16 | } 17 | 18 | function compile(fileName) { 19 | const host = createHost(); 20 | const program = createProgram(fileName, host); 21 | const sourceFile = program.getSourceFile(fileName); 22 | const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 23 | const code = printer.printFile(sourceFile); 24 | 25 | return { code }; 26 | } 27 | 28 | module.exports = { compile, createHost, createProgram }; 29 | -------------------------------------------------------------------------------- /babel/create.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const resolve = require('resolve'); 3 | 4 | function createVisitor({ onReplace, onError, fileName, compiler }) { 5 | const impDecs = new Set(); 6 | // Counts number of remaining references 7 | const impSpecs = new Map(); 8 | 9 | return { 10 | CallExpression: { 11 | exit(nodePath, state) { 12 | const currentFileName = state?.filename ?? fileName; 13 | if ( 14 | !nodePath.get('callee').isTSNonNullExpression() || 15 | !nodePath.get('callee.expression').isTSNonNullExpression() 16 | ) { 17 | return; 18 | } 19 | 20 | const macroName = nodePath.get('callee.expression.expression').node 21 | .name; 22 | let scope = nodePath.scope; 23 | let binding; 24 | while (scope) { 25 | if (macroName in scope.bindings) { 26 | binding = scope.bindings[macroName]; 27 | break; 28 | } 29 | scope = scope.parent; 30 | } 31 | if (!binding) return; 32 | const impDec = binding.path.parentPath; 33 | 34 | if (impDec.type !== 'ImportDeclaration') return; 35 | nodePath.skip(); 36 | 37 | const importStr = impDec.node.source.value; 38 | const basedir = path.parse(currentFileName).dir; 39 | const { node } = nodePath; 40 | 41 | let resolvedPath; 42 | try { 43 | resolvedPath = resolve.sync(importStr, { basedir }); 44 | } catch (error) { 45 | onError({ 46 | node: impDec, 47 | macroName, 48 | message: `Cannot find module '${importStr}'` 49 | }); 50 | 51 | return; 52 | } 53 | 54 | const module = require(resolvedPath); 55 | 56 | const func = module[macroName]; 57 | if (!func) { 58 | onError({ 59 | node: impDec, 60 | macroName, 61 | message: `Function ${macroName} does not exist in '${importStr}'` 62 | }); 63 | 64 | return; 65 | } 66 | 67 | let newCode; 68 | try { 69 | newCode = func({ node, fileName: currentFileName, compiler }); 70 | } catch (error) { 71 | onError({ 72 | node: nodePath, 73 | macroName, 74 | message: error?.message ?? 'Unknown' 75 | }); 76 | 77 | return; 78 | } 79 | 80 | onReplace(nodePath, newCode); 81 | 82 | const impSpec = binding.path; 83 | if (!impSpecs.has(impSpec)) { 84 | impSpecs.set(impSpec, binding.references); 85 | } 86 | impDecs.add(impDec); 87 | impSpecs.set(impSpec, impSpecs.get(impSpec) - 1); 88 | } 89 | }, 90 | Program: { 91 | exit() { 92 | for (const impDec of impDecs) { 93 | const removeAll = impDec.get('specifiers').every((impSpec) => { 94 | return impSpecs.get(impSpec) === 0; 95 | }); 96 | 97 | if (removeAll) { 98 | impDec.get('specifiers').forEach((impSpec) => { 99 | impSpecs.delete(impSpec); 100 | }); 101 | onReplace(impDec); 102 | } else { 103 | impDec.get('specifiers').forEach((impSpec) => { 104 | const shouldRemove = impSpecs.get(impSpec) === 0; 105 | if (!shouldRemove) return; 106 | 107 | onReplace(impSpec); 108 | }); 109 | } 110 | } 111 | } 112 | } 113 | }; 114 | } 115 | 116 | module.exports = createVisitor; 117 | -------------------------------------------------------------------------------- /babel/index.js: -------------------------------------------------------------------------------- 1 | const NAME = require('../package.json').name; 2 | const createVisitor = require('./create'); 3 | 4 | function onReplace(nodePath, newNode) { 5 | if (newNode) { 6 | nodePath.replaceWith(newNode); 7 | } else { 8 | nodePath.remove(); 9 | } 10 | } 11 | 12 | function onError({ node, macroName, message }) { 13 | throw node.buildCodeFrameError(`Macro ${macroName} error: ${message}`); 14 | } 15 | 16 | module.exports = function tscmPlugin() { 17 | return { 18 | name: NAME, 19 | visitor: createVisitor({ onReplace, onError, compiler: 'babel' }) 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /bin/tscm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../typescript/lib/tsc.js'); 3 | -------------------------------------------------------------------------------- /bin/tsserverm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../typescript/lib/tsserver.js'); 3 | -------------------------------------------------------------------------------- /docs/Bundler-setup.md: -------------------------------------------------------------------------------- 1 | # Bundler setup 2 | 3 | ## Webpack 4 | 5 | ```javascript 6 | const path = require('path'); 7 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 8 | 9 | module.exports = { 10 | entry: './src/index.ts', 11 | output: { 12 | filename: 'index.js', 13 | path: path.resolve(__dirname, 'build') 14 | }, 15 | resolve: { 16 | extensions: ['.ts', '.js'] 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.ts$/, 22 | loader: 'ts-loader', 23 | options: { 24 | compiler: 'tscm', 25 | transpileOnly: true 26 | } 27 | } 28 | ] 29 | }, 30 | plugins: [ 31 | new ForkTsCheckerWebpackPlugin({ 32 | typescript: { 33 | typescriptPath: 'tscm' 34 | } 35 | }) 36 | ] 37 | }; 38 | ``` 39 | 40 | ## Rollup 41 | 42 | ```javascript 43 | import typescript from '@rollup/plugin-typescript'; 44 | 45 | export default { 46 | input: 'src/index.ts', 47 | output: { 48 | dir: 'build', 49 | format: 'esm' 50 | }, 51 | plugins: [ 52 | typescript({ 53 | typescript: require('tscm') 54 | }) 55 | ] 56 | }; 57 | ``` 58 | 59 | ## ts-node 60 | 61 | ```javascript 62 | ts-node --compiler tscm ./src/index.ts 63 | ``` 64 | 65 | ## Jest(ts-jest) 66 | 67 | ```javascript 68 | module.exports = { 69 | globals: { 70 | 'ts-jest': { 71 | compiler: 'tscm' 72 | } 73 | } 74 | }; 75 | ``` 76 | 77 | ## Babel plugin 78 | 79 | There is also a babel plugin, to support using the same macros without the TypeScript compiler. Useful if you run babel and TypeScript in parallel for faster compile times. 80 | 81 | The `compiler` parameter can be used for plugins to determine which compiler is used, and only generate types for TypeScript compiler. 82 | 83 | ```javascript 84 | const babel = require('@babel/core'); 85 | 86 | const output = babel.transformFile('./file.js', { 87 | plugins: ['typescript', 'tscm/babel'] 88 | }); 89 | ``` 90 | -------------------------------------------------------------------------------- /docs/How-it-works.md: -------------------------------------------------------------------------------- 1 | # How it works 2 | 3 | None of this is needed to use the library, it's purely for understanding how it works. 4 | 5 | ## TL;DR 6 | 7 | The TypeScript compiler is patched so that the source code is modified before being parsed by TypeScript. An [Language Server Plugin](https://github.com/microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin) is added that translates positions between the original source and the modified code. Source maps are modified on emit by merging the maps from compiling TypeScript and from transforming macros. 8 | 9 | ## Long version 10 | 11 | TypeScript is compiled to three files of interest, that each contain all of TypeScript, plus the specific parts for each file. They are: 12 | 13 | - `typescript` - programmatic TypeScript compiler 14 | - `tsc` - CLI compiler 15 | - `tsserver` - Language Server for editor/IDE support 16 | 17 | The files all export a single namespace, `ts`, which contains the methods that need to be changed. For the `tsserver` and `typescript` files it's enough to import the original file, patch, and re-export. This does not work for `tsc`, since, being a CLI, it self-executes when loaded, before it's possible to patch it. Therefore the file is instead read into a string, the executing call is removed so that the rest of the file can be executed, `ts` is patched, and then the removed line is executed to initialize the CLI. 18 | 19 | ### Patches 20 | 21 | #### Adding LSP 22 | 23 | _Only for `tsserver`_ 24 | 25 | A Language Server Plugin is added that translates source code positions to the position in the generated code, and the reverse for the response. 26 | 27 | #### `host.getSourceFile` 28 | 29 | Where the macro generation happens. Gets called with the source code, either from disk or from the editor in the case of `tsserver`, so that types and errors can be generated without saving the file. The source code is here parsed into an AST by Babel. Babel is used instead of TypeScript for several reasons: 30 | 31 | 1. Easier to use AST/more common 32 | 2. Faster 33 | 3. Enables using the same macros in Babel, minus the type-checking 34 | 35 | The AST is then traversed, macro sites identified, and the macro functions called. For performance reasons, and to avoid unneccesary changes to the source code, only the generated node is stringified and the call site is replaced directly in the source code, using [Magic String](https://github.com/Rich-Harris/magic-string). A source map of the changes is also generated, and any errors are added to a list for inclusion in TypeScript diagnostics. 36 | 37 | #### `host.writeFile` 38 | 39 | Merges the source map generated by TypeScript compilation with the one generated from the macro replacements. Supports both inline and external source maps. 40 | 41 | #### Diagnostics 42 | 43 | Methods for getting diagnostics generated by TypeScript are patched for two reasons: 44 | 45 | 1. Filter out diagnostics that are from the macro replacements and may not be correct/useful 46 | 2. Mix in diagnostics generated from while generating macros 47 | -------------------------------------------------------------------------------- /docs/Writing-macros.md: -------------------------------------------------------------------------------- 1 | # Writing macros 2 | 3 | A macro is a function that receives a [Babel CallExpression Node](https://babeljs.io/docs/en/babel-types#callexpression) and returns a new Node. They can not change or access code elsewhere in the program. They can receive any TypeScript code that is valid in a function call, including type parameters, and can return any valid TypeScript code. 4 | 5 | ## Example 6 | 7 | A macro that takes two numbers, adds them together and returns the result. 8 | 9 | ```javascript 10 | const assert = require('assert'); 11 | const t = require('@babel/types'); 12 | 13 | /** 14 | * @type {import('tscm/macro').Macro} 15 | */ 16 | module.exports.add = function add({ node }) { 17 | assert.equal(node.arguments.length, 2, 'Expected two arguments'); 18 | const [firstNumber, secondNumber] = node.arguments; 19 | t.assertNumericLiteral(firstNumber); 20 | t.assertNumericLiteral(secondNumber); 21 | 22 | const sum = firstNumber.value + secondNumber.value; 23 | 24 | return t.numericLiteral(sum); 25 | }; 26 | ``` 27 | 28 | Use by importing and calling with two non-null-assertion operators. 29 | 30 | ```typescript 31 | import { add } from './add'; 32 | 33 | const sum = add!!(4, 3); 34 | ``` 35 | 36 | More macro examples can be found in the [tscm-examples](https://github.com/johanholmerin/tscm-examples) repository. 37 | 38 | ## Things to be aware of 39 | 40 | - Macros are not hygienic. If you use identifiers provided to the macro, they may clash with identifiers you create. Make sure to use unique names. 41 | - Since macros can't affect the rest of the program, they receive a `Node`, not a `NodePath` as receive by `@babel/traverse` visitors. 42 | - Macros are executed directly in node. Therefore they need to be in JavaScript, not TypeScript. You can of course compile them before using. 43 | - Macros have to be defined in a separate file and be imported 44 | 45 | ## Arguments 46 | 47 | Macros receive an object with `node`, `fileName`, and `compiler` properties. For details see [macro.d.ts](../macro.d.ts). 48 | 49 | ## Error reporting 50 | 51 | Issues are reported by throwing an Error with a helpful message, which will be displayed in the correct position. 52 | 53 | ## Async logic 54 | 55 | The TypeScript compiler is completely synchronous. Therefore macros can not include any asynchronous code. If you need to read a file, this means using the `fs.*Sync` APIs. There are solutions for running async code synchronously, using `child_process` or `worker_threads`, and libraries exist that make them easier to use(see below), but be aware that they may cause performance issues. If neccessary for generating types, make sure to only use them when the `typescript` compiler is used, not `babel`. 56 | 57 | ## Useful tools 58 | 59 | - [AST Builder](https://rajasegar.github.io/ast-builder/) - Generate builder 60 | calls from code. Make sure to select `babel` parser. 61 | - [AST Explorer](https://astexplorer.net/) - Make sure to select `@babel/parser` 62 | - [@babel/template](https://babeljs.io/docs/en/babel-template.html) - Create AST from string template 63 | - [do-sync](https://github.com/Zemnmez/do-sync) - Run async code synchronously usinc child_process. 64 | - [sync-threads](https://github.com/lambci/sync-threads) - Run async code synchronously using Workers. 65 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "node", 3 | "testPathIgnorePatterns": [ 4 | "__tests__/fixtures", 5 | "__tests__/utils", 6 | "__tests__/setup.js" 7 | ], 8 | "coveragePathIgnorePatterns": [ 9 | "/node_modules/", 10 | "__tests__/fixtures" 11 | ], 12 | "setupFilesAfterEnv": ["/__tests__/setup.js"], 13 | "collectCoverage": true 14 | } 15 | -------------------------------------------------------------------------------- /macro.d.ts: -------------------------------------------------------------------------------- 1 | import t from '@babel/types'; 2 | 3 | export type Compilers = 'babel' | 'typescript'; 4 | 5 | export interface MacroArgs { 6 | node: t.CallExpression; 7 | /** 8 | * Absolute file path 9 | */ 10 | fileName: string; 11 | /** 12 | * Can be used for not generating expensive types for babel 13 | */ 14 | compiler: Compilers; 15 | } 16 | 17 | export type Macro = ({ node, fileName }: MacroArgs) => t.Node; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tscm", 3 | "version": "0.1.3", 4 | "description": "TypeScript Compiler Macros", 5 | "author": "Johan Holmerin ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/johanholmerin/tscm.git" 10 | }, 11 | "main": "./typescript/lib/typescript.js", 12 | "typings": "./typescript/lib/typescript.d.ts", 13 | "bin": { 14 | "tscm": "./bin/tscm", 15 | "tsserverm": "./bin/tsserverm" 16 | }, 17 | "engines": { 18 | "node": ">=14" 19 | }, 20 | "dependencies": { 21 | "@babel/generator": "^7.15.4", 22 | "@babel/parser": "^7.15.4", 23 | "@babel/plugin-syntax-jsx": "^7.14.5", 24 | "@babel/plugin-syntax-typescript": "^7.14.5", 25 | "@babel/traverse": "^7.15.4", 26 | "@babel/types": "^7.15.6", 27 | "magic-string": "^0.25.7", 28 | "resolve": "^1.20.0", 29 | "source-map-js": "^0.6.2" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.15.5", 33 | "@babel/template": "^7.15.4", 34 | "@types/node": "^16.9.1", 35 | "@types/react": "^17.0.24", 36 | "eslint": "^7.32.0", 37 | "eslint-config-prettier": "^6.15.0", 38 | "eslint-plugin-import": "^2.22.1", 39 | "husky": "^4.3.8", 40 | "jest": "^27.2.1", 41 | "jest-file-snapshot": "^0.5.0", 42 | "lint-staged": "^10.5.4", 43 | "prettier": "^2.4.0", 44 | "typescript": "^4.4.3" 45 | }, 46 | "peerDependencies": { 47 | "typescript": "^4.0.0" 48 | }, 49 | "scripts": { 50 | "test": "jest", 51 | "lint": "eslint '**/*.js' --fix", 52 | "lint:check": "eslint '**/*.js'", 53 | "format": "prettier '**/*.{js,ts}' --write", 54 | "format:check": "prettier '**/*.{js,ts}' --check" 55 | }, 56 | "husky": { 57 | "hooks": { 58 | "pre-commit": "lint-staged" 59 | } 60 | }, 61 | "lint-staged": { 62 | "*.js": [ 63 | "npm run lint", 64 | "npm run format" 65 | ], 66 | "*.ts": [ 67 | "npm run format" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lsp.js: -------------------------------------------------------------------------------- 1 | const methods = require('./methods'); 2 | 3 | function create(info) { 4 | const proxy = { ...info.languageService }; 5 | 6 | for (const key in methods) { 7 | proxy[key] = methods[key].bind(info); 8 | } 9 | return proxy; 10 | } 11 | 12 | function init() { 13 | return { create }; 14 | } 15 | 16 | module.exports = init; 17 | -------------------------------------------------------------------------------- /src/mappers.js: -------------------------------------------------------------------------------- 1 | const { translatePosBackward } = require('../typescript/lib/mapping/position'); 2 | const { translateLocBackward } = require('./utils'); 3 | 4 | function QuickInfo(map, result) { 5 | return { 6 | ...result, 7 | textSpan: translateLocBackward(map, result.textSpan) 8 | }; 9 | } 10 | 11 | function RenameInfo(map, result) { 12 | if (!result.canRename) return result; 13 | 14 | return { 15 | ...result, 16 | triggerSpan: translateLocBackward(map, result.triggerSpan) 17 | }; 18 | } 19 | 20 | function DiagnosticList(map, result) { 21 | return result.map((loc) => { 22 | return { 23 | ...loc, 24 | ...translateLocBackward(map, loc) 25 | }; 26 | }); 27 | } 28 | 29 | function DocumentSpanList(map, result) { 30 | return result.map((loc) => { 31 | const newLoc = { 32 | ...loc, 33 | textSpan: translateLocBackward(map, loc.textSpan) 34 | }; 35 | if (loc.contextSpan) { 36 | newLoc.contextSpan = translateLocBackward(map, loc.contextSpan); 37 | } 38 | if (loc.originalTextSpan) { 39 | newLoc.originalTextSpan = translateLocBackward(map, loc.originalTextSpan); 40 | } 41 | 42 | return newLoc; 43 | }); 44 | } 45 | 46 | function DefinitionInfoAndBoundSpan(map, result) { 47 | const definitions = 48 | result.definitions && DocumentSpanList(map, result.definitions); 49 | 50 | return { 51 | ...result, 52 | definitions, 53 | textSpan: translateLocBackward(map, result.textSpan) 54 | }; 55 | } 56 | 57 | function Classifications(map, result) { 58 | return { 59 | ...result, 60 | spans: result.spans.map((span) => translatePosBackward(map, span)) 61 | }; 62 | } 63 | 64 | module.exports = { 65 | QuickInfo, 66 | RenameInfo, 67 | DiagnosticList, 68 | DocumentSpanList, 69 | DefinitionInfoAndBoundSpan, 70 | Classifications 71 | }; 72 | -------------------------------------------------------------------------------- /src/methods.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const { sources } = require('../typescript/lib/shared'); 3 | const { createMapper } = require('./utils'); 4 | const mappers = require('./mappers'); 5 | 6 | const getQuickInfoAtPosition = createMapper( 7 | 'getQuickInfoAtPosition', 8 | mappers.QuickInfo 9 | ); 10 | const getRenameInfo = createMapper('getRenameInfo', mappers.RenameInfo); 11 | const findRenameLocations = createMapper( 12 | 'findRenameLocations', 13 | mappers.DocumentSpanList 14 | ); 15 | const getSemanticDiagnostics = createMapper( 16 | 'getSemanticDiagnostics', 17 | mappers.DiagnosticList 18 | ); 19 | const getSyntacticDiagnostics = createMapper( 20 | 'getSyntacticDiagnostics', 21 | mappers.DiagnosticList 22 | ); 23 | const getSuggestionDiagnostics = createMapper( 24 | 'getSuggestionDiagnostics', 25 | mappers.DiagnosticList 26 | ); 27 | const getDefinitionAtPosition = createMapper( 28 | 'getDefinitionAtPosition', 29 | mappers.DocumentSpanList 30 | ); 31 | const getTypeDefinitionAtPosition = createMapper( 32 | 'getTypeDefinitionAtPosition', 33 | mappers.DocumentSpanList 34 | ); 35 | const getImplementationAtPosition = createMapper( 36 | 'getImplementationAtPosition', 37 | mappers.DocumentSpanList 38 | ); 39 | const getDefinitionAndBoundSpan = createMapper( 40 | 'getDefinitionAndBoundSpan', 41 | mappers.DefinitionInfoAndBoundSpan 42 | ); 43 | const getEncodedSemanticClassifications = createMapper( 44 | 'getEncodedSemanticClassifications', 45 | mappers.Classifications 46 | ); 47 | 48 | function toLineColumnOffset(fileName, position) { 49 | if (!sources.has(fileName)) { 50 | return this.languageService.toLineColumnOffset( 51 | fileName, 52 | position, 53 | position 54 | ); 55 | } 56 | 57 | if (position < 0) { 58 | return { line: -1, character: -1 }; 59 | } 60 | 61 | // Same logic as built-in, but based on source text 62 | const text = sources.get(fileName); 63 | const lineStarts = ts.getLineStarts({ text }); 64 | return ts.computeLineAndCharacterOfPosition(lineStarts, position); 65 | } 66 | 67 | module.exports = { 68 | getQuickInfoAtPosition, 69 | getRenameInfo, 70 | getSemanticDiagnostics, 71 | getSyntacticDiagnostics, 72 | getSuggestionDiagnostics, 73 | findRenameLocations, 74 | getDefinitionAtPosition, 75 | getTypeDefinitionAtPosition, 76 | getImplementationAtPosition, 77 | getDefinitionAndBoundSpan, 78 | getEncodedSemanticClassifications, 79 | toLineColumnOffset 80 | }; 81 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const { mappings } = require('../typescript/lib/shared'); 3 | const { 4 | translatePosBackward, 5 | translatePosForward 6 | } = require('../typescript/lib/mapping/position'); 7 | 8 | function log(info, ...message) { 9 | info.project.projectService.logger.info(`[tscm] ${util.format(...message)}`); 10 | } 11 | 12 | function translateLocBackward(map, { start, length }) { 13 | return { 14 | start: translatePosBackward(map, start), 15 | length 16 | }; 17 | } 18 | 19 | function createMapper(name, resultMapper) { 20 | return function mapper(fileName, position, ...rest) { 21 | if (!mappings.has(fileName)) { 22 | return this.languageService[name](fileName, position, ...rest); 23 | } 24 | 25 | const map = mappings.get(fileName); 26 | const newPosition = translatePosForward(map, position); 27 | const result = this.languageService[name](fileName, newPosition, ...rest); 28 | if (!result) return; 29 | 30 | return resultMapper.call(this, map, result); 31 | }; 32 | } 33 | 34 | module.exports = { log, translateLocBackward, createMapper }; 35 | -------------------------------------------------------------------------------- /typescript/lib/mapping/position.js: -------------------------------------------------------------------------------- 1 | const { sortReplacements } = require('../utils'); 2 | 3 | function translatePosForward(replacements, pos) { 4 | const sorted = sortReplacements(replacements); 5 | 6 | let newPos = pos; 7 | 8 | for (const [start, end, value] of sorted) { 9 | if (start <= pos) { 10 | // Inside replacement, bail 11 | if (end > pos) { 12 | return -1; 13 | } 14 | 15 | newPos += start + value.length - end; 16 | } 17 | } 18 | 19 | return newPos; 20 | } 21 | 22 | function translatePosBackward(replacements, pos) { 23 | const sorted = sortReplacements(replacements); 24 | 25 | let newPos = pos; 26 | 27 | for (const [start, end, value] of sorted) { 28 | if (start < newPos) { 29 | newPos -= start + value.length - end; 30 | } 31 | } 32 | 33 | return newPos; 34 | } 35 | 36 | module.exports = { 37 | translatePosBackward, 38 | translatePosForward 39 | }; 40 | -------------------------------------------------------------------------------- /typescript/lib/mapping/source-map.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { SourceMapConsumer, SourceMapGenerator } = require('source-map-js'); 3 | const { sourcemaps } = require('../shared'); 4 | 5 | const INLINE_MAP_PREFIX = '//# sourceMappingURL=data:application/json;base64,'; 6 | 7 | function fromB64(string) { 8 | return Buffer.from(string, 'base64').toString(); 9 | } 10 | 11 | function toB64(string) { 12 | return Buffer.from(string, 'utf8').toString('base64'); 13 | } 14 | 15 | function isSourceMap(fileName) { 16 | return fileName.endsWith('.map'); 17 | } 18 | 19 | function parseMap(string) { 20 | try { 21 | return JSON.parse(string); 22 | } catch { 23 | return; 24 | } 25 | } 26 | 27 | function translateSourceMap(fileName, map) { 28 | const newMap = SourceMapGenerator.fromSourceMap(new SourceMapConsumer(map)); 29 | 30 | map.sources.forEach((source) => { 31 | const { dir } = path.parse(fileName); 32 | const resolvedPath = path.resolve(dir, source); 33 | const sourcemap = sourcemaps.get(resolvedPath); 34 | if (!sourcemap) return; 35 | 36 | newMap.applySourceMap(new SourceMapConsumer(sourcemap), source, dir); 37 | }); 38 | 39 | return newMap.toString(); 40 | } 41 | 42 | function mapInlineSourceMap(fileName, content) { 43 | const lines = content.split('\n'); 44 | const lastLine = lines.pop(); 45 | 46 | if (!lastLine.startsWith(INLINE_MAP_PREFIX)) return content; 47 | 48 | const mapString = fromB64(lastLine.slice(INLINE_MAP_PREFIX.length)); 49 | const map = parseMap(mapString); 50 | if (!map) return content; 51 | 52 | const newMapString = translateSourceMap(fileName, map); 53 | const newLastLine = INLINE_MAP_PREFIX + toB64(newMapString); 54 | 55 | return [...lines, newLastLine].join('\n'); 56 | } 57 | 58 | function mapSourceMap(fileName, content) { 59 | if (isSourceMap(fileName)) { 60 | const jsonMap = parseMap(content); 61 | if (!jsonMap) return content; 62 | return translateSourceMap(fileName, jsonMap); 63 | } 64 | 65 | return mapInlineSourceMap(fileName, content); 66 | } 67 | 68 | module.exports = { mapSourceMap }; 69 | -------------------------------------------------------------------------------- /typescript/lib/modify-ts.js: -------------------------------------------------------------------------------- 1 | const { transformProgram } = require('./transform/program'); 2 | const { normalizeProgramOptions } = require('./utils'); 3 | 4 | module.exports = function modifyTS(ts) { 5 | const originalCreateProgram = ts.createProgram.bind(ts); 6 | 7 | ts.createProgram = function createProgram(...args) { 8 | const options = normalizeProgramOptions(...args); 9 | 10 | return transformProgram(options, ts, originalCreateProgram); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /typescript/lib/shared.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Map 3 | * @type {Map} 4 | */ 5 | const mappings = new Map(); 6 | 7 | /** 8 | * Map 9 | * @type {Map} 10 | */ 11 | const sourcemaps = new Map(); 12 | 13 | /** 14 | * Map 15 | * @type {Map} 16 | */ 17 | const sources = new Map(); 18 | 19 | /** 20 | * @type {Map} 21 | */ 22 | const diagnostics = new Map(); 23 | 24 | /** 25 | * Map of request/response sequence number to file name 26 | * @type {Map} 27 | */ 28 | const sequences = new Map(); 29 | 30 | module.exports = { mappings, sourcemaps, sources, diagnostics, sequences }; 31 | -------------------------------------------------------------------------------- /typescript/lib/transform/ast.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const generator = require('@babel/generator').default; 3 | const { default: traverse, Scope } = require('@babel/traverse'); 4 | const { mappings, diagnostics } = require('../shared'); 5 | const createVisitor = require('../../../babel/create'); 6 | 7 | // Prevent exception on colliding declarations 8 | Scope.prototype.checkBlockScopedCollisions = () => {}; 9 | 10 | function createError({ node, macroName, message, fileName, text }) { 11 | return ts.createFileDiagnostic( 12 | { 13 | fileName, 14 | text 15 | }, 16 | node.node.start, 17 | node.node.end - node.node.start, 18 | { 19 | key: 'macro_error', 20 | category: ts.DiagnosticCategory.Error, 21 | code: -1, 22 | message: `Macro ${macroName} error: ${message}`, 23 | reportsUnnecessary: {}, 24 | reportsDeprecated: {} 25 | } 26 | ); 27 | } 28 | 29 | function transformAst(ast, fileName, text) { 30 | const replacements = []; 31 | const errors = []; 32 | 33 | mappings.set(fileName, replacements); 34 | diagnostics.set(fileName, errors); 35 | 36 | function onReplace(nodePath, newNode) { 37 | if (newNode) { 38 | let newCodeString = generator(newNode).code; 39 | if (newCodeString.endsWith(';')) { 40 | newCodeString = newCodeString.slice(0, -1); 41 | } 42 | replacements.push([ 43 | nodePath.node.start, 44 | nodePath.node.end, 45 | newCodeString 46 | ]); 47 | nodePath.replaceWith(newNode); 48 | } else if (nodePath.isImportSpecifier()) { 49 | const list = nodePath.parentPath.get('specifiers'); 50 | const index = list.indexOf(nodePath); 51 | const next = list[index + 1]; 52 | const endPosition = next ? next.node.start - 1 : nodePath.node.end; 53 | 54 | replacements.push([ 55 | nodePath.node.start, 56 | endPosition, 57 | // Replacing with same length makes error messages better 58 | ''.padStart(endPosition - nodePath.node.start) 59 | ]); 60 | } else { 61 | replacements.push([ 62 | nodePath.node.start, 63 | nodePath.node.end, 64 | // Replacing with same length makes error messages better 65 | ''.padStart(nodePath.node.end - nodePath.node.start) 66 | ]); 67 | nodePath.remove(); 68 | } 69 | } 70 | 71 | function onError({ node, macroName, message }) { 72 | errors.push(createError({ node, macroName, message, fileName, text })); 73 | } 74 | 75 | traverse( 76 | ast, 77 | createVisitor({ onReplace, onError, fileName, compiler: 'typescript' }) 78 | ); 79 | } 80 | 81 | module.exports = { transformAst }; 82 | -------------------------------------------------------------------------------- /typescript/lib/transform/diagnostics.js: -------------------------------------------------------------------------------- 1 | const { diagnostics, mappings } = require('../shared'); 2 | const { translatePosBackward } = require('../mapping/position'); 3 | const { sortReplacements } = require('../utils'); 4 | 5 | function isInMacro(diag, map) { 6 | const sorted = sortReplacements(map); 7 | 8 | for (const [start, end] of sorted) { 9 | const diagEnd = translatePosBackward(map, diag.start + diag.length); 10 | if (diag.start >= start && diagEnd <= end) { 11 | return true; 12 | } 13 | } 14 | 15 | return false; 16 | } 17 | 18 | function getDiagnostics(sourceFile) { 19 | if (sourceFile) { 20 | return diagnostics.get(sourceFile.fileName) ?? []; 21 | } 22 | 23 | return Array.from(diagnostics.values()).flat(); 24 | } 25 | 26 | function patchDiagFunc(program, name, includeMacroDiag) { 27 | const originalFunc = program[name]; 28 | 29 | program[name] = function (sourceFile) { 30 | const diags = originalFunc.apply(this, arguments).filter((diag) => { 31 | const fileName = diag.file.originalFileName || diag.file.fileName; 32 | const map = mappings.get(fileName) ?? []; 33 | return !isInMacro(diag, map); 34 | }); 35 | 36 | if (includeMacroDiag) { 37 | diags.push(...getDiagnostics(sourceFile)); 38 | } 39 | 40 | return diags; 41 | }; 42 | } 43 | 44 | function patchProgramDiagnostics(program) { 45 | patchDiagFunc(program, 'getSuggestionDiagnostics', false); 46 | patchDiagFunc(program, 'getSyntacticDiagnostics', true); 47 | patchDiagFunc(program, 'getSemanticDiagnostics', true); 48 | } 49 | 50 | module.exports = { patchProgramDiagnostics }; 51 | -------------------------------------------------------------------------------- /typescript/lib/transform/host.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const babel = require('@babel/parser'); 3 | const { diagnostics } = require('../shared'); 4 | const { mapSourceMap } = require('../mapping/source-map'); 5 | const { transformAst } = require('./ast'); 6 | const { updateSourceFile } = require('./updated-source'); 7 | 8 | // Cache map from original to updated SourceFile 9 | const CACHE = new WeakMap(); 10 | 11 | function transform(text, fileName) { 12 | const ast = babel.parse(text, { 13 | errorRecovery: true, 14 | sourceType: 'module', 15 | plugins: ['typescript', 'jsx'] 16 | }); 17 | transformAst(ast, fileName, text); 18 | } 19 | 20 | function transformHost(host, rootNames) { 21 | const { getSourceFile, writeFile } = host; 22 | 23 | host.getSourceFile = function (fileName) { 24 | const sourceFile = getSourceFile.apply(this, arguments); 25 | if ( 26 | fileName.endsWith('.json') || 27 | fileName.endsWith('.d.ts') || 28 | !rootNames.includes(fileName) 29 | ) { 30 | return sourceFile; 31 | } 32 | 33 | if (CACHE.has(sourceFile)) { 34 | return CACHE.get(sourceFile); 35 | } 36 | 37 | // Report error and delegate to TS, which returns something instead of 38 | // crashing. Might lead to duplicate reporting, if TS gets the same error. 39 | try { 40 | transform(sourceFile.text, fileName); 41 | } catch (error) { 42 | const diag = ts.createFileDiagnostic(sourceFile, error.pos ?? 0, 0, { 43 | key: 'babel_error', 44 | category: ts.DiagnosticCategory.Error, 45 | code: -2, 46 | message: `Babel error: ${error.message}`, 47 | reportsUnnecessary: {}, 48 | reportsDeprecated: {} 49 | }); 50 | diagnostics.set(fileName, [diag]); 51 | return sourceFile; 52 | } 53 | 54 | const updatedSourceFile = updateSourceFile(sourceFile, ts, false); 55 | CACHE.set(sourceFile, updatedSourceFile); 56 | 57 | return updatedSourceFile; 58 | }; 59 | host.writeFile = function (fileName, data, ...rest) { 60 | const newData = mapSourceMap(fileName, data); 61 | 62 | return writeFile.call(this, fileName, newData, ...rest); 63 | }; 64 | 65 | return host; 66 | } 67 | 68 | module.exports = { transformHost }; 69 | -------------------------------------------------------------------------------- /typescript/lib/transform/program.js: -------------------------------------------------------------------------------- 1 | const { patchProgramDiagnostics } = require('./diagnostics'); 2 | const { transformHost } = require('./host'); 3 | 4 | function getHost({ host, options, rootNames }, ts) { 5 | const newHost = host ?? ts.createCompilerHost(options, true); 6 | 7 | return transformHost(newHost, rootNames); 8 | } 9 | 10 | function transformProgram(createProgramOptions, ts, originalCreateProgram) { 11 | const newHost = getHost(createProgramOptions, ts); 12 | const newProgram = originalCreateProgram({ 13 | ...createProgramOptions, 14 | host: newHost 15 | }); 16 | patchProgramDiagnostics(newProgram); 17 | 18 | return newProgram; 19 | } 20 | 21 | module.exports = { transformProgram }; 22 | -------------------------------------------------------------------------------- /typescript/lib/transform/updated-source.js: -------------------------------------------------------------------------------- 1 | const MagicString = require('magic-string'); 2 | const { mappings, sourcemaps, sources } = require('../shared'); 3 | const { generateDjb2Hash } = require('../utils'); 4 | 5 | function updateSourceFile(sourceFile, ts, includeContent) { 6 | const ms = new MagicString(sourceFile.text, { 7 | filename: sourceFile.fileName 8 | }); 9 | 10 | const replacements = mappings.get(sourceFile.fileName) ?? []; 11 | replacements.forEach(([start, end, value]) => { 12 | ms.overwrite(start, end, value); 13 | }); 14 | 15 | const updatedSourceFile = ts.createSourceFile( 16 | sourceFile.fileName, 17 | ms.toString(), 18 | sourceFile.languageVersion 19 | ); 20 | 21 | updatedSourceFile.version = generateDjb2Hash(updatedSourceFile.text); 22 | 23 | sources.set(sourceFile.fileName, sourceFile.text); 24 | sourcemaps.set( 25 | sourceFile.fileName, 26 | ms.generateMap({ 27 | includeContent, 28 | source: sourceFile.fileName, 29 | hires: true 30 | }) 31 | ); 32 | return updatedSourceFile; 33 | } 34 | 35 | module.exports = { updateSourceFile }; 36 | -------------------------------------------------------------------------------- /typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | // Replaces executing line in tsc with export 2 | 3 | const vm = require('vm'); 4 | const path = require('path'); 5 | const TSC_PATH = require.resolve('typescript/lib/tsc.js'); 6 | const tscFile = require('fs') 7 | .readFileSync(TSC_PATH, { encoding: 'utf8' }) 8 | .replace( 9 | 'ts.executeCommandLine(ts.sys, ts.noop, ts.sys.args)', 10 | 'module.exports = ts' 11 | ); 12 | const sandbox = { 13 | clearImmediate, 14 | clearInterval, 15 | clearTimeout, 16 | setImmediate, 17 | setInterval, 18 | setTimeout, 19 | global, 20 | process, 21 | module: {}, 22 | exports: {}, 23 | require, 24 | __filename: TSC_PATH, 25 | __dirname: path.dirname(TSC_PATH) 26 | }; 27 | const ts = vm.runInNewContext(tscFile, sandbox, { filename: TSC_PATH }); 28 | 29 | require('./modify-ts')(ts); 30 | 31 | ts.executeCommandLine(ts.sys, ts.noop, ts.sys.args); 32 | -------------------------------------------------------------------------------- /typescript/lib/tsserver.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript/lib/tsserver.js'); 2 | 3 | const LSP_PATH = require.resolve('../../src/lsp'); 4 | 5 | function addLSPluginOption(options) { 6 | const plugins = options.plugins || []; 7 | 8 | return { 9 | ...options, 10 | plugins: [ 11 | ...plugins, 12 | { 13 | name: LSP_PATH 14 | } 15 | ] 16 | }; 17 | } 18 | 19 | function addLSPlugin(ts) { 20 | const originalEnablePluginsWithOptions = 21 | ts.server.ConfiguredProject.prototype.enablePluginsWithOptions; 22 | 23 | ts.server.ConfiguredProject.prototype.enablePluginsWithOptions = 24 | function enablePluginsWithOptions(options, ...rest) { 25 | const newOptions = addLSPluginOption(options); 26 | 27 | return originalEnablePluginsWithOptions.call(this, newOptions, ...rest); 28 | }; 29 | } 30 | 31 | // TS does not allow paths in plugin name 32 | const { parsePackageName } = ts; 33 | ts.parsePackageName = (moduleName) => { 34 | if (moduleName === LSP_PATH) { 35 | return { packageName: moduleName, rest: '' }; 36 | } 37 | 38 | return parsePackageName(moduleName); 39 | }; 40 | addLSPlugin(ts); 41 | 42 | require('./modify-ts')(ts); 43 | -------------------------------------------------------------------------------- /typescript/lib/typescript.d.ts: -------------------------------------------------------------------------------- 1 | export * from 'typescript'; 2 | -------------------------------------------------------------------------------- /typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript/lib/typescript.js'); 2 | require('./modify-ts')(ts); 3 | module.exports = ts; 4 | -------------------------------------------------------------------------------- /typescript/lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * djb2 hashing algorithm 3 | * http://www.cse.yorku.ca/~oz/hash.html 4 | */ 5 | function generateDjb2Hash(data) { 6 | let acc = 5381; 7 | 8 | for (let i = 0; i < data.length; i++) { 9 | acc = (acc << 5) + acc + data.charCodeAt(i); 10 | } 11 | 12 | return acc.toString(); 13 | } 14 | 15 | function normalizeProgramOptions( 16 | rootNames, 17 | options, 18 | host, 19 | oldProgram, 20 | configFileParsingDiagnostics 21 | ) { 22 | if (Array.isArray(rootNames)) { 23 | return { 24 | rootNames, 25 | options, 26 | projectReferences: [], 27 | host, 28 | oldProgram, 29 | configFileParsingDiagnostics 30 | }; 31 | } 32 | 33 | return rootNames; 34 | } 35 | 36 | function sortReplacements(replacements) { 37 | return Array.from(replacements) 38 | .sort(([a], [b]) => a - b) 39 | .filter((replacement) => { 40 | // Removes nested positions 41 | const wrapping = replacements.find( 42 | ([start, end]) => replacement[0] > start && replacement[1] < end 43 | ); 44 | return !wrapping; 45 | }); 46 | } 47 | 48 | module.exports = { 49 | generateDjb2Hash, 50 | normalizeProgramOptions, 51 | sortReplacements 52 | }; 53 | -------------------------------------------------------------------------------- /typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.4.3", 3 | "_": "coc-tsserver & VS Code uses this version for feature support indication" 4 | } 5 | --------------------------------------------------------------------------------