├── tsconfig.json ├── tsup.config.ts ├── package.json ├── LICENSE ├── src ├── templates.ts ├── file-utils.ts ├── ast-edit.ts ├── cli-commands.ts └── cli.ts ├── .gitignore └── README.md /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "moduleResolution": "Node", 5 | "plugins": [ 6 | { 7 | "name": "patch-ts", 8 | "test": "asdf" 9 | } 10 | ], 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/cli.ts"], 5 | format: ["cjs", "esm"], // Build for commonJS and ESmodules 6 | dts: true, // Generate declaration file (.d.ts) 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patch-ts", 3 | "version": "1.0.0", 4 | "main": "cli.js", 5 | "scripts": { 6 | "build": "tsup" 7 | }, 8 | "bin": { 9 | "patch-ts": "./dist/cli.js" 10 | }, 11 | "keywords": [], 12 | "author": "Christian Woerz", 13 | "license": "MIT", 14 | "description": "Patch TypeScript to allow to define your own intrinsic types", 15 | "dependencies": { 16 | "commander": "^12.1.0", 17 | "ts-morph": "^24.0.0", 18 | "which": "^5.0.0" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^22.7.5", 22 | "@types/which": "^3.0.4", 23 | "esbuild": "^0.24.0", 24 | "tsup": "^8.3.0", 25 | "typescript": "^5.6.3" 26 | }, 27 | "files": ["dist"] 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Typed Rocks 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 | -------------------------------------------------------------------------------- /src/templates.ts: -------------------------------------------------------------------------------- 1 | 2 | export const TYPE_ALIAS_INSTANTIATION = ` 3 | if(typeKind > 4) { 4 | const typeValues = typeArguments?.map(t => t.value) || []; 5 | const intrinsicInfos = loaded[symbol.escapedName]; 6 | const result = intrinsicInfos.fn(...typeValues); 7 | if(result === 'NEVER') { 8 | return neverType; 9 | } 10 | if(intrinsicInfos.type === 'boolean') { 11 | return result ? trueType : falseType; 12 | } else if(intrinsicInfos.type === 'number') { 13 | return getNumberLiteralType(result); 14 | } else { 15 | return getStringLiteralType(result + ''); 16 | } 17 | }`; 18 | 19 | export const CHECK_TYPE_ALIAS = ` 20 | if(loaded[node.name.escapedText]?.fn.length !== typeParameterCount) { 21 | error2(node.type, Diagnostics.The_intrinsic_keyword_can_only_be_used_to_declare_compiler_provided_intrinsic_types); 22 | }`; 23 | 24 | export const LOAD_TO_ADD_TEMPLATE = (env: string | undefined, bundledPath: string | undefined) => ` 25 | var myFs = require('fs'); 26 | var myPath = require('path'); 27 | 28 | var loaded; 29 | 30 | var existingIntrinsic = { 31 | Uppercase: 0, 32 | Lowercase: 1, 33 | Capitalize: 2, 34 | Uncapitalize: 3, 35 | NoInfer: 4 36 | }; 37 | 38 | function getExtension(h) { 39 | const bundledFile = ${bundledPath ? '"' + bundledPath + '"' : "undefined"}; 40 | 41 | const pathToFile = bundledFile ?? process.env["${env}"]; 42 | if(!myFs.existsSync(pathToFile)) { 43 | return {}; 44 | } 45 | const readFile = myFs.readFileSync(pathToFile, 'utf-8'); 46 | return eval(readFile); 47 | }`; 48 | 49 | export function initLoaded(argName: string) { 50 | return ` 51 | if (!loaded) { 52 | loaded = getExtension(${argName}); 53 | } 54 | const addedIntrinsics = Object.keys(loaded).reduce((acc, k, i) => ({ 55 | ...acc, 56 | [k]: i + Object.keys(existingIntrinsic).length 57 | }), {}); 58 | const intrinsicTypeKinds = new Map(Object.entries({ 59 | ...existingIntrinsic, 60 | ...addedIntrinsics 61 | }));`; 62 | } 63 | -------------------------------------------------------------------------------- /src/file-utils.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as fsSync from "fs"; 3 | import * as fsAsync from "fs/promises"; 4 | import * as which from "which"; 5 | 6 | type Email = intrinsic; 7 | // ^? 8 | 9 | type U = Email<'b'>; 10 | // ^? 11 | 12 | export function exitOnAlreadyDone(libPath: string, force: boolean) { 13 | const alreadyDonePath = path.resolve(libPath, ".enhanced-init"); 14 | if (force) { 15 | return false; 16 | } 17 | if (fsSync.existsSync(alreadyDonePath)) { 18 | console.log("Already patched the lib in " + libPath); 19 | process.exit(0); 20 | } 21 | } 22 | 23 | export function existsFilesAt(p: string | undefined): p is string { 24 | if(!p) { 25 | console.log("No path to the lib was found"); 26 | return false; 27 | } 28 | let allExist = true; 29 | if(!fsSync.existsSync(path.resolve(p, "typescript.js"))) { 30 | allExist = false; 31 | console.log("typescript.js does not exist at: " + p); 32 | } 33 | if(!fsSync.existsSync(path.resolve(p, "tsc.js"))) { 34 | allExist = false; 35 | console.log("tsc.js does not exist at: " + p); 36 | } 37 | return allExist; 38 | } 39 | 40 | export function copyToLib(lib: string, bundlePath: string): string | undefined { 41 | if(!fsSync.existsSync(bundlePath) || fsSync.statSync(bundlePath).isDirectory()) { 42 | console.log("File at path does not exist: " + bundlePath); 43 | return undefined; 44 | } 45 | const file = fsSync.readFileSync(bundlePath, 'utf-8'); 46 | const target = path.resolve(lib, "bundled_intrinsics.js"); 47 | fsSync.writeFileSync(target, file); 48 | return target; 49 | } 50 | 51 | function copyLogged(from: string, to: string) { 52 | const fromLog = path.basename(from); 53 | const toLog = path.basename(to); 54 | console.log(`\u{2795} copying ${fromLog} to ${toLog}`); 55 | const copied = fsAsync.copyFile(from, to); 56 | console.log(`\u{2705} copied ${fromLog} to ${toLog}`); 57 | return copied; 58 | } 59 | export function createBackupOrRestores(normals: string[]): Promise { 60 | const allFilePromises = normals.map((normal) => { 61 | const backup = normal + ".bak"; 62 | if (fsSync.existsSync(backup)) { 63 | return copyLogged(backup, normal); 64 | } else if (fsSync.existsSync(normal)) { 65 | return copyLogged(normal, backup); 66 | } 67 | console.log(`neither ${backup} or ${normal} was found. Exiting`); 68 | process.exit(1); 69 | }); 70 | 71 | return Promise.all(allFilePromises); 72 | } 73 | 74 | export function findCurrentTscPath(): string | undefined { 75 | const binPathString = which.sync("tsc"); 76 | return binPathString ? path.resolve(path.dirname(binPathString), "..", "lib", "node_modules", "typescript", "lib") : undefined; 77 | } 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node template 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | .idea 55 | .vscode 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional stylelint cache 60 | .stylelintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | dist 77 | # dotenv environment variable files 78 | .env 79 | .env.development.local 80 | .env.test.local 81 | .env.production.local 82 | .env.local 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | out 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | 95 | # Gatsby files 96 | .cache/ 97 | # Comment in the public line in if your project uses Gatsby and not Next.js 98 | # https://nextjs.org/blog/next-9-1#public-directory-support 99 | # public 100 | 101 | # vuepress build output 102 | .vuepress/dist 103 | 104 | # vuepress v2.x temp and cache directory 105 | .temp 106 | .cache 107 | 108 | # Docusaurus cache and generated files 109 | .docusaurus 110 | 111 | # Serverless directories 112 | .serverless/ 113 | 114 | # FuseBox cache 115 | .fusebox/ 116 | 117 | # DynamoDB Local files 118 | .dynamodb/ 119 | 120 | # TernJS port file 121 | .tern-port 122 | 123 | # Stores VSCode versions used for testing VSCode extensions 124 | .vscode-test 125 | 126 | # yarn v2 127 | .yarn/cache 128 | .yarn/unplugged 129 | .yarn/build-state.yml 130 | .yarn/install-state.gz 131 | .pnp.* 132 | 133 | -------------------------------------------------------------------------------- /src/ast-edit.ts: -------------------------------------------------------------------------------- 1 | import { Node, FunctionDeclaration, SyntaxKind, Project, ScriptTarget, SourceFile } from "ts-morph"; 2 | import { 3 | CHECK_TYPE_ALIAS, 4 | initLoaded, 5 | LOAD_TO_ADD_TEMPLATE, 6 | TYPE_ALIAS_INSTANTIATION, 7 | } from "./templates"; 8 | import * as path from "path"; 9 | 10 | export function getFunctionsByNameInOrder(names: string[], node: Node): FunctionDeclaration[] { 11 | const results: (FunctionDeclaration | undefined)[] = [...names].map(_ => undefined); 12 | 13 | for (const el of node.getDescendantsOfKind(SyntaxKind.FunctionDeclaration)) { 14 | const foundIndex = names.findIndex((name) => el.getName() === name); 15 | if (foundIndex > -1) { 16 | results[foundIndex] = el; 17 | if (!results.includes(undefined)) { 18 | return results as FunctionDeclaration[]; 19 | } 20 | } 21 | } 22 | 23 | throw new Error(); 24 | } 25 | 26 | export function addFirstInIf(fn: FunctionDeclaration, addCode: string) { 27 | if (fn) { 28 | const intrinsicCheckIf = fn.getFirstDescendantByKind(SyntaxKind.IfStatement); 29 | if(intrinsicCheckIf) { 30 | const innerIf = intrinsicCheckIf.getFirstDescendantByKind(SyntaxKind.IfStatement); 31 | innerIf?.replaceWithText(`${addCode}\n${innerIf.getText()}`); 32 | } 33 | } 34 | } 35 | 36 | export function appendAfterNode(node: Node, code: string) { 37 | node?.replaceWithText(`${node.getText()}\n${code}`); 38 | } 39 | 40 | export function initProjectWithFiles(filePaths: string[]): SourceFile[] { 41 | const project = new Project({ 42 | compilerOptions: { 43 | target: ScriptTarget.ESNext, 44 | }, 45 | }); 46 | return filePaths.map((fp) => project.addSourceFileAtPath(fp)); 47 | } 48 | 49 | export async function patchFile(file: SourceFile, startNode: Node, envPath: string | undefined, bundledPath: string | undefined) { 50 | const fileLog = path.basename(file.getFilePath()); 51 | const suffix = bundledPath ? "with fixed value from " + file.getFilePath() : "with dynamic value from " + file.getFilePath(); 52 | console.log(`\u{1F58A} Patching ${fileLog} ${suffix}`); 53 | const [isInstantiatedModule, createTypeChecker] = getFunctionsByNameInOrder( 54 | ["isInstantiatedModule", "createTypeChecker"], 55 | startNode 56 | ); 57 | 58 | createTypeChecker?.insertStatements(0, initLoaded(createTypeChecker.getParameters().at(0)?.getText()!)); 59 | 60 | appendAfterNode(isInstantiatedModule, LOAD_TO_ADD_TEMPLATE(envPath, bundledPath)); 61 | 62 | const [getTypeAliasInstantiation, checkTypeAliasDeclaration] = getFunctionsByNameInOrder( 63 | ["getTypeAliasInstantiation", "checkTypeAliasDeclaration"], 64 | createTypeChecker 65 | ); 66 | addFirstInIf(getTypeAliasInstantiation, TYPE_ALIAS_INSTANTIATION); 67 | addFirstInIf(checkTypeAliasDeclaration, CHECK_TYPE_ALIAS); 68 | await file.save(); 69 | console.log(`\u{2705} Done Patching ${fileLog}`); 70 | } 71 | -------------------------------------------------------------------------------- /src/cli-commands.ts: -------------------------------------------------------------------------------- 1 | import * as fsSync from "fs"; 2 | import * as path from "path"; 3 | 4 | import { createBackupOrRestores } from "./file-utils"; 5 | import { initProjectWithFiles, patchFile } from "./ast-edit"; 6 | 7 | export async function init(libPath: string) { 8 | const typescriptFilePath = path.resolve(libPath, "typescript.js"); 9 | const tscFilePath = path.resolve(libPath, "tsc.js"); 10 | const filePaths = [typescriptFilePath, tscFilePath]; 11 | await createBackupOrRestores(filePaths); 12 | return filePaths; 13 | } 14 | 15 | export async function patchIntrinsics( 16 | filePaths: string[], 17 | libPath: string, 18 | envPath: string | undefined, 19 | bundled: string | undefined 20 | ) { 21 | const [typescriptFile, tscFile] = initProjectWithFiles(filePaths); 22 | 23 | const typescriptFileStartNode = typescriptFile.getChildren().at(0)!.getChildren().at(2)!; 24 | 25 | const patchTypescript = patchFile(typescriptFile, typescriptFileStartNode, envPath, bundled); 26 | const patchTsc = patchFile(tscFile, tscFile, envPath, bundled); 27 | await Promise.all([patchTypescript, patchTsc]); 28 | fsSync.writeFileSync(libPath + "/.enhanced-init", "initialized"); 29 | } 30 | 31 | export async function createDts(filePath: string, outputPath: string) { 32 | if (!fsSync.existsSync(filePath) || fsSync.statSync(filePath).isDirectory()) { 33 | console.log("Path " + filePath + " is not a file"); 34 | return undefined; 35 | } 36 | 37 | try { 38 | const obj = eval(fsSync.readFileSync(filePath, 'utf-8')); 39 | const isValid = validateObj(obj); 40 | if (isValid) { 41 | createDtsFile(obj, outputPath); 42 | } 43 | } catch (e) { 44 | console.log(e); 45 | console.log("Could not read object from path " + filePath); 46 | } 47 | } 48 | 49 | function createDtsFile(obj: object, outputPath: string) { 50 | const abc = ["A", "B", "C", "D", "E", "F", "G", "H"]; 51 | const types = Object.entries(obj).map(([key, value]) => { 52 | const argumentLength = value.fn.length; 53 | 54 | if(argumentLength) { 55 | const args = abc.slice(0, argumentLength).join(", "); 56 | return `type ${key}<${args}> = intrinsic;` 57 | } 58 | }); 59 | const output = types.join("\n"); 60 | 61 | fsSync.writeFileSync(outputPath, output); 62 | console.log(`Wrote types to ${outputPath}`); 63 | } 64 | 65 | function validateObj(obj: object): boolean { 66 | let allOkay = true; 67 | Object.entries(obj).forEach(([key, value]) => { 68 | if (!value.fn) { 69 | console.log(`Key ${key} has no function property 'fn', but it needs one.`); 70 | allOkay = false; 71 | } 72 | if (typeof value?.fn !== "function") { 73 | console.log(`'fn' property of ${key} is not a function`); 74 | allOkay = false; 75 | } 76 | if (value.type && !["number", "boolean", "string"].includes(value.type)) { 77 | console.log(`Property 'type' in ${key} needs to be either "number", "string" or "boolean" if set.`); 78 | allOkay = false; 79 | } 80 | }); 81 | return allOkay; 82 | } 83 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Command } from "commander"; 4 | import { createDts, init, patchIntrinsics } from "./cli-commands"; 5 | import { copyToLib, existsFilesAt, exitOnAlreadyDone, findCurrentTscPath } from "./file-utils"; 6 | import * as path from "path"; 7 | const program = new Command(); 8 | 9 | program.name("patch-ts").description("CLI to enhance the typescript compiler").version("1.0.0"); 10 | 11 | program 12 | .command("create-dts") 13 | .description("Create the d.ts file from the input") 14 | .option('-i, --input ', "The intrinsics JavaScript file.") 15 | .option('-o, --output ', "The path where the d.ts file will be generated to.") 16 | .action((options: {input: string, output: string}) => { 17 | createDts(options.input, options.output); 18 | }) 19 | 20 | program 21 | .command("patch") 22 | .description("Patch the typescript.js and tsc.js file") 23 | .option("-f, --force", "Force the patch operation") 24 | .option("-b, --bundled ", "Bundles the file at the path into the TypeScript compiler. Will not update the types when the source file is changed.") 25 | .option("-ue, --useEnv ", "Reads the intrinsic-file path from the environment variable which is passed. Example: --useEnv PATCH_TS") 26 | .option("-l, --libPath ", "Path to the lib. Uses the current lib for tsc when not specified.") 27 | .action( 28 | async (options: { 29 | intrinsicsFileName: string; 30 | force: boolean; 31 | libPath: string; 32 | bundled: string; 33 | useEnv: string; 34 | }) => { 35 | const bundledPath = options.bundled; 36 | const libPath = options.libPath || findCurrentTscPath(); 37 | if (!existsFilesAt(libPath)) { 38 | return; 39 | } 40 | 41 | exitOnAlreadyDone(libPath, options.force); 42 | 43 | console.log("Path to lib which will be patched: " + libPath); 44 | if (options.useEnv) { 45 | handleUseEnv(libPath, options.useEnv); 46 | } else if(bundledPath) { 47 | handleBundled(libPath, bundledPath); 48 | } 49 | } 50 | ); 51 | 52 | async function handleBundled(libPath: string, bundledPath: string) { 53 | const targetPath = copyToLib(libPath, bundledPath); 54 | if(!targetPath) { 55 | console.log("Can't read JavaScript file from " + bundledPath); 56 | return; 57 | } 58 | const initializedTsFiles = await init(libPath); 59 | 60 | await patchIntrinsics(initializedTsFiles, libPath, undefined, targetPath); 61 | } 62 | 63 | async function handleUseEnv(libPath: string, envName: string) { 64 | const processPath = process.env.PATCH_TS; 65 | if (!processPath) { 66 | console.log("Env for PATCH_TS is not set. Please set it first and RESTART VSCODE COMPLETELY."); 67 | return; 68 | } 69 | const initializedTsFiles = await init(libPath); 70 | await patchIntrinsics(initializedTsFiles, libPath, envName, undefined); 71 | } 72 | 73 | program 74 | .command("show-lib") 75 | .description("Get the current lib path") 76 | .action(() => { 77 | const libPath = findCurrentTscPath(); 78 | if (!existsFilesAt(libPath)) { 79 | return; 80 | } 81 | console.log(path.resolve(libPath, "..", "lib")); 82 | }); 83 | 84 | program 85 | .command("revert") 86 | .description("Revert changes made to typescript.js and tsc.js files") 87 | .action(async () => { 88 | const libPath = findCurrentTscPath(); 89 | if (!existsFilesAt(libPath)) { 90 | return; 91 | } 92 | console.log("path to lib: " + libPath); 93 | await init(libPath); 94 | }); 95 | 96 | program.parse(process.argv); 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Patch any TypeScript Compiler to add support of custom intrinsic types 2 | 3 | `patch-ts` is a command-line interface (CLI) designed to enhance the TypeScript compiler by patching its the `checker.ts` file which creates the types. All changes can be found in the `templates.ts` file. 4 | 5 | Input: 6 | 7 | ```javascript 8 | module.exports = { 9 | Email: { 10 | fn: (str) => str.includes("@") ? str : "NEVER", 11 | type: "boolean", 12 | }, 13 | }; 14 | ``` 15 | Output: 16 | 17 | ```typescript 18 | 19 | type Email = intrinsic; 20 | type IsEmail = Email<'Hi'>; 21 | // ^? false 22 | 23 | ``` 24 | 25 | 26 | 27 | 28 | 29 | ## Features 30 | 31 | - **Patch TypeScript**: Apply modifications to enhance the TypeScript compiler with the ability to parse custom `intrinsic` types. 32 | - **Create `.d.ts` file from intrinsics** Use the intrinsics.js file to create the corresponding `.d.ts` files. 33 | - **Revert Changes**: Restore the original state of patched TypeScript files. 34 | 35 | ## Prerequisites 36 | 37 | - Node.js (LTS version recommended) 38 | - TypeScript installed globally or in your project 39 | 40 | ## Installation 41 | 42 | Install it using npm: 43 | 44 | ```bash 45 | npm i -g patch-ts 46 | ``` 47 | 48 | ## Usage 49 | 50 | 1. Figure out the path of the TypeScript version you are currently using. 51 | For Example: 52 | 53 | ```bash 54 | patch-ts show-lib 55 | > /home/wrz/node/v22.2.0/lib/node_modules/typescript/lib 56 | ``` 57 | 58 | This uses the currently installed `tsc` to check the library path. 59 | 60 | 2. Make sure that your IDE (VSCode for example) points to the same TypeScript compiler. Check it by running the command-palette command: 61 | 62 | ```bash 63 | Select TypeScript Version 64 | ``` 65 | 66 | 3. If the paths do not align with the one from `patch-ts lib` you need to align it by setting it in your settings json: 67 | 68 | ```json 69 | { 70 | "typescript.tsdk": "/home/wrz/node/v22.2.0/lib/node_modules/typescript/lib" 71 | } 72 | ``` 73 | 74 | 4. Use the [patch](#patch-command) command to patch your TypeScript installation with an intrinsics file by using the environment variable "PATCH_TS". First you need to set the environment variable for example in your `.bashrc` or `.zshrc`. 75 | 76 | ```bash 77 | export PATCH_TS=/path/to/your/intrinsics.js 78 | ``` 79 | 80 | And then patch it: 81 | 82 | ```bash 83 | patch-ts patch --useEnv PATCH_TS 84 | ``` 85 | 86 | 5. Now you can add your own [intrinsic types](#intrinsics-file-structure) to the `intrinsics.js` file. 87 | 88 | 6. **RESTART**: To apply the changes made to the `intrinsics.js` file you have to restart your ts-server using `Restart TS Server`. 89 | 90 | 7. If you want to, you can now also create the types from the intrinsics file: 91 | 92 | ```bash 93 | patch-ts create-dts -i ./intrinsics.js -o ./my.d.ts 94 | ``` 95 | 96 | ## Commands 97 | 98 | ### Patch Command 99 | 100 | To patch TypeScript compiler files: 101 | 102 | ```bash 103 | patch-ts patch [options] 104 | ``` 105 | 106 | #### Options 107 | 108 | - `-f, --force`: Force the patch operation, even if already patched. 109 | - `-l, --libPath `: Specify the path to the TypeScript library. By default it uses the current lib `tsc` points to. 110 | - `-ue, --useEnv`: If set, it will use the provided environment variable and the path behind it to determine the path to the `intrinsics.js` file. Needs a restart of VSCode to find the new environment variable. 111 | - `-b, --bundled`: The file which will be bundled into the compiler. Will not be updated but only the current version is copied into the compiler. 112 | 113 | ### Show Lib 114 | 115 | Use this command to get the lib which will get patched when calling `patch-ts`. 116 | 117 | ```bash 118 | patch-ts show-lib 119 | ``` 120 | 121 | ### Create Types File from intrinsics.js 122 | 123 | ```bash 124 | patch-ts create-dts -i /path/to/intrisincs.js -o ./myfile.d.ts 125 | ``` 126 | 127 | Will use the file from the input path to create the TypeScript types file. 128 | 129 | ### Revert Command 130 | 131 | To revert any changes made by the patch: 132 | 133 | ```bash 134 | patch-ts revert 135 | ``` 136 | 137 | This command will revert the TypeScript files (`typescript.js` and `tsc.js`) to their original state before any patches applied by `patch-ts`. 138 | 139 | ### Examples 140 | 141 | Patching with a custom lib path and using the PATCH_TS environment variable to figure out where the patch-file is. Will update when the file at PATCH_TS changes and the user restarts the ts server. Also forcing an update. 142 | 143 | ```bash 144 | patch-ts patch --useEnv PATCH_TS --path /Users/wrz/IdeaProjects/TypeScript/built/local --force 145 | ``` 146 | 147 | Bundle a intrinsicts file into the compiler. Is only bundled **ONCE**. 148 | 149 | ```bash 150 | patch-ts patch --bundled /path/to/custom-intrinsics.js 151 | ``` 152 | 153 | This command will use the `custom-intrinsics.js` file for patching the typescript compiler **ONCE**. It does not update when the file changes. 154 | 155 | ## intrinsics file structure 156 | 157 | The JavaScript file has to have a single `module.exports` which exports a function which looks like that: 158 | 159 | ```javascript 160 | module.exports = function () { 161 | return { 162 | Email: { 163 | fn: (str) => str.includes("@" ? str : "NEVER"), 164 | }, 165 | Length: { 166 | fn: (str) => str.length, 167 | type: "number", 168 | }, 169 | Add: { 170 | fn: (first, second) => first + second, 171 | type: "number", 172 | }, 173 | IsNumber: { 174 | fn: nr => nr === +nr, 175 | type: "boolean" 176 | } 177 | }; 178 | }; 179 | ``` 180 | 181 | The `fn` property is always needed. It maps the generics which we pass to the generic type and transform it. By default it will always return the result as a string representation. We can specify the `type` property as either `number`, `string` or `boolean` to transform the result into the corresponding type. 182 | 183 | When `"NEVER"` is returned, it will be converted to the `never` type. 184 | 185 | This will enable us to create these types: 186 | 187 | ```typescript 188 | type Email = intrinsic; 189 | type Length = intrinsic; 190 | type Add = intrinsic; 191 | 192 | type WrongEmail = Email<"ab">; 193 | // ^? never 194 | type CorrectEmail = Email<"a@b">; 195 | // ^? "a@b" 196 | type TextLength = Length<"hi">; 197 | // ^? 2 198 | type NumberCheck = IsNumber<1>; 199 | // ^? true 200 | ``` 201 | 202 | ## Contributing 203 | 204 | Contributions to `patch-ts` are welcome! Please read the contributing guidelines in the repository for more information on how to submit pull requests. 205 | 206 | ## License 207 | 208 | This project is licensed under the [MIT License](LICENSE). 209 | 210 | ## Support 211 | 212 | For support, submit an issue in the GitHub repository or contact the project maintainers. 213 | --------------------------------------------------------------------------------