├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/.idea/jsonSchemas.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
--------------------------------------------------------------------------------