├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jsonSchemas.xml ├── misc.xml ├── modules.xml ├── ts-patch.iml ├── vcs.xml └── webResources.xml ├── .npmignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── jest.config.ts ├── package.json ├── projects ├── core │ ├── plugin.ts │ ├── resolver-hook.js │ ├── shared │ │ └── plugin-types.ts │ ├── src │ │ ├── actions │ │ │ ├── check.ts │ │ │ ├── index.ts │ │ │ ├── install.ts │ │ │ ├── patch.ts │ │ │ ├── uninstall.ts │ │ │ └── unpatch.ts │ │ ├── bin │ │ │ ├── ts-patch.ts │ │ │ └── tspc.ts │ │ ├── cli │ │ │ ├── cli.ts │ │ │ ├── commands.ts │ │ │ ├── help-menu.ts │ │ │ └── options.ts │ │ ├── compiler │ │ │ ├── package.json │ │ │ ├── tsc.js │ │ │ ├── tsserver.js │ │ │ ├── tsserverlibrary.js │ │ │ └── typescript.js │ │ ├── config.ts │ │ ├── index.ts │ │ ├── module │ │ │ ├── get-live-module.ts │ │ │ ├── index.ts │ │ │ ├── module-file.ts │ │ │ ├── module-source.ts │ │ │ ├── source-section.ts │ │ │ └── ts-module.ts │ │ ├── options.ts │ │ ├── patch │ │ │ ├── get-patched-source.ts │ │ │ ├── patch-detail.ts │ │ │ ├── patch-module.ts │ │ │ └── transformers │ │ │ │ ├── add-original-create-program.ts │ │ │ │ ├── fix-ts-early-return.ts │ │ │ │ ├── hook-tsc-exec.ts │ │ │ │ ├── index.ts │ │ │ │ ├── merge-statements.ts │ │ │ │ ├── patch-create-program.ts │ │ │ │ └── patch-emitter.ts │ │ ├── slice │ │ │ ├── module-slice.ts │ │ │ ├── ts54.ts │ │ │ ├── ts55.ts │ │ │ └── ts552.ts │ │ ├── system │ │ │ ├── cache.ts │ │ │ ├── errors.ts │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ └── types.ts │ │ ├── ts-package.ts │ │ └── utils │ │ │ ├── file-utils.ts │ │ │ ├── find-cache-dir.ts │ │ │ ├── general.ts │ │ │ └── index.ts │ └── tsconfig.json └── patch │ ├── package.json │ ├── plugin.ts │ ├── src │ ├── plugin │ │ ├── esm-intercept.ts │ │ ├── plugin-creator.ts │ │ ├── plugin.ts │ │ └── register-plugin.ts │ ├── shared.ts │ ├── ts │ │ ├── create-program.ts │ │ └── shim.ts │ └── types │ │ ├── plugin-types.ts │ │ └── typescript.ts │ └── tsconfig.json ├── scripts └── postbuild.js ├── test ├── .yarnrc ├── assets │ └── projects │ │ ├── main │ │ ├── package.json │ │ ├── src │ │ │ └── index.ts │ │ └── tsconfig.json │ │ ├── package-config │ │ ├── plugin │ │ │ ├── package.json │ │ │ ├── plugin.js │ │ │ └── transformers │ │ │ │ ├── transformer1.js │ │ │ │ └── transformer2.js │ │ ├── run-transform.js │ │ ├── src │ │ │ └── index.ts │ │ └── tsconfig.json │ │ ├── path-mapping │ │ ├── base.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── a │ │ │ │ └── a.ts │ │ │ ├── b.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsconfig.plugin.json │ │ ├── transform │ │ ├── package.json │ │ ├── run-transform.js │ │ ├── src │ │ │ └── index.ts │ │ ├── transformers │ │ │ ├── cjs │ │ │ │ ├── js-plugin.cjs │ │ │ │ ├── package.json │ │ │ │ ├── plugin.cts │ │ │ │ └── tsconfig.json │ │ │ └── esm │ │ │ │ ├── js-plugin.mjs │ │ │ │ ├── package.json │ │ │ │ ├── plugin.mts │ │ │ │ ├── plugin.ts │ │ │ │ └── tsconfig.json │ │ ├── tsconfig.cjs.json │ │ ├── tsconfig.cts.json │ │ ├── tsconfig.json │ │ ├── tsconfig.mjs.json │ │ ├── tsconfig.mts.json │ │ └── tsconfig.ts.json │ │ └── webpack │ │ ├── esm-plugin.mjs │ │ ├── esm-plugin.mts │ │ ├── hide-module.js │ │ ├── package.json │ │ ├── plugin.ts │ │ ├── src │ │ └── index.ts │ │ ├── tsconfig.esm.json │ │ ├── tsconfig.esmts.json │ │ ├── tsconfig.json │ │ └── webpack.config.js ├── package.json ├── src │ ├── cleanup.ts │ ├── config.ts │ ├── perf.ts │ ├── prepare.ts │ ├── project.ts │ └── utils │ │ └── general.ts ├── tests │ ├── actions.test.ts │ ├── package-config.test.ts │ ├── path-mapping.test.ts │ ├── transformer.test.ts │ └── webpack.test.ts └── tsconfig.json ├── tsconfig.base.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set end of line to LF 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [nonara] 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - v3-beta 9 | - next 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | # NOTE - 21+ is failing due to odd bug - maybe bug in node - see https://github.com/nonara/ts-patch/issues/153 18 | node-version: [ 18.x, 20.x ] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Cache dependencies 30 | uses: actions/cache@v2 31 | with: 32 | path: | 33 | ~/.cache/yarn 34 | node_modules 35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-yarn- 38 | 39 | - name: Install Packages 40 | run: yarn install --frozen-lockfile 41 | 42 | - name: Build 43 | run: yarn build 44 | 45 | - name: Test 46 | run: yarn run test && yarn run perf 47 | env: 48 | CI: true 49 | FORCE_COLOR: 1 50 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - v*.*.* 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Node.js to publish to npmjs.org 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: '20.x' 21 | registry-url: 'https://registry.npmjs.org' 22 | 23 | - name: Install Packages 24 | run: yarn install --frozen-lockfile 25 | 26 | - name: Build 27 | run: yarn build 28 | 29 | - name: Test 30 | run: yarn run test 31 | env: 32 | CI: true 33 | FORCE_COLOR: 1 34 | 35 | - name: Generate Release Body 36 | run: npx extract-changelog-release > RELEASE_BODY.md 37 | 38 | - name: Publish to NPM 39 | run: yarn publish dist --non-interactive 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | 43 | - name: Create GitHub Release 44 | uses: ncipollo/release-action@v1 45 | with: 46 | bodyFile: "RELEASE_BODY.md" 47 | token: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Yarn 2 | **/yarn.lock 3 | !/yarn.lock 4 | 5 | # Built 6 | *.d.ts 7 | *.js.map 8 | generator.stats.json 9 | .nyc_output 10 | dist 11 | coverage 12 | package-lock.json 13 | *.tsbuildinfo 14 | generated/ 15 | 16 | # Extensions 17 | *.seed 18 | *.log 19 | *.csv 20 | *.dat 21 | *.out 22 | *.pid 23 | *.gz 24 | 25 | # Personal 26 | .env 27 | .vscode 28 | .idea/jsLibraryMappings.xml 29 | todo/ 30 | TODO.ts 31 | 32 | # Junk 33 | temp/ 34 | .DS_Store 35 | tmp 36 | node_modules 37 | 38 | 39 | ### JetBrains ### 40 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 41 | 42 | # User-specific stuff 43 | .idea/**/workspace.xml 44 | .idea/**/tasks.xml 45 | .idea/**/usage.statistics.xml 46 | .idea/**/dictionaries 47 | .idea/**/shelf 48 | 49 | # Generated files 50 | .idea/**/contentModel.xml 51 | 52 | # Sensitive or high-churn files 53 | .idea/**/dataSources/ 54 | .idea/**/dataSources.ids 55 | .idea/**/dataSources.local.xml 56 | .idea/**/sqlDataSources.xml 57 | .idea/**/dynamic.xml 58 | .idea/**/uiDesigner.xml 59 | .idea/**/dbnavigator.xml 60 | 61 | # Gradle 62 | .idea/**/gradle.xml 63 | .idea/**/libraries 64 | 65 | # Mongo Explorer plugin 66 | .idea/**/mongoSettings.xml 67 | 68 | # File-based project format 69 | *.iws 70 | 71 | # JIRA plugin 72 | atlassian-ide-plugin.xml 73 | 74 | # Cursive Clojure plugin 75 | .idea/replstate.xml 76 | 77 | # Crashlytics plugin (for Android Studio and IntelliJ) 78 | com_crashlytics_export_strings.xml 79 | crashlytics.properties 80 | crashlytics-build.properties 81 | fabric.properties 82 | 83 | # Editor-based Rest Client 84 | .idea/httpRequests 85 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 98 | 99 | -------------------------------------------------------------------------------- /.idea/jsonSchemas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/ts-patch.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/webResources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.tsbuildinfo 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Ron Spickenagel 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 4 | files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, 5 | copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom 6 | the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 11 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 13 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | import * as os from 'os'; 3 | 4 | const config: Config.InitialOptions = { 5 | testEnvironment: "node", 6 | preset: 'ts-jest', 7 | roots: [ '/test/tests' ], 8 | testRegex: '.*(test|spec)\\.tsx?$', 9 | moduleFileExtensions: [ 'ts', 'tsx', 'js', 'jsx', 'json', 'node' ], 10 | transform: { 11 | '^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: './test/tsconfig.json' }], 12 | }, 13 | modulePaths: [ "/node_modules" ], 14 | // coveragePathIgnorePatterns: [ 15 | // 'src/installer/lib/system/errors.ts$' 16 | // ], 17 | globalSetup: '/test/src/prepare.ts', 18 | globalTeardown: '/test/src/cleanup.ts', 19 | testTimeout: 10000, 20 | transformIgnorePatterns: [ 21 | '/node_modules/(?!(ts-transformer-keys|ts-transformer-enumerate|ts-nameof)/)' 22 | ], 23 | maxConcurrency: os.cpus().length 24 | } 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-patch", 3 | "version": "3.3.0", 4 | "description": "Patch typescript to support custom transformers in tsconfig.json", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "compile": "yarn run compile:core && yarn run compile:patch", 9 | "build": "yarn run clean && yarn run compile:patch && yarn run compile:core", 10 | "compile:core": "tsc -p projects/core", 11 | "compile:patch": "tsc -p projects/patch", 12 | "------------ ": "-------------", 13 | "clean": "npx -y rimraf -g dist coverage *.tsbuildinfo test/.tmp", 14 | "clean:global": "yarn run clean && npx -y rimraf -g ./**/node_modules ./**/yarn.lock", 15 | "reset": "yarn run clean:global && yarn install && yarn build", 16 | "------------ ": "-------------", 17 | "test": "jest", 18 | "perf": "cd test && yarn run perf", 19 | "------------": "-------------", 20 | "prepare": "ts-patch install -s && yarn prepare:test", 21 | "prepare:test": "cd test && yarn install", 22 | "postbuild": "node scripts/postbuild.js" 23 | }, 24 | "private": true, 25 | "keywords": [ 26 | "typescript", 27 | "transform", 28 | "transformer", 29 | "plugin", 30 | "config", 31 | "ast" 32 | ], 33 | "author": { 34 | "name": "Ron S.", 35 | "url": "http://twitter.com/ron" 36 | }, 37 | "license": "MIT", 38 | "repository": { 39 | "type": "git", 40 | "url": "git+ssh://git@github.com/nonara/ts-patch.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/nonara/ts-patch/issues" 44 | }, 45 | "homepage": "https://github.com/nonara/ts-patch#readme", 46 | "dependencies": { 47 | "chalk": "^4.1.2", 48 | "global-prefix": "^4.0.0", 49 | "minimist": "^1.2.8", 50 | "resolve": "^1.22.2", 51 | "semver": "^7.6.3", 52 | "strip-ansi": "^6.0.1" 53 | }, 54 | "bin": { 55 | "ts-patch": "./dist/bin/ts-patch.js", 56 | "tspc": "./dist/bin/tspc.js" 57 | }, 58 | "devDependencies": { 59 | "@types/esm": "^3.2.2", 60 | "@types/jest": "^29.5.10", 61 | "@types/minimist": "^1.2.2", 62 | "@types/mock-fs": "^4.13.1", 63 | "@types/node": "^16.11.5", 64 | "@types/resolve": "^1.20.1", 65 | "@types/semver": "^7.3.13", 66 | "@types/shelljs": "^0.8.9", 67 | "esm": "^3.2.25", 68 | "jest": "^29.7.0", 69 | "rimraf": "^5.0.7", 70 | "shelljs": "^0.8.5", 71 | "standard-version": "^9.5.0", 72 | "ts-jest": "^29.1.1", 73 | "ts-node": "^10.9.1", 74 | "ts-patch": "^3.3.0", 75 | "tsconfig-paths": "^4.2.0", 76 | "typescript": "5.7.2", 77 | "ts-next": "npm:typescript@beta", 78 | "ts-expose-internals": "npm:ts-expose-internals@5.4.5" 79 | }, 80 | "workspaces": { 81 | "packages": [ 82 | "projects/path", 83 | "projects/core" 84 | ], 85 | "nohoist": [ 86 | "jest", 87 | "ts-jest", 88 | "typescript" 89 | ] 90 | }, 91 | "directories": { 92 | "resources": "./dist/resources" 93 | }, 94 | "standard-version": { 95 | "types": [ 96 | { 97 | "type": "feat", 98 | "section": "Features" 99 | }, 100 | { 101 | "type": "feature", 102 | "section": "Features" 103 | }, 104 | { 105 | "type": "fix", 106 | "section": "Bug Fixes" 107 | }, 108 | { 109 | "type": "perf", 110 | "section": "Performance Improvements" 111 | }, 112 | { 113 | "type": "revert", 114 | "section": "Reverts" 115 | }, 116 | { 117 | "type": "docs", 118 | "section": "Documentation", 119 | "hidden": true 120 | }, 121 | { 122 | "type": "style", 123 | "section": "Styles", 124 | "hidden": true 125 | }, 126 | { 127 | "type": "chore", 128 | "section": "Miscellaneous Chores", 129 | "hidden": true 130 | }, 131 | { 132 | "type": "refactor", 133 | "section": "Code Refactoring", 134 | "hidden": true 135 | }, 136 | { 137 | "type": "test", 138 | "section": "Tests", 139 | "hidden": true 140 | }, 141 | { 142 | "type": "build", 143 | "section": "Build System", 144 | "hidden": true 145 | }, 146 | { 147 | "type": "ci", 148 | "section": "Continuous Integration", 149 | "hidden": true 150 | }, 151 | { 152 | "type": "change", 153 | "section": "Changes" 154 | } 155 | ] 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /projects/core/plugin.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | import * as path from 'path'; 3 | type TS = typeof ts; 4 | 5 | 6 | /* ****************************************************************************************************************** * 7 | * Program Transformer - Merge rootDirs 8 | * ****************************************************************************************************************** */ 9 | 10 | export default function transformProgram( 11 | program: ts.Program, 12 | host: ts.CompilerHost, 13 | opt: any, 14 | { ts }: { ts: TS } 15 | ) 16 | { 17 | host ??= ts.createCompilerHost(program.getCompilerOptions(), true); 18 | const rootDirs = program.getCompilerOptions().rootDirs ?? []; 19 | 20 | hookWriteRootDirsFilenames(); 21 | 22 | return ts.createProgram(program.getRootFileNames(), program.getCompilerOptions(), host, program); 23 | 24 | function hookWriteRootDirsFilenames() { 25 | // TODO - tsei 26 | const sourceDir = (program).getCommonSourceDirectory(); 27 | const outputDir = program.getCompilerOptions().outDir!; 28 | 29 | const originalWriteFile = host.writeFile; 30 | host.writeFile = (fileName: string, data: string, ...args: any[]) => { 31 | let srcPath = path.resolve(sourceDir, path.relative(outputDir, fileName)); 32 | 33 | for (const dir of rootDirs) { 34 | const relPath = path.relative(dir, srcPath); 35 | // TODO - tsei 36 | if (relPath.slice(0, 2) !== '..') fileName = (ts).normalizePath(path.resolve(outputDir, relPath)); 37 | } 38 | 39 | return (originalWriteFile)(fileName, data, ...args); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /projects/core/resolver-hook.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Module = require('module'); 3 | 4 | 5 | /* ****************************************************************************************************************** */ 6 | // region: Helpers 7 | /* ****************************************************************************************************************** */ 8 | 9 | /** 10 | * Enable rootDirs merge support for require (used with ts-node) 11 | */ 12 | function hookRequire() { 13 | if (rootDirs.length > 0) { 14 | const originalRequire = Module.prototype.require; 15 | 16 | Module.prototype.require = function (request) { 17 | if (!path.isAbsolute(request) && request.startsWith('.')) { 18 | const moduleDir = path.dirname(this.filename); 19 | const moduleRootDir = rootDirs.find(rootDir => moduleDir.startsWith(rootDir)); 20 | 21 | if (moduleRootDir) { 22 | const moduleRelativeFromRoot = path.relative(moduleRootDir, moduleDir); 23 | 24 | if (moduleRootDir) { 25 | for (const rootDir of rootDirs) { 26 | const possiblePath = path.join(rootDir, moduleRelativeFromRoot, request); 27 | 28 | let resolvedPath; 29 | try { 30 | resolvedPath = require.resolve(possiblePath); 31 | } catch (e) { 32 | continue; 33 | } 34 | 35 | return originalRequire.call(this, resolvedPath); 36 | } 37 | } 38 | } 39 | } 40 | 41 | return originalRequire.call(this, request); 42 | }; 43 | } 44 | } 45 | 46 | // endregion 47 | 48 | /* ****************************************************************************************************************** * 49 | * Entry 50 | * ****************************************************************************************************************** */ 51 | 52 | const tsConfig = require(path.join(__dirname, 'tsconfig.json')); 53 | const rootDirs = tsConfig.compilerOptions.rootDirs.map(rootDir => path.join(__dirname, rootDir)); 54 | 55 | hookRequire(); 56 | -------------------------------------------------------------------------------- /projects/core/shared/plugin-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTE: This file is used during the build process for patch as well 3 | */ 4 | // Note: Leave as import-star, since we don't ship built file with esModuleInterop 5 | import type * as ts from 'typescript'; 6 | 7 | 8 | /* ****************************************************************************************************************** */ 9 | // region: Plugin 10 | /* ****************************************************************************************************************** */ 11 | 12 | export interface PluginConfig { 13 | [x:string]: any 14 | 15 | /** 16 | * Language Server TypeScript Plugin name 17 | */ 18 | name?: string; 19 | 20 | /** 21 | * Path to transformer or transformer module name 22 | */ 23 | transform?: string; 24 | 25 | /** 26 | * Resolve Path Aliases? 27 | */ 28 | resolvePathAliases?: boolean; 29 | 30 | /** 31 | * tsconfig.json file (for transformer) 32 | */ 33 | tsConfig?: string; 34 | 35 | /** 36 | * The optional name of the exported transform plugin in the transform module. 37 | */ 38 | import?: string; 39 | 40 | /** 41 | * Is the transformer an ES Module 42 | */ 43 | isEsm?: boolean 44 | 45 | /** 46 | * Plugin entry point format type, default is program 47 | */ 48 | type?: 'ls' | 'program' | 'config' | 'checker' | 'raw' | 'compilerOptions'; 49 | 50 | /** 51 | * Apply transformer after internal TypeScript transformers 52 | */ 53 | after?: boolean; 54 | 55 | /** 56 | * Apply transformer on d.ts files 57 | */ 58 | afterDeclarations?: boolean; 59 | 60 | /** 61 | * Transform *Program* instance (alters during createProgram()) (`type`, `after`, & `afterDeclarations` settings will 62 | * not apply) Entry point must be (program: Program, host?: CompilerHost) => Program 63 | */ 64 | transformProgram?: boolean; 65 | } 66 | 67 | export type TransformerList = Required; 68 | export type TransformerPlugin = TransformerBasePlugin | TsTransformerFactory; 69 | export type TsTransformerFactory = ts.TransformerFactory 70 | 71 | export type PluginFactory = 72 | LSPattern | ProgramPattern | ConfigPattern | CompilerOptionsPattern | TypeCheckerPattern | RawPattern; 73 | 74 | export interface TransformerBasePlugin { 75 | before?: ts.TransformerFactory | ts.TransformerFactory[]; 76 | after?: ts.TransformerFactory | ts.TransformerFactory[]; 77 | afterDeclarations?: ts.TransformerFactory | ts.TransformerFactory[]; 78 | } 79 | 80 | // endregion 81 | 82 | 83 | /* ****************************************************************************************************************** */ 84 | // region: Extras 85 | /* ****************************************************************************************************************** */ 86 | 87 | export type DiagnosticMap = WeakMap; 88 | 89 | export type TransformerExtras = { 90 | /** 91 | * Originating TypeScript instance 92 | */ 93 | ts: typeof ts; 94 | /** 95 | * TypeScript library file event was triggered in (ie. 'tsserverlibrary' or 'typescript') 96 | */ 97 | library: string 98 | addDiagnostic: (diag: ts.Diagnostic) => number, 99 | removeDiagnostic: (index: number) => void, 100 | diagnostics: readonly ts.Diagnostic[], 101 | } 102 | 103 | export type ProgramTransformerExtras = { 104 | /** 105 | * Originating TypeScript instance 106 | */ 107 | ts: typeof ts; 108 | } 109 | 110 | // endregion 111 | 112 | 113 | /* ****************************************************************************************************************** */ 114 | // region: Signatures 115 | /* ****************************************************************************************************************** */ 116 | 117 | export type ProgramTransformer = ( 118 | program: ts.Program, 119 | host: ts.CompilerHost | undefined, 120 | config: PluginConfig, 121 | extras: ProgramTransformerExtras 122 | ) => ts.Program; 123 | 124 | export type LSPattern = (ls: ts.LanguageService, config: {}) => TransformerPlugin; 125 | export type CompilerOptionsPattern = (compilerOpts: ts.CompilerOptions, config: {}) => TransformerPlugin; 126 | export type ConfigPattern = (config: {}) => TransformerPlugin; 127 | export type TypeCheckerPattern = (checker: ts.TypeChecker, config: {}) => TransformerPlugin; 128 | 129 | export type ProgramPattern = ( 130 | program: ts.Program, 131 | config: {}, 132 | extras: TransformerExtras 133 | ) => TransformerPlugin; 134 | 135 | export type RawPattern = ( 136 | context: ts.TransformationContext, 137 | program: ts.Program, 138 | config: {} 139 | ) => ts.Transformer; 140 | 141 | // endregion 142 | 143 | /* ****************************************************************************************************************** */ 144 | // region: Plugin Package 145 | /* ****************************************************************************************************************** */ 146 | 147 | export interface PluginPackageConfig { 148 | tscOptions?: { 149 | /** 150 | * Sets the JSDocParsingMode to ParseAll 151 | * 152 | * @see https://devblogs.microsoft.com/typescript/announcing-typescript-5-3/#optimizations-by-skipping-jsdoc-parsing 153 | * @default false 154 | */ 155 | parseAllJsDoc?: boolean; 156 | } 157 | } 158 | 159 | // endregion 160 | -------------------------------------------------------------------------------- /projects/core/src/actions/check.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel, PatchError } from '../system'; 2 | import chalk from 'chalk'; 3 | import { getTsPackage } from '../ts-package'; 4 | import { PatchDetail } from "../patch/patch-detail"; 5 | import { getTsModule } from "../module"; 6 | import { getInstallerOptions, InstallerOptions } from "../options"; 7 | 8 | 9 | /* ****************************************************************************************************************** */ 10 | // region: Types 11 | /* ****************************************************************************************************************** */ 12 | 13 | interface CheckResult { 14 | [moduleName: string]: PatchDetail | undefined; 15 | } 16 | 17 | // endregion 18 | 19 | 20 | /* ****************************************************************************************************************** */ 21 | // region: Utils 22 | /* ****************************************************************************************************************** */ 23 | 24 | /** 25 | * Check if files can be patched 26 | */ 27 | export function check(moduleName?: string | string[], opts?: Partial): CheckResult 28 | export function check(moduleNames?: string[], opts?: Partial): CheckResult 29 | export function check(moduleNameOrNames?: string | string[], opts?: Partial): CheckResult { 30 | let targetModuleNames = moduleNameOrNames ? [ moduleNameOrNames ].flat() : undefined; 31 | const options = getInstallerOptions(opts); 32 | const { logger: log, dir } = options; 33 | 34 | /* Load Package */ 35 | const tsPackage = getTsPackage(dir); 36 | const { packageDir, version } = tsPackage; 37 | 38 | 39 | targetModuleNames ??= tsPackage.moduleNames; 40 | 41 | /* Check Modules */ 42 | log(`Checking TypeScript ${chalk.blueBright(`v${version}`)} installation in ${chalk.blueBright(packageDir)}\r\n`); 43 | 44 | let res: CheckResult = {}; 45 | for (const moduleName of targetModuleNames) { 46 | /* Validate */ 47 | if (!tsPackage.moduleNames.includes(moduleName)) 48 | throw new PatchError(`${moduleName} is not a valid TypeScript module in ${packageDir}`); 49 | 50 | /* Report */ 51 | const tsModule = getTsModule(tsPackage, moduleName, { skipCache: options.skipCache }); 52 | const { patchDetail } = tsModule.moduleFile; 53 | 54 | if (patchDetail !== undefined) { 55 | const { isOutdated } = patchDetail; 56 | log([ '+', 57 | `${chalk.blueBright(moduleName)} is patched with ts-patch version ` + 58 | `${chalk[isOutdated ? 'redBright' : 'blueBright'](patchDetail.tspVersion)} ${isOutdated ? '(out of date)' : ''}` 59 | ]); 60 | } else log([ '-', `${chalk.blueBright(moduleName)} is not patched.` ]); 61 | 62 | res[moduleName] = patchDetail; 63 | 64 | log('', LogLevel.verbose); 65 | } 66 | 67 | return res; 68 | } 69 | 70 | // endregion 71 | -------------------------------------------------------------------------------- /projects/core/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './patch' 2 | export * from './unpatch' 3 | export * from './check' 4 | export * from './install' 5 | export * from './uninstall' 6 | -------------------------------------------------------------------------------- /projects/core/src/actions/install.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { getInstallerOptions, InstallerOptions, patch } from '..'; 3 | import { defaultInstallLibraries } from '../config'; 4 | 5 | 6 | /* ****************************************************************************************************************** */ 7 | // region: Utils 8 | /* ****************************************************************************************************************** */ 9 | 10 | /** 11 | * Patch TypeScript modules 12 | */ 13 | export function install(opts?: Partial) { 14 | const options = getInstallerOptions(opts); 15 | const { logger: log } = options; 16 | 17 | const ret = patch(defaultInstallLibraries, options); 18 | if (ret) log([ '+', chalk.green(`ts-patch installed!`) ]); 19 | 20 | return ret; 21 | } 22 | 23 | // endregion 24 | -------------------------------------------------------------------------------- /projects/core/src/actions/patch.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel, PatchError, TspError, } from '../system'; 2 | import { getTsPackage } from '../ts-package'; 3 | import chalk from 'chalk'; 4 | import { getModuleFile, getTsModule, ModuleFile } from '../module'; 5 | import path from 'path'; 6 | import { getInstallerOptions, InstallerOptions } from '../options'; 7 | import { writeFileWithLock } from '../utils'; 8 | import { getPatchedSource } from '../patch/get-patched-source'; 9 | 10 | 11 | /* ****************************************************************************************************************** */ 12 | // region: Utils 13 | /* ****************************************************************************************************************** */ 14 | 15 | /** 16 | * Patch a TypeScript module 17 | */ 18 | export function patch(moduleName: string, opts?: Partial): boolean 19 | export function patch(moduleNames: string[], opts?: Partial): boolean 20 | export function patch(moduleNameOrNames: string | string[], opts?: Partial): boolean { 21 | const targetModuleNames = [ moduleNameOrNames ].flat(); 22 | if (!targetModuleNames.length) throw new PatchError(`Must provide at least one module name to patch`); 23 | 24 | const options = getInstallerOptions(opts); 25 | const { logger: log, dir, skipCache } = options; 26 | 27 | /* Load Package */ 28 | const tsPackage = getTsPackage(dir); 29 | 30 | /* Get modules to patch and patch info */ 31 | const moduleFiles: [ string, ModuleFile ][] = 32 | targetModuleNames.map(m => [ m, getModuleFile(tsPackage.getModulePath(m)) ]); 33 | 34 | /* Determine files not already patched or outdated */ 35 | const patchableFiles = moduleFiles.filter(entry => { 36 | const [ moduleName, moduleFile ] = entry; 37 | if (!moduleFile.patchDetail || moduleFile.patchDetail.isOutdated) return true; 38 | else { 39 | log([ '!', 40 | `${chalk.blueBright(moduleName)} is already patched with the latest version. For details, run: ` + 41 | chalk.bgBlackBright('ts-patch check') 42 | ]); 43 | 44 | return false; 45 | } 46 | }); 47 | 48 | if (!patchableFiles.length) return true; 49 | 50 | /* Patch modules */ 51 | const failedModulePaths: string[] = []; 52 | for (let entry of patchableFiles) { 53 | /* Load Module */ 54 | const { 1: moduleFile } = entry; 55 | const tsModule = getTsModule(tsPackage, moduleFile, { skipCache: true }); 56 | 57 | const { moduleName, modulePath, moduleContentFilePath } = tsModule; 58 | log( 59 | [ '~', `Patching ${chalk.blueBright(moduleName)} in ${chalk.blueBright(path.dirname(modulePath ))}` ], 60 | LogLevel.verbose 61 | ); 62 | 63 | try { 64 | const { js, dts, loadedFromCache } = getPatchedSource(tsModule, { skipCache, log }); 65 | 66 | /* Write Patched Module */ 67 | log( 68 | [ 69 | '~', 70 | `Writing patched ${chalk.blueBright(moduleName)} to ` + 71 | `${chalk.blueBright(moduleContentFilePath)}${loadedFromCache ? ' (cached)' : ''}` 72 | ], 73 | LogLevel.verbose 74 | ); 75 | 76 | writeFileWithLock(moduleContentFilePath, js!); 77 | if (dts) writeFileWithLock(tsModule.dtsPath!, dts!); 78 | 79 | log([ '+', chalk.green(`Successfully patched ${chalk.bold.yellow(moduleName)}.\r\n`) ], LogLevel.verbose); 80 | } catch (e) { 81 | if (e instanceof TspError || options.logLevel >= LogLevel.verbose) log([ '!', e.message ]); 82 | failedModulePaths.push(tsModule.modulePath); 83 | } 84 | } 85 | 86 | if (failedModulePaths.length > 1) { 87 | log([ '!', 88 | `Some files can't be patched! You can run again with --verbose to get specific error detail. The following files are unable to be ` + 89 | `patched:\n - ${failedModulePaths.join('\n - ')}` 90 | ]); 91 | 92 | return false; 93 | } 94 | 95 | return true; 96 | } 97 | 98 | // endregion 99 | -------------------------------------------------------------------------------- /projects/core/src/actions/uninstall.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { defaultInstallLibraries } from '../config'; 3 | import { unpatch } from './unpatch'; 4 | import { getInstallerOptions, InstallerOptions } from "../options"; 5 | 6 | 7 | /* ****************************************************************************************************************** */ 8 | // region: Utils 9 | /* ****************************************************************************************************************** */ 10 | 11 | /** 12 | * Remove patches from TypeScript modules 13 | */ 14 | export function uninstall(opts?: Partial) { 15 | const options = getInstallerOptions(opts); 16 | const { logger: log } = options; 17 | 18 | const ret = unpatch(defaultInstallLibraries, opts); 19 | if (ret) log([ '-', chalk.green(`ts-patch removed!`) ]); 20 | 21 | return ret; 22 | } 23 | 24 | // endregion 25 | -------------------------------------------------------------------------------- /projects/core/src/actions/unpatch.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel, PatchError, RestoreError } from '../system'; 2 | import chalk from 'chalk'; 3 | import path from 'path'; 4 | import { getTsPackage } from '../ts-package'; 5 | import { getModuleFile, getTsModule, ModuleFile } from '../module'; 6 | import fs from 'fs'; 7 | import { getInstallerOptions, InstallerOptions } from '../options'; 8 | import { copyFileWithLock } from '../utils'; 9 | 10 | 11 | /* ****************************************************************************************************************** */ 12 | // region: Utils 13 | /* ****************************************************************************************************************** */ 14 | 15 | export function unpatch(moduleName: string, opts?: Partial): boolean 16 | export function unpatch(moduleNames: string[], opts?: Partial): boolean 17 | export function unpatch(moduleNameOrNames: string | string[], opts?: Partial): boolean { 18 | let res = false; 19 | 20 | const targetModuleNames = [ moduleNameOrNames ].flat(); 21 | if (!targetModuleNames.length) throw new PatchError(`Must provide at least one module name to patch`); 22 | 23 | const options = getInstallerOptions(opts); 24 | const { logger: log, dir } = options; 25 | 26 | /* Load Package */ 27 | const tsPackage = getTsPackage(dir); 28 | 29 | /* Get modules to patch and patch info */ 30 | const moduleFiles: [ string, ModuleFile ][] = 31 | targetModuleNames.map(m => [ m, getModuleFile(tsPackage.getModulePath(m)) ]); 32 | 33 | /* Determine patched files */ 34 | const unpatchableFiles = moduleFiles.filter(entry => { 35 | const [ moduleName, moduleFile ] = entry; 36 | if (moduleFile.patchDetail) return true; 37 | else { 38 | log([ '!', `${chalk.blueBright(moduleName)} is not patched. For details, run: ` + chalk.bgBlackBright('ts-patch check') ]); 39 | return false; 40 | } 41 | }); 42 | 43 | /* Restore files */ 44 | const errors: Record = {}; 45 | for (const entry of unpatchableFiles) { 46 | /* Load Module */ 47 | const { 1: moduleFile } = entry; 48 | const tsModule = getTsModule(tsPackage, moduleFile, { skipCache: true }); 49 | 50 | try { 51 | /* Get Backups */ 52 | const backupPaths: string[] = [] 53 | backupPaths.push(tsModule.backupCachePaths.js); 54 | if (tsModule.backupCachePaths.dts) backupPaths.push(tsModule.backupCachePaths.dts); 55 | 56 | const baseNames = backupPaths.map(p => path.basename(p)).join(' & '); 57 | 58 | log( 59 | [ 60 | '~', 61 | `Restoring ${chalk.blueBright(baseNames)} in ${chalk.blueBright(path.dirname(tsPackage.libDir))}` 62 | ], 63 | LogLevel.verbose 64 | ); 65 | 66 | /* Restore files */ 67 | for (const backupPath of backupPaths) { 68 | if (!fs.existsSync(backupPath)) 69 | throw new Error(`Cannot find backup file: ${backupPath}. Try reinstalling typescript.`); 70 | 71 | const moduleDir = path.dirname(tsModule.modulePath); 72 | 73 | /* Determine destination path (Need to use moduleContentPath if we're working with a cached module file */ 74 | const baseFileName = path.basename(backupPath); 75 | const destPathName = baseFileName === tsModule.moduleName 76 | ? path.basename(tsModule.moduleContentFilePath) 77 | : baseFileName; 78 | 79 | const destPath = path.join(moduleDir, destPathName); 80 | 81 | copyFileWithLock(backupPath, destPath); 82 | } 83 | 84 | log([ '+', chalk.green(`Successfully restored ${chalk.bold.yellow(baseNames)}.\r\n`) ], LogLevel.verbose); 85 | } catch (e) { 86 | errors[tsModule.moduleName] = e; 87 | } 88 | } 89 | 90 | /* Handle errors */ 91 | if (Object.keys(errors).length > 0) { 92 | Object.values(errors).forEach(e => { 93 | log([ '!', e.message ], LogLevel.verbose) 94 | }); 95 | 96 | log(''); 97 | throw new RestoreError( 98 | `[${Object.keys(errors).join(', ')}]`, 99 | 'Try reinstalling typescript.' + 100 | (options.logLevel < LogLevel.verbose ? ' (Or, run uninstall again with --verbose for specific error detail)' : '') 101 | ); 102 | } else { 103 | res = true; 104 | } 105 | 106 | return res; 107 | } 108 | 109 | // endregion 110 | -------------------------------------------------------------------------------- /projects/core/src/bin/ts-patch.ts: -------------------------------------------------------------------------------- 1 | import * as cliModule from '../cli/cli' 2 | 3 | /* ****************************************************************************************************************** * 4 | * Entry 5 | * ****************************************************************************************************************** */ 6 | 7 | cliModule.run(); 8 | -------------------------------------------------------------------------------- /projects/core/src/bin/tspc.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* ****************************************************************************************************************** * 4 | * Entry 5 | * ****************************************************************************************************************** */ 6 | 7 | // Run if main module cli 8 | if (require.main === module) { 9 | require('../compiler/tsc'); 10 | } else { 11 | throw new Error('tspc must be run as a CLI'); 12 | } 13 | -------------------------------------------------------------------------------- /projects/core/src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist'; 2 | import { createLogger, getCacheRoot, getLockFilePath, LogLevel } from '../system'; 3 | import { getTsPackage } from '../ts-package'; 4 | import chalk from 'chalk'; 5 | import * as actions from '../actions'; 6 | import { getCliOptions, getInstallerOptionsFromCliOptions } from './options'; 7 | import { getCliCommand } from './commands'; 8 | import { getHelpMenu } from './help-menu'; 9 | import { tspPackageJSON } from '../config'; 10 | import fs from 'fs'; 11 | 12 | 13 | /* ****************************************************************************************************************** */ 14 | // region: Types 15 | /* ****************************************************************************************************************** */ 16 | 17 | export type CliConfig = Record 18 | 19 | // endregion 20 | 21 | 22 | /* ****************************************************************************************************************** */ 23 | // region: Utils 24 | /* ****************************************************************************************************************** */ 25 | 26 | export function run(opt?: { cmdArgs?: string }) { 27 | /* Parse Input */ 28 | const args = minimist(opt?.cmdArgs?.split(' ') ?? process.argv.slice(2)); 29 | const cliOptions = getCliOptions(args); 30 | const cmd = getCliCommand(args); 31 | 32 | /* Setup */ 33 | const options = getInstallerOptionsFromCliOptions(cliOptions); 34 | const log = createLogger(options.logLevel, options.useColor, options.silent); 35 | 36 | try { 37 | /* Handle commands */ 38 | (() => { 39 | switch (cmd) { 40 | case 'help': 41 | return log(getHelpMenu(), LogLevel.system); 42 | 43 | case 'version': 44 | const { version: tsVersion, packageDir } = getTsPackage(options.dir); 45 | return log('\r\n' + 46 | chalk.bold.blue('ts-patch: ') + tspPackageJSON.version + '\r\n' + 47 | chalk.bold.blue('typescript: ') + tsVersion + chalk.gray(` [${packageDir}]`), 48 | LogLevel.system 49 | ); 50 | 51 | case 'install': 52 | return actions.install(options); 53 | 54 | case 'uninstall': 55 | return actions.uninstall(options); 56 | 57 | case 'patch': 58 | return actions.patch(args._.slice(1).join(' '), options); 59 | 60 | case 'unpatch': 61 | return actions.unpatch(args._.slice(1).join(' '), options); 62 | 63 | case 'check': 64 | return actions.check(undefined, options); 65 | 66 | case 'clear-cache': 67 | const cacheRoot = getCacheRoot(); 68 | 69 | /* Clear dir */ 70 | fs.rmSync(cacheRoot, { recursive: true, force: true }); 71 | 72 | /* Recreate Dirs */ 73 | getCacheRoot(); 74 | getLockFilePath(''); 75 | 76 | return log([ '+', 'Cleared cache & lock-files' ], LogLevel.system); 77 | 78 | default: 79 | log([ '!', 'Invalid command. Try ts-patch /? for more info' ], LogLevel.system) 80 | } 81 | })(); 82 | } 83 | catch (e) { 84 | log([ 85 | '!', 86 | chalk.bold.yellow(e.name && (e.name !== 'Error') ? `[${e.name}]: ` : 'Error: ') + chalk.red(e.message) 87 | ], LogLevel.system); 88 | } 89 | 90 | // Output for analysis by tests 91 | return ({ cmd, args, options }); 92 | } 93 | 94 | // endregion 95 | -------------------------------------------------------------------------------- /projects/core/src/cli/commands.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import minimist from 'minimist'; 3 | import type { CliConfig } from './cli'; 4 | 5 | 6 | /* ****************************************************************************************************************** */ 7 | // region: Config 8 | /* ****************************************************************************************************************** */ 9 | 10 | /** @internal */ 11 | export const cliCommandsConfig: CliConfig = { 12 | install: { short: 'i', caption: `Installs ts-patch (to main libraries)` }, 13 | uninstall: { short: 'u', caption: 'Restores original typescript files' }, 14 | check: { 15 | short: 'c', caption: 16 | `Check patch status (use with ${chalk.cyanBright('--dir')} to specify TS package location)` 17 | }, 18 | patch: { 19 | short: void 0, paramCaption: '', caption: 20 | 'Patch specific module(s) ' + chalk.yellow('(advanced)') 21 | }, 22 | unpatch: { 23 | short: void 0, paramCaption: '', caption: 24 | 'Un-patch specific module(s) ' + chalk.yellow('(advanced)') 25 | }, 26 | 'clear-cache': { caption: 'Clears cache and lock-files' }, 27 | version: { short: 'v', caption: 'Show version' }, 28 | help: { short: '/?', caption: 'Show help menu' }, 29 | }; 30 | 31 | // endregion 32 | 33 | 34 | /* ****************************************************************************************************************** */ 35 | // region: Utils 36 | /* ****************************************************************************************************************** */ 37 | 38 | export function getCliCommand(args: minimist.ParsedArgs) { 39 | let cmd: string | undefined = args._[0] ? args._[0].toLowerCase() : void 0; 40 | 41 | /* Handle special cases */ 42 | if ((args.v) && (!cmd)) return 'version'; 43 | if (args.h) return 'help'; 44 | 45 | if (!cmd) return cmd; 46 | 47 | /* Get long command */ 48 | cmd = Object 49 | .entries(cliCommandsConfig) 50 | .find(([ long, { short } ]) => long === cmd || short === cmd)?.[0]; 51 | 52 | return cmd; 53 | } 54 | 55 | // endregion 56 | -------------------------------------------------------------------------------- /projects/core/src/cli/help-menu.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import stripAnsi from 'strip-ansi'; 3 | import { cliCommandsConfig } from './commands'; 4 | import { cliOptionsConfig } from './options'; 5 | 6 | 7 | /* ****************************************************************************************************************** */ 8 | // region: Config 9 | /* ****************************************************************************************************************** */ 10 | 11 | const LINE_INDENT = '\r\n\t'; 12 | const COL_WIDTH = 45; 13 | 14 | // endregion 15 | 16 | 17 | /* ****************************************************************************************************************** */ 18 | // region: Utils 19 | /* ****************************************************************************************************************** */ 20 | 21 | export function getHelpMenu() { 22 | return LINE_INDENT + chalk.bold.blue('ts-patch [command] ') + chalk.blue('') + '\r\n' + LINE_INDENT + 23 | 24 | // Commands 25 | Object 26 | .entries(cliCommandsConfig) 27 | .map(([ cmd, { short, caption, paramCaption } ]) => formatLine([ cmd, short ], caption, paramCaption)) 28 | .join(LINE_INDENT) + 29 | 30 | // Options 31 | '\r\n' + LINE_INDENT + chalk.bold('Options') + LINE_INDENT + 32 | Object 33 | .entries(cliOptionsConfig) 34 | .map(([ long, { short, inverse, caption, paramCaption } ]) => formatLine([ 35 | short && `${chalk.cyanBright('-' + short)}`, 36 | long && `${chalk.cyanBright(`${inverse ? '--no-' : '--'}${long}`)}` 37 | ], caption, paramCaption)) 38 | .join(LINE_INDENT); 39 | 40 | function formatLine(left: (string | undefined)[], caption: string, paramCaption: string = '') { 41 | const leftCol = left.filter(Boolean).join(chalk.blue(', ')) + ' ' + chalk.yellow(paramCaption); 42 | const dots = chalk.grey('.'.repeat(COL_WIDTH - stripAnsi(leftCol).length)); 43 | 44 | return `${leftCol} ${dots} ${caption}`; 45 | } 46 | } 47 | 48 | // endregion 49 | -------------------------------------------------------------------------------- /projects/core/src/cli/options.ts: -------------------------------------------------------------------------------- 1 | import minimist from 'minimist'; 2 | import type { CliConfig } from './cli'; 3 | import { LogLevel, OptionsError } from '../system'; 4 | import { getInstallerOptions, InstallerOptions } from "../options"; 5 | import { getGlobalTsDir } from "../utils"; 6 | 7 | 8 | /* ****************************************************************************************************************** */ 9 | // region: Types 10 | /* ****************************************************************************************************************** */ 11 | 12 | export interface CliOptions { 13 | silent: boolean; 14 | global: boolean; 15 | verbose: boolean; 16 | dir: string; 17 | color: boolean; 18 | } 19 | 20 | // endregion 21 | 22 | 23 | /* ****************************************************************************************************************** */ 24 | // region: Config 25 | /* ****************************************************************************************************************** */ 26 | 27 | /** @internal */ 28 | export const cliOptionsConfig: CliConfig = { 29 | silent: { short: 's', caption: 'Run silently' }, 30 | global: { short: 'g', caption: 'Target global TypeScript installation' }, 31 | verbose: { short: 'v', caption: 'Chat it up' }, 32 | cache: { inverse: true, caption: 'Skip cache' }, 33 | dir: { 34 | short: 'd', 35 | paramCaption: '', 36 | caption: 'TypeScript directory or directory to resolve typescript package from' 37 | }, 38 | color: { inverse: true, caption: 'Strip ansi colours from output' } 39 | }; 40 | 41 | 42 | // endregion 43 | 44 | 45 | /* ****************************************************************************************************************** */ 46 | // region: Utils 47 | /* ****************************************************************************************************************** */ 48 | 49 | export function getCliOptions(args: minimist.ParsedArgs) { 50 | let res: CliOptions = {}; 51 | 52 | for (const [ key, { short } ] of Object.entries(cliOptionsConfig)) { 53 | if (args.hasOwnProperty(key) || (short && args.hasOwnProperty(short))) { 54 | (res)[key] = args.hasOwnProperty(key) ? args[key] : args[short!]; 55 | } 56 | } 57 | 58 | return res; 59 | } 60 | 61 | export function getInstallerOptionsFromCliOptions(cliOptions: CliOptions): InstallerOptions { 62 | let partialOptions: Partial = {}; 63 | 64 | /* Dir option */ 65 | if (cliOptions.global && cliOptions.dir) throw new OptionsError(`Cannot specify both --global and --dir`); 66 | if ('dir' in cliOptions) partialOptions.dir = cliOptions.dir; 67 | if ('global' in cliOptions) partialOptions.dir = getGlobalTsDir(); 68 | 69 | /* LogLevel option */ 70 | if (cliOptions.silent && cliOptions.verbose) throw new OptionsError(`Cannot specify both --silent and --verbose`); 71 | if (cliOptions.silent) { 72 | partialOptions.logLevel = LogLevel.system; 73 | partialOptions.silent = true; 74 | } 75 | else if (cliOptions.verbose) partialOptions.logLevel = LogLevel.verbose; 76 | 77 | /* Color option */ 78 | if (cliOptions.color) partialOptions.useColor = cliOptions.color; 79 | 80 | return getInstallerOptions(partialOptions); 81 | } 82 | 83 | // endregion 84 | -------------------------------------------------------------------------------- /projects/core/src/compiler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "main": "./typescript.js" 4 | } 5 | -------------------------------------------------------------------------------- /projects/core/src/compiler/tsc.js: -------------------------------------------------------------------------------- 1 | const indexPath = '../'; 2 | const path = require('path'); 3 | const { getLiveModule } = require(indexPath); 4 | const { runInThisContext } = require("vm"); 5 | 6 | 7 | /* ****************************************************************************************************************** * 8 | * Entry 9 | * ****************************************************************************************************************** */ 10 | 11 | const { js, tsModule } = getLiveModule('tsc.js'); 12 | 13 | const script = runInThisContext(` 14 | (function (exports, require, module, __filename, __dirname) { 15 | ${js} 16 | }); 17 | `); 18 | 19 | script.call(exports, exports, require, module, tsModule.modulePath, path.dirname(tsModule.modulePath)); 20 | -------------------------------------------------------------------------------- /projects/core/src/compiler/tsserver.js: -------------------------------------------------------------------------------- 1 | const indexPath = '../'; 2 | const path = require('path'); 3 | const { getLiveModule } = require(indexPath); 4 | const { runInThisContext } = require("vm"); 5 | 6 | 7 | /* ****************************************************************************************************************** * 8 | * Entry 9 | * ****************************************************************************************************************** */ 10 | 11 | const { js, tsModule } = getLiveModule('tsserver.js'); 12 | 13 | const script = runInThisContext(` 14 | (function (exports, require, module, __filename, __dirname) { 15 | ${js} 16 | }); 17 | `); 18 | 19 | script.call(exports, exports, require, module, tsModule.modulePath, path.dirname(tsModule.modulePath)); 20 | -------------------------------------------------------------------------------- /projects/core/src/compiler/tsserverlibrary.js: -------------------------------------------------------------------------------- 1 | const indexPath = '../'; 2 | const path = require('path'); 3 | const { getLiveModule } = require(indexPath); 4 | const { runInThisContext } = require("vm"); 5 | 6 | 7 | /* ****************************************************************************************************************** * 8 | * Entry 9 | * ****************************************************************************************************************** */ 10 | 11 | const { js, tsModule } = getLiveModule('tsserverlibrary.js'); 12 | 13 | const script = runInThisContext(` 14 | (function (exports, require, module, __filename, __dirname) { 15 | ${js} 16 | }); 17 | `); 18 | 19 | script.call(exports, exports, require, module, tsModule.modulePath, path.dirname(tsModule.modulePath)); 20 | -------------------------------------------------------------------------------- /projects/core/src/compiler/typescript.js: -------------------------------------------------------------------------------- 1 | const indexPath = '../'; 2 | const path = require('path'); 3 | const { getLiveModule } = require(indexPath); 4 | const { runInThisContext } = require("vm"); 5 | 6 | 7 | /* ****************************************************************************************************************** * 8 | * Entry 9 | * ****************************************************************************************************************** */ 10 | 11 | const { js, tsModule } = getLiveModule('typescript.js'); 12 | 13 | const script = runInThisContext(` 14 | (function (exports, require, module, __filename, __dirname) { 15 | ${js} 16 | }); 17 | `); 18 | 19 | script.call(exports, exports, require, module, tsModule.modulePath, path.dirname(tsModule.modulePath)); 20 | -------------------------------------------------------------------------------- /projects/core/src/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from "fs"; 3 | import ts from 'typescript'; 4 | 5 | 6 | /* ****************************************************************************************************************** */ 7 | // region: Library Config 8 | /* ****************************************************************************************************************** */ 9 | 10 | /** 11 | * Root directory for ts-patch 12 | */ 13 | // TODO - This should be improved at some point 14 | export const appRoot = (() => { 15 | const moduleDir = __dirname; 16 | 17 | const chkFile = (pkgFile: string) => 18 | (fs.existsSync(pkgFile) && (require(pkgFile).name === 'ts-patch')) ? path.dirname(pkgFile) : void 0; 19 | 20 | const res = chkFile(path.join(moduleDir, 'package.json')) || chkFile(path.join(moduleDir, '../../../package.json')); 21 | 22 | if (!res) throw new Error(`Error getting app root. No valid ts-patch package file found in ` + moduleDir); 23 | 24 | return res; 25 | })(); 26 | 27 | /** 28 | * Package json for ts-patch 29 | */ 30 | export const tspPackageJSON = require(path.resolve(appRoot, 'package.json')); 31 | 32 | export const RESOURCES_PATH = path.join(appRoot, tspPackageJSON.directories.resources); 33 | 34 | export const defaultNodePrinterOptions: ts.PrinterOptions = { 35 | newLine: ts.NewLineKind.LineFeed, 36 | removeComments: false 37 | }; 38 | 39 | // endregion 40 | 41 | 42 | /* ****************************************************************************************************************** */ 43 | // region: Patch Config 44 | /* ****************************************************************************************************************** */ 45 | 46 | export const defaultInstallLibraries = [ 'tsc.js', 'typescript.js' ]; 47 | 48 | export const corePatchName = ``; 49 | 50 | export const modulePatchFilePath = path.resolve(appRoot, tspPackageJSON.directories.resources, 'module-patch.js'); 51 | export const dtsPatchFilePath = path.resolve(appRoot, tspPackageJSON.directories.resources, 'module-patch.d.ts'); 52 | 53 | export const execTscCmd = 'execTsc'; 54 | 55 | // endregion 56 | 57 | 58 | /* ****************************************************************************************************************** */ 59 | // region: Cache Config 60 | /* ****************************************************************************************************************** */ 61 | 62 | export const cachedFilePatchedPrefix = 'patched.'; 63 | export const lockFileDir = 'locks'; 64 | 65 | // endregion 66 | -------------------------------------------------------------------------------- /projects/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { InstallerOptions, getInstallerOptions } from './options'; 2 | export { install, uninstall, patch, check } from './actions' 3 | export { getLiveModule } from './module' 4 | export * from './plugin-types' 5 | -------------------------------------------------------------------------------- /projects/core/src/module/get-live-module.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { getTsModule, TsModule } from './ts-module'; 3 | import { getTsPackage } from '../ts-package'; 4 | import { getPatchedSource } from '../patch/get-patched-source'; 5 | 6 | 7 | /* ****************************************************************************************************************** */ 8 | // region: Utils 9 | /* ****************************************************************************************************************** */ 10 | 11 | export function getLiveModule(moduleName: TsModule.Name) { 12 | const skipCache = process.env.TSP_SKIP_CACHE === 'true'; 13 | const tsPath = process.env.TSP_COMPILER_TS_PATH ? path.resolve(process.env.TSP_COMPILER_TS_PATH) : require.resolve('typescript'); 14 | 15 | /* Open the TypeScript module */ 16 | const tsPackage = getTsPackage(tsPath); 17 | const tsModule = getTsModule(tsPackage, moduleName, { skipCache }); 18 | 19 | /* Get patched version */ 20 | const { js } = getPatchedSource(tsModule, { skipCache, skipDts: true }); 21 | 22 | return { js, tsModule }; 23 | } 24 | 25 | // endregion 26 | -------------------------------------------------------------------------------- /projects/core/src/module/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ts-module'; 2 | export * from './module-source'; 3 | export * from './module-file'; 4 | export * from './get-live-module'; 5 | -------------------------------------------------------------------------------- /projects/core/src/module/module-file.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { PatchDetail } from '../patch/patch-detail'; 3 | import path from 'path'; 4 | import { getHash, withFileLock } from '../utils'; 5 | import { TsModule } from './ts-module'; 6 | 7 | 8 | /* ****************************************************************************************************************** */ 9 | // region: Config 10 | /* ****************************************************************************************************************** */ 11 | 12 | const SHORT_CHUNK_SIZE = 1024; 13 | const LONG_CHUNK_SIZE = 64_536; 14 | 15 | // endregion 16 | 17 | 18 | /* ****************************************************************************************************************** */ 19 | // region: Types 20 | /* ****************************************************************************************************************** */ 21 | 22 | export interface ModuleFile { 23 | moduleName: string 24 | patchDetail?: PatchDetail 25 | filePath: string 26 | contentFilePath: string 27 | get content(): string 28 | 29 | getHash(): string 30 | } 31 | 32 | // endregion 33 | 34 | 35 | /* ****************************************************************************************************************** */ 36 | // region: Helpers 37 | /* ****************************************************************************************************************** */ 38 | 39 | function readFile(filePath: string, headersOnly?: boolean) { 40 | return withFileLock(filePath, () => { 41 | let CHUNK_SIZE = headersOnly ? SHORT_CHUNK_SIZE : LONG_CHUNK_SIZE; 42 | let result = ''; 43 | let doneReadingHeaders = false; 44 | let bytesRead; 45 | let buffer = Buffer.alloc(CHUNK_SIZE); 46 | const headerLines: string[] = []; 47 | let isLastHeaderIncomplete = false; 48 | 49 | const fd = fs.openSync(filePath, 'r'); 50 | 51 | try { 52 | readFileLoop: 53 | while ((bytesRead = fs.readSync(fd, buffer, 0, CHUNK_SIZE, null)) > 0) { 54 | const chunkString = buffer.toString('utf-8', 0, bytesRead); 55 | 56 | /* Handle Header */ 57 | if (!doneReadingHeaders) { 58 | const lines = chunkString.split('\n'); 59 | 60 | lineLoop: 61 | for (let i = 0; i < lines.length; i++) { 62 | const line = lines[i]; 63 | if (i === 0 && isLastHeaderIncomplete) { 64 | headerLines[headerLines.length - 1] += line; 65 | } else { 66 | if (line.startsWith('///')) { 67 | headerLines.push(line); 68 | } else { 69 | doneReadingHeaders = true; 70 | if (!headersOnly) { 71 | result += lines.slice(i).join('\n'); 72 | CHUNK_SIZE = LONG_CHUNK_SIZE; 73 | buffer = Buffer.alloc(CHUNK_SIZE); 74 | break lineLoop; 75 | } else { 76 | break readFileLoop; 77 | } 78 | } 79 | } 80 | } 81 | 82 | if (!doneReadingHeaders) isLastHeaderIncomplete = !chunkString.endsWith('\n'); 83 | } else { 84 | /* Handle content */ 85 | result += chunkString; 86 | } 87 | } 88 | 89 | return { headerLines, content: headersOnly ? undefined : result }; 90 | } finally { 91 | fs.closeSync(fd); 92 | } 93 | }); 94 | } 95 | 96 | // endregion 97 | 98 | 99 | /* ****************************************************************************************************************** */ 100 | // region: Utils 101 | /* ****************************************************************************************************************** */ 102 | 103 | export function getModuleFile(filePath: string, loadFullContent?: boolean): ModuleFile { 104 | /* Determine shim redirect file - see: https://github.com/nonara/ts-patch/issues/174 */ 105 | const moduleName = path.basename(filePath); 106 | const contentFilePath = TsModule.getContentFilePathForModulePath(filePath); 107 | 108 | /* Get PatchDetail */ 109 | let { headerLines, content } = readFile(contentFilePath, !loadFullContent); 110 | const patchDetail = PatchDetail.fromHeader(headerLines); 111 | 112 | return { 113 | moduleName, 114 | filePath, 115 | contentFilePath, 116 | patchDetail, 117 | get content() { 118 | if (content == null) content = readFile(this.contentFilePath, false).content; 119 | return content!; 120 | }, 121 | getHash(): string { 122 | return getHash(this.content); 123 | } 124 | }; 125 | } 126 | 127 | // endregion 128 | -------------------------------------------------------------------------------- /projects/core/src/module/module-source.ts: -------------------------------------------------------------------------------- 1 | import { createSourceSection, SourceSection } from './source-section'; 2 | import { TsModule } from './ts-module'; 3 | import { sliceModule } from '../slice/module-slice'; 4 | 5 | 6 | /* ****************************************************************************************************************** */ 7 | // region: Types 8 | /* ****************************************************************************************************************** */ 9 | 10 | export interface ModuleSource { 11 | fileHeader: SourceSection; 12 | bodyHeader?: SourceSection; 13 | body: SourceSection[]; 14 | fileFooter?: SourceSection; 15 | usesTsNamespace: boolean; 16 | getSections(): [ sectionName: SourceSection['sectionName'], section: SourceSection | undefined ][]; 17 | bodyWrapper?: { 18 | start: string; 19 | end: string; 20 | } 21 | } 22 | 23 | // endregion 24 | 25 | 26 | /* ****************************************************************************************************************** */ 27 | // region: Utils 28 | /* ****************************************************************************************************************** */ 29 | 30 | export function getModuleSource(tsModule: TsModule): ModuleSource { 31 | const moduleFile = tsModule.getUnpatchedModuleFile(); 32 | 33 | const { firstSourceFileStart, fileEnd, wrapperPos, bodyPos, sourceFileStarts, bodyWrapper } = 34 | sliceModule(moduleFile, tsModule.package.version); 35 | 36 | const fileHeaderEnd = wrapperPos?.start ?? firstSourceFileStart; 37 | 38 | return { 39 | fileHeader: createSourceSection(moduleFile, 'file-header', 0, fileHeaderEnd), 40 | bodyHeader: wrapperPos && createSourceSection(moduleFile, 'body-header', bodyPos.start, firstSourceFileStart, 2), 41 | body: sourceFileStarts.map(([ srcFileName, startPos ], i) => { 42 | const endPos = sourceFileStarts[i + 1]?.[1] ?? bodyPos?.end ?? fileEnd; 43 | return createSourceSection(moduleFile, 'body', startPos, endPos, wrapperPos != null ? 2 :0, srcFileName); 44 | }), 45 | fileFooter: wrapperPos && createSourceSection(moduleFile, 'file-footer', wrapperPos.end, fileEnd), 46 | usesTsNamespace: wrapperPos != null, 47 | getSections() { 48 | return [ 49 | [ 'file-header', this.fileHeader ], 50 | [ 'body-header', this.bodyHeader ], 51 | ...this.body.map((section, i) => [ `body`, section ] as [ SourceSection['sectionName'], SourceSection ]), 52 | [ 'file-footer', this.fileFooter ], 53 | ]; 54 | }, 55 | bodyWrapper, 56 | } 57 | } 58 | 59 | // endregion 60 | -------------------------------------------------------------------------------- /projects/core/src/module/source-section.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import { ModuleFile } from './module-file'; 3 | import path from 'path'; 4 | 5 | 6 | /* ****************************************************************************************************************** */ 7 | // region: Types 8 | /* ****************************************************************************************************************** */ 9 | 10 | export interface SourceSection { 11 | readonly sectionName: 'file-header' | 'body-header' | 'body' | 'file-footer'; 12 | readonly srcFileName?: string; 13 | readonly pos: { start: number; end: number }; 14 | indentLevel: number; 15 | 16 | hasTransformed?: boolean; 17 | hasUpdatedSourceText?: boolean; 18 | 19 | get sourceText(): string; 20 | updateSourceText(newText: string): void; 21 | getSourceFile(): ts.SourceFile; 22 | getOriginalSourceFile(): ts.SourceFile; 23 | transform(transformers: ts.TransformerFactory[]): void; 24 | print(printer?: ts.Printer): string; 25 | } 26 | 27 | // endregion 28 | 29 | 30 | /* ****************************************************************************************************************** */ 31 | // region: Utils 32 | /* ****************************************************************************************************************** */ 33 | 34 | export function createSourceSection( 35 | moduleFile: ModuleFile, 36 | sectionName: SourceSection['sectionName'], 37 | startPos: number, 38 | endPos: number, 39 | indentLevel: number = 0, 40 | srcFileName?: string, 41 | ): 42 | SourceSection 43 | { 44 | let sourceText: string | undefined; 45 | let originalSourceFile: ts.SourceFile | undefined; 46 | let sourceFile: ts.SourceFile | undefined; 47 | let sourceFileName: string | undefined; 48 | 49 | return { 50 | hasTransformed: false, 51 | hasUpdatedSourceText: false, 52 | sectionName, 53 | srcFileName, 54 | indentLevel, 55 | pos: { start: startPos, end: endPos }, 56 | get sourceText() { 57 | return sourceText ??= moduleFile.content.slice(startPos, endPos); 58 | }, 59 | getSourceFile() { 60 | if (!sourceFile) { 61 | if (this.hasUpdatedSourceText) return createSourceFile(this); 62 | else return this.getOriginalSourceFile(); 63 | } 64 | return sourceFile; 65 | }, 66 | updateSourceText(newText: string) { 67 | sourceText = newText; 68 | sourceFile = undefined; 69 | }, 70 | getOriginalSourceFile() { 71 | originalSourceFile ??= createSourceFile(this); 72 | return originalSourceFile; 73 | }, 74 | transform(transformers: ts.TransformerFactory[]) { 75 | const result = ts.transform(this.getSourceFile(), transformers); 76 | sourceFile = result.transformed[0]; 77 | this.hasTransformed = true; 78 | this.indentLevel = 0; 79 | }, 80 | print(printer?: ts.Printer) { 81 | if (!this.hasTransformed) return this.sourceText; 82 | 83 | printer ??= ts.createPrinter(); 84 | return printer.printFile(this.getSourceFile()); 85 | } 86 | } 87 | 88 | function createSourceFile(sourceSection: SourceSection) { 89 | return ts.createSourceFile( 90 | getSourceFileName(), 91 | sourceSection.sourceText, 92 | ts.ScriptTarget.Latest, 93 | true, 94 | ts.ScriptKind.JS 95 | ); 96 | } 97 | 98 | function getSourceFileName() { 99 | if (!sourceFileName) { 100 | sourceFileName = srcFileName; 101 | if (!sourceFileName) { 102 | const baseName = path.basename(moduleFile.filePath, path.extname(moduleFile.filePath)); 103 | sourceFileName = `${baseName}.${sectionName}.ts`; 104 | } 105 | } 106 | return sourceFileName; 107 | } 108 | } 109 | 110 | // endregion 111 | -------------------------------------------------------------------------------- /projects/core/src/module/ts-module.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import type { TsPackage } from '../ts-package'; 4 | import { getModuleSource, ModuleSource } from './module-source'; 5 | import { getCachePath, TspError } from '../system'; 6 | import { getModuleFile, ModuleFile } from './module-file'; 7 | import { cachedFilePatchedPrefix } from '../config'; 8 | 9 | 10 | /* ****************************************************************************************************************** */ 11 | // region: Config 12 | /* ****************************************************************************************************************** */ 13 | 14 | export namespace TsModule { 15 | export const names = [ 'tsc.js', 'tsserverlibrary.js', 'typescript.js', 'tsserver.js' ]; 16 | 17 | export const contentFileMap: Record = { 18 | 'tsc.js': '_tsc.js', 19 | 'tsserver.js': '_tsserver.js' 20 | } satisfies Partial>; 21 | 22 | export function getContentFileName(moduleName: typeof names[number]): string { 23 | return contentFileMap[moduleName] || moduleName; 24 | } 25 | 26 | /* Determine shim redirect file - see: https://github.com/nonara/ts-patch/issues/174 */ 27 | export function getContentFilePathForModulePath(modulePath: string): string { 28 | const baseName = path.basename(modulePath); 29 | 30 | const redirectFile = contentFileMap[baseName]; 31 | const maybeModuleContentPath = redirectFile && path.join(path.dirname(modulePath), redirectFile); 32 | const moduleContentPath = maybeModuleContentPath && fs.existsSync(maybeModuleContentPath) 33 | ? maybeModuleContentPath 34 | : modulePath; 35 | 36 | return moduleContentPath; 37 | } 38 | } 39 | 40 | // endregion 41 | 42 | 43 | /* ****************************************************************************************************************** */ 44 | // region: Types 45 | /* ****************************************************************************************************************** */ 46 | 47 | export interface TsModule { 48 | package: TsPackage; 49 | majorVer: number; 50 | minorVer: number; 51 | isPatched: boolean; 52 | 53 | moduleName: TsModule.Name; 54 | modulePath: string; 55 | moduleContentFilePath: string; 56 | moduleFile: ModuleFile; 57 | dtsPath: string | undefined; 58 | 59 | cacheKey: string; 60 | backupCachePaths: { js: string, dts?: string }; 61 | patchedCachePaths: { js: string, dts?: string }; 62 | 63 | getUnpatchedModuleFile(): ModuleFile; 64 | getUnpatchedSource(): ModuleSource; 65 | } 66 | 67 | export namespace TsModule { 68 | export type Name = (typeof names)[number] | string; 69 | } 70 | 71 | export interface GetTsModuleOptions { 72 | skipCache?: boolean 73 | } 74 | 75 | // endregion 76 | 77 | 78 | /* ****************************************************************************************************************** */ 79 | // region: Utils 80 | /* ****************************************************************************************************************** */ 81 | 82 | export function getTsModule(tsPackage: TsPackage, moduleName: TsModule.Name, options?: GetTsModuleOptions): 83 | TsModule 84 | export function getTsModule(tsPackage: TsPackage, moduleFile: ModuleFile, options?: GetTsModuleOptions): TsModule 85 | export function getTsModule( 86 | tsPackage: TsPackage, 87 | moduleNameOrModuleFile: TsModule.Name | ModuleFile, 88 | options?: GetTsModuleOptions 89 | ): TsModule { 90 | const skipCache = options?.skipCache; 91 | 92 | /* Get Module File */ 93 | let moduleFile: ModuleFile | undefined; 94 | let moduleName: string | undefined; 95 | let modulePath: string | undefined; 96 | if (typeof moduleNameOrModuleFile === "object" && moduleNameOrModuleFile.content) { 97 | moduleFile = moduleNameOrModuleFile; 98 | moduleName = moduleFile.moduleName; 99 | modulePath = moduleFile.filePath; 100 | } else { 101 | moduleName = moduleNameOrModuleFile as TsModule.Name; 102 | } 103 | 104 | /* Handle Local Cache */ 105 | if (!skipCache && tsPackage.moduleCache.has(moduleName)) return tsPackage.moduleCache.get(moduleName)!; 106 | 107 | /* Load File (if not already) */ 108 | if (!modulePath) modulePath = path.join(tsPackage.libDir, moduleName); 109 | if (!moduleFile) moduleFile = getModuleFile(modulePath); 110 | 111 | /* Get DTS if exists */ 112 | const maybeDtsFile = modulePath.replace(/\.js$/, '.d.ts'); 113 | const dtsPath = fs.existsSync(maybeDtsFile) ? maybeDtsFile : undefined; 114 | const dtsName = dtsPath && path.basename(dtsPath); 115 | 116 | /* Get Cache Paths */ 117 | const cacheKey = moduleFile.patchDetail?.originalHash || moduleFile.getHash(); 118 | const backupCachePaths = { 119 | js: getCachePath(cacheKey, moduleName), 120 | dts: dtsName && getCachePath(cacheKey, dtsName) 121 | } 122 | const patchedCachePaths = { 123 | js: getCachePath(cacheKey, cachedFilePatchedPrefix + moduleName), 124 | dts: dtsName && getCachePath(cacheKey, cachedFilePatchedPrefix + dtsName) 125 | } 126 | 127 | /* Create Module */ 128 | const isPatched = !!moduleFile.patchDetail; 129 | let originalModuleFile: ModuleFile | undefined; 130 | const tsModule: TsModule = { 131 | package: tsPackage, 132 | majorVer: tsPackage.majorVer, 133 | minorVer: tsPackage.minorVer, 134 | isPatched, 135 | 136 | moduleName, 137 | modulePath, 138 | moduleFile, 139 | moduleContentFilePath: moduleFile.contentFilePath, 140 | dtsPath, 141 | 142 | cacheKey, 143 | backupCachePaths, 144 | patchedCachePaths, 145 | 146 | getUnpatchedSource() { 147 | return getModuleSource(this); 148 | }, 149 | 150 | getUnpatchedModuleFile() { 151 | if (!originalModuleFile) { 152 | if (isPatched) { 153 | if (!fs.existsSync(backupCachePaths.js)) throw new TspError(`Cannot find backup cache file for ${moduleName}. Please wipe node_modules and reinstall.`); 154 | originalModuleFile = getModuleFile(backupCachePaths.js); 155 | } else { 156 | originalModuleFile = isPatched ? getModuleFile(backupCachePaths.js) : moduleFile!; 157 | } 158 | } 159 | 160 | return originalModuleFile; 161 | } 162 | }; 163 | 164 | tsPackage.moduleCache.set(moduleName, tsModule); 165 | 166 | return tsModule; 167 | } 168 | 169 | // endregion 170 | -------------------------------------------------------------------------------- /projects/core/src/options.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, Logger, LogLevel } from './system'; 2 | import { PartialSome } from "./utils"; 3 | 4 | 5 | /* ****************************************************************************************************************** */ 6 | // region: Locals 7 | /* ****************************************************************************************************************** */ 8 | 9 | type PreAppOptions = PartialSome 10 | 11 | // endregion 12 | 13 | 14 | /* ****************************************************************************************************************** */ 15 | // region: Config 16 | /* ****************************************************************************************************************** */ 17 | 18 | export interface InstallerOptions { 19 | logLevel: LogLevel; 20 | useColor: boolean; 21 | dir: string; 22 | silent: boolean; 23 | logger: Logger; 24 | skipCache: boolean; 25 | } 26 | 27 | export namespace InstallerOptions { 28 | export function getDefaults() { 29 | return { 30 | logLevel: LogLevel.normal, 31 | useColor: true, 32 | dir: process.cwd(), 33 | silent: false, 34 | skipCache: false 35 | } satisfies PreAppOptions 36 | } 37 | } 38 | 39 | // endregion 40 | 41 | 42 | /* ******************************************************************************************************************** 43 | * Parser 44 | * ********************************************************************************************************************/ 45 | 46 | export function getInstallerOptions(options?: Partial): InstallerOptions { 47 | if (!options && typeof options === "object" && Object.isSealed(options)) return options as InstallerOptions; 48 | 49 | const res = { ...InstallerOptions.getDefaults(), ...options } as PreAppOptions; 50 | 51 | return Object.seal({ 52 | ...res, 53 | logger: res.logger ?? createLogger(res.logLevel, res.useColor, res.silent) 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /projects/core/src/patch/get-patched-source.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LogLevel } from '../system'; 2 | import chalk from 'chalk'; 3 | import path from 'path'; 4 | import { copyFileWithLock, mkdirIfNotExist, readFileWithLock, writeFileWithLock } from '../utils'; 5 | import fs from 'fs'; 6 | import { getModuleFile, TsModule } from '../module'; 7 | import { patchModule } from './patch-module'; 8 | 9 | 10 | /* ****************************************************************************************************************** */ 11 | // region: Types 12 | /* ****************************************************************************************************************** */ 13 | 14 | export interface GetPatchedSourceOptions { 15 | log?: Logger 16 | skipCache?: boolean 17 | skipDts?: boolean 18 | } 19 | 20 | // endregion 21 | 22 | 23 | /* ****************************************************************************************************************** */ 24 | // region: Utils 25 | /* ****************************************************************************************************************** */ 26 | 27 | export function getPatchedSource(tsModule: TsModule, options?: GetPatchedSourceOptions): 28 | { js: string, dts: string | undefined, loadedFromCache: boolean } 29 | { 30 | const { backupCachePaths, patchedCachePaths } = tsModule; 31 | const { log, skipCache } = options || {}; 32 | 33 | /* Write backup if not patched */ 34 | if (!tsModule.isPatched) { 35 | for (const [ key, backupPath ] of Object.entries(backupCachePaths)) { 36 | const srcPath = key === 'dts' ? tsModule.dtsPath : tsModule.moduleContentFilePath; 37 | if (key === 'dts' && options?.skipDts) continue; 38 | if (!srcPath) continue; 39 | 40 | log?.([ '~', `Writing backup cache to ${chalk.blueBright(backupPath)}` ], LogLevel.verbose); 41 | 42 | const cacheDir = path.dirname(backupPath); 43 | mkdirIfNotExist(cacheDir); 44 | copyFileWithLock(srcPath, backupPath); 45 | } 46 | } 47 | 48 | /* Get Patched Module */ 49 | const canUseCache = !skipCache 50 | && !tsModule.moduleFile.patchDetail?.isOutdated 51 | && (!patchedCachePaths.dts || fs.existsSync(patchedCachePaths.dts)) 52 | && fs.existsSync(patchedCachePaths.js) 53 | && !getModuleFile(patchedCachePaths.js).patchDetail?.isOutdated; 54 | 55 | let js: string | undefined; 56 | let dts: string | undefined; 57 | if (canUseCache) { 58 | js = readFileWithLock(patchedCachePaths.js); 59 | dts = !options?.skipDts && patchedCachePaths.dts ? readFileWithLock(patchedCachePaths.dts) : undefined; 60 | } else { 61 | const res = patchModule(tsModule, options?.skipDts); 62 | js = res.js; 63 | dts = res.dts; 64 | 65 | /* Write patched cache */ 66 | if (!skipCache) { 67 | const cacheDir = path.dirname(patchedCachePaths.js); 68 | 69 | for (const [ key, patchPath ] of Object.entries(patchedCachePaths)) { 70 | const srcPath = key === 'dts' ? dts : js; 71 | if (key === 'dts' && options?.skipDts) continue; 72 | if (!srcPath) continue; 73 | 74 | log?.([ '~', `Writing patched cache to ${chalk.blueBright(patchPath)}` ], LogLevel.verbose); 75 | 76 | mkdirIfNotExist(cacheDir); 77 | writeFileWithLock(patchPath, srcPath); 78 | } 79 | } 80 | } 81 | 82 | return { js, dts, loadedFromCache: canUseCache }; 83 | } 84 | 85 | // endregion 86 | -------------------------------------------------------------------------------- /projects/core/src/patch/patch-detail.ts: -------------------------------------------------------------------------------- 1 | import { TsModule } from '../module'; 2 | import { corePatchName, tspPackageJSON } from '../config'; 3 | import semver from 'semver'; 4 | import { getHash } from '../utils'; 5 | 6 | 7 | /* ****************************************************************************************************************** */ 8 | // region: Config 9 | /* ****************************************************************************************************************** */ 10 | 11 | export const tspHeaderBlockStart = '/// tsp-module:'; 12 | export const tspHeaderBlockStop = '/// :tsp-module'; 13 | export const currentPatchDetailVersion = '0.1.0'; 14 | 15 | // endregion 16 | 17 | 18 | /* ****************************************************************************************************************** */ 19 | // region: Types 20 | /* ****************************************************************************************************************** */ 21 | 22 | export interface PatchDetail { 23 | tsVersion: string 24 | tspVersion: string 25 | patchDetailVersion: string 26 | moduleName: TsModule.Name 27 | originalHash: string 28 | hash: string 29 | patches: PatchDetail.PatchEntry[] 30 | } 31 | 32 | export namespace PatchDetail { 33 | export interface PatchEntry { 34 | library: string 35 | version: string 36 | patchName?: string 37 | hash?: string 38 | blocksCache?: boolean 39 | } 40 | } 41 | 42 | // endregion 43 | 44 | 45 | /* ****************************************************************************************************************** */ 46 | // region: PatchDetail (class) 47 | /* ****************************************************************************************************************** */ 48 | 49 | export class PatchDetail { 50 | 51 | /* ********************************************************* */ 52 | // region: Methods 53 | /* ********************************************************* */ 54 | 55 | get isOutdated() { 56 | const packageVersion = tspPackageJSON.version; 57 | return semver.gt(packageVersion, this.tspVersion); 58 | } 59 | 60 | toHeader() { 61 | const lines = JSON.stringify(this, null, 2) 62 | .split('\n') 63 | .map(line => `/// ${line}`) 64 | .join('\n'); 65 | 66 | return `${tspHeaderBlockStart}\n${lines}\n${tspHeaderBlockStop}`; 67 | } 68 | 69 | static fromHeader(header: string | string[]) { 70 | const headerLines = Array.isArray(header) ? header : header.split('\n'); 71 | 72 | let patchDetail: PatchDetail | undefined; 73 | const startIdx = headerLines.findIndex(line => line === tspHeaderBlockStart) + 1; 74 | let endIdx = headerLines.findIndex(line => line === tspHeaderBlockStop); 75 | if (endIdx === -1) headerLines.length; 76 | if (startIdx && endIdx) { 77 | const patchInfoStr = headerLines 78 | .slice(startIdx, endIdx) 79 | .map(line => line.replace('/// ', '')) 80 | .join('\n'); 81 | patchDetail = Object.assign(new PatchDetail(), JSON.parse(patchInfoStr) as PatchDetail); 82 | } 83 | 84 | return patchDetail; 85 | } 86 | 87 | static fromModule(tsModule: TsModule, patchedContent: string, patches: PatchDetail.PatchEntry[] = []) { 88 | patches.unshift({ library: 'ts-patch', patchName: corePatchName, version: tspPackageJSON.version }); 89 | 90 | const patchDetail = { 91 | tsVersion: tsModule.package.version, 92 | tspVersion: tspPackageJSON.version, 93 | patchDetailVersion: currentPatchDetailVersion, 94 | moduleName: tsModule.moduleName, 95 | originalHash: tsModule.cacheKey, 96 | hash: getHash(patchedContent), 97 | patches: patches 98 | } satisfies Omit 99 | 100 | return Object.assign(new PatchDetail(), patchDetail); 101 | } 102 | 103 | // endregion 104 | } 105 | 106 | // endregion 107 | -------------------------------------------------------------------------------- /projects/core/src/patch/transformers/add-original-create-program.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import { PatchError } from '../../system'; 3 | 4 | 5 | /* ****************************************************************************************************************** */ 6 | // region: Config 7 | /* ****************************************************************************************************************** */ 8 | 9 | export const createProgramExportFiles = [ 10 | /* TS < 5.4 */ 11 | 'src/typescript/_namespaces/ts.ts', 12 | 13 | /* TS >= 5.4 */ 14 | 'src/server/_namespaces/ts.ts', 15 | 'src/typescript/typescript.ts' 16 | ] 17 | 18 | // endregion 19 | 20 | 21 | /* ****************************************************************************************************************** */ 22 | // region: Utils 23 | /* ****************************************************************************************************************** */ 24 | 25 | export function addOriginalCreateProgramTransformer(context: ts.TransformationContext) { 26 | const { factory } = context; 27 | 28 | let patchSuccess = false; 29 | 30 | return (sourceFile: ts.SourceFile) => { 31 | if (!createProgramExportFiles.includes(sourceFile.fileName)) 32 | throw new Error('Wrong emitter file sent to transformer! This should be unreachable.'); 33 | 34 | const res = factory.updateSourceFile(sourceFile, ts.visitNodes(sourceFile.statements, visitNodes) as unknown as ts.Statement[]); 35 | 36 | if (!patchSuccess) throw new PatchError('Failed to patch typescript originalCreateProgram!'); 37 | 38 | return res; 39 | 40 | function visitNodes(node: ts.Statement): ts.VisitResult { 41 | /* Handle: __export({ ... }) */ 42 | if ( 43 | ts.isExpressionStatement(node) && 44 | ts.isCallExpression(node.expression) && 45 | node.expression.expression.getText() === '__export' 46 | ) { 47 | const exportObjectLiteral = node.expression.arguments[1]; 48 | if (ts.isObjectLiteralExpression(exportObjectLiteral)) { 49 | const originalCreateProgramProperty = factory.createPropertyAssignment( 50 | 'originalCreateProgram', 51 | factory.createArrowFunction( 52 | undefined, 53 | undefined, 54 | [], 55 | undefined, 56 | factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 57 | factory.createIdentifier('originalCreateProgram') 58 | ) 59 | ); 60 | 61 | const updatedExportObjectLiteral = factory.updateObjectLiteralExpression( 62 | exportObjectLiteral, 63 | [ ...exportObjectLiteral.properties, originalCreateProgramProperty ] 64 | ); 65 | 66 | const updatedNode = factory.updateExpressionStatement( 67 | node, 68 | factory.updateCallExpression( 69 | node.expression, 70 | node.expression.expression, 71 | undefined, 72 | [ node.expression.arguments[0], updatedExportObjectLiteral ] 73 | ) 74 | ); 75 | 76 | patchSuccess = true; 77 | return updatedNode; 78 | } 79 | } 80 | 81 | /* Handle: 1 && (module.exports = { ... }) (ts5.5+) */ 82 | if ( 83 | ts.isExpressionStatement(node) && ts.isBinaryExpression(node.expression) && 84 | node.expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && 85 | ts.isParenthesizedExpression(node.expression.right) && 86 | ts.isBinaryExpression(node.expression.right.expression) && 87 | node.expression.right.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken && 88 | ts.isPropertyAccessExpression(node.expression.right.expression.left) && 89 | node.expression.right.expression.left.expression.getText() === 'module' && 90 | node.expression.right.expression.left.name.getText() === 'exports' && 91 | ts.isObjectLiteralExpression(node.expression.right.expression.right) 92 | ) { 93 | // Add originalCreateProgram to the object literal 94 | const originalCreateProgramProperty = factory.createShorthandPropertyAssignment('originalCreateProgram'); 95 | 96 | const updatedObjectLiteral = factory.updateObjectLiteralExpression( 97 | node.expression.right.expression.right, 98 | [ ...node.expression.right.expression.right.properties, originalCreateProgramProperty ] 99 | ); 100 | 101 | // Update the node 102 | const updatedNode = factory.updateExpressionStatement( 103 | node, 104 | factory.updateBinaryExpression( 105 | node.expression, 106 | node.expression.left, 107 | node.expression.operatorToken, 108 | factory.updateParenthesizedExpression( 109 | node.expression.right, 110 | factory.updateBinaryExpression( 111 | node.expression.right.expression, 112 | node.expression.right.expression.left, 113 | node.expression.right.expression.operatorToken, 114 | updatedObjectLiteral 115 | ) 116 | ) 117 | ) 118 | ); 119 | 120 | patchSuccess = true; 121 | return updatedNode; 122 | } 123 | 124 | return node; 125 | } 126 | }; 127 | } 128 | 129 | // endregion 130 | -------------------------------------------------------------------------------- /projects/core/src/patch/transformers/fix-ts-early-return.ts: -------------------------------------------------------------------------------- 1 | import ts, { isReturnStatement } from 'typescript'; 2 | 3 | 4 | /* ****************************************************************************************************************** */ 5 | // region: Utils 6 | /* ****************************************************************************************************************** */ 7 | 8 | export function fixTsEarlyReturnTransformer(context: ts.TransformationContext) { 9 | const { factory } = context; 10 | 11 | let patchSuccess = false; 12 | 13 | return (sourceFile: ts.SourceFile) => { 14 | if (sourceFile.fileName !== 'src/typescript/typescript.ts') 15 | throw new Error('Wrong emitter file sent to transformer! This should be unreachable.'); 16 | 17 | const res = factory.updateSourceFile(sourceFile, ts.visitNodes(sourceFile.statements, visitNodes) as unknown as ts.Statement[]); 18 | 19 | if (!patchSuccess) throw new Error('Failed to patch typescript early return!'); 20 | 21 | return res; 22 | 23 | function visitNodes(node: ts.Statement): ts.VisitResult { 24 | if (isReturnStatement(node)) { 25 | patchSuccess = true; 26 | 27 | return factory.createVariableStatement( 28 | undefined, 29 | factory.createVariableDeclarationList( 30 | [factory.createVariableDeclaration( 31 | factory.createIdentifier("returnResult"), 32 | undefined, 33 | undefined, 34 | node.expression! 35 | )], 36 | ts.NodeFlags.None 37 | ) 38 | ) 39 | } 40 | 41 | return node; 42 | } 43 | }; 44 | } 45 | 46 | // endregion 47 | -------------------------------------------------------------------------------- /projects/core/src/patch/transformers/hook-tsc-exec.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | import { execTscCmd } from '../../config'; 3 | 4 | 5 | /* ****************************************************************************************************************** */ 6 | // region: Utils 7 | /* ****************************************************************************************************************** */ 8 | 9 | export function hookTscExecTransformer(context: ts.TransformationContext) { 10 | const { factory } = context; 11 | 12 | let patchSuccess = false; 13 | 14 | return (sourceFile: ts.SourceFile) => { 15 | if (sourceFile.fileName !== 'src/tsc/tsc.ts') 16 | throw new Error('Wrong emitter file sent to transformer! This should be unreachable.'); 17 | 18 | const res = factory.updateSourceFile(sourceFile, ts.visitNodes(sourceFile.statements, visitNodes) as unknown as ts.Statement[]); 19 | 20 | if (!patchSuccess) throw new Error('Failed to patch tsc exec statement early return!'); 21 | 22 | return res; 23 | 24 | function visitNodes(node: ts.Statement): ts.VisitResult { 25 | if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression) && 26 | ts.isIdentifier(node.expression.expression) && node.expression.expression.text === 'executeCommandLine' 27 | ) { 28 | patchSuccess = true; 29 | 30 | return factory.createExpressionStatement(factory.createBinaryExpression( 31 | factory.createPropertyAccessExpression( 32 | factory.createIdentifier("tsp"), 33 | factory.createIdentifier("execTsc") 34 | ), 35 | factory.createToken(ts.SyntaxKind.EqualsToken), 36 | factory.createArrowFunction( 37 | undefined, 38 | undefined, 39 | [], 40 | undefined, 41 | factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 42 | node.expression 43 | ) 44 | )); 45 | } 46 | 47 | return node; 48 | } 49 | } 50 | } 51 | 52 | // endregion 53 | -------------------------------------------------------------------------------- /projects/core/src/patch/transformers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fix-ts-early-return' 2 | export * from './patch-create-program' 3 | export * from './patch-emitter' 4 | export * from './merge-statements' 5 | export * from './add-original-create-program' 6 | export * from './hook-tsc-exec' 7 | -------------------------------------------------------------------------------- /projects/core/src/patch/transformers/merge-statements.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | 4 | /* ****************************************************************************************************************** */ 5 | // region: Utils 6 | /* ****************************************************************************************************************** */ 7 | 8 | export function createMergeStatementsTransformer( 9 | baseSourceFile: ts.SourceFile, 10 | sourceFile: ts.SourceFile 11 | ): ts.TransformerFactory { 12 | const replacements = new Map(); 13 | 14 | for (const node of sourceFile.statements) { 15 | if (ts.isVariableStatement(node)) { 16 | const name = (node.declarationList.declarations[0].name as ts.Identifier).text; 17 | replacements.set(name, node); 18 | } else if (ts.isFunctionDeclaration(node) && node.name) { 19 | const name = node.name.text; 20 | replacements.set(name, node); 21 | } 22 | } 23 | 24 | return (context: ts.TransformationContext) => { 25 | const { factory } = context; 26 | 27 | return (node: ts.SourceFile) => { 28 | if (node.fileName !== baseSourceFile.fileName) return node; 29 | 30 | const transformedStatements: ts.Statement[] = []; 31 | 32 | node.statements.forEach((statement) => { 33 | if (ts.isVariableStatement(statement)) { 34 | const name = (statement.declarationList.declarations[0].name as ts.Identifier).text; 35 | if (replacements.has(name)) { 36 | transformedStatements.push(replacements.get(name)!); 37 | replacements.delete(name); 38 | } else { 39 | transformedStatements.push(statement); 40 | } 41 | } else if (ts.isFunctionDeclaration(statement) && statement.name) { 42 | const name = statement.name.text; 43 | if (replacements.has(name)) { 44 | transformedStatements.push(replacements.get(name)!); 45 | replacements.delete(name); 46 | } else { 47 | transformedStatements.push(statement); 48 | } 49 | } else { 50 | transformedStatements.push(statement); 51 | } 52 | }); 53 | 54 | replacements.forEach((value) => transformedStatements.push(value)); 55 | 56 | return factory.updateSourceFile(node, transformedStatements); 57 | }; 58 | }; 59 | } 60 | 61 | // endregion 62 | -------------------------------------------------------------------------------- /projects/core/src/patch/transformers/patch-create-program.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | 4 | /* ****************************************************************************************************************** */ 5 | // region: Utils 6 | /* ****************************************************************************************************************** */ 7 | 8 | export function patchCreateProgramTransformer(context: ts.TransformationContext) { 9 | const { factory } = context; 10 | 11 | let patchSuccess = false; 12 | 13 | return (sourceFile: ts.SourceFile) => { 14 | if (sourceFile.fileName !== 'src/compiler/program.ts') 15 | throw new Error('Wrong program file sent to transformer! This should be unreachable.'); 16 | 17 | const res = factory.updateSourceFile(sourceFile, ts.visitNodes(sourceFile.statements, visitNode) as unknown as ts.Statement[]); 18 | 19 | if (!patchSuccess) throw new Error('Failed to patch createProgram function!'); 20 | 21 | return res; 22 | 23 | function visitNode(node: ts.Statement): ts.VisitResult { 24 | if (ts.isFunctionDeclaration(node) && node.name?.getText() === 'createProgram') { 25 | const originalCreateProgram = factory.updateFunctionDeclaration( 26 | node, 27 | node.modifiers, 28 | node.asteriskToken, 29 | factory.createIdentifier('originalCreateProgram'), 30 | node.typeParameters, 31 | node.parameters, 32 | node.type, 33 | node.body 34 | ); 35 | 36 | // function createProgram() { return tsp.originalCreateProgram(...arguments); } 37 | const newCreateProgram = factory.createFunctionDeclaration( 38 | undefined, 39 | undefined, 40 | 'createProgram', 41 | undefined, 42 | [], 43 | undefined, 44 | factory.createBlock([ 45 | factory.createReturnStatement( 46 | factory.createCallExpression( 47 | factory.createPropertyAccessExpression( 48 | factory.createIdentifier('tsp'), 49 | factory.createIdentifier('createProgram') 50 | ), 51 | undefined, 52 | [ factory.createSpreadElement(factory.createIdentifier('arguments')) ] 53 | ) 54 | ), 55 | ]) 56 | ); 57 | 58 | patchSuccess = true; 59 | 60 | return [ newCreateProgram, originalCreateProgram ]; 61 | } 62 | 63 | return node; 64 | } 65 | } 66 | } 67 | 68 | // endregion 69 | -------------------------------------------------------------------------------- /projects/core/src/patch/transformers/patch-emitter.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | 4 | /* ****************************************************************************************************************** */ 5 | // region: Utils 6 | /* ****************************************************************************************************************** */ 7 | 8 | export function patchEmitterTransformer(context: ts.TransformationContext) { 9 | const { factory } = context; 10 | 11 | let patchSuccess = false; 12 | 13 | return (sourceFile: ts.SourceFile) => { 14 | if (sourceFile.fileName !== 'src/compiler/watch.ts') 15 | throw new Error('Wrong emitter file sent to transformer! This should be unreachable.'); 16 | 17 | const res = factory.updateSourceFile(sourceFile, ts.visitNodes(sourceFile.statements, visitRootNodes) as unknown as ts.Statement[]); 18 | 19 | if (!patchSuccess) throw new Error('Failed to patch emitFilesAndReportErrors function!'); 20 | 21 | return res; 22 | 23 | function visitRootNodes(node: ts.Statement): ts.VisitResult { 24 | if (ts.isFunctionDeclaration(node) && node.name && node.name.getText() === 'emitFilesAndReportErrors') { 25 | const newBodyStatements = ts.visitNodes(node.body!.statements, visitEmitterNodes) as unknown as ts.Statement[]; 26 | 27 | return factory.updateFunctionDeclaration( 28 | node, 29 | node.modifiers, 30 | node.asteriskToken, 31 | node.name, 32 | node.typeParameters, 33 | node.parameters, 34 | node.type, 35 | factory.updateBlock(node.body!, newBodyStatements) 36 | ); 37 | } 38 | 39 | return node; 40 | } 41 | 42 | function visitEmitterNodes(node: ts.Statement): ts.VisitResult { 43 | if ( 44 | ts.isVariableStatement(node) && 45 | node.declarationList.declarations.some( 46 | (declaration) => ts.isVariableDeclaration(declaration) && declaration.name.getText() === 'emitResult' 47 | ) 48 | ) { 49 | // tsp.diagnosticMap.set(program, allDiagnostics); 50 | const insertedMapSetterNode = factory.createExpressionStatement(factory.createCallExpression( 51 | factory.createPropertyAccessExpression( 52 | factory.createPropertyAccessExpression( 53 | factory.createIdentifier('tsp'), 54 | factory.createIdentifier('diagnosticMap') 55 | ), 56 | factory.createIdentifier('set') 57 | ), 58 | undefined, 59 | [ 60 | factory.createIdentifier('program'), 61 | factory.createIdentifier('allDiagnostics') 62 | ] 63 | )); 64 | 65 | patchSuccess = true; 66 | 67 | return [ insertedMapSetterNode, node ]; 68 | } 69 | 70 | return node; 71 | } 72 | }; 73 | } 74 | 75 | // endregion 76 | -------------------------------------------------------------------------------- /projects/core/src/slice/module-slice.ts: -------------------------------------------------------------------------------- 1 | import { ModuleFile } from '../module'; 2 | import { Position } from '../system'; 3 | import semver from 'semver'; 4 | import { sliceTs54 } from './ts54'; 5 | import { sliceTs55 } from './ts55'; 6 | import { sliceTs552 } from './ts552'; 7 | 8 | 9 | /* ****************************************************************************************************************** */ 10 | // region: Types 11 | /* ****************************************************************************************************************** */ 12 | 13 | export interface ModuleSlice { 14 | moduleFile: ModuleFile 15 | firstSourceFileStart: number 16 | wrapperPos?: Position 17 | bodyPos: Position 18 | fileEnd: number 19 | sourceFileStarts: [ name: string, position: number ][] 20 | bodyWrapper?: { 21 | start: string; 22 | end: string; 23 | } 24 | } 25 | 26 | // endregion 27 | 28 | 29 | /* ****************************************************************************************************************** */ 30 | // region: Utils 31 | /* ****************************************************************************************************************** */ 32 | 33 | export function sliceModule(moduleFile: ModuleFile, tsVersion: string) { 34 | const baseVersion = semver.coerce(tsVersion, { includePrerelease: false }); 35 | if (!baseVersion) throw new Error(`Could not parse TS version: ${tsVersion}`); 36 | 37 | if (semver.lt(baseVersion, '5.0.0')) { 38 | throw new Error(`Cannot patch TS version <5`); 39 | } 40 | 41 | if (semver.lt(baseVersion, '5.5.0')) { 42 | return sliceTs54(moduleFile); 43 | } 44 | 45 | if (semver.lt(baseVersion, '5.5.2')) { 46 | return sliceTs55(moduleFile); 47 | } 48 | 49 | return sliceTs552(moduleFile); 50 | } 51 | 52 | /** @internal */ 53 | export namespace ModuleSlice { 54 | export const createError = (msg?: string) => 55 | new Error(`Could not recognize TS format during slice!` + (msg ? ` — ${msg}` : '')); 56 | } 57 | 58 | // endregion 59 | 60 | -------------------------------------------------------------------------------- /projects/core/src/slice/ts54.ts: -------------------------------------------------------------------------------- 1 | import { ModuleFile } from '../module'; 2 | import { ModuleSlice } from './module-slice'; 3 | 4 | 5 | /* ****************************************************************************************************************** */ 6 | // region: Utils 7 | /* ****************************************************************************************************************** */ 8 | 9 | /** 10 | * Slice 5.0 - 5.4 11 | */ 12 | export function sliceTs54(moduleFile: ModuleFile): ModuleSlice { 13 | let firstSourceFileStart: number; 14 | let wrapperStart: number | undefined; 15 | let wrapperEnd: number | undefined; 16 | let bodyStart: number; 17 | let bodyEnd: number; 18 | let sourceFileStarts: [ name: string, position: number ][] = []; 19 | 20 | const { content } = moduleFile; 21 | 22 | /* Find Wrapper or First File */ 23 | let matcher = /^(?:\s*\/\/\s*src\/)|(?:var\s+ts\s*=.+)/gm; 24 | 25 | const firstMatch = matcher.exec(content); 26 | if (!firstMatch?.[0]) throw ModuleSlice.createError(); 27 | 28 | /* Handle wrapped */ 29 | if (firstMatch[0].startsWith('var')) { 30 | wrapperStart = firstMatch.index; 31 | bodyStart = firstMatch.index + firstMatch[0].length + 1; 32 | 33 | /* Find First File */ 34 | matcher = /^\s*\/\/\s*src\//gm; 35 | matcher.lastIndex = wrapperStart; 36 | 37 | const firstFileMatch = matcher.exec(content); 38 | if (!firstFileMatch?.[0]) throw ModuleSlice.createError(); 39 | 40 | firstSourceFileStart = firstFileMatch.index; 41 | 42 | /* Find Wrapper end */ 43 | matcher = /^}\)\(\)\s*;?/gm; 44 | matcher.lastIndex = firstFileMatch.index; 45 | const wrapperEndMatch = matcher.exec(content); 46 | if (!wrapperEndMatch?.[0]) throw ModuleSlice.createError(); 47 | 48 | bodyEnd = wrapperEndMatch.index - 1; 49 | wrapperEnd = wrapperEndMatch.index + wrapperEndMatch[0].length; 50 | } 51 | /* Handle non-wrapped */ 52 | else { 53 | firstSourceFileStart = firstMatch.index; 54 | bodyStart = firstMatch.index + firstMatch[0].length; 55 | bodyEnd = content.length; 56 | } 57 | 58 | /* Get Source File Positions */ 59 | matcher = /^\s*\/\/\s*(src\/.+)$/gm; 60 | matcher.lastIndex = firstSourceFileStart; 61 | for (let match = matcher.exec(content); match != null; match = matcher.exec(content)) { 62 | sourceFileStarts.push([ match[1], match.index ]); 63 | } 64 | 65 | return { 66 | moduleFile, 67 | firstSourceFileStart, 68 | wrapperPos: wrapperStart != null ? { start: wrapperStart, end: wrapperEnd! } : undefined, 69 | fileEnd: content.length, 70 | bodyPos: { start: bodyStart, end: bodyEnd }, 71 | sourceFileStarts, 72 | bodyWrapper: { 73 | start: 'var ts = (() => {', 74 | end: '})();' 75 | } 76 | }; 77 | } 78 | 79 | // endregion 80 | -------------------------------------------------------------------------------- /projects/core/src/slice/ts55.ts: -------------------------------------------------------------------------------- 1 | import { ModuleFile } from '../module'; 2 | import { ModuleSlice } from './module-slice'; 3 | 4 | 5 | /* ****************************************************************************************************************** */ 6 | // region: Utils 7 | /* ****************************************************************************************************************** */ 8 | 9 | /** 10 | * Slice 5.5+ 11 | */ 12 | export function sliceTs55(moduleFile: ModuleFile): ModuleSlice { 13 | let firstSourceFileStart: number; 14 | let wrapperStart: number | undefined; 15 | let wrapperEnd: number | undefined; 16 | let bodyStart: number; 17 | let bodyEnd: number; 18 | let sourceFileStarts: [ name: string, position: number ][] = []; 19 | 20 | const { content } = moduleFile; 21 | 22 | /* Find Wrapper or First File */ 23 | let matcher = /^(?:\s*\/\/\s*src\/)|(?:var\s+ts\s*=.+)/gm; 24 | 25 | const firstMatch = matcher.exec(content); 26 | if (!firstMatch?.[0]) throw ModuleSlice.createError(); 27 | let bodyWrapper: undefined | { start: string; end: string } = undefined; 28 | 29 | /* Handle wrapped */ 30 | if (firstMatch[0].startsWith('var')) { 31 | wrapperStart = firstMatch.index; 32 | bodyStart = firstMatch.index + firstMatch[0].length + 1; 33 | 34 | /* Find First File */ 35 | matcher = /^\s*\/\/\s*src\//gm; 36 | matcher.lastIndex = wrapperStart; 37 | 38 | const firstFileMatch = matcher.exec(content); 39 | if (!firstFileMatch?.[0]) throw ModuleSlice.createError(); 40 | 41 | firstSourceFileStart = firstFileMatch.index; 42 | 43 | /* Find Wrapper end */ 44 | // TODO - We may later want to find a better approach, but this will work for now 45 | matcher = /^}\)\(typeof module !== "undefined" .+$/gm; 46 | matcher.lastIndex = firstFileMatch.index; 47 | const wrapperEndMatch = matcher.exec(content); 48 | if (!wrapperEndMatch?.[0]) throw ModuleSlice.createError(); 49 | 50 | bodyEnd = wrapperEndMatch.index - 1; 51 | wrapperEnd = wrapperEndMatch.index + wrapperEndMatch[0].length; 52 | 53 | bodyWrapper = { start: firstMatch[0], end: wrapperEndMatch[0] }; 54 | } 55 | /* Handle non-wrapped */ 56 | else { 57 | firstSourceFileStart = firstMatch.index; 58 | bodyStart = firstMatch.index + firstMatch[0].length; 59 | bodyEnd = content.length; 60 | } 61 | 62 | /* Get Source File Positions */ 63 | matcher = /^\s*\/\/\s*(src\/.+)$/gm; 64 | matcher.lastIndex = firstSourceFileStart; 65 | for (let match = matcher.exec(content); match != null; match = matcher.exec(content)) { 66 | sourceFileStarts.push([ match[1], match.index ]); 67 | } 68 | 69 | return { 70 | moduleFile, 71 | firstSourceFileStart, 72 | wrapperPos: wrapperStart != null ? { start: wrapperStart, end: wrapperEnd! } : undefined, 73 | fileEnd: content.length, 74 | bodyPos: { start: bodyStart, end: bodyEnd }, 75 | sourceFileStarts, 76 | bodyWrapper 77 | }; 78 | } 79 | 80 | // endregion 81 | -------------------------------------------------------------------------------- /projects/core/src/slice/ts552.ts: -------------------------------------------------------------------------------- 1 | import { ModuleFile } from '../module'; 2 | import { ModuleSlice } from './module-slice'; 3 | 4 | 5 | /* ****************************************************************************************************************** */ 6 | // region: Utils 7 | /* ****************************************************************************************************************** */ 8 | 9 | /** 10 | * Slice 5.5.2+ 11 | */ 12 | export function sliceTs552(moduleFile: ModuleFile): ModuleSlice { 13 | let firstSourceFileStart: number; 14 | let wrapperStart: number | undefined; 15 | let wrapperEnd: number | undefined; 16 | let bodyStart: number; 17 | let bodyEnd: number; 18 | let sourceFileStarts: [ name: string, position: number ][] = []; 19 | 20 | const { content } = moduleFile; 21 | 22 | /* Find Wrapper or First File */ 23 | let matcher = /^(?:\s*\/\/\s*src\/)|(?:var\s+ts\s*=.+)/gm; 24 | 25 | const firstMatch = matcher.exec(content); 26 | if (!firstMatch?.[0]) throw ModuleSlice.createError(); 27 | let bodyWrapper: undefined | { start: string; end: string } = undefined; 28 | 29 | /* Handle wrapped */ 30 | if (firstMatch[0].startsWith('var')) { 31 | wrapperStart = firstMatch.index; 32 | bodyStart = firstMatch.index + firstMatch[0].length + 1; 33 | 34 | /* Find First File */ 35 | matcher = /^\s*\/\/\s*src\//gm; 36 | matcher.lastIndex = wrapperStart; 37 | 38 | const firstFileMatch = matcher.exec(content); 39 | if (!firstFileMatch?.[0]) throw ModuleSlice.createError(); 40 | 41 | firstSourceFileStart = firstFileMatch.index; 42 | 43 | /* Find Wrapper end */ 44 | // TODO - We may later want to find a better approach, but this will work for now 45 | matcher = /^}\)\({ get exports\(\) { return ts; }.+$/gm; 46 | matcher.lastIndex = firstFileMatch.index; 47 | const wrapperEndMatch = matcher.exec(content); 48 | if (!wrapperEndMatch?.[0]) throw ModuleSlice.createError(); 49 | 50 | bodyEnd = wrapperEndMatch.index - 1; 51 | wrapperEnd = wrapperEndMatch.index + wrapperEndMatch[0].length; 52 | 53 | bodyWrapper = { start: firstMatch[0], end: wrapperEndMatch[0] }; 54 | } 55 | /* Handle non-wrapped */ 56 | else { 57 | firstSourceFileStart = firstMatch.index; 58 | bodyStart = firstMatch.index + firstMatch[0].length; 59 | bodyEnd = content.length; 60 | } 61 | 62 | /* Get Source File Positions */ 63 | matcher = /^\s*\/\/\s*(src\/.+)$/gm; 64 | matcher.lastIndex = firstSourceFileStart; 65 | for (let match = matcher.exec(content); match != null; match = matcher.exec(content)) { 66 | sourceFileStarts.push([ match[1], match.index ]); 67 | } 68 | 69 | return { 70 | moduleFile, 71 | firstSourceFileStart, 72 | wrapperPos: wrapperStart != null ? { start: wrapperStart, end: wrapperEnd! } : undefined, 73 | fileEnd: content.length, 74 | bodyPos: { start: bodyStart, end: bodyEnd }, 75 | sourceFileStarts, 76 | bodyWrapper 77 | }; 78 | } 79 | 80 | // endregion 81 | -------------------------------------------------------------------------------- /projects/core/src/system/cache.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import * as os from 'os'; 3 | import { findCacheDirectory } from '../utils'; 4 | import { appRoot, lockFileDir } from '../config'; 5 | import fs from 'fs'; 6 | 7 | 8 | /* ****************************************************************************************************************** */ 9 | // region: Locals 10 | /* ****************************************************************************************************************** */ 11 | 12 | let cacheRoot: string | undefined; 13 | let lockFileRoot: string | undefined; 14 | 15 | // endregion 16 | 17 | 18 | /* ****************************************************************************************************************** */ 19 | // region: Utils 20 | /* ****************************************************************************************************************** */ 21 | 22 | export function getCacheRoot() { 23 | if (!cacheRoot) { 24 | cacheRoot = 25 | process.env.TSP_CACHE_DIR || 26 | findCacheDirectory({ name: 'ts-patch', cwd: path.resolve(appRoot, '..') }) || 27 | path.join(os.tmpdir(), 'ts-patch'); 28 | 29 | if (!fs.existsSync(cacheRoot)) fs.mkdirSync(cacheRoot, { recursive: true }); 30 | } 31 | 32 | return cacheRoot; 33 | } 34 | 35 | export function getCachePath(key: string, ...p: string[]) { 36 | return path.resolve(getCacheRoot(), key, ...p); 37 | } 38 | 39 | export function getLockFilePath(key: string) { 40 | if (!lockFileRoot) { 41 | lockFileRoot = path.join(getCacheRoot(), lockFileDir); 42 | if (!fs.existsSync(lockFileRoot)) fs.mkdirSync(lockFileRoot, { recursive: true }); 43 | } 44 | 45 | return path.join(getCacheRoot(), lockFileDir, key); 46 | } 47 | 48 | // endregion 49 | -------------------------------------------------------------------------------- /projects/core/src/system/errors.ts: -------------------------------------------------------------------------------- 1 | /* ******************************************************************************************************************** 2 | * Errors Classes 3 | * ********************************************************************************************************************/ 4 | 5 | export class TspError extends Error { } 6 | 7 | export class WrongTSVersion extends TspError {name = 'WrongTSVersion'} 8 | 9 | export class FileNotFound extends TspError {name = 'FileNotFound'} 10 | 11 | export class PackageError extends TspError {name = 'PackageError'} 12 | 13 | export class PatchError extends TspError {name = 'PatchError'} 14 | 15 | export class PersistenceError extends TspError {name = 'PersistenceError'} 16 | 17 | export class OptionsError extends TspError {name = 'OptionsError'} 18 | 19 | export class NPMError extends TspError {name = 'NPMError'} 20 | 21 | export class RestoreError extends TspError { 22 | constructor(public filename: string, message: string) { 23 | super(`Error restoring: ${filename}${message ? ' - ' + message : ''}`); 24 | this.name = 'RestoreError'; 25 | } 26 | } 27 | 28 | export class BackupError extends TspError { 29 | constructor(public filename: string, message: string) { 30 | super(`Error backing up ${filename}${message ? ' - ' + message : ''}`); 31 | this.name = 'BackupError'; 32 | } 33 | } 34 | 35 | export class FileWriteError extends TspError { 36 | constructor(public filename: string, message?: string) { 37 | super(`Error while trying to write to ${filename}${message ? `: ${message}` : ''}`); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/core/src/system/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cache'; 2 | export * from './errors'; 3 | export * from './logger'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /projects/core/src/system/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import stripAnsi from 'strip-ansi'; 3 | 4 | 5 | /* ****************************************************************************************************************** */ 6 | // region: Types 7 | /* ****************************************************************************************************************** */ 8 | 9 | export enum LogLevel { 10 | system = 0, 11 | normal = 1, 12 | verbose = 2, 13 | } 14 | 15 | export type Logger = (msg: string | [ string, string ], logLevel?: LogLevel) => void; 16 | 17 | // endregion 18 | 19 | 20 | /* ****************************************************************************************************************** */ 21 | // region: Utils 22 | /* ****************************************************************************************************************** */ 23 | 24 | export function createLogger(logLevel: LogLevel, useColour: boolean = true, isSilent: boolean = false): Logger { 25 | return function log(msg: string | [ string, string ], msgLogLevel: LogLevel = LogLevel.normal) { 26 | if (isSilent || msgLogLevel > logLevel) return; 27 | 28 | /* Handle Icon */ 29 | const printIcon = (icon: string) => chalk.bold.cyanBright(`[${icon}] `); 30 | 31 | let icon: string = ''; 32 | if (Array.isArray(msg)) { 33 | icon = msg[0]; 34 | 35 | // @formatter:off 36 | msg = (icon === '!') ? printIcon(chalk.bold.yellow(icon)) + chalk.yellow(msg[1]) : 37 | (icon === '~') ? printIcon(chalk.bold.cyanBright(icon)) + msg[1] : 38 | (icon === '=') ? printIcon(chalk.bold.greenBright(icon)) + msg[1] : 39 | (icon === '+') ? printIcon(chalk.bold.green(icon)) + msg[1] : 40 | (icon === '-') ? printIcon(chalk.bold.white(icon)) + msg[1] : 41 | msg[1]; 42 | // @formatter:on 43 | } 44 | 45 | /* Print message */ 46 | const isError = (icon === '!'); 47 | 48 | msg = !useColour ? stripAnsi(msg) : msg; 49 | (isError ? console.error : console.log)(msg); 50 | } 51 | } 52 | 53 | // endregion 54 | -------------------------------------------------------------------------------- /projects/core/src/system/types.ts: -------------------------------------------------------------------------------- 1 | 2 | /* ****************************************************************************************************************** */ 3 | // region: Types 4 | /* ****************************************************************************************************************** */ 5 | 6 | export interface Position { 7 | start: number 8 | end: number 9 | } 10 | 11 | // endregion 12 | -------------------------------------------------------------------------------- /projects/core/src/ts-package.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import resolve from 'resolve'; 4 | import { PackageError } from './system'; 5 | import { TsModule } from './module'; 6 | 7 | 8 | /* ****************************************************************************************************************** */ 9 | // region: Types 10 | /* ****************************************************************************************************************** */ 11 | 12 | export interface TsPackage { 13 | majorVer: number 14 | minorVer: number 15 | version: string 16 | packageFile: string 17 | packageDir: string 18 | cacheDir: string 19 | libDir: string 20 | 21 | moduleNames: TsModule.Name[] 22 | 23 | /** @internal */ 24 | moduleCache: Map 25 | 26 | getModulePath: (name: TsModule.Name) => string 27 | } 28 | 29 | // endregion 30 | 31 | 32 | /* ****************************************************************************************************************** */ 33 | // region: Utils 34 | /* ****************************************************************************************************************** */ 35 | 36 | /** 37 | * Get TypeScript package info - Resolve from dir, throws if not cannot find TS package 38 | */ 39 | export function getTsPackage(dir: string = process.cwd()): TsPackage { 40 | if (!fs.existsSync(dir)) throw new PackageError(`${dir} is not a valid directory`); 41 | 42 | const possiblePackageDirs = [ dir, () => path.dirname(resolve.sync(`typescript/package.json`, { basedir: dir })) ]; 43 | 44 | for (const d of possiblePackageDirs) { 45 | let packageDir: string; 46 | try { 47 | packageDir = typeof d === 'function' ? d() : d; 48 | } catch { 49 | break; 50 | } 51 | 52 | /* Parse package.json data */ 53 | const packageFile = path.join(packageDir, 'package.json'); 54 | if (!fs.existsSync(packageFile)) continue; 55 | 56 | const { name, version } = (() => { 57 | try { 58 | return JSON.parse(fs.readFileSync(packageFile, 'utf8')); 59 | } 60 | catch (e) { 61 | throw new PackageError(`Could not parse json data in ${packageFile}`); 62 | } 63 | })(); 64 | 65 | /* Validate */ 66 | if (name === 'typescript') { 67 | const [ sMajor, sMinor ] = version.split('.') 68 | const libDir = path.join(packageDir, 'lib'); 69 | const cacheDir = path.resolve(packageDir, '../.tsp/cache/'); 70 | 71 | /* Get all available module names in libDir */ 72 | const moduleNames: TsModule.Name[] = []; 73 | for (const fileName of fs.readdirSync(libDir)) 74 | if ((TsModule.names).includes(fileName)) moduleNames.push(fileName as TsModule.Name); 75 | 76 | const res: TsPackage = { 77 | version, 78 | majorVer: +sMajor, 79 | minorVer: +sMinor, 80 | packageFile, 81 | packageDir, 82 | moduleNames, 83 | cacheDir, 84 | libDir, 85 | moduleCache: new Map(), 86 | 87 | getModulePath: (moduleName: TsModule.Name) => { 88 | return path.join(libDir, moduleName as string); 89 | } 90 | } 91 | 92 | return res; 93 | } 94 | } 95 | 96 | throw new PackageError(`Could not find typescript package from ${dir}`); 97 | } 98 | 99 | // endregion 100 | -------------------------------------------------------------------------------- /projects/core/src/utils/file-utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { getTsPackage } from '../ts-package'; 4 | import { getLockFilePath, PackageError } from '../system'; 5 | import { getHash } from './general'; 6 | 7 | 8 | /* ****************************************************************************************************************** */ 9 | // region: Config 10 | /* ****************************************************************************************************************** */ 11 | 12 | const lockFileWaitMs = 2_000; 13 | 14 | // endregion 15 | 16 | 17 | /* ****************************************************************************************************************** */ 18 | // region: Helpers 19 | /* ****************************************************************************************************************** */ 20 | 21 | function waitForLockRelease(lockFilePath: string) { 22 | const start = Date.now(); 23 | while (fs.existsSync(lockFilePath)) { 24 | sleep(100); 25 | 26 | if ((Date.now() - start) > lockFileWaitMs) 27 | throw new Error( 28 | `Could not acquire lock to write file. If problem persists, run ts-patch clear-cache and try again. 29 | `); 30 | } 31 | 32 | function sleep(ms: number) { 33 | const wakeUpTime = Date.now() + ms; 34 | while (Date.now() < wakeUpTime) {} 35 | } 36 | } 37 | 38 | // endregion 39 | 40 | 41 | /* ****************************************************************************************************************** */ 42 | // region: Utils 43 | /* ****************************************************************************************************************** */ 44 | 45 | /** 46 | * Attempts to locate global installation of TypeScript 47 | */ 48 | export function getGlobalTsDir() { 49 | const errors = []; 50 | const dir = require('global-prefix'); 51 | const check = (dir: string) => { 52 | try { return getTsPackage(dir) } 53 | catch (e) { 54 | errors.push(e); 55 | return {}; 56 | } 57 | }; 58 | 59 | const { packageDir } = (check(dir) || check(path.join(dir, 'lib'))); 60 | 61 | if (!packageDir) 62 | throw new PackageError(`Could not find global TypeScript installation! Are you sure it's installed globally?`); 63 | 64 | return packageDir; 65 | } 66 | 67 | 68 | export const mkdirIfNotExist = (dir: string) => !fs.existsSync(dir) && fs.mkdirSync(dir, { recursive: true }); 69 | 70 | export function withFileLock(filePath: string, fn: () => T): T { 71 | const lockFileName = getHash(filePath) + '.lock'; 72 | const lockFilePath = getLockFilePath(lockFileName); 73 | try { 74 | const lockFileDir = path.dirname(lockFilePath); 75 | if (!fs.existsSync(lockFileDir)) fs.mkdirSync(lockFileDir, { recursive: true }); 76 | waitForLockRelease(lockFilePath); 77 | fs.writeFileSync(lockFilePath, ''); 78 | return fn(); 79 | } finally { 80 | if (fs.existsSync(lockFilePath)) fs.rmSync(lockFilePath, { force: true }); 81 | } 82 | } 83 | 84 | export function writeFileWithLock(filePath: string, content: string): void { 85 | withFileLock(filePath, () => { 86 | fs.writeFileSync(filePath, content); 87 | }); 88 | } 89 | 90 | export function readFileWithLock(filePath: string): string { 91 | return withFileLock(filePath, () => { 92 | return fs.readFileSync(filePath, 'utf8'); 93 | }); 94 | } 95 | 96 | export function copyFileWithLock(src: string, dest: string): void { 97 | withFileLock(src, () => { 98 | withFileLock(dest, () => { 99 | fs.copyFileSync(src, dest); 100 | }); 101 | }); 102 | } 103 | 104 | // endregion 105 | -------------------------------------------------------------------------------- /projects/core/src/utils/find-cache-dir.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @credit https://github.com/sindresorhus/find-cache-di 3 | * @license MIT 4 | * @author Sindre Sorhus 5 | * @author James Talmage 6 | * 7 | * MIT License 8 | * 9 | * Copyright (c) Sindre Sorhus (https://sindresorhus.com) 10 | * Copyright (c) James Talmage (https://github.com/jamestalmage) 11 | * 12 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import process from 'node:process'; 20 | import path from 'node:path'; 21 | import fs from 'node:fs'; 22 | 23 | 24 | /* ****************************************************************************************************************** */ 25 | // region: Types 26 | /* ****************************************************************************************************************** */ 27 | 28 | export interface FindCacheDirOptions { 29 | name: string; 30 | cwd?: string; // Default: process.cwd() 31 | create?: boolean; // Default: false 32 | } 33 | 34 | // endregion 35 | 36 | 37 | /* ****************************************************************************************************************** */ 38 | // region: Helpers 39 | /* ****************************************************************************************************************** */ 40 | 41 | const isWritable = (path: string) => { 42 | try { 43 | fs.accessSync(path, fs.constants.W_OK); 44 | return true; 45 | } 46 | catch { 47 | return false; 48 | } 49 | }; 50 | 51 | function useDirectory(directory: string, options: any) { 52 | if (options.create) { 53 | fs.mkdirSync(directory, { recursive: true }); 54 | } 55 | 56 | return directory; 57 | } 58 | 59 | function getNodeModuleDirectory(directory: string) { 60 | const nodeModules = path.join(directory, 'node_modules'); 61 | 62 | if ( 63 | !isWritable(nodeModules) 64 | && (fs.existsSync(nodeModules) || !isWritable(path.join(directory))) 65 | ) { 66 | return; 67 | } 68 | 69 | return nodeModules; 70 | } 71 | 72 | function findNearestPackageDir(startPath: string): string | null { 73 | const visitedDirs = new Set(); 74 | let currentPath = path.resolve(startPath); 75 | 76 | while (true) { 77 | const packageJsonPath = path.join(currentPath, 'package.json'); 78 | 79 | if (fs.existsSync(packageJsonPath)) { 80 | return path.dirname(packageJsonPath); 81 | } 82 | 83 | // Mark the current directory as visited 84 | visitedDirs.add(currentPath); 85 | 86 | // Move to the parent directory 87 | const parentPath = path.dirname(currentPath); 88 | 89 | // Check for a circular loop 90 | if (visitedDirs.has(parentPath) || parentPath === currentPath) { 91 | return null; 92 | } 93 | 94 | currentPath = parentPath; 95 | } 96 | } 97 | 98 | // endregion 99 | 100 | 101 | /* ****************************************************************************************************************** */ 102 | // region: Utils 103 | /* ****************************************************************************************************************** */ 104 | 105 | export function findCacheDirectory(options: FindCacheDirOptions) { 106 | /* Use ENV Cache Dir if present */ 107 | if (process.env.CACHE_DIR && ![ 'true', 'false', '1', '0' ].includes(process.env.CACHE_DIR)) 108 | return useDirectory(path.join(process.env.CACHE_DIR, options.name), options); 109 | 110 | /* Find Package Dir */ 111 | const startDir = options.cwd || process.cwd(); 112 | const pkgDir = findNearestPackageDir(startDir); 113 | if (!pkgDir) return undefined; 114 | 115 | /* Find Node Modules Dir */ 116 | const nodeModules = getNodeModuleDirectory(pkgDir); 117 | if (!nodeModules) return undefined; 118 | 119 | return useDirectory(path.join(pkgDir, 'node_modules', '.cache', options.name), options); 120 | } 121 | 122 | // endregion 123 | -------------------------------------------------------------------------------- /projects/core/src/utils/general.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | 4 | /* ****************************************************************************************************************** */ 5 | // region: Type Utils 6 | /* ****************************************************************************************************************** */ 7 | 8 | /** 9 | * Make certain properties partial 10 | */ 11 | export type PartialSome = Omit & Pick, K> 12 | 13 | // endregion 14 | 15 | 16 | /* ****************************************************************************************************************** */ 17 | // region: Crypto Utils 18 | /* ****************************************************************************************************************** */ 19 | 20 | export function getHash(fileContent: string) { 21 | return crypto.createHash('md5').update(fileContent).digest('hex'); 22 | } 23 | 24 | // endregion 25 | -------------------------------------------------------------------------------- /projects/core/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './general'; 2 | export * from './file-utils'; 3 | export * from './find-cache-dir'; 4 | -------------------------------------------------------------------------------- /projects/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../../tsconfig.base.json", 3 | "include" : [ "shared", "src" ], 4 | 5 | "compilerOptions" : { 6 | "rootDirs" : [ "src", "shared" ], 7 | "outDir" : "../../dist", 8 | "sourceMap" : true, 9 | "composite" : true, 10 | "declaration" : true, 11 | 12 | "plugins" : [ 13 | { 14 | "transform" : "./plugin.ts", 15 | "transformProgram" : true 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /projects/patch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tsp/patch", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "typescript": "link:../../node_modules/typescript" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/patch/plugin.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript' 2 | import type { ProgramTransformerExtras } from 'ts-patch'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | 6 | 7 | /* ****************************************************************************************************************** */ 8 | // region: Config 9 | /* ****************************************************************************************************************** */ 10 | 11 | const srcTypesFileName = path.resolve(__dirname, '../core/shared/plugin-types.ts'); 12 | const destTypesFileName = path.resolve(__dirname, 'src/types/plugin-types.ts'); 13 | 14 | // endregion 15 | 16 | 17 | /* ****************************************************************************************************************** */ 18 | // region: Transformers 19 | /* ****************************************************************************************************************** */ 20 | 21 | function transformPatchDeclarationsFile(this: typeof ts, ctx: ts.TransformationContext) { 22 | const { factory } = ctx; 23 | const moduleName = factory.createIdentifier('ts'); 24 | 25 | return (sourceFile: ts.SourceFile) => { 26 | const statements = sourceFile 27 | .statements 28 | .filter(node => 29 | this.isModuleDeclaration(node) && this.getJSDocTags(node).some(t => t.tagName.text === 'build-types') 30 | ) 31 | .map((node: ts.ModuleDeclaration) => 32 | factory.updateModuleDeclaration(node, node.modifiers, moduleName, node.body) 33 | ); 34 | 35 | return factory.updateSourceFile( 36 | sourceFile, 37 | statements, 38 | sourceFile.isDeclarationFile 39 | ); 40 | } 41 | } 42 | 43 | function transformPluginTypes(this: typeof ts, ctx: ts.TransformationContext) { 44 | const { factory } = ctx; 45 | return (sourceFile: ts.SourceFile) => { 46 | const moduleDeclaration = 47 | factory.createModuleDeclaration( 48 | [ factory.createModifier(this.SyntaxKind.DeclareKeyword) ], 49 | factory.createIdentifier('tsp'), 50 | factory.createModuleBlock( 51 | sourceFile 52 | .statements 53 | // TODO - remove the casting once we have tsei again 54 | .filter(node => (this).isDeclaration(node) && this.getCombinedModifierFlags(node as unknown as ts.Declaration)) 55 | ), 56 | // TODO - remove the casting once we have tsei again 57 | this.NodeFlags.Namespace | this.NodeFlags.ExportContext | (this.NodeFlags).Ambient | this.NodeFlags.ContextFlags 58 | ); 59 | 60 | return factory.updateSourceFile(sourceFile, [ moduleDeclaration ]); 61 | } 62 | } 63 | 64 | // endregion 65 | 66 | 67 | /* ****************************************************************************************************************** * 68 | * Program Transformer - Build and insert plugin-types.ts 69 | * ****************************************************************************************************************** */ 70 | 71 | export function transformProgram( 72 | program: ts.Program, 73 | host: ts.CompilerHost, 74 | opt: any, 75 | { ts }: ProgramTransformerExtras 76 | ) 77 | { 78 | host ??= ts.createCompilerHost(program.getCompilerOptions(), true); 79 | const printer = ts.createPrinter({ 80 | removeComments: true, 81 | newLine: ts.NewLineKind.LineFeed 82 | }); 83 | // TODO - remove the casting once we have tsei again 84 | const srcFileName = (ts).normalizePath(srcTypesFileName); 85 | const destFileName = (ts).normalizePath(destTypesFileName); 86 | 87 | hookWriteFile(); 88 | generatePluginTypesAndInjectToProgram(); 89 | 90 | return ts.createProgram( 91 | program.getRootFileNames().concat([ destFileName ]), 92 | program.getCompilerOptions(), 93 | host, 94 | program 95 | ); 96 | 97 | function hookWriteFile() { 98 | const originalWriteFile = host.writeFile; 99 | host.writeFile = (fileName: string, data: string, ...args: any[]) => { 100 | /* Transform declarations */ 101 | if (/module-patch.d.ts$/.test(fileName)) { 102 | let sourceFile = ts.createSourceFile(fileName, data, ts.ScriptTarget.ES2016, true, ts.ScriptKind.TS); 103 | sourceFile = ts.transform(sourceFile, [ transformPatchDeclarationsFile.bind(ts) ]).transformed[0]; 104 | return (originalWriteFile)(fileName, printer.printFile(sourceFile), ...args); 105 | } 106 | 107 | /* Strip comments from js */ 108 | if (/module-patch.js$/.test(fileName)) { 109 | /* Wrap file in closure */ 110 | data = `var tsp = (function() {\n${data}\nreturn tsp;})();`; 111 | 112 | const sourceFile = ts.createSourceFile(fileName, data, ts.ScriptTarget.ES2016, false, ts.ScriptKind.JS); 113 | return (originalWriteFile)(fileName, printer.printFile(sourceFile), ...args); 114 | } 115 | 116 | return (originalWriteFile)(fileName, data, ...args); 117 | } 118 | } 119 | 120 | function generatePluginTypesAndInjectToProgram() { 121 | let sourceFile = ts.createSourceFile(srcFileName, fs.readFileSync(srcFileName, 'utf8'), ts.ScriptTarget.ES2015, true); 122 | sourceFile = ts.transform(sourceFile, [ transformPluginTypes.bind(ts) ]).transformed[0]; 123 | 124 | const moduleBody = `// @ts-nocheck\n/** AUTO-GENERATED - DO NOT EDIT */\n\n/** @build-types */\n` + printer.printFile(sourceFile); 125 | 126 | fs.writeFileSync(destFileName, moduleBody); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /projects/patch/src/plugin/esm-intercept.ts: -------------------------------------------------------------------------------- 1 | namespace tsp { 2 | const Module = require('module'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const crypto = require('crypto'); 6 | 7 | /* ********************************************************* */ 8 | // region: Helpers 9 | /* ********************************************************* */ 10 | 11 | function getEsmLibrary() { 12 | try { 13 | return require('esm') as typeof import('esm'); 14 | } catch (e) { 15 | if (e.code === 'MODULE_NOT_FOUND') 16 | throw new TsPatchError( 17 | `Plugin is an ESM module. To enable experimental ESM support, ` + 18 | `install the 'esm' package as a (dev)-dependency or global.` 19 | ); 20 | else throw e; 21 | } 22 | } 23 | 24 | // endregion 25 | 26 | /* ********************************************************* */ 27 | // region: Utils 28 | /* ********************************************************* */ 29 | 30 | export function registerEsmIntercept(registerConfig: RegisterConfig): () => void { 31 | const originalRequire = Module.prototype.require; 32 | const builtFiles = new Map(); 33 | 34 | const getHash = () => { 35 | let hash: string; 36 | do { 37 | hash = crypto.randomBytes(16).toString('hex'); 38 | } while (builtFiles.has(hash)); 39 | 40 | return hash; 41 | } 42 | 43 | /* Create cleanup function */ 44 | const cleanup = () => { 45 | /* Cleanup temp ESM files */ 46 | for (const { 1: filePath } of builtFiles) { 47 | delete require.cache[filePath]; 48 | try { 49 | fs.rmSync(filePath, { force: true, maxRetries: 3 }); 50 | } catch (e) { 51 | if (process.env.NODE_ENV !== 'production') 52 | console.warn(`[ts-patch] Warning: Failed to delete temporary esm cache file: ${filePath}.`); 53 | } 54 | } 55 | 56 | builtFiles.clear(); 57 | Module.prototype.require = originalRequire; 58 | } 59 | 60 | /* Set Hooks */ 61 | try { 62 | Module.prototype.require = wrappedRequire; 63 | } catch (e) { 64 | cleanup(); 65 | } 66 | 67 | /* ********************************************************* * 68 | * Helpers 69 | * ********************************************************* */ 70 | 71 | function wrappedRequire(this: unknown, request: string) { 72 | try { 73 | return originalRequire.apply(this, arguments); 74 | } catch (e) { 75 | if (e.code === 'ERR_REQUIRE_ESM') { 76 | const resolvedPath = Module._resolveFilename(request, this, false); 77 | const resolvedPathExt = path.extname(resolvedPath); 78 | 79 | if (Module._cache[resolvedPath]) return Module._cache[resolvedPath].exports; 80 | 81 | /* Compile TS */ 82 | let targetFilePath: string; 83 | if (tsExtensions.includes(resolvedPathExt)) { 84 | if (!builtFiles.has(resolvedPath)) { 85 | const tsCode = fs.readFileSync(resolvedPath, 'utf8'); 86 | 87 | // NOTE - I don't know why, but if you supply a *.ts file to tsNode.compile it will be output as cjs, 88 | // regardless of the tsConfig properly specifying ESNext for module and target. Notably, this issue seems 89 | // to have started with TS v5.5, 90 | // 91 | // To work around, we will tell ts-node that it's an "mts" file. 92 | const newPath = resolvedPath.replace(/\.ts$/, '.mts'); 93 | 94 | const jsCode = registerConfig.tsNodeInstance!.compile(tsCode, newPath); 95 | const outputFileName = getHash() + '.mjs'; 96 | const outputFilePath = path.join(getTmpDir('esm'), outputFileName); 97 | fs.writeFileSync(outputFilePath, jsCode, 'utf8'); 98 | 99 | builtFiles.set(resolvedPath, outputFilePath); 100 | targetFilePath = outputFilePath; 101 | } else { 102 | targetFilePath = builtFiles.get(resolvedPath)!; 103 | } 104 | } else { 105 | targetFilePath = resolvedPath; 106 | } 107 | 108 | /* Setup new module */ 109 | const newModule = new Module(request, this); 110 | newModule.filename = resolvedPath; 111 | newModule.paths = Module._nodeModulePaths(resolvedPath); 112 | 113 | /* Add to cache */ 114 | Module._cache[resolvedPath] = newModule; 115 | 116 | /* Load with ESM library */ 117 | const res = getEsmLibrary()(newModule)(targetFilePath); 118 | newModule.filename = resolvedPath; 119 | 120 | return res; 121 | } 122 | 123 | throw e; 124 | } 125 | } 126 | 127 | return cleanup; 128 | } 129 | 130 | // endregion 131 | } 132 | -------------------------------------------------------------------------------- /projects/patch/src/plugin/register-plugin.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | namespace tsp { 4 | const path = require('path'); 5 | 6 | let configStack: RegisterConfig[] = []; 7 | 8 | /* ********************************************************* */ 9 | // region: Types 10 | /* ********************************************************* */ 11 | 12 | /** @internal */ 13 | export interface RegisterConfig { 14 | tsNodeInstance?: import('ts-node').Service 15 | tsConfigPathsCleanup?: () => void 16 | esmInterceptCleanup?: () => void 17 | isTs: boolean 18 | pluginConfig: PluginConfig 19 | isEsm: boolean 20 | tsConfig: string | undefined 21 | compilerOptions?: tsShim.CompilerOptions 22 | } 23 | 24 | // endregion 25 | 26 | /* ********************************************************* */ 27 | // region: Helpers 28 | /* ********************************************************* */ 29 | 30 | function getTsNode() { 31 | try { 32 | return require('ts-node') as typeof import('ts-node'); 33 | } catch (e) { 34 | if (e.code === 'MODULE_NOT_FOUND') 35 | throw new TsPatchError( 36 | `Cannot use a typescript-based transformer without ts-node installed. `+ 37 | `Add ts-node as a (dev)-dependency or install globally.` 38 | ); 39 | else throw e; 40 | } 41 | } 42 | 43 | function getTsConfigPaths() { 44 | try { 45 | return require('tsconfig-paths') as typeof import('tsconfig-paths'); 46 | } catch (e) { 47 | if (e.code === 'MODULE_NOT_FOUND') 48 | throw new TsPatchError( 49 | `resolvePathAliases requires the library: tsconfig-paths. `+ 50 | `Add tsconfig-paths as a (dev)-dependency or install globally.` 51 | ); 52 | else throw e; 53 | } 54 | } 55 | 56 | function getCompilerOptions(tsConfig: string) { 57 | const configFile = tsShim.readConfigFile(tsConfig, tsShim.sys.readFile); 58 | const parsedConfig = configFile && tsShim.parseJsonConfigFileContent( 59 | configFile.config, 60 | tsShim.sys, 61 | path.dirname(tsConfig) 62 | ); 63 | 64 | return parsedConfig.options; 65 | } 66 | 67 | // endregion 68 | 69 | /* ********************************************************* */ 70 | // region: Utils 71 | /* ********************************************************* */ 72 | 73 | export function unregisterPlugin() { 74 | const activeRegisterConfig = configStack.pop()!; 75 | 76 | if (activeRegisterConfig.tsConfigPathsCleanup) { 77 | activeRegisterConfig.tsConfigPathsCleanup(); 78 | delete activeRegisterConfig.tsConfigPathsCleanup; 79 | } 80 | 81 | if (activeRegisterConfig.tsNodeInstance) { 82 | activeRegisterConfig.tsNodeInstance.enabled(false); 83 | } 84 | 85 | if (activeRegisterConfig.esmInterceptCleanup) { 86 | activeRegisterConfig.esmInterceptCleanup(); 87 | delete activeRegisterConfig.esmInterceptCleanup; 88 | } 89 | } 90 | 91 | export function registerPlugin(registerConfig: RegisterConfig) { 92 | if (!registerConfig) throw new TsPatchError('requireConfig is required'); 93 | configStack.push(registerConfig); 94 | 95 | const { isTs, isEsm, tsConfig, pluginConfig } = registerConfig; 96 | 97 | /* Register ESM */ 98 | if (isEsm) { 99 | registerConfig.esmInterceptCleanup = registerEsmIntercept(registerConfig); 100 | } 101 | 102 | /* Register tsNode */ 103 | if (isTs) { 104 | const tsNode = getTsNode(); 105 | 106 | let tsNodeInstance: import('ts-node').Service; 107 | if (registerConfig.tsNodeInstance) { 108 | tsNodeInstance = registerConfig.tsNodeInstance; 109 | tsNode.register(tsNodeInstance); 110 | } else { 111 | tsNodeInstance = tsNode.register({ 112 | transpileOnly: true, 113 | ...(tsConfig ? { project: tsConfig } : { skipProject: true }), 114 | compilerOptions: { 115 | target: isEsm ? 'ESNext' : 'ES2018', 116 | jsx: 'react', 117 | esModuleInterop: true, 118 | module: isEsm ? 'ESNext' : 'commonjs', 119 | } 120 | }); 121 | } 122 | 123 | tsNodeInstance.enabled(true); 124 | registerConfig.tsNodeInstance = tsNodeInstance; 125 | } 126 | 127 | /* Register tsconfig-paths */ 128 | if (tsConfig && pluginConfig.resolvePathAliases) { 129 | registerConfig.compilerOptions ??= getCompilerOptions(tsConfig); 130 | 131 | const { paths, baseUrl } = registerConfig.compilerOptions; 132 | if (paths && baseUrl) { 133 | registerConfig.tsConfigPathsCleanup = getTsConfigPaths().register({ baseUrl, paths }); 134 | } 135 | } 136 | } 137 | 138 | // endregion 139 | } 140 | -------------------------------------------------------------------------------- /projects/patch/src/shared.ts: -------------------------------------------------------------------------------- 1 | namespace tsp { 2 | const os = require('os'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | 6 | /* ********************************************************* */ 7 | // region: Vars 8 | /* ********************************************************* */ 9 | 10 | export const diagnosticMap: tsp.DiagnosticMap = new WeakMap(); 11 | 12 | /** Injected during patch — library name minus extension */ 13 | export declare const currentLibrary: string; 14 | 15 | export const supportedExtensions = [ '.ts', '.mts', '.cts', '.js', '.mjs', '.cjs' ]; 16 | export const tsExtensions = [ '.ts', '.mts', '.cts' ]; 17 | 18 | // endregion 19 | 20 | /* ********************************************************* */ 21 | // region: Utils 22 | /* ********************************************************* */ 23 | 24 | /** @internal */ 25 | export function diagnosticExtrasFactory(program: tsShim.Program) { 26 | const diagnostics = diagnosticMap.get(program) || diagnosticMap.set(program, []).get(program)!; 27 | 28 | const addDiagnostic = (diag: tsShim.Diagnostic): number => diagnostics.push(diag); 29 | const removeDiagnostic = (index: number) => { diagnostics.splice(index, 1) }; 30 | 31 | return { addDiagnostic, removeDiagnostic, diagnostics }; 32 | } 33 | 34 | /** @internal */ 35 | export function getTmpDir(subPath?: string) { 36 | const tmpDir = path.resolve(os.tmpdir(), 'tsp', subPath); 37 | if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true }); 38 | return tmpDir; 39 | } 40 | 41 | /** @internal */ 42 | export function getTsInstance() { 43 | return (typeof ts !== 'undefined' ? ts : module.exports) as typeof import('typescript'); 44 | } 45 | 46 | // endregion 47 | 48 | /* ********************************************************* */ 49 | // region: Other 50 | /* ********************************************************* */ 51 | 52 | export class TsPatchError extends Error { 53 | constructor(message: string, public diagnostic?: tsShim.Diagnostic) { 54 | super(message); 55 | } 56 | } 57 | 58 | // endregion 59 | } 60 | -------------------------------------------------------------------------------- /projects/patch/src/ts/create-program.ts: -------------------------------------------------------------------------------- 1 | namespace tsp { 2 | const activeProgramTransformers = new Set(); 3 | const { dirname } = require('path'); 4 | 5 | /* ********************************************************* */ 6 | // region: Helpers 7 | /* ********************************************************* */ 8 | 9 | function getProjectDir(compilerOptions: tsShim.CompilerOptions) { 10 | return compilerOptions.configFilePath && dirname(compilerOptions.configFilePath); 11 | } 12 | 13 | function getProjectConfig(compilerOptions: tsShim.CompilerOptions, rootFileNames: ReadonlyArray) { 14 | let configFilePath = compilerOptions.configFilePath; 15 | let projectDir = getProjectDir(compilerOptions); 16 | 17 | if (configFilePath === undefined) { 18 | const baseDir = (rootFileNames.length > 0) ? dirname(rootFileNames[0]) : projectDir ?? process.cwd(); 19 | configFilePath = tsShim.findConfigFile(baseDir, tsShim.sys.fileExists); 20 | 21 | if (configFilePath) { 22 | const config = readConfig(configFilePath); 23 | compilerOptions = { ...config.options, ...compilerOptions }; 24 | projectDir = getProjectDir(compilerOptions); 25 | } 26 | } 27 | 28 | return ({ projectDir, compilerOptions }); 29 | } 30 | 31 | function readConfig(configFileNamePath: string) { 32 | const projectDir = dirname(configFileNamePath); 33 | const result = tsShim.readConfigFile(configFileNamePath, tsShim.sys.readFile); 34 | 35 | if (result.error) throw new TsPatchError('Error in tsconfig.json: ' + result.error.messageText); 36 | 37 | return tsShim.parseJsonConfigFileContent(result.config, tsShim.sys, projectDir, undefined, configFileNamePath); 38 | } 39 | 40 | function preparePluginsFromCompilerOptions(plugins: any): PluginConfig[] { 41 | if (!plugins) return []; 42 | 43 | // Old transformers system 44 | if ((plugins.length === 1) && plugins[0].customTransformers) { 45 | const { before = [], after = [] } = (plugins[0].customTransformers as { before: string[]; after: string[] }); 46 | 47 | return [ 48 | ...before.map((item: string) => ({ transform: item })), 49 | ...after.map((item: string) => ({ transform: item, after: true })), 50 | ]; 51 | } 52 | 53 | return plugins; 54 | } 55 | 56 | // endregion 57 | 58 | /* ********************************************************* * 59 | * Patched createProgram() 60 | * ********************************************************* */ 61 | 62 | export function createProgram( 63 | rootNamesOrOptions: ReadonlyArray | tsShim.CreateProgramOptions, 64 | options?: tsShim.CompilerOptions, 65 | host?: tsShim.CompilerHost, 66 | oldProgram?: tsShim.Program, 67 | configFileParsingDiagnostics?: ReadonlyArray 68 | ): tsShim.Program { 69 | let rootNames; 70 | 71 | /* Determine options */ 72 | const createOpts = !Array.isArray(rootNamesOrOptions) ? rootNamesOrOptions : void 0; 73 | if (createOpts) { 74 | rootNames = createOpts.rootNames; 75 | options = createOpts.options; 76 | host = createOpts.host; 77 | oldProgram = createOpts.oldProgram; 78 | configFileParsingDiagnostics = createOpts.configFileParsingDiagnostics; 79 | } else { 80 | options = options!; 81 | rootNames = rootNamesOrOptions as ReadonlyArray; 82 | } 83 | 84 | /* Get Config */ 85 | const projectConfig = getProjectConfig(options, rootNames); 86 | if ([ 'tsc', 'tsserver', 'tsserverlibrary' ].includes(tsp.currentLibrary)) { 87 | options = projectConfig.compilerOptions; 88 | if (createOpts) createOpts.options = options; 89 | } 90 | 91 | /* Prepare Plugins */ 92 | const plugins = preparePluginsFromCompilerOptions(options.plugins); 93 | const pluginCreator = new PluginCreator(plugins, { resolveBaseDir: projectConfig.projectDir ?? process.cwd() }); 94 | 95 | /* Handle JSDoc parsing in v5.3+ */ 96 | if (tsp.currentLibrary === 'tsc' && tsShim.JSDocParsingMode && pluginCreator.needsTscJsDocParsing) { 97 | host!.jsDocParsingMode = tsShim.JSDocParsingMode.ParseAll; 98 | } 99 | 100 | /* Invoke TS createProgram */ 101 | let program: tsShim.Program & { originalEmit?: tsShim.Program['emit'] } = createOpts ? 102 | tsShim.originalCreateProgram(createOpts) : 103 | tsShim.originalCreateProgram(rootNames, options, host, oldProgram, configFileParsingDiagnostics); 104 | 105 | /* Prevent recursion in Program transformers */ 106 | const programTransformers = pluginCreator.createProgramTransformers(); 107 | 108 | /* Transform Program */ 109 | for (const [ transformerKey, [ programTransformer, config ] ] of programTransformers) { 110 | if (activeProgramTransformers.has(transformerKey)) continue; 111 | activeProgramTransformers.add(transformerKey); 112 | 113 | const newProgram: any = programTransformer(program, host, config, { ts: tsp.getTsInstance() }); 114 | if (typeof newProgram?.['emit'] === 'function') program = newProgram; 115 | 116 | activeProgramTransformers.delete(transformerKey); 117 | } 118 | 119 | /* Hook emit method */ 120 | if (!program.originalEmit) { 121 | program.originalEmit = program.emit; 122 | program.emit = newEmit; 123 | } 124 | 125 | function newEmit( 126 | targetSourceFile?: tsShim.SourceFile, 127 | writeFile?: tsShim.WriteFileCallback, 128 | cancellationToken?: tsShim.CancellationToken, 129 | emitOnlyDtsFiles?: boolean, 130 | customTransformers?: tsShim.CustomTransformers, 131 | ...additionalArgs: any 132 | ): tsShim.EmitResult { 133 | /* Merge in our transformers */ 134 | const transformers = pluginCreator.createSourceTransformers({ program }, customTransformers); 135 | 136 | /* Invoke TS emit */ 137 | const result: tsShim.EmitResult = program.originalEmit!( 138 | targetSourceFile, 139 | writeFile, 140 | cancellationToken, 141 | emitOnlyDtsFiles, 142 | transformers, 143 | // @ts-ignore 144 | ...additionalArgs 145 | ); 146 | 147 | /* Merge in transformer diagnostics */ 148 | for (const diagnostic of tsp.diagnosticMap.get(program) || []) 149 | if (!result.diagnostics.includes(diagnostic)) (result.diagnostics).push(diagnostic) 150 | 151 | return result; 152 | } 153 | 154 | return program; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /projects/patch/src/ts/shim.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | namespace tsp { 3 | /** 4 | * Compensate for modules which do not wrap functions in a `ts` namespace. 5 | */ 6 | export const tsShim = new Proxy( 7 | {}, 8 | { 9 | get(_, key: string) { 10 | const target = tsp.getTsInstance(); 11 | if (target) { 12 | return (target)[key]; 13 | } else { 14 | try { 15 | return eval(key); 16 | } catch (e) { 17 | throw new TsPatchError(`Failed to find "${key}" in TypeScript shim`, e); 18 | } 19 | } 20 | }, 21 | } 22 | ) as typeof import('typescript'); 23 | 24 | export namespace tsShim { 25 | export type CompilerOptions = import('typescript').CompilerOptions; 26 | export type CreateProgramOptions = import('typescript').CreateProgramOptions; 27 | export type Program = import('typescript').Program; 28 | export type CompilerHost = import('typescript').CompilerHost; 29 | export type Diagnostic = import('typescript').Diagnostic; 30 | export type SourceFile = import('typescript').SourceFile; 31 | export type WriteFileCallback = import('typescript').WriteFileCallback; 32 | export type CancellationToken = import('typescript').CancellationToken; 33 | export type CustomTransformers = import('typescript').CustomTransformers; 34 | export type EmitResult = import('typescript').EmitResult; 35 | export type LanguageService = import('typescript').LanguageService; 36 | export type TransformationContext = import('typescript').TransformationContext; 37 | export type Node = import('typescript').Node; 38 | export type TransformerFactory = import('typescript').TransformerFactory; 39 | export type Bundle = import('typescript').Bundle; 40 | export type Path = import('typescript').Path; 41 | export type JSDocParsingMode = import('typescript').JSDocParsingMode; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /projects/patch/src/types/plugin-types.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /** AUTO-GENERATED - DO NOT EDIT */ 3 | 4 | /** @build-types */ 5 | declare namespace tsp { 6 | export interface PluginConfig { 7 | [x: string]: any; 8 | name?: string; 9 | transform?: string; 10 | resolvePathAliases?: boolean; 11 | tsConfig?: string; 12 | import?: string; 13 | isEsm?: boolean; 14 | type?: 'ls' | 'program' | 'config' | 'checker' | 'raw' | 'compilerOptions'; 15 | after?: boolean; 16 | afterDeclarations?: boolean; 17 | transformProgram?: boolean; 18 | } 19 | export type TransformerList = Required; 20 | export type TransformerPlugin = TransformerBasePlugin | TsTransformerFactory; 21 | export type TsTransformerFactory = ts.TransformerFactory; 22 | export type PluginFactory = LSPattern | ProgramPattern | ConfigPattern | CompilerOptionsPattern | TypeCheckerPattern | RawPattern; 23 | export interface TransformerBasePlugin { 24 | before?: ts.TransformerFactory | ts.TransformerFactory[]; 25 | after?: ts.TransformerFactory | ts.TransformerFactory[]; 26 | afterDeclarations?: ts.TransformerFactory | ts.TransformerFactory[]; 27 | } 28 | export type DiagnosticMap = WeakMap; 29 | export type TransformerExtras = { 30 | ts: typeof ts; 31 | library: string; 32 | addDiagnostic: (diag: ts.Diagnostic) => number; 33 | removeDiagnostic: (index: number) => void; 34 | diagnostics: readonly ts.Diagnostic[]; 35 | }; 36 | export type ProgramTransformerExtras = { 37 | ts: typeof ts; 38 | }; 39 | export type ProgramTransformer = (program: ts.Program, host: ts.CompilerHost | undefined, config: PluginConfig, extras: ProgramTransformerExtras) => ts.Program; 40 | export type LSPattern = (ls: ts.LanguageService, config: {}) => TransformerPlugin; 41 | export type CompilerOptionsPattern = (compilerOpts: ts.CompilerOptions, config: {}) => TransformerPlugin; 42 | export type ConfigPattern = (config: {}) => TransformerPlugin; 43 | export type TypeCheckerPattern = (checker: ts.TypeChecker, config: {}) => TransformerPlugin; 44 | export type ProgramPattern = (program: ts.Program, config: {}, extras: TransformerExtras) => TransformerPlugin; 45 | export type RawPattern = (context: ts.TransformationContext, program: ts.Program, config: {}) => ts.Transformer; 46 | export interface PluginPackageConfig { 47 | tscOptions?: { 48 | parseAllJsDoc?: boolean; 49 | }; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /projects/patch/src/types/typescript.ts: -------------------------------------------------------------------------------- 1 | /* ****************************************************************************************************************** * 2 | * Added Properties 3 | * ****************************************************************************************************************** */ 4 | 5 | /** @build-types */ 6 | declare namespace ts { 7 | /** @internal */ 8 | const createProgram: typeof import('typescript').createProgram; 9 | 10 | export const originalCreateProgram: typeof ts.createProgram; 11 | } 12 | -------------------------------------------------------------------------------- /projects/patch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src" 4 | ], 5 | 6 | "compilerOptions": { 7 | "outFile": "../../dist/resources/module-patch.js", 8 | "declaration": true, 9 | "types": [ "@types/node" ], 10 | 11 | "strict": true, 12 | "noUnusedLocals": false, 13 | "noImplicitReturns": true, 14 | "allowSyntheticDefaultImports": true, 15 | "stripInternal": true, 16 | 17 | "target": "ES2020", 18 | "downlevelIteration": true, 19 | "useUnknownInCatchVariables": false, 20 | "newLine": "LF", 21 | "moduleResolution": "Node", 22 | "esModuleInterop": true, 23 | 24 | "plugins": [ 25 | { 26 | "transform": "./plugin.ts", 27 | "transformProgram": true, 28 | "import": "transformProgram" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /scripts/postbuild.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const shell = require('shelljs'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const glob = require('glob'); 6 | 7 | 8 | /* ******************************************************************************************************************** 9 | * Constants 10 | * ********************************************************************************************************************/ 11 | 12 | const BASE_DIR = path.resolve('.'); 13 | const SRC_DIR = path.resolve('./src'); 14 | const DIST_DIR = path.resolve('./dist'); 15 | const DIST_RESOURCE_DIR = path.join(DIST_DIR, 'resources'); 16 | const COMPILER_DIR = path.resolve('./projects/core/src/compiler'); 17 | 18 | 19 | /* ******************************************************************************************************************** 20 | * Post-Build Steps 21 | * ********************************************************************************************************************/ 22 | 23 | /* Uncomment this if you need to temporarily build without patched ts */ 24 | // shell.mv(path.join(DIST_DIR, 'src', '*'), DIST_DIR); 25 | // shell.mv(path.join(DIST_DIR, 'shared', '*'), DIST_DIR); 26 | // shell.rm('-rf', path.join(DIST_DIR, 'src')); 27 | // shell.rm('-rf', path.join(DIST_DIR, 'shared')); 28 | 29 | /* Build package.json */ 30 | const pkgJSON = JSON.parse(fs.readFileSync(path.join(BASE_DIR, 'package.json'), 'utf8')); 31 | 32 | delete pkgJSON.scripts; 33 | delete pkgJSON.private; 34 | delete pkgJSON.workspaces; 35 | delete pkgJSON.devDependencies; 36 | 37 | // Write & remove ./dist 38 | fs.writeFileSync( 39 | path.join(DIST_DIR, 'package.json'), 40 | JSON.stringify(pkgJSON, null, 2).replace(/(?<=['"].*?)dist\//g, '') 41 | ); 42 | 43 | /* Copy Live files */ 44 | shell.cp('-r', COMPILER_DIR, DIST_DIR); 45 | 46 | /* Copy Readme & Changelog */ 47 | shell.cp(path.resolve('./README.md'), DIST_DIR); 48 | shell.cp(path.resolve('./CHANGELOG.md'), DIST_DIR); 49 | shell.cp(path.resolve('./LICENSE.md'), DIST_DIR); 50 | 51 | /* Add shebang line to bin files */ 52 | const binFiles = glob 53 | .sync(path.join(DIST_DIR, 'bin', '*.js')) 54 | .map((filePath) => [ 55 | filePath, 56 | `#!/usr/bin/env node\n\n` + fs.readFileSync(filePath, 'utf8') 57 | ]); 58 | 59 | for (const [ filePath, fileContent] of binFiles) { 60 | const fileName = path.basename(filePath); 61 | fs.writeFileSync(path.join(DIST_DIR, 'bin', fileName), fileContent, 'utf8'); 62 | } 63 | 64 | /* Ensure EOL = LF in resources */ 65 | const resFiles = glob.sync(path.join(DIST_RESOURCE_DIR, '*').replace(/\\/g, '/')); 66 | shell.sed('-i', /\r+$/, '', resFiles); 67 | -------------------------------------------------------------------------------- /test/.yarnrc: -------------------------------------------------------------------------------- 1 | --no-lockfile true 2 | -------------------------------------------------------------------------------- /test/assets/projects/main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-project", 3 | "private": true, 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /test/assets/projects/main/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonara/ts-patch/88ce20cf4d9cd7c970ac76dd5bcfaae361254064/test/assets/projects/main/src/index.ts -------------------------------------------------------------------------------- /test/assets/projects/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include" : [ "src" ], 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "esModuleInterop": true, 6 | "strict": true, 7 | 8 | "lib": [ "ESNext" ], 9 | "target": "ESNext", 10 | "module": "CommonJS", 11 | "moduleResolution": "node", 12 | 13 | "newLine": "LF", 14 | "allowSyntheticDefaultImports": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/assets/projects/package-config/plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main" : "plugin.js", 3 | "tsp": { 4 | "tscOptions": { 5 | "parseAllJsDoc": true 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/assets/projects/package-config/plugin/plugin.js: -------------------------------------------------------------------------------- 1 | const transformer1Factory = require('./transformers/transformer1'); 2 | const transformer2Factory = require('./transformers/transformer2'); 3 | 4 | module.exports = function(...args) { 5 | return { 6 | before: [transformer1Factory(...args), transformer2Factory(...args)], 7 | after: () => { throw new Error('after should be unreachable') } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/assets/projects/package-config/plugin/transformers/transformer1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Change the value of a variable declaration to the value of all jsDoc comments 3 | */ 4 | function transformer1Factory(program, config, { ts }) { 5 | return context => { 6 | const { factory } = context; 7 | 8 | function visitor(node) { 9 | if (ts.isVariableDeclaration(node) && node.initializer) { 10 | const jsDocs = ts.getJSDocTags(node); 11 | if (jsDocs.length > 0) { 12 | const jsDocComment = jsDocs.map(doc => doc.comment).filter(comment => comment).join(' '); 13 | return factory.createVariableDeclaration( 14 | node.name, 15 | undefined, 16 | undefined, 17 | factory.createStringLiteral(jsDocComment) 18 | ); 19 | } 20 | } 21 | return ts.visitEachChild(node, visitor, context); 22 | } 23 | 24 | return sourceFile => ts.visitNode(sourceFile, visitor); 25 | }; 26 | } 27 | 28 | module.exports = transformer1Factory; 29 | -------------------------------------------------------------------------------- /test/assets/projects/package-config/plugin/transformers/transformer2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Transform let to const 3 | */ 4 | function transformer2Factory(program, config, { ts }) { 5 | return context => { 6 | const { factory } = context; 7 | 8 | function visitor(node) { 9 | if (ts.isVariableStatement(node) && node.declarationList.flags & ts.NodeFlags.Let) { 10 | return factory.createVariableStatement( 11 | undefined, 12 | factory.createVariableDeclarationList(node.declarationList.declarations, ts.NodeFlags.Const) 13 | ); 14 | } 15 | return ts.visitEachChild(node, visitor, context); 16 | } 17 | 18 | return sourceFile => ts.visitNode(sourceFile, visitor); 19 | } 20 | } 21 | 22 | module.exports = transformer2Factory; 23 | -------------------------------------------------------------------------------- /test/assets/projects/package-config/run-transform.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { spawnSync } = require('child_process'); 3 | const { getLiveModule } = require('ts-patch'); 4 | const { runInThisContext } = require("vm"); 5 | 6 | 7 | /* ****************************************************************************************************************** * 8 | * Helpers 9 | * ****************************************************************************************************************** */ 10 | 11 | function runTsc(disableTspClause) { 12 | const pluginDir = path.resolve(__dirname, 'plugin'); 13 | const pluginPackageJsonPath = path.join(pluginDir, 'package.json'); 14 | 15 | process.env.TSP_SKIP_CACHE = true; 16 | const { js, tsModule } = getLiveModule('tsc.js'); 17 | 18 | let currentWriteFile = undefined; 19 | const fs = { 20 | ...require('fs'), 21 | mkdirSync: function (dirPath) { 22 | return; 23 | }, 24 | writeFileSync: function (filePath, data, options) { 25 | throw new Error('Should not be used'); 26 | }, 27 | openSync: function (filePath, flags, mode) { 28 | currentWriteFile = filePath; 29 | return 1; 30 | }, 31 | writeSync: function (fd, data, options) { 32 | // process.stdout.write(currentWriteFile + '\n'); 33 | 34 | // Check if the file is src/index.ts and output to stdout 35 | if (path.basename(currentWriteFile) === 'index.js') { 36 | process.stdout.write(data); 37 | } 38 | }, 39 | closeSync: function (fd) { 40 | currentWriteFile = undefined; 41 | }, 42 | readFileSync: function (filePath, options) { 43 | if (disableTspClause && path.normalize(filePath) === pluginPackageJsonPath) { 44 | return JSON.stringify({ }); 45 | } 46 | return require('fs').readFileSync(filePath, options); 47 | } 48 | } 49 | 50 | const myRequire = function (modulePath) { 51 | if (modulePath === 'fs') return fs; 52 | return require(modulePath); 53 | } 54 | Object.assign(myRequire, require); 55 | 56 | const script = runInThisContext(` 57 | (function (exports, require, module, __filename, __dirname) { 58 | process.argv = ['node', 'tsc.js', '--noEmit', 'false']; 59 | ${js} 60 | }); 61 | `); 62 | 63 | script.call(exports, exports, myRequire, module, tsModule.modulePath, path.dirname(tsModule.modulePath)); 64 | } 65 | 66 | 67 | /* ****************************************************************************************************************** * 68 | * Entry 69 | * ****************************************************************************************************************** */ 70 | 71 | const disableTspClause = process.argv[2] === '--disable'; 72 | runTsc(disableTspClause); 73 | -------------------------------------------------------------------------------- /test/assets/projects/package-config/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @tag1 ExampleTag1 3 | * @tag2 ExampleTag2 4 | */ 5 | let myVar = 123; 6 | let anotherVar = "test"; 7 | -------------------------------------------------------------------------------- /test/assets/projects/package-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ "src" ], 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "module": "commonjs", 6 | "target": "esnext", 7 | "noEmit": true, 8 | "plugins" : [ 9 | { "transform": "./plugin" } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/assets/projects/path-mapping/base.ts: -------------------------------------------------------------------------------- 1 | export const b = 2; 2 | -------------------------------------------------------------------------------- /test/assets/projects/path-mapping/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "path-mapping-test", 3 | "main": "src/index.ts", 4 | "dependencies": { 5 | "ts-node" : "^10.9.1", 6 | "tsconfig-paths" : "^4.2.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/assets/projects/path-mapping/src/a/a.ts: -------------------------------------------------------------------------------- 1 | export const aVar = "a"; 2 | -------------------------------------------------------------------------------- /test/assets/projects/path-mapping/src/b.ts: -------------------------------------------------------------------------------- 1 | export const bVar = "b"; 2 | -------------------------------------------------------------------------------- /test/assets/projects/path-mapping/src/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import * as ts from 'typescript'; 4 | import '@a/a'; 5 | import '@b'; 6 | 7 | const tryRequire = (path: string) => { 8 | try { 9 | require(path); 10 | } catch (e) { 11 | if (e.code === 'MODULE_NOT_FOUND') { 12 | return false; 13 | } else { 14 | throw e; 15 | } 16 | } 17 | 18 | return true; 19 | } 20 | 21 | export default function(program: ts.Program, pluginOptions: any) { 22 | return (ctx: ts.TransformationContext) => { 23 | process.stdout.write(`sub-path:${tryRequire('@a/a')}\n`); 24 | process.stdout.write(`path:${tryRequire('@b')}\n`); 25 | process.stdout.write(`non-mapped:${tryRequire('@c')}\n`); 26 | 27 | return (_: any) => _; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /test/assets/projects/path-mapping/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ "base.ts" ], 3 | "compilerOptions": { 4 | "outDir" : "dist", 5 | "moduleResolution" : "node", 6 | "module": "commonjs", 7 | "target": "ES2020", 8 | "noEmit": false, 9 | "plugins" : [ 10 | { 11 | "transform": "./src/index.ts", 12 | "tsConfig": "./tsconfig.plugin.json", 13 | "resolvePathAliases": "always", 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/assets/projects/path-mapping/tsconfig.plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ "src" ], 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "module": "commonjs", 6 | "target": "ES2020", 7 | "noEmit": true, 8 | "baseUrl" : "src", 9 | "paths": { 10 | "@a/*": [ "./a/*" ], 11 | "@b": [ "./b" ], 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/assets/projects/transform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minimal-ts-patch", 3 | "main": "src/index.ts", 4 | "dependencies": { 5 | "esm": "^3.2.25", 6 | "ts-node" : "^10.9.1", 7 | "semver" : "^7.6.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/assets/projects/transform/run-transform.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const semver = require('semver'); 3 | 4 | 5 | /* ****************************************************************************************************************** * 6 | * Helpers 7 | * ****************************************************************************************************************** */ 8 | 9 | function getTransformedFile(transformerKind) { 10 | process.env.TSP_SKIP_CACHE = true; 11 | const tsInstance = require('ts-patch/compiler'); 12 | 13 | console.log(`'TS version: ${tsInstance.version}\nNode Version: ${process.version.slice(1)}`); 14 | 15 | const configPath = path.join(__dirname, `tsconfig.${transformerKind}.json`); 16 | const configText = tsInstance.sys.readFile(configPath); 17 | 18 | /* Parse config */ 19 | let compilerOptions; 20 | if (semver.lt(tsInstance.version, '5.5.0', { includePrerelease: false })) { 21 | const configParseResult = tsInstance.parseConfigFileTextToJson(configPath, configText); 22 | compilerOptions = configParseResult.config.compilerOptions; 23 | } else { 24 | const configSourceFile = tsInstance.createSourceFile(configPath, configText, tsInstance.ScriptTarget.Latest); 25 | const configParseResult = tsInstance.parseJsonSourceFileConfigFileContent(configSourceFile, tsInstance.sys, path.dirname(configPath), undefined, configPath); 26 | compilerOptions = configParseResult.options; 27 | } 28 | 29 | /* Overwrite options */ 30 | Object.assign(compilerOptions, { 31 | noEmit: false, 32 | skipLibCheck: true, 33 | outDir: 'dist', 34 | }); 35 | 36 | const emittedFiles = new Map(); 37 | 38 | const writeFile = (fileName, content) => emittedFiles.set(fileName, content); 39 | 40 | const program = tsInstance.createProgram({ 41 | rootNames: [ path.join(__dirname, 'src', 'index.ts') ], 42 | options: compilerOptions, 43 | }); 44 | 45 | program.emit(undefined, writeFile); 46 | 47 | return emittedFiles.get('dist/index.js'); 48 | } 49 | 50 | 51 | /* ****************************************************************************************************************** * 52 | * Entry 53 | * ****************************************************************************************************************** */ 54 | 55 | const args = process.argv.slice(2); 56 | console.log(getTransformedFile(args[0])); 57 | -------------------------------------------------------------------------------- /test/assets/projects/transform/src/index.ts: -------------------------------------------------------------------------------- 1 | const a = 'before'; 2 | 3 | // Intentional error — should be ignored 4 | declare const x: string = 3; 5 | -------------------------------------------------------------------------------- /test/assets/projects/transform/transformers/cjs/js-plugin.cjs: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | 3 | if (!__dirname) throw new Error('Not handled as cjs'); 4 | 5 | module.exports.default = (program, _, { ts: tsInstance }) => { 6 | return (ctx) => { 7 | const factory = ctx.factory; 8 | 9 | return (sourceFile) => { 10 | function visit(node) { 11 | if (tsInstance.isStringLiteral(node) && node.text === 'before') { 12 | return factory.createStringLiteral('after-cjs'); 13 | } 14 | return tsInstance.visitEachChild(node, visit, ctx); 15 | } 16 | return tsInstance.visitNode(sourceFile, visit); 17 | }; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /test/assets/projects/transform/transformers/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /test/assets/projects/transform/transformers/cjs/plugin.cts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | export default function (program: ts.Program, _, { ts: tsInstance }: { ts: typeof ts }) { 4 | return (ctx: ts.TransformationContext) => { 5 | const factory = ctx.factory; 6 | 7 | return (sourceFile: ts.SourceFile) => { 8 | function visit(node: ts.Node): ts.Node { 9 | if (tsInstance.isStringLiteral(node) && node.text === 'before') { 10 | return factory.createStringLiteral('after-cts'); 11 | } 12 | return tsInstance.visitEachChild(node, visit, ctx); 13 | } 14 | return tsInstance.visitNode(sourceFile, visit); 15 | }; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /test/assets/projects/transform/transformers/cjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../../tsconfig.json", 3 | "include" : [ "." ], 4 | "compilerOptions" : { 5 | "module" : "CommonJS" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/assets/projects/transform/transformers/esm/js-plugin.mjs: -------------------------------------------------------------------------------- 1 | if (!import.meta.url) throw new Error('Not handled as esm'); 2 | 3 | export default function (program, _, { ts: tsInstance }) { 4 | return (ctx) => { 5 | const factory = ctx.factory; 6 | 7 | return (sourceFile) => { 8 | function visit(node) { 9 | if (tsInstance.isStringLiteral(node) && node.text === 'before') { 10 | return factory.createStringLiteral('after-mjs'); 11 | } 12 | return tsInstance.visitEachChild(node, visit, ctx); 13 | } 14 | return tsInstance.visitNode(sourceFile, visit); 15 | }; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /test/assets/projects/transform/transformers/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /test/assets/projects/transform/transformers/esm/plugin.mts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | if (!import.meta.url) throw new Error('Not handled as esm'); 4 | 5 | export default function (program: ts.Program, _, { ts: tsInstance }: { ts: typeof ts }) { 6 | return (ctx: ts.TransformationContext) => { 7 | const factory = ctx.factory; 8 | 9 | return (sourceFile: ts.SourceFile) => { 10 | function visit(node: ts.Node): ts.Node { 11 | if (tsInstance.isStringLiteral(node) && node.text === 'before') { 12 | return factory.createStringLiteral('after-mts'); 13 | } 14 | return tsInstance.visitEachChild(node, visit, ctx); 15 | } 16 | return tsInstance.visitNode(sourceFile, visit); 17 | }; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /test/assets/projects/transform/transformers/esm/plugin.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | if (!import.meta.url) throw new Error('Not handled as esm'); 4 | 5 | export default function (program: ts.Program, _, { ts: tsInstance }: { ts: typeof ts }) { 6 | return (ctx: ts.TransformationContext) => { 7 | const factory = ctx.factory; 8 | 9 | return (sourceFile: ts.SourceFile) => { 10 | function visit(node: ts.Node): ts.Node { 11 | if (tsInstance.isStringLiteral(node) && node.text === 'before') { 12 | return factory.createStringLiteral('after-ts'); 13 | } 14 | return tsInstance.visitEachChild(node, visit, ctx); 15 | } 16 | return tsInstance.visitNode(sourceFile, visit); 17 | }; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /test/assets/projects/transform/transformers/esm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../../tsconfig.json", 3 | "include" : [ "." ], 4 | "compilerOptions" : { 5 | "module" : "ESNext" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/assets/projects/transform/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "./tsconfig", 3 | "compilerOptions" : { 4 | "module" : "CommonJS", 5 | "plugins": [ 6 | { 7 | "transform": "./transformers/cjs/js-plugin.cjs", 8 | "tsConfig": "./transformers/cjs/tsconfig.json" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/assets/projects/transform/tsconfig.cts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "./tsconfig", 3 | "compilerOptions" : { 4 | "module" : "CommonJS", 5 | "plugins": [ 6 | { 7 | "transform": "./transformers/cjs/plugin.cts", 8 | "tsConfig": "./transformers/cjs/tsconfig.json" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/assets/projects/transform/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ "src" ], 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "module": "commonjs", 6 | "target": "esnext", 7 | "noEmit": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/assets/projects/transform/tsconfig.mjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "./tsconfig", 3 | "compilerOptions" : { 4 | "module" : "CommonJS", 5 | "plugins": [ 6 | { 7 | "transform": "./transformers/esm/js-plugin.mjs", 8 | "tsConfig": "./transformers/esm/tsconfig.json" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/assets/projects/transform/tsconfig.mts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "./tsconfig", 3 | "compilerOptions" : { 4 | "plugins": [ 5 | { 6 | "transform": "./transformers/esm/plugin.mts", 7 | "tsConfig": "./transformers/esm/tsconfig.json" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/assets/projects/transform/tsconfig.ts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "./tsconfig", 3 | "compilerOptions" : { 4 | "plugins": [ 5 | { 6 | "transform": "./transformers/esm/plugin.ts", 7 | "tsConfig": "./transformers/esm/tsconfig.json" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/assets/projects/webpack/esm-plugin.mjs: -------------------------------------------------------------------------------- 1 | export default function(program, pluginOptions) { 2 | return (ctx) => { 3 | throw new Error(`ts-patch worked (esm)`); 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /test/assets/projects/webpack/esm-plugin.mts: -------------------------------------------------------------------------------- 1 | export default function(program: any, pluginOptions: any) { 2 | return (ctx) => { 3 | throw new Error(`ts-patch worked (esmts)`); 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /test/assets/projects/webpack/hide-module.js: -------------------------------------------------------------------------------- 1 | const Module = require('module'); 2 | 3 | 4 | /* ****************************************************************************************************************** * 5 | * Config 6 | * ****************************************************************************************************************** */ 7 | 8 | const hiddenModules = (process.env.HIDE_MODULES || '').split(',').map(str => str.trim()); 9 | 10 | 11 | /* ****************************************************************************************************************** * 12 | * Entry 13 | * ****************************************************************************************************************** */ 14 | 15 | const originalRequire = Module.prototype.require; 16 | Module.prototype.require = function(requestedModule) { 17 | if (hiddenModules.includes(requestedModule)) { 18 | const error = new Error(`Cannot find module '${requestedModule}'`); 19 | error.code = 'MODULE_NOT_FOUND'; 20 | throw error; 21 | } 22 | 23 | return originalRequire.call(this, requestedModule); 24 | }; 25 | -------------------------------------------------------------------------------- /test/assets/projects/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-tspatch-project", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "webpack" 6 | }, 7 | "devDependencies" : { 8 | "ts-loader": "^9.4.2", 9 | "webpack": "^5.79.0", 10 | "webpack-cli": "^5.0.1", 11 | "esm": "^3.2.25", 12 | "ts-node" : "^10.9.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/assets/projects/webpack/plugin.ts: -------------------------------------------------------------------------------- 1 | import type * as ts from 'typescript'; 2 | 3 | export default function(program: ts.Program, pluginOptions: any) { 4 | return (ctx: ts.TransformationContext) => { 5 | throw new Error(`ts-patch worked (cjs)`); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /test/assets/projects/webpack/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nonara/ts-patch/88ce20cf4d9cd7c970ac76dd5bcfaae361254064/test/assets/projects/webpack/src/index.ts -------------------------------------------------------------------------------- /test/assets/projects/webpack/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { "transform": "./esm-plugin.mjs" } 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/assets/projects/webpack/tsconfig.esmts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { "transform": "./esm-plugin.mts" } 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/assets/projects/webpack/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ "src" ], 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "noEmit": true, 6 | "target": "ESNext", 7 | "declaration": false, 8 | "moduleResolution" : "Node", 9 | "plugins": [ 10 | { "transform": "./plugin.ts" } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/assets/projects/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: './src/index.ts', 6 | output: { 7 | path: path.resolve(__dirname, 'dist'), 8 | filename: 'bundle.js' 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(ts|tsx)$/, 14 | loader: require.resolve('ts-loader'), 15 | options: { 16 | compiler: 'ts-patch/typescript' 17 | } 18 | } 19 | ] 20 | }, 21 | resolve: { 22 | extensions: ['.ts', '.tsx', '.js'] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts" : { 4 | "perf": "ts-node -T --project tsconfig.json --files ./src/perf.ts" 5 | }, 6 | "dependencies": { 7 | "ts-latest": "npm:typescript@beta", 8 | "ts-node": "latest", 9 | "ts-patch": "link:../dist", 10 | "tsconfig-paths": "latest", 11 | "fs-extra": "^10.0.0", 12 | "@types/fs-extra": "^9.0.13" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/src/cleanup.ts: -------------------------------------------------------------------------------- 1 | import { cleanTemp } from './project'; 2 | 3 | 4 | export default function() { 5 | try { 6 | cleanTemp(); 7 | } catch (e) { 8 | console.error(e); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/src/config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | // @ts-expect-error TODO - tsei 3 | import { normalizeSlashes } from 'typescript'; 4 | 5 | 6 | /* ****************************************************************************************************************** * 7 | * Locals 8 | * ****************************************************************************************************************** */ 9 | 10 | const getTsModule = (label: string, moduleSpecifier: string) => ({ 11 | moduleSpecifier, 12 | tsDir: path.resolve(testRootDir, 'node_modules', moduleSpecifier), 13 | label 14 | }); 15 | 16 | 17 | /* ****************************************************************************************************************** */ 18 | // region: Config 19 | /* ****************************************************************************************************************** */ 20 | 21 | export const testRootDir = normalizeSlashes(path.resolve(__dirname, '..')); 22 | export const rootDir = normalizeSlashes(path.resolve(__dirname, '../../')); 23 | export const resourcesDir = normalizeSlashes(path.resolve(__dirname, '../../dist/resources')); 24 | export const assetsDir = normalizeSlashes(path.resolve(__dirname, '../assets')); 25 | export const projectsDir = normalizeSlashes(path.resolve(assetsDir, 'projects')); 26 | 27 | export const tsModules = [ 28 | getTsModule('latest', 'ts-latest'), 29 | ] 30 | 31 | export const packageManagers = [ 'npm', 'yarn', 'pnpm', 'yarn3' ]; 32 | 33 | export type PackageManager = typeof packageManagers[number]; 34 | 35 | // endregion 36 | -------------------------------------------------------------------------------- /test/src/perf.ts: -------------------------------------------------------------------------------- 1 | import { getTsPackage, TsPackage } from '../../projects/core/src/ts-package'; 2 | import { getTsModule, GetTsModuleOptions, TsModule } from '../../projects/core/src/module'; 3 | import { getPatchedSource, GetPatchedSourceOptions } from '../../projects/core/src/patch/get-patched-source'; 4 | import child_process from 'child_process'; 5 | import path from 'path'; 6 | 7 | 8 | /* ****************************************************************************************************************** */ 9 | // region: Config 10 | /* ****************************************************************************************************************** */ 11 | 12 | const tsLatestPath = path.dirname(require.resolve('ts-latest/package.json')); 13 | 14 | // endregion 15 | 16 | 17 | /* ****************************************************************************************************************** */ 18 | // region: Helpers 19 | /* ****************************************************************************************************************** */ 20 | 21 | function perf(name: string, opt: any = {}, fn: () => any) { 22 | const start = performance.now(); 23 | const res = fn(); 24 | const end = performance.now(); 25 | console.log(`${name} (${JSON.stringify(opt)}): \n — duration: ${end - start} ms\n`); 26 | return res; 27 | } 28 | 29 | function printOpt(opt: any) { 30 | const printOpt = { ...opt }; 31 | if (printOpt.tsPackage) printOpt.tsPackage = printOpt.tsPackage.packageDir; 32 | if (printOpt.tsModule) printOpt.tsModule = printOpt.tsModule.moduleName; 33 | return printOpt; 34 | } 35 | 36 | // endregion 37 | 38 | 39 | /* ****************************************************************************************************************** */ 40 | // region: Utils 41 | /* ****************************************************************************************************************** */ 42 | 43 | export function perfTsPackage(opt: { tsPath?: string } = {}) { 44 | opt.tsPath ??= tsLatestPath; 45 | perf(`tsPackage`, printOpt(opt), () => getTsPackage(opt.tsPath)); 46 | } 47 | 48 | export function perfTsModule(opt: { moduleName?: TsModule.Name, tsPackage?: TsPackage } & GetTsModuleOptions = {}) { 49 | opt.tsPackage ??= getTsPackage(tsLatestPath); 50 | opt.moduleName ??= 'typescript.js'; 51 | perf(`tsModule`, printOpt(opt), () => getTsModule(opt.tsPackage!, opt.moduleName!, opt)); 52 | } 53 | 54 | export function perfGetPatchedSource(opt: { tsModule?: TsModule } & GetPatchedSourceOptions = {}) { 55 | opt.tsModule ??= getTsModule(getTsPackage(tsLatestPath), 'typescript.js'); 56 | perf(`getPatchedSource`, printOpt(opt), () => getPatchedSource(opt.tsModule!, opt)); 57 | } 58 | 59 | export function perfTspc(opt: { skipCache?: boolean } = {}) { 60 | opt.skipCache ??= false; 61 | perf( 62 | `tspc`, 63 | printOpt(opt), 64 | () => { 65 | // Execute tspc command with node in a child process 66 | child_process.execSync(`node ${path.resolve(__dirname, '../../dist/bin/tspc.js --help')}`, { 67 | env: { 68 | ...process.env, 69 | TSP_SKIP_CACHE: opt.skipCache ? 'true' : 'false', 70 | TSP_COMPILER_TS_PATH: tsLatestPath, 71 | } 72 | }); 73 | } 74 | ); 75 | } 76 | 77 | export function perfTsc() { 78 | perf( 79 | `tsc`, 80 | {}, 81 | () => { 82 | child_process.execSync(`node ${path.join(tsLatestPath, 'lib/tsc.js')} --help`, { 83 | env: { 84 | ...process.env, 85 | TSP_COMPILER_TS_PATH: tsLatestPath, 86 | } 87 | }); 88 | } 89 | ); 90 | } 91 | 92 | // TODO - Add perfInstall with and without cache 93 | 94 | export function perfAll() { 95 | perfTsPackage(); 96 | perfTsModule(); 97 | perfGetPatchedSource(); 98 | perfTsc(); 99 | perfTspc({ skipCache: true }); 100 | perfTspc({ skipCache: false }); 101 | } 102 | 103 | // endregion 104 | 105 | 106 | /* ****************************************************************************************************************** * 107 | * Entry 108 | * ****************************************************************************************************************** */ 109 | 110 | if (require.main === module) perfAll(); 111 | -------------------------------------------------------------------------------- /test/src/prepare.ts: -------------------------------------------------------------------------------- 1 | import { cleanTemp } from './project'; 2 | 3 | 4 | export default function() { 5 | try { 6 | cleanTemp(); 7 | } catch (e) { 8 | console.error(e); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/src/project.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { PackageManager, projectsDir, rootDir } from './config'; 3 | import fs from 'fs'; 4 | import * as os from 'os'; 5 | import shell from 'shelljs'; 6 | import { PartialSome } from './utils/general'; 7 | import { execSync } from 'child_process'; 8 | 9 | 10 | /* ****************************************************************************************************************** */ 11 | // region: Config 12 | /* ****************************************************************************************************************** */ 13 | 14 | const pkgManagerInstallCmd = { 15 | npm: 'npm install --no-audit --progress=false', 16 | yarn: 'yarn --no-progress --check-cache --no-audit', 17 | pnpm: 'npx pnpm install', 18 | yarn3: 'npx yarn install --skip-builds' 19 | } satisfies Record; 20 | 21 | const pkgManagerInstallerCmd = { 22 | npm: '', 23 | yarn: 'npm install --no-save --legacy-peer-deps yarn', 24 | yarn3: 'npm install --no-save --legacy-peer-deps yarn@berry', 25 | pnpm: 'npm install --no-save --legacy-peer-deps pnpm' 26 | } satisfies Record; 27 | 28 | // endregion 29 | 30 | 31 | /* ****************************************************************************************************************** */ 32 | // region: Types 33 | /* ****************************************************************************************************************** */ 34 | 35 | export interface PrepareOptions { 36 | projectName: string; 37 | 38 | /** @default 'latest' */ 39 | tsVersion: string; 40 | 41 | /** @default 'npm' */ 42 | packageManager: PackageManager; 43 | 44 | dependencies?: Record 45 | } 46 | 47 | export namespace PrepareOptions { 48 | export type Configurable = PartialSome> 49 | 50 | export const getDefaults = () => ({ 51 | packageManager: 'npm', 52 | tsVersion: 'beta' 53 | }) satisfies Partial; 54 | } 55 | 56 | // endregion 57 | 58 | 59 | /* ****************************************************************************************************************** */ 60 | // region: Helpers 61 | /* ****************************************************************************************************************** */ 62 | 63 | function execCmd(cmd: string) { 64 | try { 65 | execSync(cmd, { stdio: [ 'ignore', 'pipe', 'pipe' ] }); 66 | } catch (e) { 67 | throw new Error(`Error during project cmd: ${e.stdout?.toString() + '\n' + e.stderr?.toString()}`); 68 | } 69 | } 70 | 71 | // endregion 72 | 73 | 74 | /* ****************************************************************************************************************** */ 75 | // region: Utils 76 | /* ****************************************************************************************************************** */ 77 | 78 | export function getProjectTempPath(projectName?: string, packageManager?: string, wipe?: boolean) { 79 | const tmpBasePath = process.env.TSP_TMP_DIR ?? os.tmpdir(); 80 | const tmpProjectPath = path.resolve(tmpBasePath, '.tsp-test/project', projectName ?? '', packageManager ?? ''); 81 | if (!fs.existsSync(tmpProjectPath)) fs.mkdirSync(tmpProjectPath, { recursive: true }); 82 | else if (wipe) shell.rm('-rf', path.join(tmpProjectPath, '*')); 83 | 84 | return tmpProjectPath; 85 | } 86 | 87 | export function getProjectPath(projectName: string) { 88 | return path.join(projectsDir, projectName); 89 | } 90 | 91 | export function prepareTestProject(opt: PrepareOptions.Configurable) { 92 | const options: PrepareOptions = { ...PrepareOptions.getDefaults(), ...opt }; 93 | const { projectName, packageManager } = options; 94 | 95 | const projectPath = getProjectPath(projectName); 96 | if (!fs.existsSync(projectPath)) throw new Error(`Project "${projectName}" does not exist`); 97 | 98 | const tmpProjectPath = getProjectTempPath(projectName, packageManager, true); 99 | 100 | /* Copy all files from projectPath to tmpProjectPath */ 101 | shell.cp('-R', path.join(projectPath, '*'), tmpProjectPath); 102 | 103 | shell.cd(tmpProjectPath); 104 | 105 | /* Copy ts-patch to node_modules */ 106 | const tspDir = path.join(tmpProjectPath, '.tsp'); 107 | if (!fs.existsSync(tspDir)) fs.mkdirSync(tspDir, { recursive: true }); 108 | shell.cp('-R', path.join(rootDir, 'dist/*'), tspDir); 109 | 110 | /* Install package manager */ 111 | if (pkgManagerInstallerCmd[packageManager]) 112 | execCmd(pkgManagerInstallerCmd[packageManager]); 113 | 114 | /* Install dependencies */ 115 | const pkgJson = JSON.parse(fs.readFileSync(path.join(tmpProjectPath, 'package.json'), 'utf8')); 116 | pkgJson.dependencies = { 117 | ...pkgJson.dependencies, 118 | ...options.dependencies, 119 | 'typescript': options.tsVersion, 120 | 'ts-patch': 'file:./.tsp' 121 | }; 122 | fs.writeFileSync(path.join(tmpProjectPath, 'package.json'), JSON.stringify(pkgJson, null, 2)); 123 | 124 | execCmd(pkgManagerInstallCmd[packageManager]); 125 | 126 | return { projectPath, tmpProjectPath }; 127 | } 128 | 129 | export function cleanTemp() { 130 | if (!process.env.TSP_TMP_DIR) 131 | fs.rmSync(getProjectTempPath(), { recursive: true, force: true, retryDelay: 200, maxRetries: 5 }); 132 | } 133 | 134 | // endregion 135 | -------------------------------------------------------------------------------- /test/src/utils/general.ts: -------------------------------------------------------------------------------- 1 | 2 | /* ****************************************************************************************************************** */ 3 | // region: Type Utils 4 | /* ****************************************************************************************************************** */ 5 | 6 | export type PartialSome = Omit & Pick, K> 7 | 8 | // endregion 9 | -------------------------------------------------------------------------------- /test/tests/package-config.test.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import path from 'path'; 3 | import { assetsDir } from '../src/config'; 4 | 5 | 6 | /* ****************************************************************************************************************** */ 7 | // region: Config 8 | /* ****************************************************************************************************************** */ 9 | 10 | const modes = ['enabled', 'disabled']; 11 | 12 | // endregion 13 | 14 | 15 | /* ****************************************************************************************************************** * 16 | * Tests 17 | * ****************************************************************************************************************** */ 18 | 19 | describe(`Project Package Config`, () => { 20 | const projectPath = path.resolve(assetsDir, 'projects/package-config'); 21 | 22 | describe.each(modes)(`Config present = %s`, (mode) => { 23 | let output: string; 24 | 25 | beforeAll(() => { 26 | const command = `node run-transform.js ${mode === 'disabled' ? '--disable' : ''}`; 27 | output = execSync(command, { cwd: projectPath }).toString('utf8'); 28 | }); 29 | 30 | test(`Tags are ${mode === 'enabled' ? '' : 'not '}identified`, () => { 31 | const expectedOutput = mode === 'enabled' ? 'const myVar = "ExampleTag1 ExampleTag2";' : 'const myVar = 123;'; 32 | expect(output).toMatch(new RegExp(`^${expectedOutput}$`, 'm')); 33 | }); 34 | 35 | test(`Chained transformer works`, () => { 36 | // All lines should be prefixed with `const ` instead of `let ` 37 | expect(output).toMatch(/^const /m); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/tests/path-mapping.test.ts: -------------------------------------------------------------------------------- 1 | import { prepareTestProject } from '../src/project'; 2 | import { execSync } from 'child_process'; 3 | import path from 'path'; 4 | 5 | 6 | /* ****************************************************************************************************************** * 7 | * Tests 8 | * ****************************************************************************************************************** */ 9 | 10 | describe(`Path Mapping`, () => { 11 | let projectPath: string; 12 | let output: string[]; 13 | 14 | beforeAll(() => { 15 | const prepRes = prepareTestProject({ projectName: 'path-mapping', packageManager: 'yarn' }); 16 | projectPath = prepRes.tmpProjectPath; 17 | 18 | let commandOutput: string; 19 | try { 20 | commandOutput = execSync('tspc', { 21 | cwd: projectPath, 22 | env: { 23 | ...process.env, 24 | PATH: `${projectPath}/node_modules/.bin${path.delimiter}${process.env.PATH}` 25 | } 26 | }).toString(); 27 | } catch (e) { 28 | const err = new Error(e.stdout.toString() + '\n' + e.stderr.toString()); 29 | console.error(err); 30 | throw e; 31 | } 32 | 33 | output = commandOutput.trim().split('\n'); 34 | }); 35 | 36 | test(`Resolves sub-paths`, () => { 37 | expect(output[0]).toEqual('sub-path:true'); 38 | }); 39 | 40 | test(`Resolves direct paths`, () => { 41 | expect(output[1]).toEqual('path:true'); 42 | }); 43 | 44 | test(`Cannot resolve unmapped paths`, () => { 45 | expect(output[2]).toEqual('non-mapped:false'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/tests/transformer.test.ts: -------------------------------------------------------------------------------- 1 | import { prepareTestProject } from '../src/project'; 2 | import { execSync } from 'child_process'; 3 | 4 | 5 | /* ****************************************************************************************************************** */ 6 | // region: Config 7 | /* ****************************************************************************************************************** */ 8 | 9 | const transformerKinds = [ 10 | 'mts', 11 | 'ts', 12 | 'cts', 13 | 'mjs', 14 | 'cjs' 15 | ]; 16 | 17 | // endregion 18 | 19 | 20 | /* ****************************************************************************************************************** * 21 | * Tests 22 | * ****************************************************************************************************************** */ 23 | 24 | describe(`Transformer`, () => { 25 | let projectPath: string; 26 | let loaderResolve: (value?: unknown) => void; 27 | let loaderPromise = new Promise(resolve => loaderResolve = resolve); 28 | beforeAll(() => { 29 | const prepRes = prepareTestProject({ 30 | projectName: 'transform', 31 | packageManager: 'yarn', 32 | tsVersion: '5.5.2', 33 | }); 34 | projectPath = prepRes.tmpProjectPath; 35 | loaderResolve(); 36 | }); 37 | 38 | test.concurrent.each(transformerKinds)(`%s transformer works`, async (transformerKind: string) => { 39 | await loaderPromise; 40 | 41 | const res = execSync(`node run-transform.js ${transformerKind}`, { cwd: projectPath }); 42 | expect(res.toString('utf8')).toMatch(new RegExp(`^(?:var|const) a = "after-${transformerKind}";?$`, 'm')); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/tests/webpack.test.ts: -------------------------------------------------------------------------------- 1 | import { execSync, ExecSyncOptions } from 'child_process'; 2 | import { prepareTestProject } from '../src/project'; 3 | 4 | 5 | /* ****************************************************************************************************************** */ 6 | // region: Helpers 7 | /* ****************************************************************************************************************** */ 8 | 9 | function execAndGetErr(projectPath: string, projectFile: string = '', hideModules?: string) { 10 | const extraOpts: ExecSyncOptions = { 11 | ...(hideModules ? { env: { ...process.env, HIDE_MODULES: hideModules } } : {}) 12 | }; 13 | 14 | const cmd = `ts-node ${hideModules ? '-r ./hide-module.js' : ''} -C ts-patch/compiler${projectFile ? ` -P ${projectFile}` : ''}` 15 | try { 16 | execSync( 17 | cmd, 18 | { 19 | cwd: projectPath, 20 | stdio: [ 'ignore', 'pipe', 'pipe' ], 21 | ...extraOpts 22 | }); 23 | } catch (e) { 24 | return e.stderr.toString(); 25 | } 26 | 27 | throw new Error('Expected error to be thrown, but none was'); 28 | } 29 | 30 | // endregion 31 | 32 | 33 | /* ****************************************************************************************************************** * 34 | * Tests 35 | * ****************************************************************************************************************** */ 36 | 37 | describe('Webpack', () => { 38 | let projectPath: string; 39 | beforeAll(() => { 40 | const prepRes = prepareTestProject({ projectName: 'webpack', packageManager: 'yarn' }); 41 | projectPath = prepRes.tmpProjectPath; 42 | }); 43 | 44 | test(`Compiler with CJS transformer works`, () => { 45 | const err = execAndGetErr(projectPath); 46 | expect(err).toContain('Error: ts-patch worked (cjs)'); 47 | }); 48 | 49 | test(`Compiler with ESM TS transformer works`, () => { 50 | const err = execAndGetErr(projectPath, './tsconfig.esmts.json'); 51 | expect(err).toContain('Error: ts-patch worked (esmts)'); 52 | }); 53 | 54 | test(`Compiler with ESM JS transformer works`, () => { 55 | const err = execAndGetErr(projectPath, './tsconfig.esm.json'); 56 | expect(err).toContain('Error: ts-patch worked (esm)'); 57 | }); 58 | 59 | test(`Compiler with ESM transformer throws if no ESM package`, () => { 60 | const err = execAndGetErr(projectPath, './tsconfig.esm.json', 'esm'); 61 | expect(err).toContain('To enable experimental ESM support, install the \'esm\' package'); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "include": [ "src", "tests" ], 4 | 5 | "compilerOptions": { 6 | "noEmit": true, 7 | "target": "ESNext", 8 | "skipDefaultLibCheck": true, 9 | "skipLibCheck": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "strict": true, 5 | "noUnusedLocals": false, 6 | "noImplicitReturns": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "useUnknownInCatchVariables": false, 9 | 10 | "lib": [ "es2020", "dom" ], 11 | "outDir": "dist", 12 | "target": "ES2020", 13 | "module": "CommonJS", 14 | "moduleResolution": "node", 15 | 16 | "newLine": "LF", 17 | "allowJs": false, 18 | "allowSyntheticDefaultImports": true 19 | } 20 | } 21 | --------------------------------------------------------------------------------